Skip to content

Commit 7cf0195

Browse files
authoredNov 28, 2024
Merge pull request #236 from dapper91/dev
- pydantic 2.10 mypy plugin compatibility fixed. See #232 - recursive model bug fixed. See #227.
2 parents 203d9aa + bb1b697 commit 7cf0195

File tree

7 files changed

+62
-41
lines changed

7 files changed

+62
-41
lines changed
 

‎CHANGELOG.rst

+8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
Changelog
22
=========
33

4+
2.14.1 (2024-11-28)
5+
-------------------
6+
7+
- pydantic 2.10 mypy plugin compatibility fixed. See https://github.com/dapper91/pydantic-xml/issues/232
8+
- recursive model bug fixed. See https://github.com/dapper91/pydantic-xml/issues/227.
9+
10+
411
2.14.0 (2024-11-09)
512
-------------------
13+
614
- union validation error location fixed.
715
- potential memory leak fixed. See https://github.com/dapper91/pydantic-xml/issues/222.
816
- python 3.13 support added.

‎examples/snippets/element_namespace.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ class Company(BaseXmlModel, tag='company'):
1111
ns='co',
1212
nsmap={'co': 'http://www.company.com/co'},
1313
)
14-
website: HttpUrl = element(tag='web-size')
14+
website: HttpUrl = element(tag='web-site')
1515
# [model-end]
1616

1717

1818
# [xml-start]
1919
xml_doc = '''
2020
<company>
2121
<co:founded xmlns:co="http://www.company.com/co">2002-03-14</co:founded>
22-
<web-size>https://www.spacex.com</web-size>
22+
<web-site>https://www.spacex.com</web-site>
2323
</company>
2424
''' # [xml-end]
2525

‎examples/snippets/element_namespace_global.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ class Company(
1313
nsmap={'co': 'http://www.company.com/co'},
1414
):
1515
founded: dt.date = element()
16-
website: HttpUrl = element(tag='web-size', ns='co')
16+
website: HttpUrl = element(tag='web-site', ns='co')
1717
# [model-end]
1818

1919

2020
# [xml-start]
2121
xml_doc = '''
2222
<co:company xmlns:co="http://www.company.com/co">
2323
<co:founded>2002-03-14</co:founded>
24-
<co:web-size>https://www.spacex.com</co:web-size>
24+
<co:web-site>https://www.spacex.com</co:web-site>
2525
</co:company>
2626
''' # [xml-end]
2727

‎examples/snippets/element_primitive.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88
# [model-start]
99
class Company(BaseXmlModel, tag='company'):
1010
founded: dt.date = element()
11-
website: HttpUrl = element(tag='web-size')
11+
website: HttpUrl = element(tag='web-site')
1212
# [model-end]
1313

1414

1515
# [xml-start]
1616
xml_doc = '''
1717
<company>
1818
<founded>2002-03-14</founded>
19-
<web-size>https://www.spacex.com</web-size>
19+
<web-site>https://www.spacex.com</web-site>
2020
</company>
2121
''' # [xml-end]
2222

‎pydantic_xml/model.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,12 @@ def __init_subclass__(
429429

430430
cls.__xml_field_serializers__ = {}
431431
cls.__xml_field_validators__ = {}
432-
for attr_name in dir(cls):
432+
433+
# find custom validators/serializers in all defined attributes
434+
# though we want to skip any Base(Xml)Model attributes, as these can never be field
435+
# serializers/validators, and getting certain pydantic fields, like __pydantic_post_init__
436+
# may cause recursion errors for recursive / self-referential models
437+
for attr_name in set(dir(cls)) - set(dir(BaseXmlModel)):
433438
if func := getattr(cls, attr_name, None):
434439
if fields := getattr(func, '__xml_field_serializer__', None):
435440
for field in fields:

‎pydantic_xml/mypy.py

+41-33
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Callable, Optional, Tuple, Union
22

33
from mypy import nodes
4-
from mypy.plugin import ClassDefContext, FunctionContext, Plugin, Type
4+
from mypy.plugin import ClassDefContext, Plugin
55
from pydantic.mypy import PydanticModelTransformer, PydanticPlugin
66

77
MODEL_METACLASS_FULLNAME = 'pydantic_xml.model.XmlModelMeta'
@@ -21,38 +21,6 @@ def get_metaclass_hook(self, fullname: str) -> Optional[Callable[[ClassDefContex
2121
return self._pydantic_model_metaclass_marker_callback
2222
return super().get_metaclass_hook(fullname)
2323

24-
def get_function_hook(self, fullname: str) -> Optional[Callable[[FunctionContext], Type]]:
25-
sym = self.lookup_fully_qualified(fullname)
26-
if sym and sym.fullname == ATTR_FULLNAME:
27-
return self._attribute_callback
28-
elif sym and sym.fullname == ELEMENT_FULLNAME:
29-
return self._element_callback
30-
elif sym and sym.fullname == WRAPPED_FULLNAME:
31-
return self._wrapped_callback
32-
33-
return super().get_function_hook(fullname)
34-
35-
def _attribute_callback(self, ctx: FunctionContext) -> Type:
36-
return super()._pydantic_field_callback(self._pop_first_args(ctx, 2))
37-
38-
def _element_callback(self, ctx: FunctionContext) -> Type:
39-
return super()._pydantic_field_callback(self._pop_first_args(ctx, 4))
40-
41-
def _wrapped_callback(self, ctx: FunctionContext) -> Type:
42-
return super()._pydantic_field_callback(self._pop_first_args(ctx, 4))
43-
44-
def _pop_first_args(self, ctx: FunctionContext, num: int) -> FunctionContext:
45-
return FunctionContext(
46-
arg_types=ctx.arg_types[num:],
47-
arg_kinds=ctx.arg_kinds[num:],
48-
callee_arg_names=ctx.callee_arg_names[num:],
49-
arg_names=ctx.arg_names[num:],
50-
default_return_type=ctx.default_return_type,
51-
args=ctx.args[num:],
52-
context=ctx.context,
53-
api=ctx.api,
54-
)
55-
5624
def _pydantic_model_class_maker_callback(self, ctx: ClassDefContext) -> bool:
5725
transformer = PydanticXmlModelTransformer(ctx.cls, ctx.reason, ctx.api, self.plugin_config)
5826
return transformer.transform()
@@ -100,3 +68,43 @@ def get_alias_info(stmt: nodes.AssignmentStmt) -> Tuple[Union[str, None], bool]:
10068
return None, True
10169

10270
return PydanticModelTransformer.get_alias_info(stmt)
71+
72+
@staticmethod
73+
def get_strict(stmt: nodes.AssignmentStmt) -> Optional[bool]:
74+
expr = stmt.rvalue
75+
if (
76+
isinstance(expr, nodes.CallExpr) and
77+
isinstance(expr.callee, nodes.RefExpr) and
78+
expr.callee.fullname in ENTITIES_FULLNAME
79+
):
80+
for arg, name in zip(expr.args, expr.arg_names):
81+
if name != 'strict':
82+
continue
83+
if isinstance(arg, nodes.NameExpr):
84+
if arg.fullname == 'builtins.True':
85+
return True
86+
elif arg.fullname == 'builtins.False':
87+
return False
88+
return None
89+
90+
return PydanticModelTransformer.get_strict(stmt)
91+
92+
@staticmethod
93+
def is_field_frozen(stmt: nodes.AssignmentStmt) -> bool:
94+
expr = stmt.rvalue
95+
if isinstance(expr, nodes.TempNode):
96+
return False
97+
98+
if not (
99+
isinstance(expr, nodes.CallExpr) and
100+
isinstance(expr.callee, nodes.RefExpr) and
101+
expr.callee.fullname in ENTITIES_FULLNAME
102+
):
103+
return False
104+
105+
for i, arg_name in enumerate(expr.arg_names):
106+
if arg_name == 'frozen':
107+
arg = expr.args[i]
108+
return isinstance(arg, nodes.NameExpr) and arg.fullname == 'builtins.True'
109+
110+
return PydanticModelTransformer.is_field_frozen(stmt)

‎pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pydantic-xml"
3-
version = "2.14.0"
3+
version = "2.14.1"
44
description = "pydantic xml extension"
55
authors = ["Dmitry Pershin <dapper1291@gmail.com>"]
66
license = "Unlicense"

0 commit comments

Comments
 (0)