Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev #236

Merged
merged 7 commits into from
Nov 28, 2024
Merged

Dev #236

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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