diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index ffbb342..854aeeb 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,8 +1,16 @@
Changelog
=========
+2.14.1 (2024-11-28)
+-------------------
+
+- pydantic 2.10 mypy plugin compatibility fixed. See https://github.com/dapper91/pydantic-xml/issues/232
+- recursive model bug fixed. See https://github.com/dapper91/pydantic-xml/issues/227.
+
+
2.14.0 (2024-11-09)
-------------------
+
- union validation error location fixed.
- potential memory leak fixed. See https://github.com/dapper91/pydantic-xml/issues/222.
- python 3.13 support added.
diff --git a/examples/snippets/element_namespace.py b/examples/snippets/element_namespace.py
index 3dd7ed4..08def87 100644
--- a/examples/snippets/element_namespace.py
+++ b/examples/snippets/element_namespace.py
@@ -11,7 +11,7 @@ class Company(BaseXmlModel, tag='company'):
ns='co',
nsmap={'co': 'http://www.company.com/co'},
)
- website: HttpUrl = element(tag='web-size')
+ website: HttpUrl = element(tag='web-site')
# [model-end]
@@ -19,7 +19,7 @@ class Company(BaseXmlModel, tag='company'):
xml_doc = '''
2002-03-14
- https://www.spacex.com
+ https://www.spacex.com
''' # [xml-end]
diff --git a/examples/snippets/element_namespace_global.py b/examples/snippets/element_namespace_global.py
index 5239db3..03f605f 100644
--- a/examples/snippets/element_namespace_global.py
+++ b/examples/snippets/element_namespace_global.py
@@ -13,7 +13,7 @@ class Company(
nsmap={'co': 'http://www.company.com/co'},
):
founded: dt.date = element()
- website: HttpUrl = element(tag='web-size', ns='co')
+ website: HttpUrl = element(tag='web-site', ns='co')
# [model-end]
@@ -21,7 +21,7 @@ class Company(
xml_doc = '''
2002-03-14
- https://www.spacex.com
+ https://www.spacex.com
''' # [xml-end]
diff --git a/examples/snippets/element_primitive.py b/examples/snippets/element_primitive.py
index eba6701..bd978c2 100644
--- a/examples/snippets/element_primitive.py
+++ b/examples/snippets/element_primitive.py
@@ -8,7 +8,7 @@
# [model-start]
class Company(BaseXmlModel, tag='company'):
founded: dt.date = element()
- website: HttpUrl = element(tag='web-size')
+ website: HttpUrl = element(tag='web-site')
# [model-end]
@@ -16,7 +16,7 @@ class Company(BaseXmlModel, tag='company'):
xml_doc = '''
2002-03-14
- https://www.spacex.com
+ https://www.spacex.com
''' # [xml-end]
diff --git a/pydantic_xml/model.py b/pydantic_xml/model.py
index 34ee69f..8e72409 100644
--- a/pydantic_xml/model.py
+++ b/pydantic_xml/model.py
@@ -429,7 +429,12 @@ def __init_subclass__(
cls.__xml_field_serializers__ = {}
cls.__xml_field_validators__ = {}
- for attr_name in dir(cls):
+
+ # find custom validators/serializers in all defined attributes
+ # though we want to skip any Base(Xml)Model attributes, as these can never be field
+ # serializers/validators, and getting certain pydantic fields, like __pydantic_post_init__
+ # may cause recursion errors for recursive / self-referential models
+ for attr_name in set(dir(cls)) - set(dir(BaseXmlModel)):
if func := getattr(cls, attr_name, None):
if fields := getattr(func, '__xml_field_serializer__', None):
for field in fields:
diff --git a/pydantic_xml/mypy.py b/pydantic_xml/mypy.py
index d62e56a..5dde656 100644
--- a/pydantic_xml/mypy.py
+++ b/pydantic_xml/mypy.py
@@ -1,7 +1,7 @@
from typing import Callable, Optional, Tuple, Union
from mypy import nodes
-from mypy.plugin import ClassDefContext, FunctionContext, Plugin, Type
+from mypy.plugin import ClassDefContext, Plugin
from pydantic.mypy import PydanticModelTransformer, PydanticPlugin
MODEL_METACLASS_FULLNAME = 'pydantic_xml.model.XmlModelMeta'
@@ -21,38 +21,6 @@ def get_metaclass_hook(self, fullname: str) -> Optional[Callable[[ClassDefContex
return self._pydantic_model_metaclass_marker_callback
return super().get_metaclass_hook(fullname)
- def get_function_hook(self, fullname: str) -> Optional[Callable[[FunctionContext], Type]]:
- sym = self.lookup_fully_qualified(fullname)
- if sym and sym.fullname == ATTR_FULLNAME:
- return self._attribute_callback
- elif sym and sym.fullname == ELEMENT_FULLNAME:
- return self._element_callback
- elif sym and sym.fullname == WRAPPED_FULLNAME:
- return self._wrapped_callback
-
- return super().get_function_hook(fullname)
-
- def _attribute_callback(self, ctx: FunctionContext) -> Type:
- return super()._pydantic_field_callback(self._pop_first_args(ctx, 2))
-
- def _element_callback(self, ctx: FunctionContext) -> Type:
- return super()._pydantic_field_callback(self._pop_first_args(ctx, 4))
-
- def _wrapped_callback(self, ctx: FunctionContext) -> Type:
- return super()._pydantic_field_callback(self._pop_first_args(ctx, 4))
-
- def _pop_first_args(self, ctx: FunctionContext, num: int) -> FunctionContext:
- return FunctionContext(
- arg_types=ctx.arg_types[num:],
- arg_kinds=ctx.arg_kinds[num:],
- callee_arg_names=ctx.callee_arg_names[num:],
- arg_names=ctx.arg_names[num:],
- default_return_type=ctx.default_return_type,
- args=ctx.args[num:],
- context=ctx.context,
- api=ctx.api,
- )
-
def _pydantic_model_class_maker_callback(self, ctx: ClassDefContext) -> bool:
transformer = PydanticXmlModelTransformer(ctx.cls, ctx.reason, ctx.api, self.plugin_config)
return transformer.transform()
@@ -100,3 +68,43 @@ def get_alias_info(stmt: nodes.AssignmentStmt) -> Tuple[Union[str, None], bool]:
return None, True
return PydanticModelTransformer.get_alias_info(stmt)
+
+ @staticmethod
+ def get_strict(stmt: nodes.AssignmentStmt) -> Optional[bool]:
+ expr = stmt.rvalue
+ if (
+ isinstance(expr, nodes.CallExpr) and
+ isinstance(expr.callee, nodes.RefExpr) and
+ expr.callee.fullname in ENTITIES_FULLNAME
+ ):
+ for arg, name in zip(expr.args, expr.arg_names):
+ if name != 'strict':
+ continue
+ if isinstance(arg, nodes.NameExpr):
+ if arg.fullname == 'builtins.True':
+ return True
+ elif arg.fullname == 'builtins.False':
+ return False
+ return None
+
+ return PydanticModelTransformer.get_strict(stmt)
+
+ @staticmethod
+ def is_field_frozen(stmt: nodes.AssignmentStmt) -> bool:
+ expr = stmt.rvalue
+ if isinstance(expr, nodes.TempNode):
+ return False
+
+ if not (
+ isinstance(expr, nodes.CallExpr) and
+ isinstance(expr.callee, nodes.RefExpr) and
+ expr.callee.fullname in ENTITIES_FULLNAME
+ ):
+ return False
+
+ for i, arg_name in enumerate(expr.arg_names):
+ if arg_name == 'frozen':
+ arg = expr.args[i]
+ return isinstance(arg, nodes.NameExpr) and arg.fullname == 'builtins.True'
+
+ return PydanticModelTransformer.is_field_frozen(stmt)
diff --git a/pyproject.toml b/pyproject.toml
index 41dc73b..2109ed5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pydantic-xml"
-version = "2.14.0"
+version = "2.14.1"
description = "pydantic xml extension"
authors = ["Dmitry Pershin "]
license = "Unlicense"