Skip to content

Commit

Permalink
Merge pull request #236 from dapper91/dev
Browse files Browse the repository at this point in the history
- pydantic 2.10 mypy plugin compatibility fixed. See #232
- recursive model bug fixed. See #227.
  • Loading branch information
dapper91 authored Nov 28, 2024
2 parents 203d9aa + bb1b697 commit 7cf0195
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 41 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 2 additions & 2 deletions examples/snippets/element_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ 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]


# [xml-start]
xml_doc = '''
<company>
<co:founded xmlns:co="http://www.company.com/co">2002-03-14</co:founded>
<web-size>https://www.spacex.com</web-size>
<web-site>https://www.spacex.com</web-site>
</company>
''' # [xml-end]

Expand Down
4 changes: 2 additions & 2 deletions examples/snippets/element_namespace_global.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ 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]


# [xml-start]
xml_doc = '''
<co:company xmlns:co="http://www.company.com/co">
<co:founded>2002-03-14</co:founded>
<co:web-size>https://www.spacex.com</co:web-size>
<co:web-site>https://www.spacex.com</co:web-site>
</co:company>
''' # [xml-end]

Expand Down
4 changes: 2 additions & 2 deletions examples/snippets/element_primitive.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
# [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]


# [xml-start]
xml_doc = '''
<company>
<founded>2002-03-14</founded>
<web-size>https://www.spacex.com</web-size>
<web-site>https://www.spacex.com</web-site>
</company>
''' # [xml-end]

Expand Down
7 changes: 6 additions & 1 deletion pydantic_xml/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
74 changes: 41 additions & 33 deletions pydantic_xml/mypy.py
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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()
Expand Down Expand Up @@ -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)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pydantic-xml"
version = "2.14.0"
version = "2.14.1"
description = "pydantic xml extension"
authors = ["Dmitry Pershin <[email protected]>"]
license = "Unlicense"
Expand Down

0 comments on commit 7cf0195

Please sign in to comment.