Source code for xsdata.formats.dataclass.models.elements

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)})"


[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 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}
[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, 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
[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
[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")
[docs] 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)
[docs] 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