import itertools
import operator
import sys
from typing import Any
from typing import Callable
from typing import Dict
from typing import Iterator
from typing import List
from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import 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
from xsdata.utils.namespaces import 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)})"
[docs]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 sequential: 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
"""
__slots__ = (
"index",
"name",
"qname",
"types",
"clazz",
"init",
"mixed",
"factory",
"tokens_factory",
"format",
"derived",
"any_type",
"required",
"nillable",
"sequential",
"default",
"namespaces",
"elements",
"wildcards",
# 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,
required: bool,
nillable: bool,
sequential: bool,
default: Any,
xml_type: str,
namespaces: Sequence[str],
elements: Mapping[str, "XmlVar"],
wildcards: Sequence["XmlVar"],
**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.required = required
self.nillable = nillable
self.sequential = sequential
self.list_element = factory in (list, tuple)
self.default = default
self.namespaces = namespaces
self.elements = elements
self.wildcards = wildcards
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.ELEMENT or self.clazz:
self.is_element = True
elif xml_type == XmlType.ELEMENTS:
self.is_elements = 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}
[docs] 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)
[docs] def find_value_choice(self, value: Any, check_subclass: bool) -> Optional["XmlVar"]:
"""Match and return a choice field that matches the given value
type."""
if collections.is_array(value):
tp = type(None) if not value else type(value[0])
tokens = True
check_subclass = False
else:
tp = type(value)
tokens = False
return self.find_type_choice(value, tp, tokens, check_subclass)
[docs] def find_type_choice(
self, value: Any, tp: Type, tokens: bool, check_subclass: bool
) -> Optional["XmlVar"]:
"""Match and return a choice field that matches the given type."""
for element in self.elements.values():
if element.any_type or tokens != element.tokens:
continue
if tp is NoneType:
if element.nillable:
return element
elif self.match_type(value, tp, element.types, check_subclass):
return element
return None
[docs] 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
@classmethod
def match_type(
cls, value: Any, tp: Type, types: Sequence[Type], check_subclass: bool
) -> bool:
for candidate in types:
if tp == candidate or (check_subclass and issubclass(tp, candidate)):
return True
if isinstance(value, list):
return all(converter.test(val, types) for val in value)
return converter.test(value, types)
[docs] 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")
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