From 56b3f9a68996fd7a3f0932c91b82ca4762d46b0b Mon Sep 17 00:00:00 2001 From: Patrick Song Date: Fri, 3 Nov 2023 13:31:18 +1100 Subject: [PATCH 1/4] fix issue where submodel default namespace is overriden by parent --- pydantic_xml/serializers/factories/mapping.py | 6 ++-- pydantic_xml/serializers/factories/model.py | 4 +-- .../serializers/factories/primitive.py | 14 +++++++--- pydantic_xml/serializers/factories/raw.py | 4 +-- pydantic_xml/serializers/factories/wrapper.py | 4 +-- pydantic_xml/serializers/serializer.py | 4 ++- pydantic_xml/utils.py | 8 ++++++ tests/test_namespaces.py | 28 +++++++++++++++++++ 8 files changed, 58 insertions(+), 14 deletions(-) diff --git a/pydantic_xml/serializers/factories/mapping.py b/pydantic_xml/serializers/factories/mapping.py index a26c1a7..d42a97d 100644 --- a/pydantic_xml/serializers/factories/mapping.py +++ b/pydantic_xml/serializers/factories/mapping.py @@ -6,13 +6,13 @@ from pydantic_xml.element import XmlElementReader, XmlElementWriter from pydantic_xml.serializers.serializer import TYPE_FAMILY, SchemaTypeFamily, SearchMode, Serializer from pydantic_xml.typedefs import EntityLocation, NsMap -from pydantic_xml.utils import QName, merge_nsmaps +from pydantic_xml.utils import QName, merge_nsmaps, select_ns class AttributesSerializer(Serializer): @classmethod def from_core_schema(cls, schema: pcs.CoreSchema, ctx: Serializer.Context) -> 'AttributesSerializer': - ns = ctx.entity_ns or ctx.parent_ns + ns = select_ns(ctx.entity_ns, ctx.parent_ns) nsmap = merge_nsmaps(ctx.entity_nsmap, ctx.parent_nsmap) namespaced_attrs = ctx.namespaced_attrs computed = ctx.field_computed @@ -66,7 +66,7 @@ class ElementSerializer(AttributesSerializer): @classmethod def from_core_schema(cls, schema: pcs.CoreSchema, ctx: Serializer.Context) -> 'ElementSerializer': name = ctx.entity_path or ctx.field_alias or ctx.field_name - ns = ctx.entity_ns or ctx.parent_ns + ns = select_ns(ctx.entity_ns, ctx.parent_ns) nsmap = merge_nsmaps(ctx.entity_nsmap, ctx.parent_nsmap) namespaced_attrs = ctx.namespaced_attrs search_mode = ctx.search_mode diff --git a/pydantic_xml/serializers/factories/model.py b/pydantic_xml/serializers/factories/model.py index ef95d53..a2383f2 100644 --- a/pydantic_xml/serializers/factories/model.py +++ b/pydantic_xml/serializers/factories/model.py @@ -11,7 +11,7 @@ from pydantic_xml.element import XmlElementReader, XmlElementWriter from pydantic_xml.serializers.serializer import SearchMode, Serializer, XmlEntityInfoP from pydantic_xml.typedefs import EntityLocation, NsMap -from pydantic_xml.utils import QName, merge_nsmaps +from pydantic_xml.utils import QName, merge_nsmaps, select_ns class BaseModelSerializer(Serializer, abc.ABC): @@ -283,7 +283,7 @@ def from_core_schema(cls, schema: pcs.ModelSchema, ctx: Serializer.Context) -> ' assert issubclass(model_cls, pxml.BaseXmlModel), "unexpected model type" name = ctx.entity_path or model_cls.__xml_tag__ or ctx.field_alias or ctx.field_name or model_cls.__name__ - ns = ctx.entity_ns or model_cls.__xml_ns__ or ctx.parent_ns + ns = select_ns(ctx.entity_ns, model_cls.__xml_ns__, ctx.parent_ns) nsmap = merge_nsmaps(ctx.entity_nsmap, model_cls.__xml_nsmap__, ctx.parent_nsmap) search_mode = ctx.search_mode computed = ctx.field_computed diff --git a/pydantic_xml/serializers/factories/primitive.py b/pydantic_xml/serializers/factories/primitive.py index 43e1af5..404c393 100644 --- a/pydantic_xml/serializers/factories/primitive.py +++ b/pydantic_xml/serializers/factories/primitive.py @@ -6,7 +6,7 @@ from pydantic_xml.element import XmlElementReader, XmlElementWriter from pydantic_xml.serializers.serializer import SearchMode, Serializer, encode_primitive from pydantic_xml.typedefs import EntityLocation, NsMap -from pydantic_xml.utils import QName, merge_nsmaps +from pydantic_xml.utils import QName, merge_nsmaps, select_ns PrimitiveTypeSchema = Union[ pcs.NoneSchema, @@ -67,8 +67,8 @@ class AttributeSerializer(Serializer): def from_core_schema(cls, schema: PrimitiveTypeSchema, ctx: Serializer.Context) -> 'AttributeSerializer': namespaced_attrs = ctx.namespaced_attrs name = ctx.entity_path or ctx.field_alias or ctx.field_name - ns = ctx.entity_ns or (ctx.parent_ns if namespaced_attrs else None) - nsmap = merge_nsmaps(ctx.entity_nsmap, ctx.parent_nsmap) + ns = select_ns(ctx.entity_ns, ctx.parent_ns if namespaced_attrs else None) + nsmap = cls._merge_attr_nsmaps(ctx.entity_nsmap, ctx.parent_nsmap) computed = ctx.field_computed if name is None: @@ -108,12 +108,18 @@ def deserialize( return element.pop_attrib(self._attr_name) + @staticmethod + def _merge_attr_nsmaps(*maps: Optional[NsMap]) -> NsMap: + nsmap = merge_nsmaps(*maps) + nsmap.pop('', None) + return nsmap + class ElementSerializer(TextSerializer): @classmethod def from_core_schema(cls, schema: PrimitiveTypeSchema, ctx: Serializer.Context) -> 'ElementSerializer': name = ctx.entity_path or ctx.field_alias or ctx.field_name - ns = ctx.entity_ns or ctx.parent_ns + ns = select_ns(ctx.entity_ns, ctx.parent_ns) nsmap = merge_nsmaps(ctx.entity_nsmap, ctx.parent_nsmap) search_mode = ctx.search_mode computed = ctx.field_computed diff --git a/pydantic_xml/serializers/factories/raw.py b/pydantic_xml/serializers/factories/raw.py index acd1d97..37a86f5 100644 --- a/pydantic_xml/serializers/factories/raw.py +++ b/pydantic_xml/serializers/factories/raw.py @@ -6,14 +6,14 @@ from pydantic_xml.element import XmlElementReader, XmlElementWriter from pydantic_xml.serializers.serializer import SearchMode, Serializer from pydantic_xml.typedefs import EntityLocation, NsMap -from pydantic_xml.utils import QName, merge_nsmaps +from pydantic_xml.utils import QName, merge_nsmaps, select_ns class ElementSerializer(Serializer): @classmethod def from_core_schema(cls, schema: pcs.IsInstanceSchema, ctx: Serializer.Context) -> 'ElementSerializer': name = ctx.entity_path or ctx.field_alias or ctx.field_name - ns = ctx.entity_ns or ctx.parent_ns + ns = select_ns(ctx.entity_ns, ctx.parent_ns) nsmap = merge_nsmaps(ctx.entity_nsmap, ctx.parent_nsmap) search_mode = ctx.search_mode computed = ctx.field_computed diff --git a/pydantic_xml/serializers/factories/wrapper.py b/pydantic_xml/serializers/factories/wrapper.py index e0628d7..217e76b 100644 --- a/pydantic_xml/serializers/factories/wrapper.py +++ b/pydantic_xml/serializers/factories/wrapper.py @@ -5,14 +5,14 @@ from pydantic_xml.element import XmlElementReader, XmlElementWriter from pydantic_xml.serializers.serializer import SearchMode, Serializer from pydantic_xml.typedefs import NsMap -from pydantic_xml.utils import QName, merge_nsmaps +from pydantic_xml.utils import QName, merge_nsmaps, select_ns class ElementPathSerializer(Serializer): @classmethod def from_core_schema(cls, schema: pcs.CoreSchema, ctx: Serializer.Context) -> 'ElementPathSerializer': path = ctx.entity_path - ns = ctx.entity_ns or ctx.parent_ns + ns = select_ns(ctx.entity_ns, ctx.parent_ns) nsmap = merge_nsmaps(ctx.entity_nsmap, ctx.parent_nsmap) search_mode = ctx.search_mode computed = ctx.field_computed diff --git a/pydantic_xml/serializers/serializer.py b/pydantic_xml/serializers/serializer.py index 8989967..0b33517 100644 --- a/pydantic_xml/serializers/serializer.py +++ b/pydantic_xml/serializers/serializer.py @@ -11,6 +11,7 @@ from pydantic_xml.element import SearchMode, XmlElementReader, XmlElementWriter from pydantic_xml.errors import ModelError from pydantic_xml.typedefs import EntityLocation, NsMap +from pydantic_xml.utils import select_ns from . import factories @@ -143,7 +144,8 @@ def entity_wrapped(self) -> Optional['XmlEntityInfoP']: @cached_property def parent_ns(self) -> Optional[str]: if parent_ctx := self.parent_ctx: - return parent_ctx.entity_ns or parent_ctx.parent_ns + ns = select_ns(parent_ctx.entity_ns, parent_ctx.parent_ns) + return ns return None diff --git a/pydantic_xml/utils.py b/pydantic_xml/utils.py index 94a3abf..a001470 100644 --- a/pydantic_xml/utils.py +++ b/pydantic_xml/utils.py @@ -84,3 +84,11 @@ def register_nsmap(nsmap: NsMap) -> None: def get_slots(o: object) -> Iterable[str]: return it.chain.from_iterable(getattr(cls, '__slots__', []) for cls in o.__class__.__mro__) + + +def select_ns(*nss: Optional[str]) -> Optional[str]: + for ns in nss: + if ns is not None: + return ns + + return None diff --git a/tests/test_namespaces.py b/tests/test_namespaces.py index 77d057a..b5224b8 100644 --- a/tests/test_namespaces.py +++ b/tests/test_namespaces.py @@ -357,3 +357,31 @@ class TestModel(BaseTestModel, tag='model', ns='tst', nsmap={'tst': 'http://test actual_xml = actual_obj.to_xml() assert_xml_equal(actual_xml, xml1) + + +def test_submodel_namespaces_default_namespace_inheritance(): + class TestSubModel(BaseXmlModel, tag='submodel', ns='', nsmap={'': 'http://test2.org'}): + attr1: int = attr() + attr2: int = attr() + element1: str = element() + + class TestModel(BaseXmlModel, tag='model', ns='tst', nsmap={'tst': 'http://test1.org'}): + submodel: TestSubModel + + xml = ''' + + + value + + + ''' + + actual_obj = TestModel.from_xml(xml) + expected_obj = TestModel( + submodel=TestSubModel(element1='value', attr1=1, attr2=2), + ) + + assert actual_obj == expected_obj + + actual_xml = actual_obj.to_xml() + assert_xml_equal(actual_xml, xml) From 34e6d7419e3402f9e7ad543fbc862a5cdbfe63bc Mon Sep 17 00:00:00 2001 From: Patrick Song Date: Mon, 6 Nov 2023 09:33:26 +1100 Subject: [PATCH 2/4] raise model error when attribute is defined with a default namespace --- pydantic_xml/serializers/factories/primitive.py | 10 +++------- tests/test_namespaces.py | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pydantic_xml/serializers/factories/primitive.py b/pydantic_xml/serializers/factories/primitive.py index 404c393..8557083 100644 --- a/pydantic_xml/serializers/factories/primitive.py +++ b/pydantic_xml/serializers/factories/primitive.py @@ -68,9 +68,11 @@ def from_core_schema(cls, schema: PrimitiveTypeSchema, ctx: Serializer.Context) namespaced_attrs = ctx.namespaced_attrs name = ctx.entity_path or ctx.field_alias or ctx.field_name ns = select_ns(ctx.entity_ns, ctx.parent_ns if namespaced_attrs else None) - nsmap = cls._merge_attr_nsmaps(ctx.entity_nsmap, ctx.parent_nsmap) + nsmap = merge_nsmaps(ctx.entity_nsmap, ctx.parent_nsmap) computed = ctx.field_computed + if ns == '': + raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "attributes with default namespace are forbidden") if name is None: raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "entity name is not provided") @@ -108,12 +110,6 @@ def deserialize( return element.pop_attrib(self._attr_name) - @staticmethod - def _merge_attr_nsmaps(*maps: Optional[NsMap]) -> NsMap: - nsmap = merge_nsmaps(*maps) - nsmap.pop('', None) - return nsmap - class ElementSerializer(TextSerializer): @classmethod diff --git a/tests/test_namespaces.py b/tests/test_namespaces.py index b5224b8..d9d0b0b 100644 --- a/tests/test_namespaces.py +++ b/tests/test_namespaces.py @@ -42,7 +42,7 @@ class TestModel(BaseXmlModel, tag='model'): @pytest.mark.skipif(not is_lxml_native(), reason='not lxml used') def test_lxml_default_namespace_serialisation(): class TestSubModel(BaseXmlModel, tag='submodel', ns='', nsmap={'': 'http://test3.org', 'tst': 'http://test4.org'}): - attr1: int = attr(ns='') + attr1: int = attr() attr2: int = attr(ns='tst') element1: str = element(ns='') From 21371202ee2b887d763258f40456bf26b1566ef9 Mon Sep 17 00:00:00 2001 From: Patrick Song Date: Mon, 6 Nov 2023 16:06:29 +1100 Subject: [PATCH 3/4] fix precommit issues --- pydantic_xml/serializers/factories/primitive.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pydantic_xml/serializers/factories/primitive.py b/pydantic_xml/serializers/factories/primitive.py index 8557083..d137e3f 100644 --- a/pydantic_xml/serializers/factories/primitive.py +++ b/pydantic_xml/serializers/factories/primitive.py @@ -72,7 +72,11 @@ def from_core_schema(cls, schema: PrimitiveTypeSchema, ctx: Serializer.Context) computed = ctx.field_computed if ns == '': - raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "attributes with default namespace are forbidden") + raise errors.ModelFieldError( + ctx.model_name, + ctx.field_name, + "attributes with default namespace are forbidden", + ) if name is None: raise errors.ModelFieldError(ctx.model_name, ctx.field_name, "entity name is not provided") From eb8efc055f16047046407835d99b5c0028f409f9 Mon Sep 17 00:00:00 2001 From: Dmitry Pershin Date: Mon, 6 Nov 2023 10:18:07 +0500 Subject: [PATCH 4/4] bump version 2.4.0. --- CHANGELOG.rst | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 31d72a7..9c4744a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Changelog ========= + +2.4.0 (2023-11-06) +------------------ + +- attributes with default namespace bug fixed. See https://github.com/dapper91/pydantic-xml/issues/137. + + 2.3.0 (2023-10-22) ------------------ diff --git a/pyproject.toml b/pyproject.toml index c531307..2ab26c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pydantic-xml" -version = "2.3.0" +version = "2.4.0" description = "pydantic xml extension" authors = ["Dmitry Pershin "] license = "Unlicense"