Repository: tefra/xsdata (python)

Issue:
When a schema overrides attributes (with use="required") this is not reflected in the output
Considering the example below.

```
	<xsd:element name="TimeDemandTypeAssignment" abstract="false" substitutionGroup="Assignment_">
		<xsd:annotation>
			<xsd:documentation>The assignment of a TIME DEMAND TYPE to a TIME BAND depending on the DAY TYPE and GROUP OF TIMING LINKS.</xsd:documentation>
		</xsd:annotation>
		<xsd:complexType>
			<xsd:complexContent>
				<xsd:restriction base="TimeDemandTypeAssignment_VersionStructure">
					<xsd:sequence>
						<xsd:sequence>
							<xsd:sequence>
								<xsd:group ref="EntityInVersionGroup" minOccurs="0"/>
							</xsd:sequence>
							<xsd:sequence>
								<xsd:group ref="DataManagedObjectGroup"/>
							</xsd:sequence>
							<xsd:sequence>
								<xsd:group ref="AssignmentGroup"/>
							</xsd:sequence>
						</xsd:sequence>
						<xsd:sequence>
							<xsd:group ref="TimeDemandTypeAssignmentGroup"/>
						</xsd:sequence>
					</xsd:sequence>
					<xsd:attribute name="id" type="TimeDemandTypeAssignmentIdType" use="required"/>
				</xsd:restriction>
			</xsd:complexContent>
		</xsd:complexType>
	</xsd:element>
```

Which ultimately resolves to:
```
        <xsd:complexType name="EntityStructure" abstract="true">
                <xsd:annotation>
                        <xsd:documentation>Type for ENTITY.</xsd:documentation>
                </xsd:annotation>
                <xsd:attribute name="nameOfClass" type="NameOfClass" use="optional">
                        <xsd:annotation>
                                <xsd:documentation>Name of Class of the ENTITY. Allows reflection. Fixed for each ENTITY type.</xsd:documentation>
                        </xsd:annotation>
                </xsd:attribute>
                <xsd:attribute name="id" type="ObjectIdType" use="optional">
                        <xsd:annotation>
                                <xsd:documentation>Identifier of ENTITY.</xsd:documentation>
                        </xsd:annotation>
                </xsd:attribute>
        </xsd:complexType>
```

What I notice is that the use="required" is never overriden.
```
<xsd:attribute name="id" type="TimeDemandTypeAssignmentIdType" use="required"/>
```

What I would like to see happening is overriding the field in the class that does not inherits its (ultimate) parents properties.
```
@dataclass(unsafe_hash=True)
class TimeDemandTypeAssignmentVersionStructure(AssignmentVersionStructure1):
    """
    Type for TIME DEMAND TYPE ASSIGNMENT.
    """
    class Meta:
        name = "TimeDemandTypeAssignment_VersionStructure"

    time_demand_type_ref: Optional[TimeDemandTypeRef] = field(
        default=None,
        metadata={
            "name": "TimeDemandTypeRef",
            "type": "Element",
            "namespace": "http://www.netex.org.uk/netex",
        }
    )
    timeband_ref: Optional[TimebandRef] = field(
        default=None,
        metadata={
            "name": "TimebandRef",
            "type": "Element",
            "namespace": "http://www.netex.org.uk/netex",
        }
    )
    group_of_timing_links_ref: Optional[GroupOfTimingLinksRef] = field(
        default=None,
        metadata={
            "name": "GroupOfTimingLinksRef",
            "type": "Element",
            "namespace": "http://www.netex.org.uk/netex",
        }
    )
   # The current generation does not add this:
    id: str = field(
         default=None,
         metadata={
             "type": "Attribute",
         }
    )
```

Pull request:
Fix use="required" for attributes in inner classes.
## 📒 Description

When an element has a restriction, it may _override_ an attribute's use-attribute, for example from optional to required. This patch reflects this change.

Resolves #819

## 🔗 What I've Done

> Write a description of the steps taken to resolve the issue

#819 contains the minimum XSD to check the generation. I basically step by step went though the generation process until I found that RESOLVE causes the attributes of 'test' to disappear.

## 💬 Comments

First _true_ contribution? ;-)

## 🛫 Checklist

- [ ] Updated docs
- [ ] Added unit-tests
- [ ] [Sample tests](https://github.com/tefra/xsdata-samples) pass
- [ ] [W3C tests](https://github.com/tefra/xsdata-w3c-tests) pass


Patch:
diff --git a/xsdata/codegen/handlers/calculate_attribute_paths.py b/xsdata/codegen/handlers/calculate_attribute_paths.py
index 0a1ed88b..93ca644e 100644
--- a/xsdata/codegen/handlers/calculate_attribute_paths.py
+++ b/xsdata/codegen/handlers/calculate_attribute_paths.py
@@ -36,8 +36,6 @@ class CalculateAttributePaths(HandlerInterface):
                 if not attr.restrictions.sequence:
                     attr.restrictions.sequence = index
             elif name == CHOICE:
-                if mi <= 1:
-                    mi = 0
                 if not attr.restrictions.choice:
                     attr.restrictions.choice = index
             elif name == GROUP:
diff --git a/xsdata/codegen/handlers/create_compound_fields.py b/xsdata/codegen/handlers/create_compound_fields.py
index 534115a0..d4432b5a 100644
--- a/xsdata/codegen/handlers/create_compound_fields.py
+++ b/xsdata/codegen/handlers/create_compound_fields.py
@@ -17,6 +17,11 @@ from xsdata.models.enums import DataType
 from xsdata.models.enums import Tag
 from xsdata.utils.collections import group_by
 
+ALL = "a"
+GROUP = "g"
+SEQUENCE = "s"
+CHOICE = "c"
+
 
 class CreateCompoundFields(RelativeHandlerInterface):
     """Group attributes that belong in the same choice and replace them by
@@ -35,13 +40,25 @@ class CreateCompoundFields(RelativeHandlerInterface):
             for choice, attrs in groups.items():
                 if choice and len(attrs) > 1:
                     self.group_fields(target, attrs)
+        else:
+            for attr in target.attrs:
+                if attr.restrictions.choice:
+                    self.calculate_choice_min_occurs(attr)
+
+    @classmethod
+    def calculate_choice_min_occurs(cls, attr: Attr):
+        for path in attr.restrictions.path:
+            name, index, mi, ma = path
+            if name == CHOICE and mi <= 1:
+                attr.restrictions.min_occurs = 0
 
     @classmethod
     def update_counters(cls, attr: Attr, counters: Dict):
         started = False
         choice = attr.restrictions.choice
         for path in attr.restrictions.path:
-            if not started and path[0] != "c" and path[1] != choice:
+            name, index, mi, ma = path
+            if not started and name != CHOICE and index != choice:
                 continue
 
             started = True
@@ -49,6 +66,9 @@ class CreateCompoundFields(RelativeHandlerInterface):
                 counters[path] = {"min": [], "max": []}
             counters = counters[path]
 
+            if mi <= 1:
+                attr.restrictions.min_occurs = 0
+
         counters["min"].append(attr.restrictions.min_occurs)
         counters["max"].append(attr.restrictions.max_occurs)
 
diff --git a/xsdata/codegen/handlers/validate_attributes_overrides.py b/xsdata/codegen/handlers/validate_attributes_overrides.py
index edef2247..9e93bfc9 100644
--- a/xsdata/codegen/handlers/validate_attributes_overrides.py
+++ b/xsdata/codegen/handlers/validate_attributes_overrides.py
@@ -60,6 +60,8 @@ class ValidateAttributesOverrides(RelativeHandlerInterface):
             and bool_eq(attr.mixed, source_attr.mixed)
             and bool_eq(attr.restrictions.tokens, source_attr.restrictions.tokens)
             and bool_eq(attr.restrictions.nillable, source_attr.restrictions.nillable)
+            and bool_eq(attr.is_prohibited, source_attr.is_prohibited)
+            and bool_eq(attr.is_optional, source_attr.is_optional)
         ):
             cls.remove_attribute(target, attr)
 


Test patch:
diff --git a/tests/codegen/handlers/test_calculate_attribute_paths.py b/tests/codegen/handlers/test_calculate_attribute_paths.py
index 97a6f394..cb334965 100644
--- a/tests/codegen/handlers/test_calculate_attribute_paths.py
+++ b/tests/codegen/handlers/test_calculate_attribute_paths.py
@@ -69,6 +69,13 @@ class CalculateAttributePathsTests(FactoryTestCase):
                         min_occurs=1,
                         max_occurs=1,
                         path=[("s", 1, 1, 1), ("c", 5, 2, 2)],
+                    ),
+                ),
+                AttrFactory.element(
+                    restrictions=Restrictions(
+                        min_occurs=1,
+                        max_occurs=1,
+                        path=[("s", 1, 1, 1), ("c", 6, 1, 21)],
                     )
                 ),
             ]
@@ -99,5 +106,6 @@ class CalculateAttributePathsTests(FactoryTestCase):
             (1, None, 0, 1),
             (1, None, 2, 2),
             (1, None, 2, 2),
+            (1, None, 1, 21),
         ]
         self.assertEqual(expected, actual)
diff --git a/tests/codegen/handlers/test_create_compound_fields.py b/tests/codegen/handlers/test_create_compound_fields.py
index 197ff8c2..903adcda 100644
--- a/tests/codegen/handlers/test_create_compound_fields.py
+++ b/tests/codegen/handlers/test_create_compound_fields.py
@@ -45,6 +45,31 @@ class CreateCompoundFieldsTests(FactoryTestCase):
             ]
         )
 
+    def test_process_with_config_enabled_false_calculate_min_occurs(self):
+        self.processor.config.enabled = False
+        target = ClassFactory.elements(5)
+        target.attrs[0].restrictions.choice = 1
+        target.attrs[1].restrictions.choice = 1
+        target.attrs[2].restrictions.choice = 2
+        target.attrs[3].restrictions.choice = 2
+
+        for attr in target.attrs:
+            attr.restrictions.min_occurs = 2
+            attr.restrictions.max_occurs = 3
+
+        target.attrs[0].restrictions.path = [("g", 0, 1, 1), ("c", 1, 2, 1)]
+        target.attrs[1].restrictions.path = [("g", 0, 1, 1), ("c", 1, 2, 1)]
+        target.attrs[2].restrictions.path = [("g", 0, 1, 1), ("c", 2, 1, 1)]
+        target.attrs[3].restrictions.path = [("g", 0, 1, 1), ("c", 2, 1, 1)]
+        self.processor.process(target)
+
+        actual = [
+            (attr.restrictions.min_occurs, attr.restrictions.max_occurs)
+            for attr in target.attrs
+        ]
+        expected = [(2, 3), (2, 3), (0, 3), (0, 3), (2, 3)]
+        self.assertEqual(expected, actual)
+
     def test_group_fields(self):
         target = ClassFactory.create(attrs=AttrFactory.list(4))
         target.attrs[0].restrictions.choice = 1
@@ -91,7 +116,7 @@ class CreateCompoundFieldsTests(FactoryTestCase):
                 ),
             ],
         )
-        expected_res = Restrictions(min_occurs=1, max_occurs=20)
+        expected_res = Restrictions(min_occurs=0, max_occurs=20)
 
         self.processor.group_fields(target, list(target.attrs))
         self.assertEqual(1, len(target.attrs))
@@ -241,3 +266,23 @@ class CreateCompoundFieldsTests(FactoryTestCase):
 
         result = self.processor.sum_counters(counters)
         self.assertEqual((0, 3), (sum(result[0]), sum(result[1])))
+
+    def test_update_counters(self):
+        attr = AttrFactory.create()
+        attr.restrictions.min_occurs = 2
+        attr.restrictions.max_occurs = 3
+        attr.restrictions.path = [("c", 0, 1, 1)]
+
+        counters = {}
+        self.processor.update_counters(attr, counters)
+
+        expected = {("c", 0, 1, 1): {"max": [3], "min": [0]}}
+        self.assertEqual(expected, counters)
+
+        attr.restrictions.min_occurs = 2
+        attr.restrictions.path = [("c", 0, 2, 1)]
+
+        counters = {}
+        self.processor.update_counters(attr, counters)
+        expected = {("c", 0, 2, 1): {"max": [3], "min": [2]}}
+        self.assertEqual(expected, counters)
diff --git a/tests/codegen/handlers/test_validate_attributes_overrides.py b/tests/codegen/handlers/test_validate_attributes_overrides.py
index 78da4350..af25d29d 100644
--- a/tests/codegen/handlers/test_validate_attributes_overrides.py
+++ b/tests/codegen/handlers/test_validate_attributes_overrides.py
@@ -102,12 +102,19 @@ class ValidateAttributesOverridesTests(FactoryTestCase):
         attr_b.fixed = attr_a.fixed
         attr_a.restrictions.tokens = not attr_b.restrictions.tokens
         attr_a.restrictions.nillable = not attr_b.restrictions.nillable
+        attr_a.restrictions.min_occurs = 0
+        attr_b.restrictions.min_occurs = 1
+        attr_a.restrictions.max_occurs = 0
+        attr_b.restrictions.max_occurs = 1
+
         self.processor.validate_override(target, attr_a, attr_b)
         self.assertEqual(1, len(target.attrs))
 
         # Restrictions are compatible again
         attr_a.restrictions.tokens = attr_b.restrictions.tokens
         attr_a.restrictions.nillable = attr_b.restrictions.nillable
+        attr_a.restrictions.min_occurs = attr_b.restrictions.min_occurs = 1
+        attr_a.restrictions.max_occurs = attr_b.restrictions.max_occurs = 1
         self.processor.validate_override(target, attr_a, attr_b)
         self.assertEqual(0, len(target.attrs))