Skip to content

Commit 9fb9ca5

Browse files
authored
Merge pull request #269 from dapper91/dev
- fix: multiple field annotations bug fixed.
2 parents 405b3bb + 8c219ca commit 9fb9ca5

File tree

12 files changed

+426
-305
lines changed

12 files changed

+426
-305
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ Changelog
22
=========
33

44

5+
2.17.1 (2025-06-13)
6+
-------------------
7+
8+
- fix: multiple field annotations bug fixed. See https://github.com/dapper91/pydantic-xml/pull/268.
9+
10+
511
2.17.0 (2025-05-18)
612
-------------------
713

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Features
3131
--------
3232

3333
- pydantic v1 / v2 support
34-
- flexable attributes, elements and text binding
34+
- flexible attributes, elements and text binding
3535
- python collection types support (``Dict``, ``TypedDict``, ``List``, ``Set``, ``Tuple``, ...)
3636
- ``Union`` type support
3737
- pydantic `generic models <https://docs.pydantic.dev/latest/usage/models/#generic-models>`_ support

docs/source/pages/data-binding/generics.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Generic xml model can be declared the same way:
5555

5656

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

6060
*model.py:*
6161

pydantic_xml/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44

55
from . import config, errors, model
66
from .errors import ModelError, ParsingError
7-
from .model import BaseXmlModel, RootXmlModel, XmlFieldSerializer, XmlFieldValidator, attr, computed_attr
8-
from .model import computed_element, create_model, element, wrapped, xml_field_serializer, xml_field_validator
7+
from .fields import XmlFieldSerializer, XmlFieldValidator, attr, computed_attr, computed_element, element, wrapped
8+
from .fields import xml_field_serializer, xml_field_validator
9+
from .model import BaseXmlModel, RootXmlModel, create_model
910

1011
__all__ = (
1112
'BaseXmlModel',

pydantic_xml/fields.py

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
import dataclasses as dc
2+
import typing
3+
from typing import Any, Callable, Optional, Type, TypeVar, Union
4+
5+
import pydantic as pd
6+
import pydantic_core as pdc
7+
from pydantic._internal._model_construction import ModelMetaclass # noqa
8+
from pydantic.root_model import _RootModelMetaclass as RootModelMetaclass # noqa
9+
10+
from . import config, model, utils
11+
from .element import XmlElementReader, XmlElementWriter
12+
from .typedefs import EntityLocation
13+
from .utils import NsMap
14+
15+
__all__ = (
16+
'attr',
17+
'computed_attr',
18+
'computed_element',
19+
'computed_entity',
20+
'element',
21+
'wrapped',
22+
'xml_field_serializer',
23+
'xml_field_validator',
24+
'ComputedXmlEntityInfo',
25+
'SerializerFunc',
26+
'ValidatorFunc',
27+
'XmlEntityInfo',
28+
'XmlEntityInfoP',
29+
'XmlFieldSerializer',
30+
'XmlFieldValidator',
31+
)
32+
33+
34+
class XmlEntityInfoP(typing.Protocol):
35+
location: Optional[EntityLocation]
36+
path: Optional[str]
37+
ns: Optional[str]
38+
nsmap: Optional[NsMap]
39+
nillable: Optional[bool]
40+
wrapped: Optional['XmlEntityInfoP']
41+
42+
43+
class XmlEntityInfo(pd.fields.FieldInfo, XmlEntityInfoP):
44+
"""
45+
Field xml meta-information.
46+
"""
47+
48+
__slots__ = ('location', 'path', 'ns', 'nsmap', 'nillable', 'wrapped')
49+
50+
@staticmethod
51+
def merge_field_infos(*field_infos: pd.fields.FieldInfo, **overrides: Any) -> pd.fields.FieldInfo:
52+
location, path, ns, nsmap, nillable, wrapped = None, None, None, None, None, None
53+
54+
for field_info in field_infos:
55+
if isinstance(field_info, XmlEntityInfo):
56+
location = field_info.location if field_info.location is not None else location
57+
path = field_info.path if field_info.path is not None else path
58+
ns = field_info.ns if field_info.ns is not None else ns
59+
nsmap = field_info.nsmap if field_info.nsmap is not None else nsmap
60+
nillable = field_info.nillable if field_info.nillable is not None else nillable
61+
wrapped = field_info.wrapped if field_info.wrapped is not None else wrapped
62+
63+
field_info = pd.fields.FieldInfo.merge_field_infos(*field_infos, **overrides)
64+
65+
xml_entity_info = XmlEntityInfo(
66+
location,
67+
path=path,
68+
ns=ns,
69+
nsmap=nsmap,
70+
nillable=nillable,
71+
wrapped=wrapped if isinstance(wrapped, XmlEntityInfo) else None,
72+
**field_info._attributes_set,
73+
)
74+
xml_entity_info.metadata = field_info.metadata
75+
76+
return xml_entity_info
77+
78+
def __init__(
79+
self,
80+
location: Optional[EntityLocation],
81+
/,
82+
path: Optional[str] = None,
83+
ns: Optional[str] = None,
84+
nsmap: Optional[NsMap] = None,
85+
nillable: Optional[bool] = None,
86+
wrapped: Optional[pd.fields.FieldInfo] = None,
87+
**kwargs: Any,
88+
):
89+
wrapped_metadata: list[Any] = []
90+
if wrapped is not None:
91+
# copy arguments from the wrapped entity to let pydantic know how to process the field
92+
for entity_field_name in utils.get_slots(wrapped):
93+
if entity_field_name in pd.fields._FIELD_ARG_NAMES:
94+
kwargs[entity_field_name] = getattr(wrapped, entity_field_name)
95+
wrapped_metadata = wrapped.metadata
96+
97+
if kwargs.get('serialization_alias') is None:
98+
kwargs['serialization_alias'] = kwargs.get('alias')
99+
100+
if kwargs.get('validation_alias') is None:
101+
kwargs['validation_alias'] = kwargs.get('alias')
102+
103+
super().__init__(**kwargs)
104+
self.metadata.extend(wrapped_metadata)
105+
106+
self.location = location
107+
self.path = path
108+
self.ns = ns
109+
self.nsmap = nsmap
110+
self.nillable = nillable
111+
self.wrapped: Optional[XmlEntityInfoP] = wrapped if isinstance(wrapped, XmlEntityInfo) else None
112+
113+
if config.REGISTER_NS_PREFIXES and nsmap:
114+
utils.register_nsmap(nsmap)
115+
116+
117+
_Unset: Any = pdc.PydanticUndefined
118+
119+
120+
def attr(
121+
name: Optional[str] = None,
122+
ns: Optional[str] = None,
123+
*,
124+
default: Any = pdc.PydanticUndefined,
125+
default_factory: Optional[Callable[[], Any]] = _Unset,
126+
**kwargs: Any,
127+
) -> Any:
128+
"""
129+
Marks a pydantic field as an xml attribute.
130+
131+
:param name: attribute name
132+
:param ns: attribute xml namespace
133+
:param default: the default value of the field.
134+
:param default_factory: the factory function used to construct the default for the field.
135+
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
136+
"""
137+
138+
return XmlEntityInfo(
139+
EntityLocation.ATTRIBUTE,
140+
path=name, ns=ns, default=default, default_factory=default_factory,
141+
**kwargs,
142+
)
143+
144+
145+
def element(
146+
tag: Optional[str] = None,
147+
ns: Optional[str] = None,
148+
nsmap: Optional[NsMap] = None,
149+
nillable: Optional[bool] = None,
150+
*,
151+
default: Any = pdc.PydanticUndefined,
152+
default_factory: Optional[Callable[[], Any]] = _Unset,
153+
**kwargs: Any,
154+
) -> Any:
155+
"""
156+
Marks a pydantic field as an xml element.
157+
158+
:param tag: element tag
159+
:param ns: element xml namespace
160+
:param nsmap: element xml namespace map
161+
:param nillable: is element nillable. See https://www.w3.org/TR/xmlschema-1/#xsi_nil.
162+
:param default: the default value of the field.
163+
:param default_factory: the factory function used to construct the default for the field.
164+
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
165+
"""
166+
167+
return XmlEntityInfo(
168+
EntityLocation.ELEMENT,
169+
path=tag, ns=ns, nsmap=nsmap, nillable=nillable, default=default, default_factory=default_factory,
170+
**kwargs,
171+
)
172+
173+
174+
def wrapped(
175+
path: str,
176+
entity: Optional[pd.fields.FieldInfo] = None,
177+
ns: Optional[str] = None,
178+
nsmap: Optional[NsMap] = None,
179+
*,
180+
default: Any = pdc.PydanticUndefined,
181+
default_factory: Optional[Callable[[], Any]] = _Unset,
182+
**kwargs: Any,
183+
) -> Any:
184+
"""
185+
Marks a pydantic field as a wrapped xml entity.
186+
187+
:param entity: wrapped entity
188+
:param path: entity path
189+
:param ns: element xml namespace
190+
:param nsmap: element xml namespace map
191+
:param default: the default value of the field.
192+
:param default_factory: the factory function used to construct the default for the field.
193+
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
194+
"""
195+
196+
return XmlEntityInfo(
197+
EntityLocation.WRAPPED,
198+
path=path, ns=ns, nsmap=nsmap, wrapped=entity, default=default, default_factory=default_factory,
199+
**kwargs,
200+
)
201+
202+
203+
@dc.dataclass
204+
class ComputedXmlEntityInfo(pd.fields.ComputedFieldInfo, XmlEntityInfoP):
205+
"""
206+
Computed field xml meta-information.
207+
"""
208+
209+
__slots__ = ('location', 'path', 'ns', 'nsmap', 'nillable', 'wrapped')
210+
211+
location: Optional[EntityLocation]
212+
path: Optional[str]
213+
ns: Optional[str]
214+
nsmap: Optional[NsMap]
215+
nillable: Optional[bool]
216+
wrapped: Optional[XmlEntityInfoP] # to be compliant with XmlEntityInfoP protocol
217+
218+
def __post_init__(self) -> None:
219+
if config.REGISTER_NS_PREFIXES and self.nsmap:
220+
utils.register_nsmap(self.nsmap)
221+
222+
223+
PropertyT = typing.TypeVar('PropertyT')
224+
225+
226+
def computed_entity(
227+
location: EntityLocation,
228+
prop: Optional[PropertyT] = None,
229+
**kwargs: Any,
230+
) -> Union[PropertyT, Callable[[PropertyT], PropertyT]]:
231+
def decorator(prop: Any) -> Any:
232+
path = kwargs.pop('path', None)
233+
ns = kwargs.pop('ns', None)
234+
nsmap = kwargs.pop('nsmap', None)
235+
nillable = kwargs.pop('nillable', None)
236+
237+
descriptor_proxy = pd.computed_field(**kwargs)(prop)
238+
descriptor_proxy.decorator_info = ComputedXmlEntityInfo(
239+
location=location,
240+
path=path,
241+
ns=ns,
242+
nsmap=nsmap,
243+
nillable=nillable,
244+
wrapped=None,
245+
**dc.asdict(descriptor_proxy.decorator_info),
246+
)
247+
248+
return descriptor_proxy
249+
250+
if prop is None:
251+
return decorator
252+
else:
253+
return decorator(prop)
254+
255+
256+
def computed_attr(
257+
prop: Optional[PropertyT] = None,
258+
*,
259+
name: Optional[str] = None,
260+
ns: Optional[str] = None,
261+
**kwargs: Any,
262+
) -> Union[PropertyT, Callable[[PropertyT], PropertyT]]:
263+
"""
264+
Marks a property as an xml attribute.
265+
266+
:param prop: decorated property
267+
:param name: attribute name
268+
:param ns: attribute xml namespace
269+
:param kwargs: pydantic computed field arguments. See :py:class:`pydantic.computed_field`
270+
"""
271+
272+
return computed_entity(EntityLocation.ATTRIBUTE, prop, path=name, ns=ns, **kwargs)
273+
274+
275+
def computed_element(
276+
prop: Optional[PropertyT] = None,
277+
*,
278+
tag: Optional[str] = None,
279+
ns: Optional[str] = None,
280+
nsmap: Optional[NsMap] = None,
281+
nillable: Optional[bool] = None,
282+
**kwargs: Any,
283+
) -> Union[PropertyT, Callable[[PropertyT], PropertyT]]:
284+
"""
285+
Marks a property as an xml element.
286+
287+
:param prop: decorated property
288+
:param tag: element tag
289+
:param ns: element xml namespace
290+
:param nsmap: element xml namespace map
291+
:param nillable: is element nillable. See https://www.w3.org/TR/xmlschema-1/#xsi_nil.
292+
:param kwargs: pydantic computed field arguments. See :py:class:`pydantic.computed_field`
293+
"""
294+
295+
return computed_entity(EntityLocation.ELEMENT, prop, path=tag, ns=ns, nsmap=nsmap, nillable=nillable, **kwargs)
296+
297+
298+
ValidatorFunc = Callable[[Type['model.BaseXmlModel'], XmlElementReader, str], Any]
299+
ValidatorFuncT = TypeVar('ValidatorFuncT', bound=ValidatorFunc)
300+
301+
302+
def xml_field_validator(field: str, /, *fields: str) -> Callable[[ValidatorFuncT], ValidatorFuncT]:
303+
"""
304+
Marks the method as a field xml validator.
305+
306+
:param field: field to be validated
307+
:param fields: fields to be validated
308+
"""
309+
310+
def wrapper(func: ValidatorFuncT) -> ValidatorFuncT:
311+
setattr(func, '__xml_field_validator__', (field, *fields))
312+
return func
313+
314+
return wrapper
315+
316+
317+
SerializerFunc = Callable[['model.BaseXmlModel', XmlElementWriter, Any, str], Any]
318+
SerializerFuncT = TypeVar('SerializerFuncT', bound=SerializerFunc)
319+
320+
321+
def xml_field_serializer(field: str, /, *fields: str) -> Callable[[SerializerFuncT], SerializerFuncT]:
322+
"""
323+
Marks the method as a field xml serializer.
324+
325+
:param field: field to be serialized
326+
:param fields: fields to be serialized
327+
"""
328+
329+
def wrapper(func: SerializerFuncT) -> SerializerFuncT:
330+
setattr(func, '__xml_field_serializer__', (field, *fields))
331+
return func
332+
333+
return wrapper
334+
335+
336+
@dc.dataclass(frozen=True)
337+
class XmlFieldValidator:
338+
func: ValidatorFunc
339+
340+
341+
@dc.dataclass(frozen=True)
342+
class XmlFieldSerializer:
343+
func: SerializerFunc

0 commit comments

Comments
 (0)