Repository: tefra/xsdata

Issue:
XML Schema restrictions are not reflected within __dataclass_fields__ due to inheritance
I have yesterday encountered a situation where there was a presumable recursive definition of an object. https://github.com/NeTEx-CEN/NeTEx/issues/526 I have learned this definitionis actually prevented by an XML Schema restriction, which implicitly removes the fields that are not in use. xsData seems to do the right job, _but_ for anyone that is using inspection on the dataclasses is faced with inheritance from the subsitutionGroup.

```xsd
        <xsd:element name="AlternativeText" substitutionGroup="VersionedChild">
                <xsd:annotation>
                        <xsd:documentation>Alternative Text. +v1.1</xsd:documentation>
                </xsd:annotation>
                <xsd:complexType>
                        <xsd:complexContent>
                                <xsd:restriction base="AlternativeText_VersionedChildStructure">
                                        <xsd:sequence>
                                                <xsd:sequence>
                                                        <xsd:element name="DataManagedObjectRef" type="VersionOfObjectRefStructure" minOccurs="0">
                                                                <xsd:annotation>
                                                                        <xsd:documentation>Object for  attribute for which ALTERNATIVE TEXTprovides an alias. May be omitted if given by context.</xsd:documentation>
                                                                </xsd:annotation>
                                                        </xsd:element>
                                                        <xsd:element name="Text" type="MultilingualString">
                                                                <xsd:annotation>
                                                                        <xsd:documentation>Name of the entity.</xsd:documentation>
                                                                </xsd:annotation>
                                                        </xsd:element>
                                                </xsd:sequence>
                                        </xsd:sequence>
                                        <xsd:attribute name="id" type="AlternativeTextIdType" use="optional">
                                                <xsd:annotation>
                                                        <xsd:documentation>Identifier of ENTITY.</xsd:documentation>
                                                </xsd:annotation>
                                        </xsd:attribute>
                                        <xsd:attribute name="attributeName" type="xsd:NCName">
                                                <xsd:annotation>
                                                        <xsd:documentation>Name of text attribute for which this is the  alternative tex. Must be an existing attribute name </xsd:documentation>
                                                </xsd:annotation>
                                        </xsd:attribute>
                                </xsd:restriction>
                        </xsd:complexContent>
                </xsd:complexType>
        </xsd:element>
```

Is transformed into:
```python
@dataclass(unsafe_hash=True, kw_only=True)
class AlternativeTextVersionedChildStructure(VersionedChildStructure):
    """
    Type for ALTERNATIVE TEXT.

    :ivar data_managed_object_ref: Object for  attribute for which
        ALTERNATIVE TEXTprovides an alias. May be omitted if given by
        context.
    :ivar text: Name of the entity.
    :ivar attribute_name: Name of text attribute for which this is the
        alternative tex. Must be an existing attribute name
    :ivar use_for_language: Name of language for which this is to be
        used.
    :ivar order: Order of name.
    """ 
    class Meta:
        name = "AlternativeText_VersionedChildStructure"
        
    data_managed_object_ref: Optional[VersionOfObjectRefStructure] = field(
        default=None,
        metadata={
            "name": "DataManagedObjectRef",
            "type": "Element",
            "namespace": "http://www.netex.org.uk/netex",
        }
    )       
    text: MultilingualString = field(
        metadata={
            "name": "Text",
            "type": "Element",
            "namespace": "http://www.netex.org.uk/netex",
            "required": True,
        }
    )
    attribute_name: Optional[str] = field(
        default=None,
        metadata={
            "name": "attributeName",
            "type": "Attribute",
        }
    )
    use_for_language: Optional[str] = field(
        default=None,
        metadata={
            "name": "useForLanguage",
            "type": "Attribute",
        }
    )
    order: Optional[int] = field(
        default=None,
        metadata={
            "type": "Attribute",
        }
    )
```

But the effect when looking at AlternativeText.__dataclass_fields__ still include the fields outside the restriction inhereted via VersionedChildStructure:
![image](https://github.com/tefra/xsdata/assets/502394/1fc9dffa-3220-482c-b21f-317bc383103b)

I have been naively started to experiment if there was a clean way to clear the upstream inheritance, which was surprisingly simple. By just setting the already created field to _None_, __dataclass_fields__ would ignore it.

```
@dataclass(unsafe_hash=True, kw_only=True)
class AlternativeTextVersionedChildStructure(VersionedChildStructure):
    """
    Type for ALTERNATIVE TEXT.

    :ivar data_managed_object_ref: Object for  attribute for which
        ALTERNATIVE TEXTprovides an alias. May be omitted if given by
        context.
    :ivar text: Name of the entity.
    :ivar attribute_name: Name of text attribute for which this is the
        alternative tex. Must be an existing attribute name
    :ivar use_for_language: Name of language for which this is to be
        used.
    :ivar order: Order of name.
    """
    class Meta:
        name = "AlternativeText_VersionedChildStructure"

    alternative_texts: None

    validity_conditions_or_valid_between: None

    extensions: None

    data_managed_object_ref: Optional[VersionOfObjectRefStructure] = field(
        default=None,
        metadata={
            "name": "DataManagedObjectRef",
            "type": "Element",
            "namespace": "http://www.netex.org.uk/netex",
        }
    )
    text: MultilingualString = field(
        metadata={
            "name": "Text",
            "type": "Element",
            "namespace": "http://www.netex.org.uk/netex",
            "required": True,
        }
    )
    attribute_name: Optional[str] = field(
        default=None,
        metadata={
            "name": "attributeName",
            "type": "Attribute",
        }
    )
    use_for_language: Optional[str] = field(
        default=None,
        metadata={
            "name": "useForLanguage",
            "type": "Attribute",
        }
    )
    order: Optional[int] = field(
        default=None,
        metadata={
            "type": "Attribute",
        }
    )
```

![image](https://github.com/tefra/xsdata/assets/502394/77900013-4aeb-42ae-82c8-313d157bf0d7)


Discussion:
sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=908) **Quality Gate passed**  
Kudos, no new issues were introduced!

[0 New issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=908&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=908&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=908&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=908)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/tefra/xsdata/pull/908?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`5a4883d`)](https://app.codecov.io/gh/tefra/xsdata/commit/5a4883de153ff593cd7f01034a2659c735e1225f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) 100.00% compared to head [(`abc523e`)](https://app.codecov.io/gh/tefra/xsdata/pull/908?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) 99.95%.


<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##              main     #908      +/-   ##
===========================================
- Coverage   100.00%   99.95%   -0.05%     
===========================================
  Files          105      105              
  Lines         8728     8747      +19     
  Branches      2067     1601     -466     
===========================================
+ Hits          8728     8743      +15     
- Misses           0        2       +2     
- Partials         0        2       +2     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tefra/xsdata/pull/908?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).


Patch:
diff --git a/xsdata/codegen/handlers/validate_attributes_overrides.py b/xsdata/codegen/handlers/validate_attributes_overrides.py
index f2a1a05f..3df1447f 100644
--- a/xsdata/codegen/handlers/validate_attributes_overrides.py
+++ b/xsdata/codegen/handlers/validate_attributes_overrides.py
@@ -1,5 +1,5 @@
 import sys
-from typing import Dict, List, Optional
+from typing import Dict, List, Optional, Set
 
 from xsdata.codegen.mixins import RelativeHandlerInterface
 from xsdata.codegen.models import Attr, Class, get_slug
@@ -9,30 +9,56 @@ from xsdata.utils import collections
 
 
 class ValidateAttributesOverrides(RelativeHandlerInterface):
-    """
-    Check override attributes are valid.
-
-    Steps:
-        1. The attribute is a valid override, leave it alone
-        2. The attribute is unnecessary remove it
-        3. The attribute is an invalid override, rename one of them
-    """
+    """Validate override and restricted attributes."""
 
     __slots__ = ()
 
     def process(self, target: Class):
         base_attrs_map = self.base_attrs_map(target)
+        # We need the original class attrs before validation, in order to
+        # prohibit the rest of the parent attrs later...
+        restricted_attrs = {
+            attr.slug for attr in target.attrs if attr.can_be_restricted()
+        }
+        self.validate_attrs(target, base_attrs_map)
+        if target.is_restricted:
+            self.prohibit_parent_attrs(target, restricted_attrs, base_attrs_map)
+
+    @classmethod
+    def prohibit_parent_attrs(
+        cls,
+        target: Class,
+        restricted_attrs: Set[str],
+        base_attrs_map: Dict[str, List[Attr]],
+    ):
+        """
+        Prepend prohibited parent attrs to the target class.
+
+        Reset the types and default value in order to avoid conflicts
+        later.
+        """
+        for slug, attrs in reversed(base_attrs_map.items()):
+            attr = attrs[0]
+            if attr.can_be_restricted() and slug not in restricted_attrs:
+                attr_restricted = attr.clone()
+                attr_restricted.restrictions.max_occurs = 0
+                attr_restricted.default = None
+                attr_restricted.types.clear()
+                target.attrs.insert(0, attr_restricted)
+
+    @classmethod
+    def validate_attrs(cls, target: Class, base_attrs_map: Dict[str, List[Attr]]):
         for attr in list(target.attrs):
             base_attrs = base_attrs_map.get(attr.slug)
 
             if base_attrs:
                 base_attr = base_attrs[0]
-                if self.overrides(attr, base_attr):
-                    self.validate_override(target, attr, base_attr)
+                if cls.overrides(attr, base_attr):
+                    cls.validate_override(target, attr, base_attr)
                 else:
-                    self.resolve_conflict(attr, base_attr)
+                    cls.resolve_conflict(attr, base_attr)
             elif attr.is_prohibited:
-                self.remove_attribute(target, attr)
+                cls.remove_attribute(target, attr)
 
     @classmethod
     def overrides(cls, a: Attr, b: Attr) -> bool:
diff --git a/xsdata/codegen/models.py b/xsdata/codegen/models.py
index a9d675ee..5b39b25a 100644
--- a/xsdata/codegen/models.py
+++ b/xsdata/codegen/models.py
@@ -333,6 +333,10 @@ class Attr:
             if datatype:
                 yield datatype.type
 
+    def can_be_restricted(self) -> bool:
+        """Return whether this attribute can be restricted."""
+        return self.xml_type not in (Tag.ATTRIBUTE, None)
+
 
 @dataclass(unsafe_hash=True)
 class Extension:
@@ -460,6 +464,12 @@ class Class:
         """Return whether this class supports mixed content."""
         return self.mixed or any(x.mixed for x in self.attrs)
 
+    @property
+    def is_restricted(self) -> bool:
+        return any(
+            True for extension in self.extensions if extension.tag == Tag.RESTRICTION
+        )
+
     @property
     def is_service(self) -> bool:
         """Return whether this instance is derived from wsdl:operation."""
diff --git a/xsdata/formats/dataclass/compat.py b/xsdata/formats/dataclass/compat.py
index 6058ff0d..64c1d51e 100644
--- a/xsdata/formats/dataclass/compat.py
+++ b/xsdata/formats/dataclass/compat.py
@@ -85,7 +85,9 @@ class ClassType(abc.ABC):
             return 0.0
 
         if self.is_model(obj):
-            return sum(score(getattr(obj, var.name)) for var in self.get_fields(obj))
+            return sum(
+                score(getattr(obj, var.name, None)) for var in self.get_fields(obj)
+            )
 
         return score(obj)
 


Test patch:
diff --git a/tests/codegen/handlers/test_validate_attributes_overrides.py b/tests/codegen/handlers/test_validate_attributes_overrides.py
index db1b59be..e7d79591 100644
--- a/tests/codegen/handlers/test_validate_attributes_overrides.py
+++ b/tests/codegen/handlers/test_validate_attributes_overrides.py
@@ -21,9 +21,46 @@ class ValidateAttributesOverridesTests(FactoryTestCase):
         self.container = ClassContainer(config=GeneratorConfig())
         self.processor = ValidateAttributesOverrides(container=self.container)
 
+    def test_prohibit_parent_attrs(self):
+        child = ClassFactory.create(
+            status=Status.FLATTENING,
+            attrs=[
+                AttrFactory.create(name="el", tag=Tag.ELEMENT),
+                AttrFactory.create(name="at", tag=Tag.ATTRIBUTE),
+            ],
+        )
+
+        parent = ClassFactory.create(
+            status=Status.FLATTENED,
+            attrs=[
+                AttrFactory.element(default="foo"),
+                AttrFactory.attribute(),
+                AttrFactory.extension(),
+                AttrFactory.element(default="bar"),
+            ],
+        )
+
+        child.extensions.append(
+            ExtensionFactory.reference(parent.qname, tag=Tag.RESTRICTION)
+        )
+        self.container.extend((parent, child))
+        self.processor.process(child)
+
+        self.assertEqual(4, len(child.attrs))
+
+        self.assertEqual(parent.attrs[0].name, child.attrs[0].name)
+        self.assertEqual([], child.attrs[0].types)
+        self.assertIsNone(child.attrs[0].default)
+        self.assertTrue(child.attrs[0].is_prohibited)
+
+        self.assertEqual(parent.attrs[3].name, child.attrs[1].name)
+        self.assertEqual([], child.attrs[1].types)
+        self.assertIsNone(child.attrs[1].default)
+        self.assertTrue(child.attrs[1].is_prohibited)
+
     @mock.patch.object(ValidateAttributesOverrides, "resolve_conflict")
     @mock.patch.object(ValidateAttributesOverrides, "validate_override")
-    def test_process(self, mock_validate_override, mock_resolve_conflict):
+    def test_validate_attrs(self, mock_validate_override, mock_resolve_conflict):
         class_a = ClassFactory.create(
             status=Status.FLATTENING,
             attrs=[
@@ -35,8 +72,12 @@ class ValidateAttributesOverridesTests(FactoryTestCase):
         class_b = ClassFactory.elements(2, status=Status.FLATTENED)
         class_c = ClassFactory.create(status=Status.FLATTENED)
 
-        class_b.extensions.append(ExtensionFactory.reference(class_c.qname))
-        class_a.extensions.append(ExtensionFactory.reference(class_b.qname))
+        class_b.extensions.append(
+            ExtensionFactory.reference(class_c.qname, tag=Tag.EXTENSION)
+        )
+        class_a.extensions.append(
+            ExtensionFactory.reference(class_b.qname, tag=Tag.EXTENSION)
+        )
 
         class_c.attrs.append(class_a.attrs[0].clone())
         class_c.attrs.append(class_a.attrs[1].clone())
@@ -52,7 +93,7 @@ class ValidateAttributesOverridesTests(FactoryTestCase):
             class_a.attrs[1], class_c.attrs[1]
         )
 
-    def test_process_remove_non_overriding_prohibited_attrs(self):
+    def test_validate_attrs_remove_non_overriding_prohibited_attrs(self):
         target = ClassFactory.elements(1)
         target.attrs[0].restrictions.max_occurs = 0
 
diff --git a/tests/codegen/models/test_attr.py b/tests/codegen/models/test_attr.py
index 42501eae..3cfe7675 100644
--- a/tests/codegen/models/test_attr.py
+++ b/tests/codegen/models/test_attr.py
@@ -25,6 +25,12 @@ class AttrTests(FactoryTestCase):
         attr.namespace = __file__
         self.assertNotEqual(attr, clone)
 
+    def test_can_be_restricted(self):
+        self.assertFalse(AttrFactory.create(tag=Tag.ATTRIBUTE).can_be_restricted())
+        self.assertFalse(AttrFactory.create(tag=Tag.EXTENSION).can_be_restricted())
+        self.assertFalse(AttrFactory.create(tag=Tag.RESTRICTION).can_be_restricted())
+        self.assertTrue(AttrFactory.create(tag=Tag.ELEMENT).can_be_restricted())
+
     def test_property_key(self):
         attr = AttrFactory.attribute(name="a", namespace="b")
         self.assertEqual("Attribute.b.a", attr.key)
diff --git a/tests/codegen/models/test_class.py b/tests/codegen/models/test_class.py
index 46c69e69..7aa3cc98 100644
--- a/tests/codegen/models/test_class.py
+++ b/tests/codegen/models/test_class.py
@@ -144,6 +144,16 @@ class ClassTests(FactoryTestCase):
         obj.attrs.append(AttrFactory.create(tag=Tag.EXTENSION))
         self.assertFalse(obj.is_global_type)
 
+    def test_property_is_restricted(self):
+        obj = ClassFactory.create()
+        ext = ExtensionFactory.create(tag=Tag.EXTENSION)
+        obj.extensions.append(ext)
+
+        self.assertFalse(obj.is_restricted)
+
+        ext.tag = Tag.RESTRICTION
+        self.assertTrue(obj.is_restricted)
+
     def test_property_is_simple_type(self):
         obj = ClassFactory.elements(2)
 
diff --git a/tests/fixtures/models.py b/tests/fixtures/models.py
index a4fa9933..e358c14c 100644
--- a/tests/fixtures/models.py
+++ b/tests/fixtures/models.py
@@ -1,6 +1,6 @@
 from dataclasses import dataclass
 from dataclasses import field
-from typing import Dict
+from typing import Dict, Any
 from typing import List
 from typing import Optional
 from typing import Type
@@ -30,6 +30,7 @@ class TypeC:
     y: str
     z: float
     fixed: str = field(init=False, default="ignored")
+    restricted: Any = field(init=False, metadata={"type": "Ignore"})
 
 
 @dataclass