Source code for pydantic_xml.model

import functools as ft
import typing
from typing import Any, Callable, ClassVar, Dict, Optional, Tuple, Type, TypeVar, Union

import pydantic as pd
import pydantic.fields
import pydantic.generics
import pydantic.json

from . import config, errors, serializers, utils
from .element import SearchMode
from .element.native import XmlElement, etree
from .serializers.factories import ModelSerializerFactory
from .utils import NsMap, register_nsmap


class XmlEntityInfo(pd.fields.FieldInfo):
    """
    Field xml meta-information.
    """


[docs]class XmlAttributeInfo(XmlEntityInfo): """ Field xml attribute meta-information. :param name: attribute name :param ns: attribute xml namespace :param kwargs: pydantic field arguments. See :py:class:`pydantic.Field` """ __slots__ = ('_name', '_ns') def __init__( self, name: Optional[str] = None, ns: Optional[str] = None, **kwargs: Any, ): super().__init__(**kwargs) self._name = name self._ns = ns @property def name(self) -> Optional[str]: return self._name @property def ns(self) -> Optional[str]: return self._ns
[docs]class XmlElementInfo(XmlEntityInfo): """ Field xml element meta-information. :param tag: element tag :param ns: element xml namespace :param nsmap: element xml namespace map :param kwargs: pydantic field arguments """ __slots__ = ('_tag', '_ns', '_nsmap') def __init__( self, tag: Optional[str] = None, ns: Optional[str] = None, nsmap: Optional[NsMap] = None, **kwargs: Any, ): super().__init__(**kwargs) self._tag = tag self._ns = ns self._nsmap = nsmap if config.REGISTER_NS_PREFIXES and nsmap: register_nsmap(nsmap) @property def tag(self) -> Optional[str]: return self._tag @property def ns(self) -> Optional[str]: return self._ns @property def nsmap(self) -> Optional[NsMap]: return self._nsmap
[docs]class XmlWrapperInfo(XmlEntityInfo): """ Field xml wrapper meta-information. :param entity: wrapped entity :param path: entity path :param ns: element xml namespace :param nsmap: element xml namespace map :param kwargs: pydantic field arguments """ __slots__ = ('_entity', '_path', '_ns', '_nsmap') def __init__( self, path: str, entity: Optional[XmlEntityInfo] = None, ns: Optional[str] = None, nsmap: Optional[NsMap] = None, **kwargs: Any, ): if entity is not None: # copy arguments from the wrapped entity to let pydantic know how to process the field for entity_field_name in utils.get_slots(entity): kwargs[entity_field_name] = getattr(entity, entity_field_name) super().__init__(**kwargs) self._entity = entity self._path = path self._ns = ns self._nsmap = nsmap if config.REGISTER_NS_PREFIXES and nsmap: register_nsmap(nsmap) @property def entity(self) -> Optional[XmlEntityInfo]: return self._entity @property def path(self) -> str: return self._path @property def ns(self) -> Optional[str]: return self._ns @property def nsmap(self) -> Optional[NsMap]: return self._nsmap
[docs]def attr(**kwargs: Any) -> Any: """ Marks a pydantic field as an xml attribute. :param kwargs: see :py:class:`pydantic_xml.XmlAttributeInfo` """ return XmlAttributeInfo(**kwargs)
[docs]def element(**kwargs: Any) -> Any: """ Marks a pydantic field as an xml element. :param kwargs: see :py:class:`pydantic_xml.XmlElementInfo` """ return XmlElementInfo(**kwargs)
[docs]def wrapped(*args: Any, **kwargs: Any) -> Any: """ Marks a pydantic field as a wrapped xml entity. :param args: see :py:class:`pydantic_xml.XmlWrapperInfo` :param kwargs: see :py:class:`pydantic_xml.XmlWrapperInfo` """ return XmlWrapperInfo(*args, **kwargs)
class XmlModelMeta(pd.main.ModelMetaclass): """ Xml model metaclass. """ __is_base_model_defined__ = False def __new__(mcls, name: str, bases: Tuple[type], namespace: Dict[str, Any], **kwargs: Any) -> Type['BaseXmlModel']: if mcls.__is_base_model_defined__: mcls._merge_configs(bases, namespace) cls = super().__new__(mcls, name, bases, namespace, **kwargs) if mcls.__is_base_model_defined__: cls.__init_serializer__() else: mcls.__is_base_model_defined__ = True return cls @classmethod def _merge_configs(mcls, bases: Tuple[type], namespace: Dict[str, Any]) -> None: xml_encoders: Dict[Type[Any], Callable[[Any], Any]] = {} for base in reversed(bases): if issubclass(base, BaseXmlModel) and base != BaseXmlModel: xml_encoders.update(getattr(base.__config__, 'xml_encoders', {})) if self_config := namespace.get('Config'): xml_encoders.update(getattr(self_config, 'xml_encoders', {})) setattr(self_config, 'xml_encoders', xml_encoders) ModelT = TypeVar('ModelT', bound='BaseXmlModel')
[docs]class BaseXmlModel(pd.BaseModel, metaclass=XmlModelMeta): """ Base pydantic-xml model. """ __xml_tag__: ClassVar[Optional[str]] __xml_ns__: ClassVar[Optional[str]] __xml_nsmap__: ClassVar[Optional[NsMap]] __xml_ns_attrs__: ClassVar[bool] __xml_search_mode__: ClassVar[SearchMode] __xml_encoder__: ClassVar[serializers.XmlEncoder] __xml_serializer__: ClassVar[Optional[ModelSerializerFactory.RootSerializer]] = None def __init_subclass__( cls, *args: Any, tag: Optional[str] = None, ns: Optional[str] = None, nsmap: Optional[NsMap] = None, ns_attrs: Optional[bool] = None, search_mode: Optional[SearchMode] = None, **kwargs: Any, ): """ Initializes a subclass. :param tag: element tag :param ns: element namespace :param nsmap: element namespace map :param ns_attrs: use namespaced attributes :param search_mode: element search mode """ super().__init_subclass__(*args, **kwargs) cls.__xml_tag__ = tag if tag is not None else getattr(cls, '__xml_tag__', None) cls.__xml_ns__ = ns if ns is not None else getattr(cls, '__xml_ns__', None) cls.__xml_nsmap__ = nsmap if nsmap is not None else getattr(cls, '__xml_nsmap__', None) cls.__xml_ns_attrs__ = ns_attrs if ns_attrs is not None else getattr(cls, '__xml_ns_attrs__', False) cls.__xml_search_mode__ = search_mode if search_mode is not None \ else getattr(cls, '__xml_search_mode__', SearchMode.STRICT) default_xml_encoder: Callable[[Any], Any] if xml_encoders := getattr(cls.Config, 'xml_encoders', None): default_xml_encoder = ft.partial(pd.json.custom_pydantic_encoder, xml_encoders) else: default_xml_encoder = pd.json.pydantic_encoder cls.__xml_encoder__ = serializers.XmlEncoder(default=default_xml_encoder) @classmethod def __init_serializer__(cls) -> None: if config.REGISTER_NS_PREFIXES and cls.__xml_nsmap__: register_nsmap(cls.__xml_nsmap__) cls.__xml_serializer__ = ModelSerializerFactory.build_root(cls)
[docs] @classmethod def update_forward_refs(cls, **kwargs: Any) -> None: super().update_forward_refs(**kwargs) if cls.__xml_serializer__ is not None: cls.__xml_serializer__.resolve_forward_refs()
[docs] @classmethod def from_xml_tree(cls: Type[ModelT], root: etree.Element) -> ModelT: """ Deserializes an xml element tree to an object of `cls` type. :param root: xml element to deserialize the object from :return: deserialized object """ assert cls.__xml_serializer__ is not None, f"model {cls.__name__} is partially initialized" if root.tag == cls.__xml_serializer__.element_name: obj = typing.cast(ModelT, cls.__xml_serializer__.deserialize(XmlElement.from_native(root))) return obj else: raise errors.ParsingError( f"root element not found (actual: {root.tag}, expected: {cls.__xml_serializer__.element_name})", )
[docs] @classmethod def from_xml(cls: Type[ModelT], source: Union[str, bytes]) -> ModelT: """ Deserializes an xml string to an object of `cls` type. :param source: xml string :return: deserialized object """ return cls.from_xml_tree(etree.fromstring(source))
[docs] def to_xml_tree( self, *, encoder: Optional[serializers.XmlEncoder] = None, skip_empty: bool = False, ) -> etree.Element: """ Serializes the object to an xml tree. :param encoder: xml type encoder :param skip_empty: skip empty elements (elements without sub-elements, attributes and text, Nones) :return: object xml representation """ encoder = encoder or self.__xml_encoder__ assert self.__xml_serializer__ is not None, f"model {type(self).__name__} is partially initialized" root = XmlElement(tag=self.__xml_serializer__.element_name, nsmap=self.__xml_serializer__.nsmap) self.__xml_serializer__.serialize(root, self, encoder=encoder, skip_empty=skip_empty) return root.to_native()
[docs] def to_xml( self, *, encoder: Optional[serializers.XmlEncoder] = None, skip_empty: bool = False, **kwargs: Any, ) -> Union[str, bytes]: """ Serializes the object to an xml string. :param encoder: xml type encoder :param skip_empty: skip empty elements (elements without sub-elements, attributes and text, Nones) :param kwargs: additional xml serialization arguments :return: object xml representation """ return etree.tostring(self.to_xml_tree(encoder=encoder, skip_empty=skip_empty), **kwargs)
GenericModelT = TypeVar('GenericModelT', bound='BaseGenericXmlModel')
[docs]class BaseGenericXmlModel(BaseXmlModel, pd.generics.GenericModel): """ Base pydantic-xml generic model. """ def __class_getitem__(cls, params: Union[Type[Any], Tuple[Type[Any], ...]]) -> Type[Any]: model = super().__class_getitem__(params) model.__xml_tag__ = cls.__xml_tag__ model.__xml_ns__ = cls.__xml_ns__ model.__xml_nsmap__ = cls.__xml_nsmap__ model.__xml_ns_attrs__ = cls.__xml_ns_attrs__ model.__xml_search_mode__ = cls.__xml_search_mode__ model.__init_serializer__() return model @classmethod def __init_serializer__(cls) -> None: # checks that the model is not generic if not getattr(cls, '__concrete__', True): cls.__xml_serializer__ = None else: super().__init_serializer__()
[docs] @classmethod def from_xml_tree(cls: Type[GenericModelT], root: etree.Element) -> GenericModelT: """ Deserializes an xml element tree to an object of `cls` type. :param root: xml element to deserialize the object from :return: deserialized object """ if cls.__xml_serializer__ is None: raise errors.ModelError(f"{cls.__name__} model is generic") return super().from_xml_tree(root)