Skip to content

Commit 5fb6756

Browse files
authored
Merge pull request #297 from dapper91/dev
- fix: pydantic 2.12 compatibility problems fixed.
2 parents 7c7590a + e89e011 commit 5fb6756

File tree

8 files changed

+125
-109
lines changed

8 files changed

+125
-109
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ Changelog
22
=========
33

44

5+
2.18.0 (2025-10-11)
6+
-------------------
7+
8+
- fix: pydantic 2.12 compatibility problems fixed.
9+
10+
511
2.17.3 (2025-07-13)
612
-------------------
713

814
- fix: xml_field_validator/serializer type annotations fixed. See https://github.com/dapper91/pydantic-xml/pull/277
915

1016

11-
1217
2.17.2 (2025-06-21)
1318
-------------------
1419

pydantic_xml/compat.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
pydantic compatibility module.
3+
"""
4+
5+
import pydantic as pd
6+
from pydantic._internal._model_construction import ModelMetaclass # noqa
7+
from pydantic.root_model import _RootModelMetaclass as RootModelMetaclass # noqa
8+
9+
PYDANTIC_VERSION = tuple(map(int, pd.__version__.partition('+')[0].split('.')))
10+
11+
12+
def merge_field_infos(*field_infos: pd.fields.FieldInfo) -> pd.fields.FieldInfo:
13+
if PYDANTIC_VERSION >= (2, 12, 0):
14+
return pd.fields.FieldInfo._construct(field_infos) # type: ignore[attr-defined]
15+
else:
16+
return pd.fields.FieldInfo.merge_field_infos(*field_infos)

pydantic_xml/fields.py

Lines changed: 90 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import dataclasses as dc
22
import typing
3-
from typing import Any, Callable, Optional, Union
3+
from typing import Any, Callable, Dict, Optional, Union
44

55
import pydantic as pd
66
import pydantic_core as pdc
7-
from pydantic._internal._model_construction import ModelMetaclass # noqa
8-
from pydantic.root_model import _RootModelMetaclass as RootModelMetaclass # noqa
97

10-
from . import config, model, utils
8+
from . import compat, config, model, utils
119
from .typedefs import EntityLocation
1210
from .utils import NsMap
1311

@@ -17,6 +15,7 @@
1715
'computed_element',
1816
'computed_entity',
1917
'element',
18+
'extract_field_xml_entity_info',
2019
'wrapped',
2120
'xml_field_serializer',
2221
'xml_field_validator',
@@ -37,83 +36,79 @@ class XmlEntityInfoP(typing.Protocol):
3736
wrapped: Optional['XmlEntityInfoP']
3837

3938

40-
class XmlEntityInfo(pd.fields.FieldInfo, XmlEntityInfoP):
39+
@dc.dataclass(frozen=True)
40+
class XmlEntityInfo(XmlEntityInfoP):
4141
"""
4242
Field xml meta-information.
4343
"""
4444

45-
__slots__ = ('location', 'path', 'ns', 'nsmap', 'nillable', 'wrapped')
45+
location: Optional[EntityLocation]
46+
path: Optional[str] = None
47+
ns: Optional[str] = None
48+
nsmap: Optional[NsMap] = None
49+
nillable: Optional[bool] = None
50+
wrapped: Optional[XmlEntityInfoP] = None
51+
52+
def __post_init__(self) -> None:
53+
if config.REGISTER_NS_PREFIXES and self.nsmap:
54+
utils.register_nsmap(self.nsmap)
4655

4756
@staticmethod
48-
def merge_field_infos(*field_infos: pd.fields.FieldInfo, **overrides: Any) -> pd.fields.FieldInfo:
49-
location, path, ns, nsmap, nillable, wrapped = None, None, None, None, None, None
50-
51-
for field_info in field_infos:
52-
if isinstance(field_info, XmlEntityInfo):
53-
location = field_info.location if field_info.location is not None else location
54-
path = field_info.path if field_info.path is not None else path
55-
ns = field_info.ns if field_info.ns is not None else ns
56-
nsmap = field_info.nsmap if field_info.nsmap is not None else nsmap
57-
nillable = field_info.nillable if field_info.nillable is not None else nillable
58-
wrapped = field_info.wrapped if field_info.wrapped is not None else wrapped
59-
60-
field_info = pd.fields.FieldInfo.merge_field_infos(*field_infos, **overrides)
61-
62-
xml_entity_info = XmlEntityInfo(
63-
location,
57+
def merge(*entity_infos: XmlEntityInfoP) -> 'XmlEntityInfo':
58+
location: Optional[EntityLocation] = None
59+
path: Optional[str] = None
60+
ns: Optional[str] = None
61+
nsmap: Optional[NsMap] = None
62+
nillable: Optional[bool] = None
63+
wrapped: Optional[XmlEntityInfoP] = None
64+
65+
for entity_info in entity_infos:
66+
if entity_info.location is not None:
67+
location = entity_info.location
68+
if entity_info.wrapped is not None:
69+
wrapped = entity_info.wrapped
70+
if entity_info.path is not None:
71+
path = entity_info.path
72+
if entity_info.ns is not None:
73+
ns = entity_info.ns
74+
if entity_info.nsmap is not None:
75+
nsmap = utils.merge_nsmaps(entity_info.nsmap, nsmap)
76+
if entity_info.nillable is not None:
77+
nillable = entity_info.nillable
78+
79+
return XmlEntityInfo(
80+
location=location,
6481
path=path,
6582
ns=ns,
6683
nsmap=nsmap,
6784
nillable=nillable,
68-
wrapped=wrapped if isinstance(wrapped, XmlEntityInfo) else None,
69-
**field_info._attributes_set,
85+
wrapped=wrapped,
7086
)
71-
xml_entity_info.metadata = field_info.metadata
72-
73-
return xml_entity_info
74-
75-
def __init__(
76-
self,
77-
location: Optional[EntityLocation],
78-
/,
79-
path: Optional[str] = None,
80-
ns: Optional[str] = None,
81-
nsmap: Optional[NsMap] = None,
82-
nillable: Optional[bool] = None,
83-
wrapped: Optional[pd.fields.FieldInfo] = None,
84-
**kwargs: Any,
85-
):
86-
wrapped_metadata: list[Any] = []
87-
if wrapped is not None:
88-
# copy arguments from the wrapped entity to let pydantic know how to process the field
89-
for entity_field_name in utils.get_slots(wrapped):
90-
if entity_field_name in pd.fields._FIELD_ARG_NAMES:
91-
kwargs[entity_field_name] = getattr(wrapped, entity_field_name)
92-
wrapped_metadata = wrapped.metadata
93-
94-
if kwargs.get('serialization_alias') is None:
95-
kwargs['serialization_alias'] = kwargs.get('alias')
96-
97-
if kwargs.get('validation_alias') is None:
98-
kwargs['validation_alias'] = kwargs.get('alias')
99-
100-
super().__init__(**kwargs)
101-
self.metadata.extend(wrapped_metadata)
102-
103-
self.location = location
104-
self.path = path
105-
self.ns = ns
106-
self.nsmap = nsmap
107-
self.nillable = nillable
108-
self.wrapped: Optional[XmlEntityInfoP] = wrapped if isinstance(wrapped, XmlEntityInfo) else None
109-
110-
if config.REGISTER_NS_PREFIXES and nsmap:
111-
utils.register_nsmap(nsmap)
87+
88+
89+
def extract_field_xml_entity_info(field_info: pd.fields.FieldInfo) -> Optional[XmlEntityInfoP]:
90+
entity_info_list = list(filter(lambda meta: isinstance(meta, XmlEntityInfo), field_info.metadata))
91+
if entity_info_list:
92+
entity_info = XmlEntityInfo.merge(*entity_info_list)
93+
else:
94+
entity_info = None
95+
96+
return entity_info
11297

11398

11499
_Unset: Any = pdc.PydanticUndefined
115100

116101

102+
def prepare_field_kwargs(kwargs: Dict[str, Any]) -> Dict[str, Any]:
103+
if kwargs.get('serialization_alias') in (None, pdc.PydanticUndefined):
104+
kwargs['serialization_alias'] = kwargs.get('alias')
105+
106+
if kwargs.get('validation_alias') in (None, pdc.PydanticUndefined):
107+
kwargs['validation_alias'] = kwargs.get('alias')
108+
109+
return kwargs
110+
111+
117112
def attr(
118113
name: Optional[str] = None,
119114
ns: Optional[str] = None,
@@ -132,12 +127,15 @@ def attr(
132127
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
133128
"""
134129

135-
return XmlEntityInfo(
136-
EntityLocation.ATTRIBUTE,
137-
path=name, ns=ns, default=default, default_factory=default_factory,
138-
**kwargs,
130+
kwargs = prepare_field_kwargs(kwargs)
131+
132+
field_info = pd.fields.FieldInfo(default=default, default_factory=default_factory, **kwargs)
133+
field_info.metadata.append(
134+
XmlEntityInfo(EntityLocation.ATTRIBUTE, path=name, ns=ns),
139135
)
140136

137+
return field_info
138+
141139

142140
def element(
143141
tag: Optional[str] = None,
@@ -161,12 +159,15 @@ def element(
161159
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
162160
"""
163161

164-
return XmlEntityInfo(
165-
EntityLocation.ELEMENT,
166-
path=tag, ns=ns, nsmap=nsmap, nillable=nillable, default=default, default_factory=default_factory,
167-
**kwargs,
162+
kwargs = prepare_field_kwargs(kwargs)
163+
164+
field_info = pd.fields.FieldInfo(default=default, default_factory=default_factory, **kwargs)
165+
field_info.metadata.append(
166+
XmlEntityInfo(EntityLocation.ELEMENT, path=tag, ns=ns, nsmap=nsmap, nillable=nillable),
168167
)
169168

169+
return field_info
170+
170171

171172
def wrapped(
172173
path: str,
@@ -190,12 +191,22 @@ def wrapped(
190191
:param kwargs: pydantic field arguments. See :py:class:`pydantic.Field`
191192
"""
192193

193-
return XmlEntityInfo(
194-
EntityLocation.WRAPPED,
195-
path=path, ns=ns, nsmap=nsmap, wrapped=entity, default=default, default_factory=default_factory,
196-
**kwargs,
194+
if entity is None:
195+
wrapped_entity_info = None
196+
field_info = pd.fields.FieldInfo(default=default, default_factory=default_factory, **kwargs)
197+
else:
198+
wrapped_entity_info = extract_field_xml_entity_info(entity)
199+
field_info = compat.merge_field_infos(
200+
pd.fields.FieldInfo(default=default, default_factory=default_factory, **kwargs),
201+
entity,
202+
)
203+
204+
field_info.metadata.append(
205+
XmlEntityInfo(EntityLocation.WRAPPED, path=path, ns=ns, nsmap=nsmap, wrapped=wrapped_entity_info),
197206
)
198207

208+
return field_info
209+
199210

200211
@dc.dataclass
201212
class ComputedXmlEntityInfo(pd.fields.ComputedFieldInfo, XmlEntityInfoP):
@@ -293,7 +304,7 @@ def computed_element(
293304

294305

295306
def xml_field_validator(
296-
field: str, /, *fields: str
307+
field: str, /, *fields: str,
297308
) -> 'Callable[[model.ValidatorFuncT[model.ModelT]], model.ValidatorFuncT[model.ModelT]]':
298309
"""
299310
Marks the method as a field xml validator.
@@ -312,7 +323,7 @@ def wrapper(func: model.ValidatorFuncT[model.ModelT]) -> model.ValidatorFuncT[mo
312323

313324

314325
def xml_field_serializer(
315-
field: str, /, *fields: str
326+
field: str, /, *fields: str,
316327
) -> 'Callable[[model.SerializerFuncT[model.ModelT]], model.SerializerFuncT[model.ModelT]]':
317328
"""
318329
Marks the method as a field xml serializer.

pydantic_xml/model.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
import pydantic_core as pdc
66
import typing_extensions as te
77
from pydantic import BaseModel, RootModel
8-
from pydantic._internal._model_construction import ModelMetaclass # noqa
9-
from pydantic.root_model import _RootModelMetaclass as RootModelMetaclass # noqa
108

119
from . import config, errors, utils
10+
from .compat import ModelMetaclass, RootModelMetaclass
1211
from .element import SearchMode, XmlElementReader, XmlElementWriter
1312
from .element.native import ElementT, XmlElement, etree
1413
from .fields import XmlEntityInfo, XmlFieldSerializer, XmlFieldValidator, attr, element, wrapped

pydantic_xml/serializers/factories/model.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import pydantic_xml as pxml
1010
from pydantic_xml import errors, utils
1111
from pydantic_xml.element import XmlElementReader, XmlElementWriter, is_element_nill, make_element_nill
12-
from pydantic_xml.fields import ComputedXmlEntityInfo, XmlEntityInfoP
12+
from pydantic_xml.fields import ComputedXmlEntityInfo, XmlEntityInfoP, extract_field_xml_entity_info
1313
from pydantic_xml.serializers.serializer import SearchMode, Serializer
1414
from pydantic_xml.typedefs import EntityLocation, Location, NsMap
1515
from pydantic_xml.utils import QName, merge_nsmaps, select_ns
@@ -79,15 +79,10 @@ def from_core_schema(cls, schema: pcs.ModelSchema, ctx: Serializer.Context) -> '
7979
fields_validation_aliases[field_name] = validation_alias
8080

8181
field_info = model_cls.model_fields[field_name]
82-
if isinstance(field_info, pxml.model.XmlEntityInfo):
83-
entity_info = field_info
84-
else:
85-
entity_info = None
86-
8782
field_ctx = ctx.child(
8883
field_name=field_name,
8984
field_alias=field_alias,
90-
entity_info=entity_info,
85+
entity_info=extract_field_xml_entity_info(field_info),
9186
)
9287
fields_serializers[field_name] = Serializer.parse_core_schema(model_field['schema'], field_ctx)
9388

@@ -234,16 +229,10 @@ def from_core_schema(cls, schema: pcs.ModelSchema, ctx: Serializer.Context) -> '
234229

235230
assert issubclass(model_cls, pxml.BaseXmlModel), "model class must be a BaseXmlModel subclass"
236231

237-
entity_info: Optional[XmlEntityInfoP]
238232
field_info = model_cls.model_fields['root']
239-
if isinstance(field_info, pxml.model.XmlEntityInfo):
240-
entity_info = field_info
241-
else:
242-
entity_info = None
243-
244233
field_ctx = ctx.child(
245234
field_name=None,
246-
entity_info=entity_info,
235+
entity_info=extract_field_xml_entity_info(field_info),
247236
)
248237
root_serializer = Serializer.parse_core_schema(root_schema, field_ctx)
249238

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pydantic-xml"
3-
version = "2.17.3"
3+
version = "2.18.0"
44
description = "pydantic xml extension"
55
authors = ["Dmitry Pershin <[email protected]>"]
66
license = "Unlicense"

tests/test_encoder.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,9 @@ def validate_model_before(cls, data: Dict[str, Any]) -> 'TestModel':
282282
}
283283

284284
@model_validator(mode='after')
285-
def validate_model_after(cls, obj: 'TestModel') -> 'TestModel':
286-
obj.field1 = obj.field1.replace(tzinfo=dt.timezone.utc)
287-
return obj
285+
def validate_model_after(self) -> 'TestModel':
286+
self.field1 = self.field1.replace(tzinfo=dt.timezone.utc)
287+
return self
288288

289289
@model_validator(mode='wrap')
290290
def validate_model_wrap(cls, obj: 'TestModel', handler: Callable) -> 'TestModel':

0 commit comments

Comments
 (0)