1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
|
import itertools
import operator
import sys
from typing import (
Any,
Callable,
Dict,
Iterator,
List,
Mapping,
Optional,
Sequence,
Set,
Tuple,
Type,
)
from xsdata.formats.converter import converter
from xsdata.models.enums import NamespaceType
from xsdata.utils import collections
from xsdata.utils.namespaces import local_name, target_uri
NoneType = type(None)
class XmlType:
"""Xml node types."""
TEXT = sys.intern("Text")
ELEMENT = sys.intern("Element")
ELEMENTS = sys.intern("Elements")
WILDCARD = sys.intern("Wildcard")
ATTRIBUTE = sys.intern("Attribute")
ATTRIBUTES = sys.intern("Attributes")
IGNORE = sys.intern("Ignore")
class MetaMixin:
"""Use this mixin for unit tests only!!!"""
__slots__: Tuple[str, ...] = ()
def __eq__(self, other: Any) -> bool:
return tuple(self) == tuple(other)
def __iter__(self) -> Iterator:
for name in self.__slots__:
yield getattr(self, name)
def __repr__(self) -> str:
params = (f"{name}={getattr(self, name)!r}" for name in self.__slots__)
return f"{self.__class__.__qualname__}({', '.join(params)})"
class XmlVar(MetaMixin):
"""
Class field binding metadata.
:param index: Field ordering
:param name: Field name
:param qname: Qualified name
:param types: List of all the supported data types
:param init: Include field in the constructor
:param mixed: Field supports mixed content type values
:param tokens: Field is derived from xs:list
:param format: Value format information
:param derived: Wrap parsed values with a generic type
:param any_type: Field supports dynamic value types
:param required: Field is mandatory
:param nillable: Field supports nillable content
:param sequence: Render values in sequential mode
:param list_element: Field is a list of elements
:param default: Field default value or factory
:param xml_Type: Field xml type
:param namespaces: List of the supported namespaces
:param elements: Mapping of qname-repeatable elements
:param wildcards: List of repeatable wildcards
:param wrapper: A name for the wrapper. Applies for list types only.
"""
__slots__ = (
"index",
"name",
"qname",
"types",
"clazz",
"init",
"mixed",
"factory",
"tokens_factory",
"format",
"derived",
"any_type",
"process_contents",
"required",
"nillable",
"sequence",
"default",
"namespaces",
"elements",
"wildcards",
"wrapper",
# Calculated
"tokens",
"list_element",
"is_text",
"is_element",
"is_elements",
"is_wildcard",
"is_attribute",
"is_attributes",
"namespace_matches",
"is_clazz_union",
"local_name",
)
def __init__(
self,
index: int,
name: str,
qname: str,
types: Sequence[Type],
clazz: Optional[Type],
init: bool,
mixed: bool,
factory: Optional[Callable],
tokens_factory: Optional[Callable],
format: Optional[str],
derived: bool,
any_type: bool,
process_contents: str,
required: bool,
nillable: bool,
sequence: Optional[int],
default: Any,
xml_type: str,
namespaces: Sequence[str],
elements: Mapping[str, "XmlVar"],
wildcards: Sequence["XmlVar"],
wrapper: Optional[str] = None,
**kwargs: Any,
):
self.index = index
self.name = name
self.qname = qname
self.types = types
self.clazz = clazz
self.init = init
self.mixed = mixed
self.tokens = tokens_factory is not None
self.format = format
self.derived = derived
self.any_type = any_type
self.process_contents = process_contents
self.required = required
self.nillable = nillable
self.sequence = sequence
self.list_element = factory in (list, tuple)
self.default = default
self.namespaces = namespaces
self.elements = elements
self.wildcards = wildcards
self.wrapper = wrapper
self.factory = factory
self.tokens_factory = tokens_factory
self.namespace_matches: Optional[Dict[str, bool]] = None
self.is_clazz_union = self.clazz and len(types) > 1
self.local_name = local_name(qname)
self.is_text = False
self.is_element = False
self.is_elements = False
self.is_wildcard = False
self.is_attribute = False
self.is_attributes = False
if xml_type == XmlType.ELEMENTS:
self.is_elements = True
elif xml_type == XmlType.ELEMENT or self.clazz:
self.is_element = True
elif xml_type == XmlType.ATTRIBUTE:
self.is_attribute = True
elif xml_type == XmlType.ATTRIBUTES:
self.is_attributes = True
elif xml_type == XmlType.WILDCARD:
self.is_wildcard = True
else:
self.is_text = True
@property
def element_types(self) -> Set[Type]:
return {tp for element in self.elements.values() for tp in element.types}
def find_choice(self, qname: str) -> Optional["XmlVar"]:
"""Match and return a choice field by its qualified name."""
match = self.elements.get(qname)
return match or find_by_namespace(self.wildcards, qname)
def find_value_choice(self, value: Any, is_class: bool) -> Optional["XmlVar"]:
"""
Match and return a choice field that matches the given value.
Cases:
- value is none or empty tokens list: look for a nillable choice
- value is a dataclass: look for exact type or a subclass
- value is primitive: test value against the converter
"""
is_tokens = collections.is_array(value)
if value is None or (not value and is_tokens):
return self.find_nillable_choice(is_tokens)
if is_class:
return self.find_clazz_choice(type(value))
return self.find_primitive_choice(value, is_tokens)
def find_nillable_choice(self, is_tokens: bool) -> Optional["XmlVar"]:
return collections.first(
element
for element in self.elements.values()
if element.nillable and is_tokens == element.tokens
)
def find_clazz_choice(self, tp: Type) -> Optional["XmlVar"]:
derived = None
for element in self.elements.values():
if element.clazz:
if tp in element.types:
return element
if derived is None and any(issubclass(tp, t) for t in element.types):
derived = element
return derived
def find_primitive_choice(self, value: Any, is_tokens: bool) -> Optional["XmlVar"]:
tp = type(value) if not is_tokens else type(value[0])
for element in self.elements.values():
if (element.any_type or element.clazz) or element.tokens != is_tokens:
continue
if tp in element.types:
return element
if is_tokens and all(converter.test(val, element.types) for val in value):
return element
if converter.test(value, element.types):
return element
return None
def is_optional(self, value: Any) -> bool:
"""Return whether this var instance is not required and the given value
matches the default one."""
if self.required:
return False
if callable(self.default):
return self.default() == value
return self.default == value
def match_namespace(self, qname: str) -> bool:
"""Match the given qname to the wildcard allowed namespaces."""
if self.namespace_matches is None:
self.namespace_matches = {}
matches = self.namespace_matches.get(qname)
if matches is None:
matches = self._match_namespace(qname)
self.namespace_matches[qname] = matches
return matches
def _match_namespace(self, qname: str) -> bool:
uri = target_uri(qname)
if not self.namespaces and uri is None:
return True
for check in self.namespaces:
if (
(not check and uri is None)
or check == uri
or check == NamespaceType.ANY_NS
or (check and check[0] == "!" and check[1:] != uri)
):
return True
return False
get_index = operator.attrgetter("index")
class XmlMeta(MetaMixin):
"""
Class binding metadata.
:param clazz: The dataclass type
:param qname: The namespace qualified name.
:param target_qname: The target namespace qualified name.
:param nillable: Specifies whether an explicit empty value can be
assigned.
:param mixed_content: Has a wildcard with mixed flag enabled
:param text: Text var
:param choices: List of compound vars
:param elements: Mapping of qname-element vars
:param wildcards: List of wildcard vars
:param attributes: Mapping of qname-attribute vars
:param any_attributes: List of wildcard attributes vars
"""
__slots__ = (
"clazz",
"qname",
"target_qname",
"nillable",
"text",
"choices",
"elements",
"wildcards",
"attributes",
"any_attributes",
"wrappers",
# Calculated
"namespace",
"mixed_content",
)
def __init__(
self,
clazz: Type,
qname: str,
target_qname: Optional[str],
nillable: bool,
text: Optional[XmlVar],
choices: Sequence[XmlVar],
elements: Mapping[str, Sequence[XmlVar]],
wildcards: Sequence[XmlVar],
attributes: Mapping[str, XmlVar],
any_attributes: Sequence[XmlVar],
wrappers: Mapping[str, Sequence[XmlVar]],
**kwargs: Any,
):
self.clazz = clazz
self.qname = qname
self.namespace = target_uri(qname)
self.target_qname = target_qname
self.nillable = nillable
self.text = text
self.choices = choices
self.elements = elements
self.wildcards = wildcards
self.attributes = attributes
self.any_attributes = any_attributes
self.mixed_content = any(wildcard.mixed for wildcard in self.wildcards)
self.wrappers = wrappers
@property
def element_types(self) -> Set[Type]:
return {
tp
for elements in self.elements.values()
for element in elements
for tp in element.types
}
def get_element_vars(self) -> List[XmlVar]:
result = list(
itertools.chain(self.wildcards, self.choices, *self.elements.values())
)
if self.text:
result.append(self.text)
return sorted(result, key=get_index)
def get_attribute_vars(self) -> List[XmlVar]:
result = itertools.chain(self.any_attributes, self.attributes.values())
return sorted(result, key=get_index)
def get_all_vars(self) -> List[XmlVar]:
result = list(
itertools.chain(
self.wildcards,
self.choices,
self.any_attributes,
self.attributes.values(),
*self.elements.values(),
)
)
if self.text:
result.append(self.text)
return sorted(result, key=get_index)
def find_attribute(self, qname: str) -> Optional[XmlVar]:
return self.attributes.get(qname)
def find_any_attributes(self, qname: str) -> Optional[XmlVar]:
return find_by_namespace(self.any_attributes, qname)
def find_wildcard(self, qname: str) -> Optional[XmlVar]:
"""Match the given qualified name to a wildcard and optionally to one
of its choice elements."""
wildcard = find_by_namespace(self.wildcards, qname)
if wildcard and wildcard.elements:
choice = wildcard.find_choice(qname)
if choice:
return choice
return wildcard
def find_any_wildcard(self) -> Optional[XmlVar]:
if self.wildcards:
return self.wildcards[0]
return None
def find_children(self, qname: str) -> Iterator[XmlVar]:
elements = self.elements.get(qname)
if elements:
yield from elements
for choice in self.choices:
match = choice.find_choice(qname)
if match:
yield match
chd = self.find_wildcard(qname)
if chd:
yield chd
def find_by_namespace(xml_vars: Sequence[XmlVar], qname: str) -> Optional[XmlVar]:
for xml_var in xml_vars:
if xml_var.match_namespace(qname):
return xml_var
return None
|