From 3cbfaeb29ee5f41e15f7914fc3bf60c0e2328493 Mon Sep 17 00:00:00 2001 From: Stefan de Konink Date: Fri, 20 Oct 2023 00:09:14 +0200 Subject: [PATCH 1/7] This is a prototype for #854 When a restriction is set, the ideal situation for the datamodel would be that only the properties that fall under the restriction are available. From https://www.reddit.com/r/learnpython/comments/12qhzi6/dataclasses_with_inheritance/ I found that kw_only was used to resolve the issue with non-default argument 'class name' follows default argument. --- .../handlers/validate_attributes_overrides.py | 16 +++++++++++++++- xsdata/codegen/models.py | 1 + xsdata/formats/dataclass/filters.py | 7 +++++++ xsdata/formats/dataclass/templates/class.jinja2 | 2 +- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/xsdata/codegen/handlers/validate_attributes_overrides.py b/xsdata/codegen/handlers/validate_attributes_overrides.py index 9e93bfc97..620b92292 100644 --- a/xsdata/codegen/handlers/validate_attributes_overrides.py +++ b/xsdata/codegen/handlers/validate_attributes_overrides.py @@ -4,7 +4,7 @@ from typing import Optional from xsdata.codegen.mixins import RelativeHandlerInterface -from xsdata.codegen.models import Attr +from xsdata.codegen.models import Attr, Restrictions from xsdata.codegen.models import Class from xsdata.codegen.models import get_slug from xsdata.codegen.utils import ClassUtils @@ -24,6 +24,10 @@ class ValidateAttributesOverrides(RelativeHandlerInterface): __slots__ = () def process(self, target: Class): + original_attrs = [] + if len([ext for ext in target.extensions if ext.tag == 'Restriction']) > 0: + original_attrs = list(target.attrs) + base_attrs_map = self.base_attrs_map(target) for attr in list(target.attrs): base_attrs = base_attrs_map.get(attr.slug) @@ -37,6 +41,16 @@ def process(self, target: Class): elif attr.is_prohibited: self.remove_attribute(target, attr) + if len([ext for ext in target.extensions if ext.tag == 'Restriction']) > 0: + # What we want here is to check the restriction.attrs against base_attrs_map + # restriction_attrs = {a.slug: a for a in self.base_attrs(self.container.find(target.extensions[0].type.qname))} + restriction_attrs = {a.slug: a for a in original_attrs} + all_attrs = dict(base_attrs_map.items()) + for slug, attr in all_attrs.items(): + if slug not in restriction_attrs: + attr_new = Attr(tag=attr[0].tag, name=attr[0].name, restrictions=Restrictions(is_null=True)) + target.attrs.append(attr_new) + @classmethod def overrides(cls, a: Attr, b: Attr) -> bool: return a.xml_type == b.xml_type and a.namespace == b.namespace diff --git a/xsdata/codegen/models.py b/xsdata/codegen/models.py index a054a653b..37a66a3b0 100644 --- a/xsdata/codegen/models.py +++ b/xsdata/codegen/models.py @@ -62,6 +62,7 @@ class Restrictions: group: Optional[int] = field(default=None) process_contents: Optional[str] = field(default=None) path: List[Tuple[str, int, int, int]] = field(default_factory=list) + is_null: bool = field(default=False) @property def is_list(self) -> bool: diff --git a/xsdata/formats/dataclass/filters.py b/xsdata/formats/dataclass/filters.py index e7e114b38..c75bfd962 100644 --- a/xsdata/formats/dataclass/filters.py +++ b/xsdata/formats/dataclass/filters.py @@ -127,6 +127,7 @@ def register(self, env: Environment): "field_default": self.field_default_value, "field_metadata": self.field_metadata, "field_definition": self.field_definition, + "is_null": self.is_null, "class_name": self.class_name, "class_bases": self.class_bases, "class_annotations": self.class_annotations, @@ -223,6 +224,12 @@ def apply_substitutions(self, name: str, obj_type: ObjectType) -> str: return name + def is_null( + self, + attr: Attr + ) -> bool: + return attr.restrictions.is_null + def field_definition( self, attr: Attr, diff --git a/xsdata/formats/dataclass/templates/class.jinja2 b/xsdata/formats/dataclass/templates/class.jinja2 index 4075911ae..3a3a586f7 100644 --- a/xsdata/formats/dataclass/templates/class.jinja2 +++ b/xsdata/formats/dataclass/templates/class.jinja2 @@ -44,7 +44,7 @@ class {{ class_name }}{{"({})".format(base_classes) if base_classes }}: {%- for attr in obj.attrs %} {%- set field_typing = attr|field_type(parents) %} {%- set field_definition = attr|field_definition(obj.ns_map, parent_namespace, parents) %} - {{ attr.name|field_name(obj.name) }}: {{ field_typing }} = {{ field_definition }} + {{ attr.name|field_name(obj.name) }}: {%- if attr|is_null %} None {%- else %} {{ field_typing }} = {{ field_definition }} {%- endif -%} {%- endfor -%} {%- for inner in obj.inner %} {%- set tpl = "enum.jinja2" if inner.is_enumeration else "class.jinja2" -%} From c253b336106d42402e7abcfcdfb5e24c44feecd1 Mon Sep 17 00:00:00 2001 From: Stefan de Konink Date: Wed, 25 Oct 2023 00:51:42 +0200 Subject: [PATCH 2/7] It seems that attributes may always remain according to the specs --- xsdata/codegen/handlers/validate_attributes_overrides.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xsdata/codegen/handlers/validate_attributes_overrides.py b/xsdata/codegen/handlers/validate_attributes_overrides.py index 620b92292..961748d7c 100644 --- a/xsdata/codegen/handlers/validate_attributes_overrides.py +++ b/xsdata/codegen/handlers/validate_attributes_overrides.py @@ -47,8 +47,8 @@ def process(self, target: Class): restriction_attrs = {a.slug: a for a in original_attrs} all_attrs = dict(base_attrs_map.items()) for slug, attr in all_attrs.items(): - if slug not in restriction_attrs: - attr_new = Attr(tag=attr[0].tag, name=attr[0].name, restrictions=Restrictions(is_null=True)) + if not attr[0].is_attribute and slug not in restriction_attrs: + attr_new = Attr(tag=attr[0].tag, name=attr[0].name, index=attr[0].index, restrictions=Restrictions(is_null=True)) target.attrs.append(attr_new) @classmethod From f77abacdbd4a0c9a5c9fdb45ede674d062fdc4f0 Mon Sep 17 00:00:00 2001 From: Stefan de Konink Date: Sun, 5 Nov 2023 10:36:13 +0100 Subject: [PATCH 3/7] Update to RestrictedVar --- .../handlers/validate_attributes_overrides.py | 13 +++++-------- xsdata/codegen/models.py | 2 +- xsdata/formats/dataclass/filters.py | 6 +++--- xsdata/formats/dataclass/templates/class.jinja2 | 2 +- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/xsdata/codegen/handlers/validate_attributes_overrides.py b/xsdata/codegen/handlers/validate_attributes_overrides.py index 961748d7c..d3e12efcd 100644 --- a/xsdata/codegen/handlers/validate_attributes_overrides.py +++ b/xsdata/codegen/handlers/validate_attributes_overrides.py @@ -24,9 +24,9 @@ class ValidateAttributesOverrides(RelativeHandlerInterface): __slots__ = () def process(self, target: Class): - original_attrs = [] + restriction_attrs = [] if len([ext for ext in target.extensions if ext.tag == 'Restriction']) > 0: - original_attrs = list(target.attrs) + restriction_attrs = {attr.slug: attr for attr in target.attrs if not attr.is_attribute} base_attrs_map = self.base_attrs_map(target) for attr in list(target.attrs): @@ -43,13 +43,10 @@ def process(self, target: Class): if len([ext for ext in target.extensions if ext.tag == 'Restriction']) > 0: # What we want here is to check the restriction.attrs against base_attrs_map - # restriction_attrs = {a.slug: a for a in self.base_attrs(self.container.find(target.extensions[0].type.qname))} - restriction_attrs = {a.slug: a for a in original_attrs} - all_attrs = dict(base_attrs_map.items()) - for slug, attr in all_attrs.items(): + for slug, attr in base_attrs_map.items(): if not attr[0].is_attribute and slug not in restriction_attrs: - attr_new = Attr(tag=attr[0].tag, name=attr[0].name, index=attr[0].index, restrictions=Restrictions(is_null=True)) - target.attrs.append(attr_new) + attr_restricted = Attr(tag=attr[0].tag, name=attr[0].name, index=attr[0].index, restrictions=Restrictions(is_restricted=True)) + target.attrs.append(attr_restricted) @classmethod def overrides(cls, a: Attr, b: Attr) -> bool: diff --git a/xsdata/codegen/models.py b/xsdata/codegen/models.py index 37a66a3b0..5a95cc8ee 100644 --- a/xsdata/codegen/models.py +++ b/xsdata/codegen/models.py @@ -62,7 +62,7 @@ class Restrictions: group: Optional[int] = field(default=None) process_contents: Optional[str] = field(default=None) path: List[Tuple[str, int, int, int]] = field(default_factory=list) - is_null: bool = field(default=False) + is_restricted: bool = field(default=False) @property def is_list(self) -> bool: diff --git a/xsdata/formats/dataclass/filters.py b/xsdata/formats/dataclass/filters.py index c75bfd962..fcc0c770a 100644 --- a/xsdata/formats/dataclass/filters.py +++ b/xsdata/formats/dataclass/filters.py @@ -127,7 +127,7 @@ def register(self, env: Environment): "field_default": self.field_default_value, "field_metadata": self.field_metadata, "field_definition": self.field_definition, - "is_null": self.is_null, + "is_restricted": self.is_restricted, "class_name": self.class_name, "class_bases": self.class_bases, "class_annotations": self.class_annotations, @@ -224,11 +224,11 @@ def apply_substitutions(self, name: str, obj_type: ObjectType) -> str: return name - def is_null( + def is_restricted( self, attr: Attr ) -> bool: - return attr.restrictions.is_null + return attr.restrictions.is_restricted def field_definition( self, diff --git a/xsdata/formats/dataclass/templates/class.jinja2 b/xsdata/formats/dataclass/templates/class.jinja2 index 3a3a586f7..75384b4fb 100644 --- a/xsdata/formats/dataclass/templates/class.jinja2 +++ b/xsdata/formats/dataclass/templates/class.jinja2 @@ -44,7 +44,7 @@ class {{ class_name }}{{"({})".format(base_classes) if base_classes }}: {%- for attr in obj.attrs %} {%- set field_typing = attr|field_type(parents) %} {%- set field_definition = attr|field_definition(obj.ns_map, parent_namespace, parents) %} - {{ attr.name|field_name(obj.name) }}: {%- if attr|is_null %} None {%- else %} {{ field_typing }} = {{ field_definition }} {%- endif -%} + {{ attr.name|field_name(obj.name) }}: {%- if attr|is_restricted %} RestrictedVar {%- else %} {{ field_typing }} = {{ field_definition }} {%- endif -%} {%- endfor -%} {%- for inner in obj.inner %} {%- set tpl = "enum.jinja2" if inner.is_enumeration else "class.jinja2" -%} From 0f3c34bc04cf82a3facf5b2d60f4166307eddc62 Mon Sep 17 00:00:00 2001 From: Stefan de Konink Date: Sun, 5 Nov 2023 13:56:26 +0100 Subject: [PATCH 4/7] Lets test a CI run --- xsdata/formats/dataclass/templates/imports.jinja2 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/xsdata/formats/dataclass/templates/imports.jinja2 b/xsdata/formats/dataclass/templates/imports.jinja2 index 7c59053d8..1d535a370 100644 --- a/xsdata/formats/dataclass/templates/imports.jinja2 +++ b/xsdata/formats/dataclass/templates/imports.jinja2 @@ -9,3 +9,7 @@ from {{ source | import_module(module) }} import ( ) {% endif -%} {%- endfor %} +{# +TODO: remove +#} +from typing import ClassVar as RestrictedVar From 736fc270f8b0a0720109c10e797d8f8eb7ce408c Mon Sep 17 00:00:00 2001 From: Stefan de Konink Date: Sun, 5 Nov 2023 14:06:05 +0100 Subject: [PATCH 5/7] Format related changes --- .../handlers/validate_attributes_overrides.py | 14 +++++++++++--- xsdata/formats/dataclass/filters.py | 5 +---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/xsdata/codegen/handlers/validate_attributes_overrides.py b/xsdata/codegen/handlers/validate_attributes_overrides.py index d3e12efcd..eff54e1f5 100644 --- a/xsdata/codegen/handlers/validate_attributes_overrides.py +++ b/xsdata/codegen/handlers/validate_attributes_overrides.py @@ -4,9 +4,10 @@ from typing import Optional from xsdata.codegen.mixins import RelativeHandlerInterface -from xsdata.codegen.models import Attr, Restrictions +from xsdata.codegen.models import Attr from xsdata.codegen.models import Class from xsdata.codegen.models import get_slug +from xsdata.codegen.models import Restrictions from xsdata.codegen.utils import ClassUtils from xsdata.utils import collections @@ -26,7 +27,9 @@ class ValidateAttributesOverrides(RelativeHandlerInterface): def process(self, target: Class): restriction_attrs = [] if len([ext for ext in target.extensions if ext.tag == 'Restriction']) > 0: - restriction_attrs = {attr.slug: attr for attr in target.attrs if not attr.is_attribute} + restriction_attrs = { + attr.slug: attr for attr in target.attrs if not attr.is_attribute + } base_attrs_map = self.base_attrs_map(target) for attr in list(target.attrs): @@ -45,7 +48,12 @@ def process(self, target: Class): # What we want here is to check the restriction.attrs against base_attrs_map for slug, attr in base_attrs_map.items(): if not attr[0].is_attribute and slug not in restriction_attrs: - attr_restricted = Attr(tag=attr[0].tag, name=attr[0].name, index=attr[0].index, restrictions=Restrictions(is_restricted=True)) + attr_restricted = Attr( + tag=attr[0].tag, + name=attr[0].name, + index=attr[0].index, + restrictions=Restrictions(is_restricted=True) + ) target.attrs.append(attr_restricted) @classmethod diff --git a/xsdata/formats/dataclass/filters.py b/xsdata/formats/dataclass/filters.py index fcc0c770a..c5888ba7b 100644 --- a/xsdata/formats/dataclass/filters.py +++ b/xsdata/formats/dataclass/filters.py @@ -224,10 +224,7 @@ def apply_substitutions(self, name: str, obj_type: ObjectType) -> str: return name - def is_restricted( - self, - attr: Attr - ) -> bool: + def is_restricted(self, attr: Attr) -> bool: return attr.restrictions.is_restricted def field_definition( From 656346e5bed33c4bbe1e002d3394a54ca6998628 Mon Sep 17 00:00:00 2001 From: Stefan de Konink Date: Sun, 5 Nov 2023 14:14:45 +0100 Subject: [PATCH 6/7] Format related changes --- xsdata/codegen/handlers/validate_attributes_overrides.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xsdata/codegen/handlers/validate_attributes_overrides.py b/xsdata/codegen/handlers/validate_attributes_overrides.py index eff54e1f5..13273298b 100644 --- a/xsdata/codegen/handlers/validate_attributes_overrides.py +++ b/xsdata/codegen/handlers/validate_attributes_overrides.py @@ -26,7 +26,7 @@ class ValidateAttributesOverrides(RelativeHandlerInterface): def process(self, target: Class): restriction_attrs = [] - if len([ext for ext in target.extensions if ext.tag == 'Restriction']) > 0: + if len([ext for ext in target.extensions if ext.tag == "Restriction"]) > 0: restriction_attrs = { attr.slug: attr for attr in target.attrs if not attr.is_attribute } @@ -44,7 +44,7 @@ def process(self, target: Class): elif attr.is_prohibited: self.remove_attribute(target, attr) - if len([ext for ext in target.extensions if ext.tag == 'Restriction']) > 0: + if len([ext for ext in target.extensions if ext.tag == "Restriction"]) > 0: # What we want here is to check the restriction.attrs against base_attrs_map for slug, attr in base_attrs_map.items(): if not attr[0].is_attribute and slug not in restriction_attrs: @@ -52,7 +52,7 @@ def process(self, target: Class): tag=attr[0].tag, name=attr[0].name, index=attr[0].index, - restrictions=Restrictions(is_restricted=True) + restrictions=Restrictions(is_restricted=True), ) target.attrs.append(attr_restricted) From cd7c5dea9d53aa41834fb5441c12e27f0a3b7084 Mon Sep 17 00:00:00 2001 From: Stefan de Konink Date: Sun, 5 Nov 2023 14:21:10 +0100 Subject: [PATCH 7/7] Format related changes --- xsdata/codegen/handlers/validate_attributes_overrides.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xsdata/codegen/handlers/validate_attributes_overrides.py b/xsdata/codegen/handlers/validate_attributes_overrides.py index 13273298b..c0c8691b9 100644 --- a/xsdata/codegen/handlers/validate_attributes_overrides.py +++ b/xsdata/codegen/handlers/validate_attributes_overrides.py @@ -25,7 +25,7 @@ class ValidateAttributesOverrides(RelativeHandlerInterface): __slots__ = () def process(self, target: Class): - restriction_attrs = [] + restriction_attrs: dict[str, Attr] = {} if len([ext for ext in target.extensions if ext.tag == "Restriction"]) > 0: restriction_attrs = { attr.slug: attr for attr in target.attrs if not attr.is_attribute