From feedda78aaefcc120b006291089e93ce1bb30531 Mon Sep 17 00:00:00 2001 From: Dmitry Pershin Date: Thu, 10 Nov 2022 22:45:28 +0500 Subject: [PATCH 1/3] recursive models support added. `inherit_ns` flag dropped due to recursive models implementation details. --- README.md | 120 +++++++++++++++++++-- examples/quickstart.py | 16 ++- examples/recursive.py | 33 ++++++ pydantic_xml/model.py | 8 +- pydantic_xml/serializers.py | 73 +++++++++---- tests/test_misc.py | 47 +++++++++ tests/test_namespaces.py | 202 +----------------------------------- tests/test_submodels.py | 3 + tests/test_wrapped.py | 3 +- 9 files changed, 266 insertions(+), 239 deletions(-) create mode 100644 examples/recursive.py diff --git a/README.md b/README.md index 3f3922a..a47305c 100644 --- a/README.md +++ b/README.md @@ -192,8 +192,14 @@ from pydantic import conint, HttpUrl from pydantic_xml import BaseXmlModel, attr, element, wrapped +NSMAP = { + 'co': 'http://www.test.com/contact', + 'hq': 'http://www.test.com/hq', + 'pd': 'http://www.test.com/prod', +} + -class Headquarters(BaseXmlModel, ns='hq', nsmap={'hq': 'http://www.test.com/hq'}): +class Headquarters(BaseXmlModel, ns='hq', nsmap=NSMAP): country: str = element() state: str = element() city: str = element() @@ -209,12 +215,12 @@ class Industries(BaseXmlModel): __root__: Set[str] = element(tag='Industry') -class Social(BaseXmlModel, ns_attrs=True, inherit_ns=True): +class Social(BaseXmlModel, ns_attrs=True, ns='co', nsmap=NSMAP): type: str = attr() url: str -class Product(BaseXmlModel, ns_attrs=True, inherit_ns=True): +class Product(BaseXmlModel, ns_attrs=True, ns='pd', nsmap=NSMAP): status: Literal['running', 'development'] = attr() launched: Optional[int] = attr() title: str @@ -236,7 +242,7 @@ class COO(Person): position: Literal['COO'] = attr() -class Company(BaseXmlModel, tag='Company', nsmap={'pd': 'http://www.test.com/prod'}): +class Company(BaseXmlModel, tag='Company', nsmap=NSMAP): class CompanyType(str, Enum): PRIVATE = 'Private' PUBLIC = 'Public' @@ -254,9 +260,9 @@ class Company(BaseXmlModel, tag='Company', nsmap={'pd': 'http://www.test.com/pro headquarters: Headquarters socials: List[Social] = wrapped( 'contacts/socials', - element(tag='social', default_factory=set), + element(tag='social', default_factory=list), ns='co', - nsmap={'co': 'http://www.test.com/contact'} + nsmap=NSMAP, ) products: Tuple[Product, ...] = element(tag='product', ns='pd') @@ -428,6 +434,108 @@ print(request.json(indent=4)) ``` +### Self-referencing models: + +`pydantic` library supports [self-referencing models](https://pydantic-docs.helpmanual.io/usage/postponed_annotations/#self-referencing-models). +`pydantic-xml` supports it either. + +*request.xml:* + +```xml + + + + + + + + + + + +``` + +*main.py:* + +```python +from typing import List, Optional + +import pydantic_xml as pxml + + +class File(pxml.BaseXmlModel, tag="File"): + name: str = pxml.attr(name='Name') + mode: str = pxml.attr(name='Mode') + + +class Directory(pxml.BaseXmlModel, tag="Directory"): + name: str = pxml.attr(name='Name') + mode: str = pxml.attr(name='Mode') + dirs: Optional[List['Directory']] = pxml.element(tag='Directory') + files: Optional[List[File]] = pxml.element(tag='File', default_factory=list) + + +with open('request.xml') as file: + xml = file.read() + +root = Directory.from_xml(xml) +print(root.json(indent=4)) + +``` + +*output:* + +```json +{ + "name": "root", + "mode": "rwxr-xr-x", + "dirs": [ + { + "name": "etc", + "mode": "rwxr-xr-x", + "dirs": [ + { + "name": "ssh", + "mode": "rwxr-xr-x", + "dirs": [], + "files": [] + } + ], + "files": [ + { + "name": "passwd", + "mode": "-rw-r--r--" + }, + { + "name": "hosts", + "mode": "-rw-r--r--" + } + ] + }, + { + "name": "bin", + "mode": "rwxr-xr-x", + "dirs": [], + "files": [] + }, + { + "name": "usr", + "mode": "rwxr-xr-x", + "dirs": [ + { + "name": "bin", + "mode": "rwxr-xr-x", + "dirs": [], + "files": [] + } + ], + "files": [] + } + ], + "files": [] +} +``` + ### JSON Since `pydantic` supports json serialization, `pydantic-xml` could be used as xml-to-json transcoder: diff --git a/examples/quickstart.py b/examples/quickstart.py index 7e2352d..2c66823 100644 --- a/examples/quickstart.py +++ b/examples/quickstart.py @@ -45,8 +45,14 @@ ''' +NSMAP = { + 'co': 'http://www.test.com/contact', + 'hq': 'http://www.test.com/hq', + 'pd': 'http://www.test.com/prod', +} -class Headquarters(BaseXmlModel, ns='hq', nsmap={'hq': 'http://www.test.com/hq'}): + +class Headquarters(BaseXmlModel, ns='hq', nsmap=NSMAP): country: str = element() state: str = element() city: str = element() @@ -62,12 +68,12 @@ class Industries(BaseXmlModel): __root__: Set[str] = element(tag='Industry') -class Social(BaseXmlModel, ns_attrs=True, inherit_ns=True): +class Social(BaseXmlModel, ns_attrs=True, ns='co', nsmap=NSMAP): type: str = attr() url: str -class Product(BaseXmlModel, ns_attrs=True, inherit_ns=True): +class Product(BaseXmlModel, ns_attrs=True, ns='pd', nsmap=NSMAP): status: Literal['running', 'development'] = attr() launched: Optional[int] = attr() title: str @@ -89,7 +95,7 @@ class COO(Person): position: Literal['COO'] = attr() -class Company(BaseXmlModel, tag='Company', nsmap={'pd': 'http://www.test.com/prod'}): +class Company(BaseXmlModel, tag='Company', nsmap=NSMAP): class CompanyType(str, Enum): PRIVATE = 'Private' PUBLIC = 'Public' @@ -109,7 +115,7 @@ class CompanyType(str, Enum): 'contacts/socials', element(tag='social', default_factory=list), ns='co', - nsmap={'co': 'http://www.test.com/contact'}, + nsmap=NSMAP, ) products: Tuple[Product, ...] = element(tag='product', ns='pd') diff --git a/examples/recursive.py b/examples/recursive.py new file mode 100644 index 0000000..d6f5a99 --- /dev/null +++ b/examples/recursive.py @@ -0,0 +1,33 @@ +from typing import List, Optional + +import pydantic_xml as pxml + +xml = ''' + + + + + + + + + + + +''' + + +class File(pxml.BaseXmlModel, tag="File"): + name: str = pxml.attr(name='Name') + mode: str = pxml.attr(name='Mode') + + +class Directory(pxml.BaseXmlModel, tag="Directory"): + name: str = pxml.attr(name='Name') + mode: str = pxml.attr(name='Mode') + dirs: Optional[List['Directory']] = pxml.element(tag='Directory') + files: Optional[List[File]] = pxml.element(tag='File', default_factory=list) + + +root = Directory.from_xml(xml) +print(root.json(indent=4)) diff --git a/pydantic_xml/model.py b/pydantic_xml/model.py index a63d2d6..b8c4253 100644 --- a/pydantic_xml/model.py +++ b/pydantic_xml/model.py @@ -172,7 +172,6 @@ class BaseXmlModel(pd.BaseModel, metaclass=XmlModelMeta): __xml_tag__: ClassVar[Optional[str]] __xml_ns__: ClassVar[Optional[str]] __xml_nsmap__: ClassVar[Optional[NsMap]] - __xml_inherit_ns__: ClassVar[bool] __xml_ns_attrs__: ClassVar[bool] __xml_serializer__: ClassVar[Optional[serializers.ModelSerializerFactory.RootSerializer]] @@ -182,7 +181,6 @@ def __init_subclass__( tag: Optional[str] = None, ns: Optional[str] = None, nsmap: Optional[NsMap] = None, - inherit_ns: bool = False, ns_attrs: bool = False, **kwargs: Any, ): @@ -192,7 +190,6 @@ def __init_subclass__( :param tag: element tag :param ns: element namespace :param nsmap: element namespace map - :param inherit_ns: if `True` and ns argument is not provided - inherits namespace from the outer model :param ns_attrs: use namespaced attributes """ @@ -201,7 +198,6 @@ def __init_subclass__( cls.__xml_tag__ = tag cls.__xml_ns__ = ns cls.__xml_nsmap__ = nsmap - cls.__xml_inherit_ns__ = inherit_ns cls.__xml_ns_attrs__ = ns_attrs @classmethod @@ -220,7 +216,7 @@ def from_xml_tree(cls, root: etree.Element) -> 'BaseXmlModel': :return: deserialized object """ - assert cls.__xml_serializer__ is not None + assert cls.__xml_serializer__ is not None, "model is partially initialized" obj = cls.__xml_serializer__.deserialize(root) return cls.parse_obj(obj) @@ -254,6 +250,7 @@ def to_xml_tree( assert self.__xml_serializer__ is not None root = self.__xml_serializer__.serialize(None, self, encoder=encoder, skip_empty=skip_empty) + assert root is not None if self.__xml_nsmap__ and (default_ns := self.__xml_nsmap__.get('')): root.set('xmlns', default_ns) @@ -289,7 +286,6 @@ def __class_getitem__(cls, params: Union[Type[Any], Tuple[Type[Any], ...]]) -> T model.__xml_tag__ = cls.__xml_tag__ model.__xml_ns__ = cls.__xml_ns__ model.__xml_nsmap__ = cls.__xml_nsmap__ - model.__xml_inherit_ns__ = cls.__xml_inherit_ns__ model.__xml_ns_attrs__ = cls.__xml_ns_attrs__ model.__init_serializer__() diff --git a/pydantic_xml/serializers.py b/pydantic_xml/serializers.py index 9b8316d..f873a8f 100644 --- a/pydantic_xml/serializers.py +++ b/pydantic_xml/serializers.py @@ -284,16 +284,14 @@ class ModelSerializerFactory: Model serializer factory. """ - class BaseSerializer(Serializer, abc.ABC): + class RootSerializer(Serializer): def __init__( self, - model_field: Optional[pd.fields.ModelField], model: Type['pxml.BaseXmlModel'], ctx: Serializer.Context, ): - field_name = model_field.name if model_field else None - name = ctx.entity_name or model.__xml_tag__ or field_name or model.__name__ - ns = ctx.entity_ns or model.__xml_ns__ or (ctx.parent_ns if model.__xml_inherit_ns__ else None) + name = ctx.entity_name or model.__xml_tag__ or model.__name__ + ns = ctx.entity_ns or model.__xml_ns__ nsmap = merge_nsmaps(ctx.entity_nsmap, model.__xml_nsmap__, ctx.parent_nsmap) is_root = model.__custom_root_type__ ctx = dc.replace( @@ -310,10 +308,12 @@ def __init__( for field_name, model_subfield in model.__fields__.items() } - class RootSerializer(BaseSerializer): def serialize( self, element: Optional[etree.Element], value: Any, *, encoder: XmlEncoder, skip_empty: bool = False, - ) -> etree.Element: + ) -> Optional[etree.Element]: + if value is None: + return None + if element is None: element = etree.Element(self.element_name) @@ -332,16 +332,33 @@ def deserialize(self, element: etree.Element) -> Any: else: return result - class ElementSerializer(BaseSerializer): + class ElementSerializer(Serializer): + + def __init__( + self, + model_field: Optional[pd.fields.ModelField], + model: Type['pxml.BaseXmlModel'], + ctx: Serializer.Context, + ): + field_name = model_field.name if model_field else None + name = ctx.entity_name or model.__xml_tag__ or field_name or model.__name__ + ns = ctx.entity_ns or model.__xml_ns__ + nsmap = merge_nsmaps(ctx.entity_nsmap, model.__xml_nsmap__, ctx.parent_nsmap) + + self.element_name = QName.from_alias(tag=name, ns=ns, nsmap=nsmap).uri + self.model = model + def serialize( self, element: etree.Element, value: Any, *, encoder: XmlEncoder, skip_empty: bool = False, ) -> Optional[etree.Element]: + assert self.model.__xml_serializer__ is not None, "model is partially initialized" + + if value is None: + return None + sub_element = etree.Element(self.element_name) - for field_name, field_serializer in self.field_serializers.items(): - field_serializer.serialize( - sub_element, getattr(value, field_name), encoder=encoder, skip_empty=skip_empty, - ) + self.model.__xml_serializer__.serialize(sub_element, value, encoder=encoder, skip_empty=skip_empty) if not skip_empty or sub_element.text or sub_element.attrib or len(sub_element) != 0: element.append(sub_element) @@ -350,21 +367,33 @@ def serialize( return None def deserialize(self, element: etree.Element) -> Optional[Dict[str, Any]]: + assert self.model.__xml_serializer__ is not None, "model is partially initialized" + if (sub_element := element.find(self.element_name)) is not None: - result = { - field_name: field_serializer.deserialize(sub_element) - for field_name, field_serializer in self.field_serializers.items() - } - if self.is_root: - return result['__root__'] - else: - return result + return self.model.__xml_serializer__.deserialize(sub_element) else: return None + class DeferredSerializer(Serializer): + + def __init__(self, model: Type['pxml.BaseXmlModel']): + self.model = model + + def serialize( + self, element: etree.Element, value: Any, *, encoder: XmlEncoder, skip_empty: bool = False, + ) -> Optional[etree.Element]: + assert self.model.__xml_serializer__ is not None, "model is partially initialized" + + return self.model.__xml_serializer__.serialize(element, value, encoder=encoder, skip_empty=skip_empty) + + def deserialize(self, element: etree.Element) -> Optional[Dict[str, Any]]: + assert self.model.__xml_serializer__ is not None, "model is partially initialized" + + return self.model.__xml_serializer__.deserialize(element) + @classmethod def from_model(cls, model: Type['pxml.BaseXmlModel']) -> 'RootSerializer': - return cls.RootSerializer(None, model, Serializer.Context(parent_is_root=True)) + return cls.RootSerializer(model, Serializer.Context(parent_is_root=True)) @classmethod def build( @@ -386,7 +415,7 @@ def build( elif not ctx.parent_is_root and field_location is Location.MISSING: return cls.ElementSerializer(model_field, sub_model, ctx) elif ctx.parent_is_root and field_location is Location.MISSING: - return cls.RootSerializer(model_field, sub_model, ctx) + return cls.DeferredSerializer(sub_model) elif field_location is Location.ATTRIBUTE: raise errors.ModelFieldError( model.__name__, model_field.name, "attributes of model type are not supported", diff --git a/tests/test_misc.py b/tests/test_misc.py index 91f82c1..82d0971 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -43,3 +43,50 @@ class TestModel(BaseXmlModel, tag='model'): actual_xml = obj.to_xml(skip_empty=True) assert_xml_equal(actual_xml, xml.encode()) + + +def test_recursive_models(): + class TestModel(BaseXmlModel, tag='model'): + attr1: int = attr() + element1: float = element() + + model1: Optional['TestModel'] = element(tag='model1') + models: Optional[List['TestModel']] = element(tag='item') + + xml = ''' + + 1.1 + + 2.2 + + + + 3.3 + + + 4.4 + + + ''' + + obj = TestModel( + attr1=1, + element1=1.1, + model1=TestModel( + attr1=2, + element1=2.2, + ), + models=[ + TestModel( + attr1=3, + element1=3.3, + ), + TestModel( + attr1=4, + element1=4.4, + ), + ], + ) + + actual_xml = obj.to_xml(skip_empty=True) + assert_xml_equal(actual_xml, xml.encode()) diff --git a/tests/test_namespaces.py b/tests/test_namespaces.py index b444666..85a6384 100644 --- a/tests/test_namespaces.py +++ b/tests/test_namespaces.py @@ -7,7 +7,7 @@ def test_default_namespaces(): - class TestSubMode2(BaseXmlModel, nsmap={'tst': 'http://test3.org'}): + class TestSubMode2(BaseXmlModel, nsmap={'': 'http://test2.org', 'tst': 'http://test3.org'}): attr1: int = attr() attr2: int = attr(ns='tst') element: str = element() @@ -117,202 +117,6 @@ class TestModel( assert_xml_equal(actual_xml, xml) -@pytest.mark.parametrize( - 'inherit_ns, model_ns, submodel_ns, element_ns, expected_model_ns, expected_submodel_ns', - [ - (True, 'tst1', 'tst2', 'tst3', 'tst1', 'tst3'), - ], -) -def test_namespace_inheritance( - inherit_ns, model_ns, submodel_ns, element_ns, expected_model_ns, expected_submodel_ns, -): - class TestSubModel(BaseXmlModel, ns=submodel_ns, inherit_ns=inherit_ns): - element: float = element() - - class TestModel( - BaseXmlModel, - tag='model1', - ns=model_ns, - nsmap={'tst1': 'http://test1.org', 'tst2': 'http://test2.org', 'tst3': 'http://test3.org'}, - ): - model: TestSubModel = element(tag='model2', ns=element_ns) - - xml = ''' - <{ns_pref1}model1 xmlns:tst1="http://test1.org" xmlns:tst2="http://test2.org" xmlns:tst3="http://test3.org"> - <{ns_pref2}model2> - <{ns_pref2}element>1.1 - - - '''.format( - ns_pref1=f"{expected_model_ns}:" if expected_model_ns else "", - ns_pref2=f"{expected_submodel_ns}:" if expected_submodel_ns else "", - ) - - actual_obj = TestModel.from_xml(xml) - expected_obj = TestModel( - model=TestSubModel(element=1.1), - ) - - assert actual_obj == expected_obj - - actual_xml = actual_obj.to_xml() - assert_xml_equal(actual_xml, xml) - - -def test_recursive_namespace_inheritance(): - class TestSubModel3(BaseXmlModel, tag='model4', inherit_ns=True): - element: str = element() - - class TestSubModel2(BaseXmlModel, tag='model3', ns='tst2', nsmap={'tst2': 'http://test.org'}): - element: TestSubModel3 - - class TestSubModel1(BaseXmlModel, tag='model2'): - element: TestSubModel2 - - class TestModel(BaseXmlModel, tag='model1', ns='tst1', nsmap={'tst1': 'http://test.org'}): - element: TestSubModel1 - - xml = ''' - - - - - string1 - - - - - ''' - - actual_obj = TestModel.from_xml(xml) - expected_obj = TestModel( - element=TestSubModel1( - element=TestSubModel2( - element=TestSubModel3(element="string1"), - ), - ), - ) - - assert actual_obj == expected_obj - - actual_xml = actual_obj.to_xml() - assert_xml_equal(actual_xml, xml) - - -@pytest.mark.parametrize( - 'ns_attrs, expected_attr_ns', - [ - (True, 'tst1'), - (False, None), - ], -) -def test_mapping_namespace_inheritance(ns_attrs, expected_attr_ns): - class TestModel(BaseXmlModel, tag='model', ns='tst1', nsmap={'tst1': 'http://test.org'}, ns_attrs=ns_attrs): - attrs: Dict[str, int] - - xml = ''' - - '''.format( - ns_pref1=f"{expected_attr_ns}:" if expected_attr_ns else '', - ) - - actual_obj = TestModel.from_xml(xml) - expected_obj = TestModel( - attrs={ - 'attr1': 1, - 'attr2': 2, - }, - ) - - assert actual_obj == expected_obj - - actual_xml = actual_obj.to_xml() - assert_xml_equal(actual_xml, xml) - - -@pytest.mark.parametrize( - 'inherit_ns, model_ns, submodel_ns, element_ns, expected_model_ns, expected_submodel_ns, expected_element_ns', - [ - (True, 'tst1', 'tst2', 'tst3', 'tst1', 'tst3', 'tst2'), - (True, 'tst1', None, 'tst3', 'tst1', 'tst3', 'tst3'), - (True, 'tst1', None, None, 'tst1', 'tst1', 'tst1'), - (False, 'tst1', None, 'tst3', 'tst1', 'tst3', None), - (False, None, None, None, None, None, None), - ], -) -def test_homogeneous_collections_namespace_inheritance( - inherit_ns, model_ns, submodel_ns, element_ns, expected_model_ns, expected_submodel_ns, expected_element_ns, -): - class TestSubModel(BaseXmlModel, ns=submodel_ns, inherit_ns=inherit_ns): - element: float = element() - - class RootModel( - BaseXmlModel, - tag='model1', - ns=model_ns, - nsmap={'tst1': 'http://test1.org', 'tst2': 'http://test2.org', 'tst3': 'http://test3.org'}, - ): - elements: List[TestSubModel] = element(tag='model2', ns=element_ns) - - xml = ''' - <{ns_pref1}model1 xmlns:tst1="http://test1.org" xmlns:tst2="http://test2.org" xmlns:tst3="http://test3.org"> - <{ns_pref2}model2> - <{ns_pref3}element>1.1 - - <{ns_pref2}model2> - <{ns_pref3}element>2.2 - - - '''.format( - ns_pref1=f"{expected_model_ns}:" if expected_model_ns else "", - ns_pref2=f"{expected_submodel_ns}:" if expected_submodel_ns else "", - ns_pref3=f"{expected_element_ns}:" if expected_element_ns else "", - ) - - actual_obj = RootModel.from_xml(xml) - expected_obj = RootModel( - elements=[ - TestSubModel(element=1.1), - TestSubModel(element=2.2), - ], - ) - - assert actual_obj == expected_obj - - actual_xml = actual_obj.to_xml() - assert_xml_equal(actual_xml, xml) - - -def test_heterogeneous_collections_namespace_inheritance(): - class TestSubModel1(BaseXmlModel, ns_attrs=True, inherit_ns=True): - attr1: int = attr() - element: float = element() - - class TestModel(BaseXmlModel, tag='model1', ns='tst1', nsmap={'tst1': 'http://test.org'}): - elements: Tuple[TestSubModel1, TestSubModel1] = element(tag='model2', ns='tst1') - - xml = ''' - - - 1.1 - - - 2.2 - - - ''' - - actual_obj = TestModel.from_xml(xml) - expected_obj = TestModel( - elements=(TestSubModel1(attr1=1, element=1.1), TestSubModel1(attr1=2, element=2.2)), - ) - - assert actual_obj == expected_obj - - actual_xml = actual_obj.to_xml() - assert_xml_equal(actual_xml, xml) - - def test_wrapper_namespaces(): class TestModel(BaseXmlModel, tag='model1', ns='tst1', nsmap={'tst1': 'http://test1.org'}): data: int = wrapped('model2/model3', ns='tst2', nsmap={'tst2': 'http://test2.org'}) @@ -358,7 +162,7 @@ class TestModel(BaseXmlModel, tag='model1', ns='tst1', nsmap={'tst1': 'http://te def test_homogeneous_collection_wrapper_namespace_inheritance(): - class TestSubModel1(BaseXmlModel, inherit_ns=True): + class TestSubModel1(BaseXmlModel): data: int class TestModel(BaseXmlModel, tag='model1', ns='tst1', nsmap={'tst1': 'http://test1.org'}): @@ -388,7 +192,7 @@ class TestModel(BaseXmlModel, tag='model1', ns='tst1', nsmap={'tst1': 'http://te def test_heterogeneous_collection_wrapper_namespace_inheritance(): - class TestSubModel1(BaseXmlModel, inherit_ns=True): + class TestSubModel1(BaseXmlModel): data: int class TestModel(BaseXmlModel, tag='model1', ns='tst1', nsmap={'tst1': 'http://test1.org'}): diff --git a/tests/test_submodels.py b/tests/test_submodels.py index a4b7a89..d83f647 100644 --- a/tests/test_submodels.py +++ b/tests/test_submodels.py @@ -1,3 +1,5 @@ +from typing import Optional + import pytest from helpers import assert_xml_equal @@ -11,6 +13,7 @@ class TestSubModel(BaseXmlModel, tag='model2'): class TestModel(BaseXmlModel, tag='model1'): model2: TestSubModel + model3: Optional[TestSubModel] = element(tag='model3') xml = ''' diff --git a/tests/test_wrapped.py b/tests/test_wrapped.py index e032d36..d3b56f4 100644 --- a/tests/test_wrapped.py +++ b/tests/test_wrapped.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict, List, Optional from helpers import assert_xml_equal @@ -30,6 +30,7 @@ class TestSubModel(BaseXmlModel, tag='model4'): class TestModel(BaseXmlModel, tag='model1'): data: TestSubModel = wrapped('model2/model3', element()) + empty: Optional[TestSubModel] = wrapped('model2/model4', element()) xml = ''' From 5fee36d8e8dfea9353d805b2690f5b5af4084816 Mon Sep 17 00:00:00 2001 From: Dmitry Pershin Date: Fri, 11 Nov 2022 00:11:53 +0500 Subject: [PATCH 2/3] python 3.11 support added. --- .github/workflows/test.yml | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f040cdb..a878334 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index f95e20c..690e268 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ] [tool.poetry.dependencies] From d7eddb2eae4635e531f4a5b20a655802c59b599b Mon Sep 17 00:00:00 2001 From: Dmitry Pershin Date: Thu, 10 Nov 2022 23:59:07 +0500 Subject: [PATCH 3/3] bump version 0.3.0. --- CHANGELOG.rst | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eab6a47..a5aa350 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Changelog ========= +0.3.0 (2022-11-10) +------------------ + +- recursive (self-referencing) models support added. +- inherit_ns flag dropped due to recursive models implementation details. + + 0.2.2 (2022-10-07) ------------------ diff --git a/pyproject.toml b/pyproject.toml index 690e268..e7d6188 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pydantic-xml" -version = "0.2.2" +version = "0.3.0" description = "pydantic xml serialization/deserialization extension" authors = ["Dmitry Pershin "] license = "Unlicense"