Skip to content
Merged

Dev #269

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ Changelog
=========


2.17.1 (2025-06-13)
-------------------

- fix: multiple field annotations bug fixed. See https://github.com/dapper91/pydantic-xml/pull/268.


2.17.0 (2025-05-18)
-------------------

Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Features
--------

- pydantic v1 / v2 support
- flexable attributes, elements and text binding
- flexible attributes, elements and text binding
- python collection types support (``Dict``, ``TypedDict``, ``List``, ``Set``, ``Tuple``, ...)
- ``Union`` type support
- pydantic `generic models <https://docs.pydantic.dev/latest/usage/models/#generic-models>`_ support
Expand Down
2 changes: 1 addition & 1 deletion docs/source/pages/data-binding/generics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Generic xml model can be declared the same way:


A generic model can be of one or more types and organized in a recursive structure.
The following example illustrate how to describes a flexable SOAP request model:
The following example illustrates how to describe a flexible SOAP request model:

*model.py:*

Expand Down
5 changes: 3 additions & 2 deletions pydantic_xml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

from . import config, errors, model
from .errors import ModelError, ParsingError
from .model import BaseXmlModel, RootXmlModel, XmlFieldSerializer, XmlFieldValidator, attr, computed_attr
from .model import computed_element, create_model, element, wrapped, xml_field_serializer, xml_field_validator
from .fields import XmlFieldSerializer, XmlFieldValidator, attr, computed_attr, computed_element, element, wrapped
from .fields import xml_field_serializer, xml_field_validator
from .model import BaseXmlModel, RootXmlModel, create_model

__all__ = (
'BaseXmlModel',
Expand Down
343 changes: 343 additions & 0 deletions pydantic_xml/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
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


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,
)


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,
)


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)


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)


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)


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)


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


@dc.dataclass(frozen=True)
class XmlFieldValidator:
func: ValidatorFunc


@dc.dataclass(frozen=True)
class XmlFieldSerializer:
func: SerializerFunc
Loading