import dataclasses as dc
import typing
from typing import Any, Callable, Optional, Type, TypeVar, Union
import pydantic as pd
import pydantic_core as pdc
from pydantic._internal._model_construction import ModelMetaclass # noqa
from pydantic.root_model import _RootModelMetaclass as RootModelMetaclass # noqa
from . import config, model, utils
from .element import XmlElementReader, XmlElementWriter
from .typedefs import EntityLocation
from .utils import NsMap
__all__ = (
'attr',
'computed_attr',
'computed_element',
'computed_entity',
'element',
'wrapped',
'xml_field_serializer',
'xml_field_validator',
'ComputedXmlEntityInfo',
'SerializerFunc',
'ValidatorFunc',
'XmlEntityInfo',
'XmlEntityInfoP',
'XmlFieldSerializer',
'XmlFieldValidator',
)
class XmlEntityInfoP(typing.Protocol):
location: Optional[EntityLocation]
path: Optional[str]
ns: Optional[str]
nsmap: Optional[NsMap]
nillable: Optional[bool]
wrapped: Optional['XmlEntityInfoP']
class XmlEntityInfo(pd.fields.FieldInfo, XmlEntityInfoP):
"""
Field xml meta-information.
"""
__slots__ = ('location', 'path', 'ns', 'nsmap', 'nillable', 'wrapped')
@staticmethod
def merge_field_infos(*field_infos: pd.fields.FieldInfo, **overrides: Any) -> pd.fields.FieldInfo:
location, path, ns, nsmap, nillable, wrapped = None, None, None, None, None, None
for field_info in field_infos:
if isinstance(field_info, XmlEntityInfo):
location = field_info.location if field_info.location is not None else location
path = field_info.path if field_info.path is not None else path
ns = field_info.ns if field_info.ns is not None else ns
nsmap = field_info.nsmap if field_info.nsmap is not None else nsmap
nillable = field_info.nillable if field_info.nillable is not None else nillable
wrapped = field_info.wrapped if field_info.wrapped is not None else wrapped
field_info = pd.fields.FieldInfo.merge_field_infos(*field_infos, **overrides)
xml_entity_info = XmlEntityInfo(
location,
path=path,
ns=ns,
nsmap=nsmap,
nillable=nillable,
wrapped=wrapped if isinstance(wrapped, XmlEntityInfo) else None,
**field_info._attributes_set,
)
xml_entity_info.metadata = field_info.metadata
return xml_entity_info
def __init__(
self,
location: Optional[EntityLocation],
/,
path: Optional[str] = None,
ns: Optional[str] = None,
nsmap: Optional[NsMap] = None,
nillable: Optional[bool] = None,
wrapped: Optional[pd.fields.FieldInfo] = None,
**kwargs: Any,
):
wrapped_metadata: list[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):
if entity_field_name in pd.fields._FIELD_ARG_NAMES:
kwargs[entity_field_name] = getattr(wrapped, entity_field_name)
wrapped_metadata = wrapped.metadata
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.metadata.extend(wrapped_metadata)
self.location = location
self.path = path
self.ns = ns
self.nsmap = nsmap
self.nillable = nillable
self.wrapped: Optional[XmlEntityInfoP] = wrapped if isinstance(wrapped, XmlEntityInfo) else None
if config.REGISTER_NS_PREFIXES and nsmap:
utils.register_nsmap(nsmap)
_Unset: Any = pdc.PydanticUndefined
[docs]def attr(
name: Optional[str] = None,
ns: Optional[str] = None,
*,
default: Any = pdc.PydanticUndefined,
default_factory: Optional[Callable[[], Any]] = _Unset,
**kwargs: Any,
) -> Any:
"""
Marks a pydantic field as an xml attribute.
:param name: attribute name
:param ns: attribute xml namespace
:param default: the default value of the field.
:param default_factory: the factory function used to construct the default for the field.
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
"""
return XmlEntityInfo(
EntityLocation.ATTRIBUTE,
path=name, ns=ns, default=default, default_factory=default_factory,
**kwargs,
)
[docs]def element(
tag: Optional[str] = None,
ns: Optional[str] = None,
nsmap: Optional[NsMap] = None,
nillable: Optional[bool] = None,
*,
default: Any = pdc.PydanticUndefined,
default_factory: Optional[Callable[[], Any]] = _Unset,
**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 nillable: is element nillable. See https://www.w3.org/TR/xmlschema-1/#xsi_nil.
:param default: the default value of the field.
:param default_factory: the factory function used to construct the default for the field.
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
"""
return XmlEntityInfo(
EntityLocation.ELEMENT,
path=tag, ns=ns, nsmap=nsmap, nillable=nillable, default=default, default_factory=default_factory,
**kwargs,
)
[docs]def wrapped(
path: str,
entity: Optional[pd.fields.FieldInfo] = None,
ns: Optional[str] = None,
nsmap: Optional[NsMap] = None,
*,
default: Any = pdc.PydanticUndefined,
default_factory: Optional[Callable[[], Any]] = _Unset,
**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 default: the default value of the field.
:param default_factory: the factory function used to construct the default for the field.
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
"""
return XmlEntityInfo(
EntityLocation.WRAPPED,
path=path, ns=ns, nsmap=nsmap, wrapped=entity, default=default, default_factory=default_factory,
**kwargs,
)
@dc.dataclass
class ComputedXmlEntityInfo(pd.fields.ComputedFieldInfo, XmlEntityInfoP):
"""
Computed field xml meta-information.
"""
__slots__ = ('location', 'path', 'ns', 'nsmap', 'nillable', 'wrapped')
location: Optional[EntityLocation]
path: Optional[str]
ns: Optional[str]
nsmap: Optional[NsMap]
nillable: Optional[bool]
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)
nillable = kwargs.pop('nillable', None)
descriptor_proxy = pd.computed_field(**kwargs)(prop)
descriptor_proxy.decorator_info = ComputedXmlEntityInfo(
location=location,
path=path,
ns=ns,
nsmap=nsmap,
nillable=nillable,
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,
nillable: Optional[bool] = 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 nillable: is element nillable. See https://www.w3.org/TR/xmlschema-1/#xsi_nil.
:param kwargs: pydantic computed field arguments. See :py:class:`pydantic.computed_field`
"""
return computed_entity(EntityLocation.ELEMENT, prop, path=tag, ns=ns, nsmap=nsmap, nillable=nillable, **kwargs)
ValidatorFunc = Callable[[Type['model.BaseXmlModel'], XmlElementReader, str], Any]
ValidatorFuncT = TypeVar('ValidatorFuncT', bound=ValidatorFunc)
[docs]def xml_field_validator(field: str, /, *fields: str) -> Callable[[ValidatorFuncT], ValidatorFuncT]:
"""
Marks the method as a field xml validator.
:param field: field to be validated
:param fields: fields to be validated
"""
def wrapper(func: ValidatorFuncT) -> ValidatorFuncT:
setattr(func, '__xml_field_validator__', (field, *fields))
return func
return wrapper
SerializerFunc = Callable[['model.BaseXmlModel', XmlElementWriter, Any, str], Any]
SerializerFuncT = TypeVar('SerializerFuncT', bound=SerializerFunc)
[docs]def xml_field_serializer(field: str, /, *fields: str) -> Callable[[SerializerFuncT], SerializerFuncT]:
"""
Marks the method as a field xml serializer.
:param field: field to be serialized
:param fields: fields to be serialized
"""
def wrapper(func: SerializerFuncT) -> SerializerFuncT:
setattr(func, '__xml_field_serializer__', (field, *fields))
return func
return wrapper
[docs]@dc.dataclass(frozen=True)
class XmlFieldValidator:
func: ValidatorFunc
[docs]@dc.dataclass(frozen=True)
class XmlFieldSerializer:
func: SerializerFunc