import dataclasses as dc
import typing
from typing import Any, Callable, ClassVar, Dict, Generic, Optional, Tuple, Type, TypeVar, Union
import pydantic as pd
import pydantic_core as pdc
from pydantic import BaseModel, RootModel
from pydantic._internal._model_construction import ModelMetaclass # noqa
from . import config, errors, utils
from .element import SearchMode
from .element.native import XmlElement, etree
from .serializers.factories.model import BaseModelSerializer
from .serializers.serializer import Serializer, XmlEntityInfoP
from .typedefs import EntityLocation
from .utils import NsMap
__all__ = (
'attr',
'element',
'wrapped',
'computed_attr',
'computed_element',
'BaseXmlModel',
'RootXmlModel',
)
@dc.dataclass
class ComputedXmlEntityInfo(pd.fields.ComputedFieldInfo):
"""
Computed field xml meta-information.
"""
__slots__ = ('location', 'path', 'ns', 'nsmap', 'wrapped')
location: Optional[EntityLocation]
path: Optional[str]
ns: Optional[str]
nsmap: Optional[NsMap]
wrapped: Optional[XmlEntityInfoP] # to be compliant with XmlEntityInfoP protocol
def __post_init__(self) -> None:
if config.REGISTER_NS_PREFIXES and self.nsmap:
utils.register_nsmap(self.nsmap)
PropertyT = typing.TypeVar('PropertyT')
def computed_entity(
location: EntityLocation,
prop: Optional[PropertyT] = None,
**kwargs: Any,
) -> Union[PropertyT, Callable[[PropertyT], PropertyT]]:
def decorator(prop: Any) -> Any:
path = kwargs.pop('path', None)
ns = kwargs.pop('ns', None)
nsmap = kwargs.pop('nsmap', None)
descriptor_proxy = pd.computed_field(**kwargs)(prop)
descriptor_proxy.decorator_info = ComputedXmlEntityInfo(
location=location,
path=path,
ns=ns,
nsmap=nsmap,
wrapped=None,
**dc.asdict(descriptor_proxy.decorator_info),
)
return descriptor_proxy
if prop is None:
return decorator
else:
return decorator(prop)
[docs]def computed_attr(
prop: Optional[PropertyT] = None,
*,
name: Optional[str] = None,
ns: Optional[str] = None,
**kwargs: Any,
) -> Union[PropertyT, Callable[[PropertyT], PropertyT]]:
"""
Marks a property as an xml attribute.
:param prop: decorated property
:param name: attribute name
:param ns: attribute xml namespace
:param kwargs: pydantic computed field arguments. See :py:class:`pydantic.computed_field`
"""
return computed_entity(EntityLocation.ATTRIBUTE, prop, path=name, ns=ns, **kwargs)
[docs]def computed_element(
prop: Optional[PropertyT] = None,
*,
tag: Optional[str] = None,
ns: Optional[str] = None,
nsmap: Optional[NsMap] = None,
**kwargs: Any,
) -> Union[PropertyT, Callable[[PropertyT], PropertyT]]:
"""
Marks a property as an xml element.
:param prop: decorated property
:param tag: element tag
:param ns: element xml namespace
:param nsmap: element xml namespace map
:param kwargs: pydantic computed field arguments. See :py:class:`pydantic.computed_field`
"""
return computed_entity(EntityLocation.ELEMENT, prop, path=tag, ns=ns, nsmap=nsmap, **kwargs)
class XmlEntityInfo(pd.fields.FieldInfo):
"""
Field xml meta-information.
"""
__slots__ = ('location', 'path', 'ns', 'nsmap', 'wrapped')
def __init__(
self,
location: Optional[EntityLocation],
/,
path: Optional[str] = None,
ns: Optional[str] = None,
nsmap: Optional[NsMap] = None,
wrapped: Optional[pd.fields.FieldInfo] = None,
**kwargs: Any,
):
if wrapped 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(wrapped):
kwargs[entity_field_name] = getattr(wrapped, entity_field_name)
if kwargs.get('serialization_alias') is None:
kwargs['serialization_alias'] = kwargs.get('alias')
if kwargs.get('validation_alias') is None:
kwargs['validation_alias'] = kwargs.get('alias')
super().__init__(**kwargs)
self.location = location
self.path = path
self.ns = ns
self.nsmap = nsmap
self.wrapped: Optional[XmlEntityInfoP] = wrapped if isinstance(wrapped, XmlEntityInfo) else None
if config.REGISTER_NS_PREFIXES and nsmap:
utils.register_nsmap(nsmap)
[docs]def attr(name: Optional[str] = None, ns: Optional[str] = None, **kwargs: Any) -> Any:
"""
Marks a pydantic field as an xml attribute.
:param name: attribute name
:param ns: attribute xml namespace
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
"""
return XmlEntityInfo(EntityLocation.ATTRIBUTE, path=name, ns=ns, **kwargs)
[docs]def element(tag: Optional[str] = None, ns: Optional[str] = None, nsmap: Optional[NsMap] = None, **kwargs: Any) -> Any:
"""
Marks a pydantic field as an xml element.
:param tag: element tag
:param ns: element xml namespace
:param nsmap: element xml namespace map
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
"""
return XmlEntityInfo(EntityLocation.ELEMENT, path=tag, ns=ns, nsmap=nsmap, **kwargs)
[docs]def wrapped(
path: str,
entity: Optional[pd.fields.FieldInfo] = None,
ns: Optional[str] = None,
nsmap: Optional[NsMap] = None,
**kwargs: Any,
) -> Any:
"""
Marks a pydantic field as a wrapped xml entity.
:param entity: wrapped entity
:param path: entity path
:param ns: element xml namespace
:param nsmap: element xml namespace map
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
"""
return XmlEntityInfo(EntityLocation.WRAPPED, path=path, ns=ns, nsmap=nsmap, wrapped=entity, **kwargs)
class XmlModelMeta(ModelMetaclass):
"""
Xml model metaclass.
"""
def __new__(
mcls,
name: str,
bases: Tuple[type],
namespace: Dict[str, Any],
**kwargs: Any,
) -> Type['BaseXmlModel']:
is_abstract: bool = kwargs.pop('__xml_abstract__', False)
cls = typing.cast(Type['BaseXmlModel'], super().__new__(mcls, name, bases, namespace, **kwargs))
if not is_abstract:
cls.__build_serializer__()
return cls
ModelT = TypeVar('ModelT', bound='BaseXmlModel')
[docs]class BaseXmlModel(BaseModel, __xml_abstract__=True, 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_serializer__: ClassVar[Optional[BaseModelSerializer]] = None
def __init_subclass__(
cls,
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__(**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)
@classmethod
def __build_serializer__(cls) -> None:
if cls is BaseXmlModel:
return
# checks that all generic parameters are provided
if cls.__pydantic_root_model__:
if cls.__pydantic_generic_metadata__['parameters']:
if cls.model_fields.get('root') is None or isinstance(cls.model_fields['root'].annotation, TypeVar):
cls.__xml_serializer__ = None
return
else:
if cls.__pydantic_generic_metadata__['parameters']:
cls.__xml_serializer__ = None
return
if config.REGISTER_NS_PREFIXES and cls.__xml_nsmap__:
utils.register_nsmap(cls.__xml_nsmap__)
if cls.__pydantic_complete__: # checks that all forward refs are resolved
serializer = Serializer.parse_core_schema(
schema=cls.__pydantic_core_schema__,
ctx=Serializer.Context(
model_name=cls.__name__,
namespaced_attrs=cls.__xml_ns_attrs__,
search_mode=cls.__xml_search_mode__,
entity_info=XmlEntityInfo(
EntityLocation.ELEMENT,
path=cls.__xml_tag__,
ns=cls.__xml_ns__,
nsmap=cls.__xml_nsmap__,
),
),
)
assert isinstance(serializer, BaseModelSerializer), "unexpected serializer type"
cls.__xml_serializer__ = serializer
else:
cls.__xml_serializer__ = None
[docs] @classmethod
def model_rebuild(cls, **kwargs: Any) -> None:
super().model_rebuild(**kwargs)
if cls.__xml_serializer__ is None and cls.__pydantic_complete__:
cls.__build_serializer__()
[docs] @classmethod
def from_xml_tree(cls: Type[ModelT], root: etree.Element, context: Optional[Dict[str, Any]] = None) -> ModelT:
"""
Deserializes an xml element tree to an object of `cls` type.
:param root: xml element to deserialize the object from
:param context: pydantic validation context
: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), context=context))
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], context: Optional[Dict[str, Any]] = None) -> ModelT:
"""
Deserializes an xml string to an object of `cls` type.
:param source: xml string
:param context: pydantic validation context
:return: deserialized object
"""
return cls.from_xml_tree(etree.fromstring(source), context=context)
[docs] def to_xml_tree(self, *, skip_empty: bool = False) -> etree.Element:
"""
Serializes the object to an xml tree.
:param skip_empty: skip empty elements (elements without sub-elements, attributes and text, Nones)
:return: object xml representation
"""
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, pdc.to_jsonable_python(self, by_alias=False), skip_empty=skip_empty,
)
return root.to_native()
[docs] def to_xml(self, *, skip_empty: bool = False, **kwargs: Any) -> Union[str, bytes]:
"""
Serializes the object to an xml string.
: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(skip_empty=skip_empty), **kwargs)
RootModelRootType = TypeVar('RootModelRootType')
[docs]class RootXmlModel(
RootModel[RootModelRootType],
BaseXmlModel,
Generic[RootModelRootType],
__xml_abstract__=True,
):
"""
Base pydantic-xml root model.
"""
@classmethod
def __build_serializer__(cls) -> None:
if cls is RootXmlModel:
return
super().__build_serializer__()