Repository: tefra/xsdata
Pull request #784 (2023-05-07 16:10:15)

Update the secret sauce
## 📒 Description

xsdata started with the naive approach that schema designs would always avoid complicated scenarios. This worked great for a lot of schemas.

Unfortunately nested sequences/choices/groups was the analyzer's Achilles heal, as it couldn't really extract the correct min/max occurs and set required flags accordingly. Which is blocking any serious data validation...


There is still one more major part to complete this refactor!

## 🔗 What I've Done

- Store the container's path for each attribute/element
- Calculate max/min occurs by the containers paths 
- Update handlers to use the new paths to run calculations on repeatable sequences, effective choices and compound fields.


## 💬 Comments

> A place to write any comments to the reviewer.
>

## 🛫 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/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f7f22f7a..52a3792e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ exclude: tests/fixtures
 
 repos:
   - repo: https://github.com/asottile/pyupgrade
-    rev: v3.3.1
+    rev: v3.4.0
     hooks:
       - id: pyupgrade
         args: [ --py37-plus ]
@@ -15,7 +15,7 @@ repos:
     hooks:
       - id: black
   - repo: https://github.com/PyCQA/autoflake
-    rev: v2.1.0
+    rev: v2.1.1
     hooks:
       - id: autoflake
   - repo: https://github.com/PyCQA/flake8
@@ -36,7 +36,7 @@ repos:
       - id: end-of-file-fixer
       - id: debug-statements
   - repo: https://github.com/PyCQA/docformatter
-    rev: v1.5.1
+    rev: v1.6.5
     hooks:
       - id: docformatter
         args: [ "--in-place", "--pre-summary-newline" ]
diff --git a/benchmark.py b/benchmark.py
new file mode 100644
index 00000000..ab1c64f7
--- /dev/null
+++ b/benchmark.py
@@ -0,0 +1,59 @@
+import argparse
+import statistics
+from timeit import Timer
+
+from tests import xsdata_temp_dir
+from tests.integration.benchmarks.utils import make_books
+from tests.integration.benchmarks.utils import parse
+from tests.integration.benchmarks.utils import parse_json
+from tests.integration.benchmarks.utils import write
+from tests.integration.benchmarks.utils import write_json
+from xsdata.formats.dataclass.parsers import handlers
+from xsdata.formats.dataclass.parsers import JsonParser
+from xsdata.formats.dataclass.serializers import JsonSerializer
+from xsdata.formats.dataclass.serializers import writers
+
+if __name__ == "__main__":
+    components = [
+        "LxmlEventHandler",
+        "XmlEventHandler",
+        "LxmlEventWriter",
+        "XmlEventWriter",
+        "JsonParser",
+        "JsonSerializer",
+    ]
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-c", "--component", choices=components, required=True)
+    parser.add_argument("-n", "--number", default=1000, type=int)
+    parser.add_argument("-r", "--repeat", default=10, type=int)
+    args = parser.parse_args()
+
+    if args.component in writers.__all__:
+        component = getattr(writers, args.component)
+        books = make_books(args.number)
+        t = Timer(lambda: write(args.number, books, component))
+    elif args.component in handlers.__all__:
+        fixture = xsdata_temp_dir.joinpath(f"benchmark_{args.number}.xml")
+        if not fixture.exists():
+            write(args.number, make_books(args.number), writers.XmlEventWriter)
+
+        component = getattr(handlers, args.component)
+        t = Timer(lambda: parse(fixture.read_bytes(), component))
+    elif args.component == "JsonParser":
+        component = JsonParser
+        fixture = xsdata_temp_dir.joinpath(f"benchmark_{args.number}.json")
+        if not fixture.exists():
+            write_json(args.number, make_books(args.number))
+
+        t = Timer(lambda: parse_json(fixture.read_bytes()))
+    elif args.component == "JsonSerializer":
+        component = JsonSerializer
+        books = make_books(args.number)
+        t = Timer(lambda: write_json(args.number, books))
+
+    print(f"Benchmark {component.__name__} - n{args.number}/r{args.repeat}")
+    result = t.repeat(repeat=args.repeat, number=1)
+    print(f"avg {statistics.mean(result)}")
+    print(f"med {statistics.median(result)}")
+    print(f"stdev {statistics.stdev(result)}")
diff --git a/xsdata/cli.py b/xsdata/cli.py
index c36dc314..af9e40d0 100644
--- a/xsdata/cli.py
+++ b/xsdata/cli.py
@@ -43,7 +43,7 @@ logging.captureWarnings(True)
 @click.pass_context
 @click.version_option(__version__)
 def cli(ctx: click.Context, **kwargs: Any):
-    """xsdata command line interface."""
+    """Xsdata command line interface."""
     logger.setLevel(logging.INFO)
     formatwarning_orig = warnings.formatwarning
     logger.info(
diff --git a/xsdata/codegen/container.py b/xsdata/codegen/container.py
index f6cd78ac..617ecfef 100644
--- a/xsdata/codegen/container.py
+++ b/xsdata/codegen/container.py
@@ -5,6 +5,7 @@ from typing import List
 from typing import Optional
 
 from xsdata.codegen.handlers import AddAttributeSubstitutions
+from xsdata.codegen.handlers import CalculateAttributePaths
 from xsdata.codegen.handlers import CreateCompoundFields
 from xsdata.codegen.handlers import DesignateClassPackages
 from xsdata.codegen.handlers import FilterClasses
@@ -15,6 +16,8 @@ from xsdata.codegen.handlers import ProcessAttributeTypes
 from xsdata.codegen.handlers import ProcessMixedContentClass
 from xsdata.codegen.handlers import RenameDuplicateAttributes
 from xsdata.codegen.handlers import RenameDuplicateClasses
+from xsdata.codegen.handlers import ResetAttributeSequenceNumbers
+from xsdata.codegen.handlers import ResetAttributeSequences
 from xsdata.codegen.handlers import SanitizeAttributesDefaultValue
 from xsdata.codegen.handlers import SanitizeEnumerationClass
 from xsdata.codegen.handlers import UnnestInnerClasses
@@ -54,8 +57,10 @@ class ClassContainer(ContainerInterface):
                 FlattenAttributeGroups(self),
             ],
             Steps.FLATTEN: [
+                CalculateAttributePaths(),
                 FlattenClassExtensions(self),
                 SanitizeEnumerationClass(self),
+                UpdateAttributesEffectiveChoice(),
                 UnnestInnerClasses(self),
                 AddAttributeSubstitutions(self),
                 ProcessAttributeTypes(self),
@@ -63,7 +68,7 @@ class ClassContainer(ContainerInterface):
                 ProcessMixedContentClass(),
             ],
             Steps.SANITIZE: [
-                UpdateAttributesEffectiveChoice(),
+                ResetAttributeSequences(),
                 SanitizeAttributesDefaultValue(self),
             ],
             Steps.RESOLVE: [
@@ -73,6 +78,8 @@ class ClassContainer(ContainerInterface):
             Steps.FINALIZE: [
                 VacuumInnerClasses(),
                 CreateCompoundFields(self),
+                # Prettify things!!!
+                ResetAttributeSequenceNumbers(self),
             ],
         }
 
diff --git a/xsdata/codegen/handlers/__init__.py b/xsdata/codegen/handlers/__init__.py
index dd6521c6..e9497e64 100644
--- a/xsdata/codegen/handlers/__init__.py
+++ b/xsdata/codegen/handlers/__init__.py
@@ -1,4 +1,5 @@
 from .add_attribute_substitutions import AddAttributeSubstitutions
+from .calculate_attribute_paths import CalculateAttributePaths
 from .create_compound_fields import CreateCompoundFields
 from .designate_class_packages import DesignateClassPackages
 from .filter_classes import FilterClasses
@@ -9,6 +10,8 @@ from .process_attributes_types import ProcessAttributeTypes
 from .process_mixed_content_class import ProcessMixedContentClass
 from .rename_duplicate_attributes import RenameDuplicateAttributes
 from .rename_duplicate_classes import RenameDuplicateClasses
+from .reset_attribute_sequence_numbers import ResetAttributeSequenceNumbers
+from .reset_attribute_sequences import ResetAttributeSequences
 from .sanitize_attributes_default_value import SanitizeAttributesDefaultValue
 from .sanitize_enumeration_class import SanitizeEnumerationClass
 from .unnest_inner_classes import UnnestInnerClasses
@@ -18,6 +21,7 @@ from .validate_attributes_overrides import ValidateAttributesOverrides
 
 __all__ = [
     "AddAttributeSubstitutions",
+    "CalculateAttributePaths",
     "CreateCompoundFields",
     "DesignateClassPackages",
     "FilterClasses",
@@ -28,6 +32,8 @@ __all__ = [
     "ProcessMixedContentClass",
     "RenameDuplicateAttributes",
     "RenameDuplicateClasses",
+    "ResetAttributeSequences",
+    "ResetAttributeSequenceNumbers",
     "SanitizeAttributesDefaultValue",
     "SanitizeEnumerationClass",
     "UnnestInnerClasses",
diff --git a/xsdata/codegen/handlers/add_attribute_substitutions.py b/xsdata/codegen/handlers/add_attribute_substitutions.py
index 71ecf8fa..99d075ed 100644
--- a/xsdata/codegen/handlers/add_attribute_substitutions.py
+++ b/xsdata/codegen/handlers/add_attribute_substitutions.py
@@ -56,7 +56,10 @@ class AddAttributeSubstitutions(RelativeHandlerInterface):
             attr_type.substituted = True
             for substitution in self.substitutions.get(attr_type.qname, []):
                 attr.restrictions.min_occurs = 0
+
                 clone = ClassUtils.clone_attribute(substitution, attr.restrictions)
+                clone.restrictions.min_occurs = 0
+                clone.restrictions.max_occurs = attr.restrictions.max_occurs
 
                 pos = collections.find(target.attrs, clone)
                 index = pos + 1 if pos > -1 else index
diff --git a/xsdata/codegen/handlers/calculate_attribute_paths.py b/xsdata/codegen/handlers/calculate_attribute_paths.py
new file mode 100644
index 00000000..ce54e856
--- /dev/null
+++ b/xsdata/codegen/handlers/calculate_attribute_paths.py
@@ -0,0 +1,53 @@
+from xsdata.codegen.mixins import HandlerInterface
+from xsdata.codegen.models import Attr
+from xsdata.codegen.models import Class
+
+
+ALL = "a"
+GROUP = "g"
+SEQUENCE = "s"
+CHOICE = "c"
+
+
+class CalculateAttributePaths(HandlerInterface):
+    """Calculate min/max occurs and sequence/choice/group from the schema
+    path."""
+
+    __slots__ = ()
+
+    @classmethod
+    def process(cls, target: Class):
+        for attr in target.attrs:
+            if (
+                attr.restrictions.path
+                and not attr.is_attribute
+                and not attr.is_enumeration
+            ):
+                cls.process_attr_path(attr)
+
+    @classmethod
+    def process_attr_path(cls, attr: Attr):
+        min_occurs = 1
+        max_occurs = 1
+        for path in attr.restrictions.path:
+            name, index, mi, ma = path
+
+            if name == SEQUENCE:
+                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:
+                attr.restrictions.group = index
+
+            min_occurs *= mi
+            max_occurs *= ma
+
+        assert attr.restrictions.min_occurs is not None
+        assert attr.restrictions.max_occurs is not None
+
+        attr.restrictions.min_occurs *= min_occurs
+        attr.restrictions.max_occurs *= max_occurs
diff --git a/xsdata/codegen/handlers/create_compound_fields.py b/xsdata/codegen/handlers/create_compound_fields.py
index d7cfcb93..534115a0 100644
--- a/xsdata/codegen/handlers/create_compound_fields.py
+++ b/xsdata/codegen/handlers/create_compound_fields.py
@@ -1,8 +1,8 @@
 from collections import Counter
-from collections import defaultdict
 from typing import Dict
 from typing import List
 from typing import Set
+from typing import Tuple
 
 from xsdata.codegen.mixins import ContainerInterface
 from xsdata.codegen.mixins import RelativeHandlerInterface
@@ -36,32 +36,42 @@ class CreateCompoundFields(RelativeHandlerInterface):
                 if choice and len(attrs) > 1:
                     self.group_fields(target, attrs)
 
-        for index in range(len(target.attrs)):
-            self.reset_sequence(target, index)
+    @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:
+                continue
+
+            started = True
+            if path not in counters:
+                counters[path] = {"min": [], "max": []}
+            counters = counters[path]
+
+        counters["min"].append(attr.restrictions.min_occurs)
+        counters["max"].append(attr.restrictions.max_occurs)
 
     def group_fields(self, target: Class, attrs: List[Attr]):
         """Group attributes into a new compound field."""
         pos = target.attrs.index(attrs[0])
         choice = attrs[0].restrictions.choice
-        sum_occurs = choice and choice.startswith("effective_")
+
+        assert choice is not None
 
         names = []
         choices = []
-        min_occurs_groups: Dict[int, int] = defaultdict(int)
-        max_occurs_groups: Dict[int, int] = defaultdict(int)
+        counters: Dict = {"min": [], "max": []}
+
         for attr in attrs:
             ClassUtils.remove_attribute(target, attr)
             names.append(attr.local_name)
-
-            key = self.attr_group_key(attr)
-            min_occurs_groups[key] += attr.restrictions.min_occurs or 0
-            max_occurs_groups[key] += attr.restrictions.max_occurs or 0
-
             choices.append(self.build_attr_choice(attr))
 
+            self.update_counters(attr, counters)
+
+        min_occurs, max_occurs = self.sum_counters(counters)
         name = self.choose_name(target, names)
-        min_occurs = min_occurs_groups.values()
-        max_occurs = max_occurs_groups.values()
 
         target.attrs.insert(
             pos,
@@ -71,13 +81,29 @@ class CreateCompoundFields(RelativeHandlerInterface):
                 types=[AttrType(qname=str(DataType.ANY_TYPE), native=True)],
                 tag=Tag.CHOICE,
                 restrictions=Restrictions(
-                    min_occurs=sum(min_occurs) if sum_occurs else min(min_occurs),
-                    max_occurs=sum(max_occurs) if sum_occurs else max(max_occurs),
+                    min_occurs=sum(min_occurs),
+                    max_occurs=max(max_occurs) if choice > 0 else sum(max_occurs),
                 ),
                 choices=choices,
             ),
         )
 
+    def sum_counters(self, counters: Dict) -> Tuple[List[int], List[int]]:
+        min_occurs = counters.pop("min", [])
+        max_occurs = counters.pop("max", [])
+
+        for path, counter in counters.items():
+            mi, ma = self.sum_counters(counter)
+
+            if path[0] == "c":
+                min_occurs.append(min(mi))
+                max_occurs.append(max(ma))
+            else:
+                min_occurs.append(sum(mi))
+                max_occurs.append(sum(ma))
+
+        return min_occurs, max_occurs
+
     def choose_name(self, target: Class, names: List[str]) -> str:
         if (
             self.config.force_default_name
@@ -119,34 +145,8 @@ class CreateCompoundFields(RelativeHandlerInterface):
         return Attr(
             name=attr.local_name,
             namespace=attr.namespace,
-            default=attr.default,
             types=attr.types,
             tag=attr.tag,
             help=attr.help,
             restrictions=restrictions,
         )
-
-    @classmethod
-    def attr_group_key(cls, attr: Attr) -> int:
-        return attr.restrictions.group or attr.restrictions.sequence or id(attr)
-
-    @classmethod
-    def reset_sequence(cls, target: Class, index: int):
-        """Reset the attribute at the given index if it has no siblings with
-        the sequence restriction."""
-
-        attr = target.attrs[index]
-        before = target.attrs[index - 1] if index - 1 >= 0 else None
-        after = target.attrs[index + 1] if index + 1 < len(target.attrs) else None
-
-        if not attr.is_list:
-            attr.restrictions.sequence = None
-
-        if (
-            attr.restrictions.sequence is None
-            or (before and before.restrictions.sequence is not None)
-            or (after and after.restrictions.sequence is not None and after.is_list)
-        ):
-            return
-
-        attr.restrictions.sequence = None
diff --git a/xsdata/codegen/handlers/reset_attribute_sequence_numbers.py b/xsdata/codegen/handlers/reset_attribute_sequence_numbers.py
new file mode 100644
index 00000000..9a2214ac
--- /dev/null
+++ b/xsdata/codegen/handlers/reset_attribute_sequence_numbers.py
@@ -0,0 +1,37 @@
+from collections import defaultdict
+
+from xsdata.codegen.mixins import RelativeHandlerInterface
+from xsdata.codegen.models import Class
+
+
+class ResetAttributeSequenceNumbers(RelativeHandlerInterface):
+    """
+    Reset attributes sequence numbers.
+
+    Until now all sequence numbers point to the id of sequence class!!!
+    """
+
+    __slots__ = ()
+
+    def process(self, target: Class):
+        groups = defaultdict(list)
+        for attr in target.attrs:
+            if attr.restrictions.sequence:
+                groups[attr.restrictions.sequence].append(attr)
+
+        if groups:
+            next_sequence_number = self.find_next_sequence_number(target)
+            for attrs in groups.values():
+                for attr in attrs:
+                    attr.restrictions.sequence = next_sequence_number
+
+                next_sequence_number += 1
+
+    def find_next_sequence_number(self, target: Class) -> int:
+        return (
+            max(
+                (attr.restrictions.sequence or 0 for attr in self.base_attrs(target)),
+                default=0,
+            )
+            + 1
+        )
diff --git a/xsdata/codegen/handlers/reset_attribute_sequences.py b/xsdata/codegen/handlers/reset_attribute_sequences.py
new file mode 100644
index 00000000..87c3435f
--- /dev/null
+++ b/xsdata/codegen/handlers/reset_attribute_sequences.py
@@ -0,0 +1,38 @@
+from xsdata.codegen.mixins import HandlerInterface
+from xsdata.codegen.models import Attr
+from xsdata.codegen.models import Class
+from xsdata.codegen.models import get_restriction_sequence
+from xsdata.utils import collections
+
+
+class ResetAttributeSequences(HandlerInterface):
+    """Validate if fields are part of a repeatable sequence otherwise reset the
+    sequence flag."""
+
+    __slots__ = ()
+
+    def process(self, target: Class):
+        groups = collections.group_by(target.attrs, get_restriction_sequence)
+        for sequence, attrs in groups.items():
+            if not sequence:
+                continue
+
+            if len(attrs) == 1:
+                attrs[0].restrictions.sequence = None
+            else:
+                for attr in attrs:
+                    if not self.is_repeatable_sequence(attr):
+                        attr.restrictions.sequence = None
+
+    @classmethod
+    def is_repeatable_sequence(cls, attr: Attr) -> bool:
+        seq = attr.restrictions.sequence
+        if seq:
+            for path in attr.restrictions.path:
+                if path[0] == "s" and path[1] == seq:
+                    return path[3] > 1 if path else False
+
+                if path[3] > 1:
+                    return True
+
+        return False
diff --git a/xsdata/codegen/handlers/sanitize_attributes_default_value.py b/xsdata/codegen/handlers/sanitize_attributes_default_value.py
index 589e53ac..a8d5bc98 100644
--- a/xsdata/codegen/handlers/sanitize_attributes_default_value.py
+++ b/xsdata/codegen/handlers/sanitize_attributes_default_value.py
@@ -191,13 +191,7 @@ class SanitizeAttributesDefaultValue(RelativeHandlerInterface):
         return attr.default is not None and (
             attr.is_xsi_type
             or attr.is_list
-            or (
-                attr.is_optional
-                and (
-                    attr.restrictions.sequence is not None
-                    or attr.restrictions.choice is not None
-                )
-            )
+            or (not attr.is_attribute and attr.is_optional)
         )
 
     @classmethod
diff --git a/xsdata/codegen/handlers/update_attributes_effective_choice.py b/xsdata/codegen/handlers/update_attributes_effective_choice.py
index dda4b68a..c30cf746 100644
--- a/xsdata/codegen/handlers/update_attributes_effective_choice.py
+++ b/xsdata/codegen/handlers/update_attributes_effective_choice.py
@@ -1,27 +1,114 @@
+from collections import defaultdict
 from typing import List
+from typing import Tuple
 
 from xsdata.codegen.mixins import HandlerInterface
 from xsdata.codegen.models import Attr
 from xsdata.codegen.models import Class
+from xsdata.codegen.models import get_restriction_choice
+from xsdata.utils import collections
 
 
 class UpdateAttributesEffectiveChoice(HandlerInterface):
-    """Look for sequence lists and non list elements and set effective choice
-    group for compound fields."""
+    """
+    Look for fields that are repeated and mark them effectively as choices if
+    they are not part of symmetrical sequences.
+
+    valid eg: <a/><b/><a/> symmetrical sequence: <a/><b/><a/><b/>
+    """
 
     __slots__ = ()
 
     def process(self, target: Class):
-        groups: List[List[Attr]] = [[]]
-        for attr in target.attrs:
-            # If attr is sequence and is list or the group is not empty
-            if attr.restrictions.sequence is not None and (attr.is_list or groups[-1]):
-                groups[-1].append(attr)
-            elif groups[-1]:
-                groups.append([])
-
-        for idx, group in enumerate(groups):
-            total_lists = sum(attr.restrictions.is_list for attr in group)
-            if total_lists != len(group) and total_lists > 0:
-                for attr in group:
-                    attr.restrictions.choice = f"effective_{idx}"
+        if target.is_enumeration:
+            return
+
+        groups = self.group_repeating_attrs(target)
+        if groups:
+            groups = list(collections.connected_components(groups))
+            target.attrs = self.merge_attrs(target, groups)
+
+            self.reset_symmetrical_choices(target)
+
+    @classmethod
+    def reset_symmetrical_choices(cls, target: Class):
+        groups = collections.group_by(target.attrs, get_restriction_choice)
+        for choice, attrs in groups.items():
+            if choice is None or choice > 0:
+                continue
+
+            min_occurs = set()
+            max_occurs = set()
+            sequences = set()
+            for attr in attrs:
+                min_occurs.add(attr.restrictions.min_occurs)
+                max_occurs.add(attr.restrictions.max_occurs)
+
+                if attr.restrictions.sequence:
+                    sequences.add(attr.restrictions.sequence)
+
+            if len(min_occurs) == len(max_occurs) == len(sequences) == 1:
+                for attr in attrs:
+                    assert attr.restrictions.max_occurs is not None
+                    assert attr.restrictions.sequence is not None
+
+                    attr.restrictions.choice = None
+                    cls.reset_effective_choice(
+                        attr.restrictions.path,
+                        "s",
+                        attr.restrictions.sequence,
+                        attr.restrictions.max_occurs,
+                    )
+
+    @classmethod
+    def reset_effective_choice(
+        cls,
+        paths: List[Tuple[str, int, int, int]],
+        name: str,
+        index: int,
+        max_occur: int,
+    ):
+        for i, path in enumerate(paths):
+            if path[0] == name and path[1] == index and path[3] == 1:
+                new_path = (*path[:-1], max_occur)
+                paths[i] = new_path
+                break
+
+    @classmethod
+    def merge_attrs(cls, target: Class, groups: List[List[int]]) -> List[Attr]:
+        attrs = []
+
+        for index, attr in enumerate(target.attrs):
+            group = collections.find_connected_component(groups, index)
+
+            if group == -1:
+                attrs.append(attr)
+                continue
+
+            pos = collections.find(attrs, attr)
+            if pos == -1:
+                attr.restrictions.choice = (group * -1) - 1
+                attrs.append(attr)
+            else:
+                existing = attrs[pos]
+                assert existing.restrictions.min_occurs is not None
+                assert existing.restrictions.max_occurs is not None
+
+                existing.restrictions.min_occurs += attr.restrictions.min_occurs or 0
+                existing.restrictions.max_occurs += attr.restrictions.max_occurs or 0
+
+        return attrs
+
+    @classmethod
+    def group_repeating_attrs(cls, target: Class) -> List[List[int]]:
+        counters = defaultdict(list)
+        for index, attr in enumerate(target.attrs):
+            if not attr.is_attribute:
+                counters[attr.key].append(index)
+
+        groups = []
+        for x in counters.values():
+            if len(x) > 1:
+                groups.append(list(range(x[0], x[-1] + 1)))
+
+        return groups
diff --git a/xsdata/codegen/mappers/dtd.py b/xsdata/codegen/mappers/dtd.py
index 079e6dcc..0d3c8789 100644
--- a/xsdata/codegen/mappers/dtd.py
+++ b/xsdata/codegen/mappers/dtd.py
@@ -133,7 +133,7 @@ class DtdMapper:
             params = cls.build_occurs(content.occur)
             params.update(
                 {
-                    "choice": str(id(content)),
+                    "choice": id(content),
                     "min_occurs": 0,
                 }
             )
diff --git a/xsdata/codegen/mappers/element.py b/xsdata/codegen/mappers/element.py
index 41cae90a..f1cd364e 100644
--- a/xsdata/codegen/mappers/element.py
+++ b/xsdata/codegen/mappers/element.py
@@ -3,7 +3,6 @@ from collections import defaultdict
 from typing import Any
 from typing import List
 from typing import Optional
-from typing import Set
 
 from xsdata.codegen.models import Attr
 from xsdata.codegen.models import AttrType
@@ -66,8 +65,8 @@ class ElementMapper:
     def build_elements(
         cls, target: Class, element: AnyElement, namespace: Optional[str]
     ):
-        sequential_set = cls.sequence_names(element)
-        for child in element.children:
+        sequences = cls.sequential_groups(element)
+        for index, child in enumerate(element.children):
             if isinstance(child, AnyElement) and child.qname:
                 if child.tail:
                     target.mixed = True
@@ -79,15 +78,14 @@ class ElementMapper:
                 else:
                     attr_type = cls.build_attribute_type(child.qname, child.text)
 
-                sequence = 1 if child.qname in sequential_set else None
-
+                sequence = collections.find_connected_component(sequences, index)
                 cls.build_attribute(
                     target,
                     child.qname,
                     attr_type,
                     namespace,
                     Tag.ELEMENT,
-                    sequence,
+                    sequence + 1,
                 )
 
     @classmethod
@@ -128,7 +126,7 @@ class ElementMapper:
         attr_type: AttrType,
         parent_namespace: Optional[str] = None,
         tag: str = Tag.ELEMENT,
-        sequence: Optional[int] = None,
+        sequence: int = 0,
     ):
         namespace, name = split_qname(qname)
         namespace = cls.select_namespace(namespace, parent_namespace, tag)
@@ -136,7 +134,10 @@ class ElementMapper:
 
         attr = Attr(index=index, name=name, tag=tag, namespace=namespace)
         attr.types.append(attr_type)
-        attr.restrictions.sequence = sequence
+
+        if sequence:
+            attr.restrictions.path.append(("s", sequence, 1, sys.maxsize))
+
         attr.restrictions.min_occurs = 1
         attr.restrictions.max_occurs = 1
         cls.add_attribute(target, attr)
@@ -149,9 +150,7 @@ class ElementMapper:
             existing = target.attrs[pos]
             existing.restrictions.max_occurs = sys.maxsize
             existing.types.extend(attr.types)
-
-            if attr.restrictions.sequence is not None:
-                existing.restrictions.sequence = attr.restrictions.sequence
+            existing.types = collections.unique_sequence(existing.types, key="qname")
         else:
             target.attrs.append(attr)
 
@@ -171,22 +170,21 @@ class ElementMapper:
         return namespace
 
     @classmethod
-    def sequence_names(cls, element: AnyElement) -> Set[str]:
-        mapping = defaultdict(list)
-        names = [
-            child.qname
-            for child in element.children
-            if isinstance(child, AnyElement) and child.qname
-        ]
-        indices: Set[int] = set()
+    def sequential_groups(cls, element: AnyElement) -> List[List[int]]:
+        groups = cls.group_repeating_attrs(element)
+        return list(collections.connected_components(groups))
 
-        for index, name in enumerate(names):
-            mapping[name].append(index)
-
-        for pos in mapping.values():
-            total = len(pos)
+    @classmethod
+    def group_repeating_attrs(cls, element: AnyElement) -> List[List[int]]:
+        counters = defaultdict(list)
+        for index, child in enumerate(element.children):
+            if isinstance(child, AnyElement) and child.qname:
+                counters[child.qname].append(index)
 
-            if 1 < total < pos[-1] - pos[0] + 1:
-                indices.update(list(range(pos[0], pos[-1] + 1)))
+        groups = []
+        if len(counters) > 1:
+            for x in counters.values():
+                if len(x) > 1:
+                    groups.append(list(range(x[0], x[-1] + 1)))
 
-        return {names[index] for index in indices}
+        return groups
diff --git a/xsdata/codegen/mappers/schema.py b/xsdata/codegen/mappers/schema.py
index 01cef5e8..93d01cbb 100644
--- a/xsdata/codegen/mappers/schema.py
+++ b/xsdata/codegen/mappers/schema.py
@@ -146,8 +146,8 @@ class SchemaMapper:
             if child.is_property:
                 yield child, parent_restrictions
             else:
-                restrictions = parent_restrictions.clone()
-                restrictions.merge(Restrictions.from_element(child))
+                restrictions = Restrictions.from_element(child)
+                restrictions.merge(parent_restrictions)
                 yield from cls.element_children(child, restrictions)
 
     @classmethod
diff --git a/xsdata/codegen/models.py b/xsdata/codegen/models.py
index 8bdf65bf..3afdfc6b 100644
--- a/xsdata/codegen/models.py
+++ b/xsdata/codegen/models.py
@@ -10,6 +10,7 @@ from typing import Dict
 from typing import Iterator
 from typing import List
 from typing import Optional
+from typing import Tuple
 from typing import Type
 
 from xsdata.exceptions import CodeGenerationError
@@ -78,8 +79,9 @@ class Restrictions:
     sequence: Optional[int] = field(default=None)
     tokens: Optional[bool] = field(default=None)
     format: Optional[str] = field(default=None)
-    choice: Optional[str] = field(default=None)
+    choice: Optional[int] = field(default=None)
     group: Optional[int] = field(default=None)
+    path: List[Tuple[str, int, int, int]] = field(default_factory=list)
 
     @property
     def is_list(self) -> bool:
@@ -99,27 +101,18 @@ class Restrictions:
         """Update properties from another instance."""
         self.update(source)
 
-        min_occurs = source.min_occurs
-        max_occurs = source.max_occurs
-        is_list = max_occurs is not None and max_occurs > 1
+        self.path = source.path + self.path
+        self.sequence = self.sequence or source.sequence
+        self.choice = self.choice or source.choice
+        self.tokens = self.tokens or source.tokens
+        self.format = self.format or source.format
+        self.group = self.group or source.group
 
-        # Update the sequence number if new value is valid and restrictions indicate
-        # the field was and still is a list.
-        if source.sequence is not None and (is_list or not self.is_list):
-            self.sequence = source.sequence
+        if self.min_occurs is None and source.min_occurs is not None:
+            self.min_occurs = source.min_occurs
 
-        self.choice = source.choice or self.choice
-        self.tokens = source.tokens or self.tokens
-        self.format = source.format or self.format
-        self.group = source.group or self.group
-
-        # Update min occurs if current value is None or the new value is more than one.
-        if self.min_occurs is None or (min_occurs is not None and min_occurs != 1):
-            self.min_occurs = min_occurs
-
-        # Update max occurs if current value is None or the new value is more than one.
-        if self.max_occurs is None or (max_occurs is not None and max_occurs != 1):
-            self.max_occurs = max_occurs
+        if self.max_occurs is None and source.max_occurs is not None:
+            self.max_occurs = source.max_occurs
 
     def update(self, source: "Restrictions"):
         keys = (
@@ -161,7 +154,13 @@ class Restrictions:
             result["required"] = True
 
         for key, value in asdict(self).items():
-            if value is None or key in ("choice", "group", "min_occurs", "max_occurs"):
+            if value is None or key in (
+                "choice",
+                "group",
+                "min_occurs",
+                "max_occurs",
+                "path",
+            ):
                 continue
 
             if key.endswith("clusive") and types:
@@ -266,6 +265,10 @@ class Attr:
     def __post_init__(self):
         self.local_name = self.name
 
+    @property
+    def key(self) -> str:
+        return f"{self.tag}.{self.namespace}.{self.local_name}"
+
     @property
     def is_attribute(self) -> bool:
         """Return whether this attribute is derived from an xs:attribute or
@@ -641,6 +644,7 @@ get_name = operator.attrgetter("name")
 get_qname = operator.attrgetter("qname")
 get_tag = operator.attrgetter("tag")
 get_restriction_choice = operator.attrgetter("restrictions.choice")
+get_restriction_sequence = operator.attrgetter("restrictions.sequence")
 get_slug = operator.attrgetter("slug")
 get_target_namespace = operator.attrgetter("target_namespace")
 is_enumeration = operator.attrgetter("is_enumeration")
diff --git a/xsdata/codegen/parsers/schema.py b/xsdata/codegen/parsers/schema.py
index e1bddd83..ba325bde 100644
--- a/xsdata/codegen/parsers/schema.py
+++ b/xsdata/codegen/parsers/schema.py
@@ -28,7 +28,7 @@ class SchemaParser(UserXmlParser):
     A simple parser to convert an xsd schema to an easy to handle data
     structure based on dataclasses.
 
-    The parser is a dummy as possible but it will try to normalize
+    The parser is as a dummy as possible, but it will try to normalize
     certain things like apply parent properties to children.
 
     :param location:
diff --git a/xsdata/codegen/transformer.py b/xsdata/codegen/transformer.py
index 69721afe..79d3fb7b 100644
--- a/xsdata/codegen/transformer.py
+++ b/xsdata/codegen/transformer.py
@@ -106,6 +106,8 @@ class SchemaTransformer:
     def process(self, uris: List[str], cache: bool = False):
         cache_file = self.get_cache_file(uris) if cache else None
         if cache_file and cache_file.exists():
+            logger.info(f"Loading from cache {cache_file}")
+
             self.classes = pickle.loads(cache_file.read_bytes())
         else:
             self.process_sources(uris)
diff --git a/xsdata/codegen/utils.py b/xsdata/codegen/utils.py
index c0a97786..d9e1f281 100644
--- a/xsdata/codegen/utils.py
+++ b/xsdata/codegen/utils.py
@@ -96,7 +96,6 @@ class ClassUtils:
 
         for source_attr in source.attrs:
             clone = cls.clone_attribute(source_attr, attr.restrictions)
-            clone.restrictions.group = id(attr)
             target.attrs.insert(index, clone)
             index += 1
 
diff --git a/xsdata/formats/dataclass/serializers/code.py b/xsdata/formats/dataclass/serializers/code.py
index 0522a8b6..4d04c3e2 100644
--- a/xsdata/formats/dataclass/serializers/code.py
+++ b/xsdata/formats/dataclass/serializers/code.py
@@ -71,10 +71,8 @@ class PycodeSerializer(AbstractSerializer):
         for tp in types:
             module = tp.__module__
             name = tp.__qualname__
-            if module == "builtins" or "." in name:
-                continue
-
-            imports.append(f"from {module} import {name}\n")
+            if module != "builtins" and "." not in name:
+                imports.append(f"from {module} import {name}\n")
 
         return "".join(sorted(imports))
 
diff --git a/xsdata/formats/dataclass/serializers/xml.py b/xsdata/formats/dataclass/serializers/xml.py
index 3122a1a6..504af7aa 100644
--- a/xsdata/formats/dataclass/serializers/xml.py
+++ b/xsdata/formats/dataclass/serializers/xml.py
@@ -340,10 +340,10 @@ class XmlSerializer(AbstractSerializer):
                 index += 1
                 continue
 
-            indices = range(index + 1, stop)
-            end = next((i for i in indices if attrs[i].sequence is None), stop)
-            sequence = attrs[index:end]
-            index = end
+            indices = range(index, stop)
+            end = next(i for i in indices[::-1] if attrs[i].sequence == var.sequence)
+            sequence = attrs[index : end + 1]
+            index = end + 1
             j = 0
 
             rolling = True
@@ -351,12 +351,17 @@ class XmlSerializer(AbstractSerializer):
                 rolling = False
                 for var in sequence:
                     values = getattr(obj, var.name)
-                    if j < len(values):
+                    if collections.is_array(values):
+                        if j < len(values):
+                            rolling = True
+                            value = values[j]
+                            if value is not None or var.nillable:
+                                yield var, value
+                    elif j == 0:
                         rolling = True
-                        value = values[j]
+                        if values is not None or var.nillable:
+                            yield var, values
 
-                        if value is not None or var.nillable:
-                            yield var, value
                 j += 1
 
     @classmethod
diff --git a/xsdata/models/xsd.py b/xsdata/models/xsd.py
index 2499e419..f8ede51c 100644
--- a/xsdata/models/xsd.py
+++ b/xsdata/models/xsd.py
@@ -441,8 +441,7 @@ class All(AnnotationBase):
         max_occurs = sys.maxsize if self.max_occurs == "unbounded" else self.max_occurs
 
         return {
-            "min_occurs": self.min_occurs,
-            "max_occurs": max_occurs,
+            "path": [("a", id(self), self.min_occurs, max_occurs)],
         }
 
 
@@ -472,9 +471,7 @@ class Sequence(AnnotationBase):
         max_occurs = sys.maxsize if self.max_occurs == "unbounded" else self.max_occurs
 
         return {
-            "sequence": self.index,
-            "min_occurs": self.min_occurs,
-            "max_occurs": max_occurs,
+            "path": [("s", id(self), self.min_occurs, max_occurs)],
         }
 
 
@@ -501,13 +498,10 @@ class Choice(AnnotationBase):
     any: Array["Any"] = array_element()
 
     def get_restrictions(self) -> Dict[str, Anything]:
-        min_occurs = self.min_occurs if self.min_occurs > 1 else 0
         max_occurs = sys.maxsize if self.max_occurs == "unbounded" else self.max_occurs
 
         return {
-            "choice": str(id(self)),
-            "min_occurs": min_occurs,
-            "max_occurs": max_occurs,
+            "path": [("c", id(self), self.min_occurs, max_occurs)],
         }
 
 
@@ -546,8 +540,7 @@ class Group(AnnotationBase):
         max_occurs = sys.maxsize if self.max_occurs == "unbounded" else self.max_occurs
 
         return {
-            "min_occurs": self.min_occurs,
-            "max_occurs": max_occurs,
+            "path": [("g", id(self), self.min_occurs, max_occurs)],
         }
 
 
@@ -1053,8 +1046,7 @@ class Alternative(AnnotationBase):
 
     def get_restrictions(self) -> Dict[str, Anything]:
         return {
-            "choice": str(id(self)),
-            "min_occurs": 0,
+            "path": [("alt", id(self), 0, 1)],
         }
 
 
diff --git a/xsdata/utils/collections.py b/xsdata/utils/collections.py
index a8526265..9ac3f858 100644
--- a/xsdata/utils/collections.py
+++ b/xsdata/utils/collections.py
@@ -7,16 +7,17 @@ from typing import Iterator
 from typing import List
 from typing import Optional
 from typing import Sequence
+from typing import Set
 from typing import TypeVar
 
 T = TypeVar("T")
 
 
 def is_array(value: Any) -> bool:
-    if isinstance(value, (list, tuple, set)):
+    if isinstance(value, tuple):
         return not hasattr(value, "_fields")
 
-    return False
+    return isinstance(value, (list, set, frozenset))
 
 
 def unique_sequence(items: Iterable[T], key: Optional[str] = None) -> List[T]:
@@ -77,3 +78,37 @@ def first(items: Iterator[T]) -> Optional[T]:
 def prepend(target: List, *args: Any):
     """Prepend items to the target list."""
     target[:0] = args
+
+
+def connected_components(lists: List[List[Any]]) -> Iterator[List[Any]]:
+    """
+    Merge lists of lists that share common elements.
+
+    https://stackoverflow.com/questions/4842613/merge-lists-that-share-
+    common-elements
+    """
+    neighbors = defaultdict(set)
+    for each in lists:
+        for item in each:
+            neighbors[item].update(each)
+
+    def component(node: Any, neigh: Dict[Any, Set], see: Set[Any]):
+        nodes = {node}
+        while nodes:
+            next_node = nodes.pop()
+            see.add(next_node)
+            nodes |= neigh[next_node] - see
+            yield next_node
+
+    seen: Set[Any] = set()
+    for item in neighbors:
+        if item not in seen:
+            yield sorted(component(item, neighbors, seen))
+
+
+def find_connected_component(groups: List[List[Any]], value: Any) -> int:
+    for index, group in enumerate(groups):
+        if value in group:
+            return index
+
+    return -1


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 927bca36..c9419262 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -90,16 +90,16 @@ jobs:
       - uses: actions/checkout@v3
       - uses: actions/setup-python@v4
         with:
-          python-version: '3.7'
+          python-version: '3.9'
       - name: Install pip install .
         run: |
           pip install -e .
       - name: Verify
         run: |
-          python tests/integration/benchmarks/conftest.py -c XmlEventWriter
-          python tests/integration/benchmarks/conftest.py -c XmlEventHandler
-          python tests/integration/benchmarks/conftest.py -c JsonParser
-          python tests/integration/benchmarks/conftest.py -c JsonSerializer
+          python benchmark.py -c XmlEventWriter
+          python benchmark.py -c XmlEventHandler
+          python benchmark.py -c JsonParser
+          python benchmark.py -c JsonSerializer
           xsdata | xargs -0 python -c "import sys; assert 'Install cli' in sys.argv[1]"
   pre-commit:
     name: Pre-commit Hooks
diff --git a/tests/codegen/handlers/test_calculate_attribute_paths.py b/tests/codegen/handlers/test_calculate_attribute_paths.py
new file mode 100644
index 00000000..828111bd
--- /dev/null
+++ b/tests/codegen/handlers/test_calculate_attribute_paths.py
@@ -0,0 +1,103 @@
+from xsdata.codegen.handlers import CalculateAttributePaths
+from xsdata.codegen.models import Restrictions
+from xsdata.utils.testing import AttrFactory
+from xsdata.utils.testing import ClassFactory
+from xsdata.utils.testing import FactoryTestCase
+
+
+class CalculateAttributePathsTests(FactoryTestCase):
+    def setUp(self):
+        super().setUp()
+
+        self.processor = CalculateAttributePaths()
+
+    def test_process(self):
+        target = ClassFactory.create(
+            attrs=[
+                AttrFactory.enumeration(),
+                AttrFactory.attribute(),
+                AttrFactory.element(),
+                AttrFactory.element(
+                    restrictions=Restrictions(
+                        min_occurs=1,
+                        max_occurs=1,
+                        path=[("s", 1, 1, 1), ("s", 2, 1, 2)],
+                    )
+                ),
+                AttrFactory.element(
+                    restrictions=Restrictions(
+                        min_occurs=1, max_occurs=1, path=[("s", 1, 1, 1)]
+                    )
+                ),
+                AttrFactory.element(
+                    restrictions=Restrictions(
+                        min_occurs=1,
+                        max_occurs=1,
+                        path=[("s", 1, 1, 1), ("g", 3, 1, 1), ("g", 4, 1, 2)],
+                    )
+                ),
+                AttrFactory.element(
+                    restrictions=Restrictions(
+                        min_occurs=1,
+                        max_occurs=1,
+                        path=[("s", 1, 1, 1), ("g", 3, 1, 1)],
+                    )
+                ),
+                AttrFactory.element(
+                    restrictions=Restrictions(
+                        min_occurs=1,
+                        max_occurs=1,
+                        path=[("s", 1, 1, 1), ("c", 4, 0, 1)],
+                    )
+                ),
+                AttrFactory.element(
+                    restrictions=Restrictions(
+                        min_occurs=1,
+                        max_occurs=1,
+                        path=[("s", 1, 1, 1), ("c", 4, 0, 1)],
+                    )
+                ),
+                AttrFactory.element(
+                    restrictions=Restrictions(
+                        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", 5, 2, 2)],
+                    )
+                ),
+            ]
+        )
+        self.processor.process(target)
+
+        actual = []
+        for attr in target.attrs:
+            restrictions = attr.restrictions
+            actual.append(
+                (
+                    restrictions.sequence,
+                    restrictions.group,
+                    restrictions.min_occurs,
+                    restrictions.max_occurs,
+                )
+            )
+
+        expected = [
+            (None, None, None, None),
+            (None, None, None, None),
+            (None, None, None, None),
+            (1, None, 1, 2),
+            (1, None, 1, 1),
+            (1, 4, 1, 2),
+            (1, 3, 1, 1),
+            (1, None, 0, 1),
+            (1, None, 0, 1),
+            (1, None, 2, 2),
+            (1, None, 2, 2),
+        ]
+        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 6a1c9198..197ff8c2 100644
--- a/tests/codegen/handlers/test_create_compound_fields.py
+++ b/tests/codegen/handlers/test_create_compound_fields.py
@@ -2,7 +2,6 @@ from unittest import mock
 
 from xsdata.codegen.container import ClassContainer
 from xsdata.codegen.handlers import CreateCompoundFields
-from xsdata.codegen.models import Class
 from xsdata.codegen.models import Restrictions
 from xsdata.models.config import GeneratorConfig
 from xsdata.models.enums import DataType
@@ -48,8 +47,8 @@ class CreateCompoundFieldsTests(FactoryTestCase):
 
     def test_group_fields(self):
         target = ClassFactory.create(attrs=AttrFactory.list(4))
-        target.attrs[0].restrictions.choice = "1"
-        target.attrs[1].restrictions.choice = "1"
+        target.attrs[0].restrictions.choice = 1
+        target.attrs[1].restrictions.choice = 1
         target.attrs[0].restrictions.min_occurs = 10
         target.attrs[0].restrictions.max_occurs = 15
         target.attrs[1].restrictions.min_occurs = 5
@@ -58,8 +57,11 @@ class CreateCompoundFieldsTests(FactoryTestCase):
         target.attrs[2].restrictions.max_occurs = 4
         target.attrs[3].restrictions.min_occurs = 1
         target.attrs[3].restrictions.max_occurs = 3
-        target.attrs[2].restrictions.group = 1
-        target.attrs[3].restrictions.group = 1
+
+        target.attrs[0].restrictions.path = [("g", 0, 1, 1), ("c", 1, 1, 1)]
+        target.attrs[1].restrictions.path = [("g", 0, 1, 1), ("c", 1, 1, 1)]
+        target.attrs[2].restrictions.path = [("g", 0, 1, 1), ("c", 1, 1, 1)]
+        target.attrs[3].restrictions.path = [("g", 0, 1, 1), ("c", 1, 1, 1)]
 
         expected = AttrFactory.create(
             name="choice",
@@ -89,7 +91,7 @@ class CreateCompoundFieldsTests(FactoryTestCase):
                 ),
             ],
         )
-        expected_res = Restrictions(min_occurs=3, max_occurs=20)
+        expected_res = Restrictions(min_occurs=1, max_occurs=20)
 
         self.processor.group_fields(target, list(target.attrs))
         self.assertEqual(1, len(target.attrs))
@@ -98,8 +100,8 @@ class CreateCompoundFieldsTests(FactoryTestCase):
 
     def test_group_fields_with_effective_choices_sums_occurs(self):
         target = ClassFactory.create(attrs=AttrFactory.list(2))
-        target.attrs[0].restrictions.choice = "effective_1"
-        target.attrs[1].restrictions.choice = "effective_1"
+        target.attrs[0].restrictions.choice = -1
+        target.attrs[1].restrictions.choice = -1
         target.attrs[0].restrictions.min_occurs = 1
         target.attrs[0].restrictions.max_occurs = 2
         target.attrs[1].restrictions.min_occurs = 3
@@ -201,55 +203,41 @@ class CreateCompoundFieldsTests(FactoryTestCase):
 
         self.assertEqual(attr.local_name, actual.name)
         self.assertEqual(attr.namespace, actual.namespace)
-        self.assertEqual(attr.default, actual.default)
+        self.assertIsNone(actual.default)
         self.assertEqual(attr.tag, actual.tag)
         self.assertEqual(attr.types, actual.types)
         self.assertEqual(expected_res, actual.restrictions)
         self.assertEqual(attr.help, actual.help)
         self.assertFalse(actual.fixed)
 
-    def test_attr_group_key(self):
-        attr = AttrFactory.create()
-        self.assertEqual(id(attr), self.processor.attr_group_key(attr))
-
-        attr.restrictions.sequence = 1
-        self.assertEqual(1, self.processor.attr_group_key(attr))
-
-        attr.restrictions.group = 2
-        self.assertEqual(2, self.processor.attr_group_key(attr))
-
-    def test_reset_sequence(self):
-        def len_sequence(target: Class):
-            return len([attr for attr in target.attrs if attr.restrictions.sequence])
-
-        restrictions = Restrictions(max_occurs=2, sequence=1)
-        target = ClassFactory.create(
-            attrs=[
-                AttrFactory.create(restrictions=restrictions.clone()),
-                AttrFactory.create(restrictions=restrictions.clone()),
-            ]
-        )
-
-        attrs_clone = [attr.clone() for attr in target.attrs]
-
-        self.processor.config.enabled = False
-        self.processor.reset_sequence(target, 0)
-        self.assertEqual(2, len_sequence(target))
-
-        target.attrs[0].restrictions.sequence = None
-        self.processor.reset_sequence(target, 0)
-        self.assertEqual(1, len_sequence(target))
-
-        self.processor.reset_sequence(target, 1)
-        self.assertEqual(0, len_sequence(target))
-
-        target.attrs = attrs_clone
-        target.attrs[1].restrictions.sequence = None
-        self.processor.reset_sequence(target, 0)
-        self.assertEqual(0, len_sequence(target))
-
-        target.attrs[0].restrictions.sequence = 1
-        target.attrs[0].restrictions.max_occurs = 0
-        target.attrs[1].restrictions.sequence = 1
-        self.processor.reset_sequence(target, 0)
-        self.assertEqual(1, len_sequence(target))
+    def test_sum_counters(self):
+        counters = {
+            ("g", 184, 1, 1): {
+                "min": [],
+                "max": [],
+                ("s", 183, 1, 1): {
+                    "min": [],
+                    "max": [],
+                    ("g", 185, 1, 1): {
+                        "min": [],
+                        "max": [],
+                        ("s", 188, 1, 1): {
+                            "min": [],
+                            "max": [],
+                            ("c", 192, 1, 1): {
+                                "min": [0],
+                                "max": [1],
+                                ("s", 193, 1, 1): {
+                                    "min": [0, 0],
+                                    "max": [1, 1],
+                                    ("c", 200, 1, 1): {"min": [0, 0], "max": [1, 1]},
+                                },
+                            },
+                        },
+                    },
+                },
+            }
+        }
+
+        result = self.processor.sum_counters(counters)
+        self.assertEqual((0, 3), (sum(result[0]), sum(result[1])))
diff --git a/tests/codegen/handlers/test_reset_attribute_sequence_numbers.py b/tests/codegen/handlers/test_reset_attribute_sequence_numbers.py
new file mode 100644
index 00000000..3db7fe4c
--- /dev/null
+++ b/tests/codegen/handlers/test_reset_attribute_sequence_numbers.py
@@ -0,0 +1,81 @@
+from xsdata.codegen.container import ClassContainer
+from xsdata.codegen.container import Steps
+from xsdata.codegen.handlers import ResetAttributeSequenceNumbers
+from xsdata.codegen.models import Restrictions
+from xsdata.codegen.models import Status
+from xsdata.models.config import GeneratorConfig
+from xsdata.utils.testing import AttrFactory
+from xsdata.utils.testing import ClassFactory
+from xsdata.utils.testing import ExtensionFactory
+from xsdata.utils.testing import FactoryTestCase
+
+
+class ResetAttributeSequencesTests(FactoryTestCase):
+    def setUp(self):
+        super().setUp()
+
+        self.config = GeneratorConfig()
+        self.container = ClassContainer(config=self.config)
+        self.container.step = Steps.FINALIZE
+        self.processor = ResetAttributeSequenceNumbers(container=self.container)
+
+    def test_process_without_sequence_fields(self):
+        target = ClassFactory.elements(2)
+        self.processor.process(target)
+
+    def test_process_without_parent_class(self):
+        target = ClassFactory.create(
+            attrs=[
+                AttrFactory.create(restrictions=Restrictions(sequence=100)),
+                AttrFactory.create(restrictions=Restrictions(sequence=100)),
+                AttrFactory.create(),
+                AttrFactory.create(restrictions=Restrictions(sequence=101)),
+                AttrFactory.create(restrictions=Restrictions(sequence=101)),
+            ]
+        )
+        self.processor.process(target)
+
+        actual = [x.restrictions.sequence for x in target.attrs]
+        expected = [1, 1, None, 2, 2]
+        self.assertEqual(expected, actual)
+
+    def test_process_with_parent_classes(self):
+        target = ClassFactory.create(
+            status=Status.FINALIZING,
+            attrs=[
+                AttrFactory.create(restrictions=Restrictions(sequence=100)),
+                AttrFactory.create(restrictions=Restrictions(sequence=100)),
+                AttrFactory.create(),
+                AttrFactory.create(restrictions=Restrictions(sequence=101)),
+                AttrFactory.create(restrictions=Restrictions(sequence=101)),
+            ],
+        )
+
+        parent_a = ClassFactory.create(
+            status=Status.RESOLVED,
+            attrs=[
+                AttrFactory.create(restrictions=Restrictions(sequence=102)),
+            ],
+        )
+
+        parent_b = ClassFactory.create(
+            status=Status.RESOLVED,
+            attrs=[
+                AttrFactory.create(restrictions=Restrictions(sequence=103)),
+            ],
+        )
+
+        parent_a.extensions.append(ExtensionFactory.reference(parent_b.qname))
+        target.extensions.append(ExtensionFactory.reference(parent_a.qname))
+
+        self.container.add(target)
+        self.container.add(parent_a)
+        self.container.add(parent_b)
+        self.processor.process(target)
+
+        actual = [x.restrictions.sequence for x in target.attrs]
+        expected = [3, 3, None, 4, 4]
+        self.assertEqual(expected, actual)
+
+        self.assertEqual(2, parent_a.attrs[0].restrictions.sequence)
+        self.assertEqual(1, parent_b.attrs[0].restrictions.sequence)
diff --git a/tests/codegen/handlers/test_reset_attribute_sequences.py b/tests/codegen/handlers/test_reset_attribute_sequences.py
new file mode 100644
index 00000000..fb65c810
--- /dev/null
+++ b/tests/codegen/handlers/test_reset_attribute_sequences.py
@@ -0,0 +1,57 @@
+from xsdata.codegen.handlers import ResetAttributeSequences
+from xsdata.codegen.models import Restrictions
+from xsdata.utils.testing import AttrFactory
+from xsdata.utils.testing import ClassFactory
+from xsdata.utils.testing import FactoryTestCase
+
+
+class ResetAttributeSequencesTests(FactoryTestCase):
+    def setUp(self):
+        super().setUp()
+
+        self.processor = ResetAttributeSequences()
+
+    def test_process(self):
+        target = ClassFactory.create(
+            attrs=[
+                AttrFactory.element(),
+                AttrFactory.element(restrictions=Restrictions(sequence=1)),
+                AttrFactory.element(
+                    restrictions=Restrictions(sequence=2, path=[("s", 2, 1, 1)]),
+                ),
+                AttrFactory.element(
+                    restrictions=Restrictions(sequence=2, path=[("s", 2, 1, 1)]),
+                ),
+                AttrFactory.element(
+                    restrictions=Restrictions(sequence=3, path=[("s", 3, 1, 1)]),
+                ),
+                # Effective choices
+                AttrFactory.element(
+                    restrictions=Restrictions(sequence=3, path=[("s", 3, 1, 2)]),
+                ),
+                AttrFactory.element(
+                    restrictions=Restrictions(sequence=3, path=[("s", 3, 1, 2)]),
+                ),
+                # Nested Group Sequence
+                AttrFactory.element(
+                    restrictions=Restrictions(
+                        sequence=4, path=[("g", 3, 1, 2), ("s", 4, 1, 1)]
+                    ),
+                ),
+                AttrFactory.element(
+                    restrictions=Restrictions(
+                        sequence=4, path=[("g", 3, 1, 2), ("s", 4, 1, 1)]
+                    ),
+                ),
+            ]
+        )
+
+        self.processor.process(target)
+
+        actual = [x.restrictions.sequence for x in target.attrs]
+        expected = [None, None, None, None, None, 3, 3, 4, 4]
+        self.assertEqual(expected, actual)
+
+    def test_is_repeatable_sequence_with_no_sequence(self):
+        attr = AttrFactory.create()
+        self.assertFalse(self.processor.is_repeatable_sequence(attr))
diff --git a/tests/codegen/handlers/test_sanitize_attributes_default_value.py b/tests/codegen/handlers/test_sanitize_attributes_default_value.py
index d5a4536b..ce5294ca 100644
--- a/tests/codegen/handlers/test_sanitize_attributes_default_value.py
+++ b/tests/codegen/handlers/test_sanitize_attributes_default_value.py
@@ -6,6 +6,7 @@ from xsdata.codegen.models import Restrictions
 from xsdata.models.config import GeneratorConfig
 from xsdata.models.enums import DataType
 from xsdata.models.enums import Namespace
+from xsdata.models.enums import Tag
 from xsdata.utils.testing import AttrFactory
 from xsdata.utils.testing import AttrTypeFactory
 from xsdata.utils.testing import ClassFactory
@@ -128,16 +129,19 @@ class SanitizeAttributesDefaultValueTests(FactoryTestCase):
         attr.default = "abc"
         self.assertTrue(self.processor.should_reset_default(attr))
 
-        attr = AttrFactory.create(
-            default="abc", restrictions=Restrictions(min_occurs=0)
+        attr = AttrFactory.element(
+            default="abc", restrictions=Restrictions(min_occurs=1)
         )
         self.assertFalse(self.processor.should_reset_default(attr))
-
-        attr.restrictions.sequence = 1
+        attr.restrictions.max_occurs = 2
         self.assertTrue(self.processor.should_reset_default(attr))
 
-        attr.restrictions.choice = "foo"
-        attr.restrictions.sequence = None
+        attr = AttrFactory.attribute(
+            default="abc", restrictions=Restrictions(min_occurs=0)
+        )
+        self.assertFalse(self.processor.should_reset_default(attr))
+
+        attr.tag = Tag.ELEMENT
         self.assertTrue(self.processor.should_reset_default(attr))
 
     @mock.patch(
diff --git a/tests/codegen/handlers/test_update_attributes_effective_choice.py b/tests/codegen/handlers/test_update_attributes_effective_choice.py
index 36f471e3..23567764 100644
--- a/tests/codegen/handlers/test_update_attributes_effective_choice.py
+++ b/tests/codegen/handlers/test_update_attributes_effective_choice.py
@@ -8,42 +8,120 @@ from xsdata.utils.testing import FactoryTestCase
 class UpdateAttributesEffectiveChoiceTests(FactoryTestCase):
     def setUp(self):
         super().setUp()
-
         self.processor = UpdateAttributesEffectiveChoice()
 
     def test_process(self):
-        target = ClassFactory.create()
-        attrs = [
-            AttrFactory.any(),
-            AttrFactory.any(),
-            # first group
-            AttrFactory.any(restrictions=Restrictions(sequence=1)),
-            AttrFactory.any(restrictions=Restrictions(sequence=1, max_occurs=2)),
-            AttrFactory.any(restrictions=Restrictions(sequence=1, max_occurs=2)),
-            # break attr
-            AttrFactory.any(),
-            # second group
-            AttrFactory.any(restrictions=Restrictions(sequence=1, max_occurs=2)),
-            AttrFactory.any(restrictions=Restrictions(sequence=1, max_occurs=1)),
-            AttrFactory.any(restrictions=Restrictions(sequence=1, max_occurs=2)),
+        target = ClassFactory.create(
+            attrs=[
+                AttrFactory.attribute(
+                    name="i",
+                    namespace="b",
+                    restrictions=Restrictions(min_occurs=1, max_occurs=1),
+                ),
+                AttrFactory.element(
+                    name="a",
+                    namespace="b",
+                    restrictions=Restrictions(min_occurs=1, max_occurs=1),
+                ),
+                AttrFactory.element(
+                    name="b",
+                    namespace="b",
+                    restrictions=Restrictions(min_occurs=1, max_occurs=1),
+                ),
+                AttrFactory.element(
+                    name="a",
+                    namespace="b",
+                    restrictions=Restrictions(min_occurs=1, max_occurs=1),
+                ),
+                AttrFactory.element(
+                    name="b",
+                    namespace="b",
+                    restrictions=Restrictions(min_occurs=1, max_occurs=1),
+                ),
+                AttrFactory.element(
+                    name="a",
+                    namespace="b",
+                    restrictions=Restrictions(min_occurs=1, max_occurs=1),
+                ),
+            ]
+        )
+
+        self.processor.process(target)
+
+        self.assertEqual(3, len(target.attrs))
+
+        actual = [
+            (
+                x.name,
+                x.restrictions.choice,
+                x.restrictions.min_occurs,
+                x.restrictions.max_occurs,
+            )
+            for x in target.attrs
         ]
-        target.attrs.extend(attrs)
+        expected = [("i", None, 1, 1), ("a", -1, 3, 3), ("b", -1, 2, 2)]
+        self.assertEqual(expected, actual)
+
+    def test_process_symmetrical_sequence(self):
+        restrictions = Restrictions(
+            sequence=1,
+            min_occurs=1,
+            max_occurs=1,
+            path=[("g", 0, 1, 1), ("s", 1, 1, 1)],
+        )
+        target = ClassFactory.create(
+            attrs=[
+                AttrFactory.element(
+                    name="a", namespace="b", restrictions=restrictions.clone()
+                ),
+                AttrFactory.element(
+                    name="b", namespace="b", restrictions=restrictions.clone()
+                ),
+                AttrFactory.element(
+                    name="a", namespace="b", restrictions=restrictions.clone()
+                ),
+                AttrFactory.element(
+                    name="b", namespace="b", restrictions=restrictions.clone()
+                ),
+                AttrFactory.element(name="c", namespace="b"),
+                AttrFactory.element(
+                    name="d", namespace="b", restrictions=Restrictions(choice=1)
+                ),
+            ]
+        )
 
         self.processor.process(target)
 
-        self.assertIsNone(attrs[0].restrictions.choice)
-        self.assertIsNone(attrs[1].restrictions.choice)
+        self.assertEqual(4, len(target.attrs))
 
-        # Part of the group but precedes list siblings
-        self.assertIsNone(attrs[2].restrictions.choice)
-        # Part of the group but both are lists
-        self.assertIsNone(attrs[3].restrictions.choice)
-        self.assertIsNone(attrs[4].restrictions.choice)
+        actual = [
+            (
+                x.name,
+                x.restrictions.choice,
+                x.restrictions.sequence,
+                x.restrictions.min_occurs,
+                x.restrictions.max_occurs,
+                x.restrictions.path,
+            )
+            for x in target.attrs
+        ]
+        expected = [
+            ("a", None, 1, 2, 2, [("g", 0, 1, 1), ("s", 1, 1, 2)]),
+            ("b", None, 1, 2, 2, [("g", 0, 1, 1), ("s", 1, 1, 2)]),
+            ("c", None, None, None, None, []),
+            ("d", 1, None, None, None, []),
+        ]
+        self.assertEqual(expected, actual)
 
-        # break attr
-        self.assertIsNone(attrs[5].restrictions.choice)
+    def test_process_enumeration(self):
+        target = ClassFactory.create(
+            attrs=[
+                AttrFactory.enumeration(name="a"),
+                AttrFactory.enumeration(name="a"),
+                AttrFactory.enumeration(name="a"),
+            ]
+        )
+
+        self.processor.process(target)
 
-        # Second group, mixed list non list sequential elements
-        self.assertEqual("effective_1", attrs[6].restrictions.choice)
-        self.assertEqual("effective_1", attrs[7].restrictions.choice)
-        self.assertEqual("effective_1", attrs[8].restrictions.choice)
+        self.assertEqual(3, len(target.attrs))
diff --git a/tests/codegen/mappers/test_dtd.py b/tests/codegen/mappers/test_dtd.py
index cb98fd6e..857ac114 100644
--- a/tests/codegen/mappers/test_dtd.py
+++ b/tests/codegen/mappers/test_dtd.py
@@ -288,7 +288,7 @@ class DtdMapperTests(FactoryTestCase):
         DtdMapper.build_content(target, content, nillable=True)
         self.assertEqual(2, len(target.attrs))
         for attr in target.attrs:
-            self.assertEqual(str(id(content)), attr.restrictions.choice)
+            self.assertEqual(id(content), attr.restrictions.choice)
             self.assertEqual(0, attr.restrictions.min_occurs)
             self.assertEqual(sys.maxsize, attr.restrictions.max_occurs)
 
diff --git a/tests/codegen/mappers/test_element.py b/tests/codegen/mappers/test_element.py
index 7188cc1f..a60f38f9 100644
--- a/tests/codegen/mappers/test_element.py
+++ b/tests/codegen/mappers/test_element.py
@@ -116,6 +116,90 @@ class ElementMapperTests(FactoryTestCase):
         actual.inner.clear()
         self.assertEqual(expected, actual)
 
+    def test_build_class_with_sequences(self):
+        element = AnyElement(
+            qname="root",
+            children=[
+                AnyElement(qname="a"),
+                AnyElement(qname="b"),
+                AnyElement(qname="a"),
+                AnyElement(qname="b"),
+                AnyElement(qname="c"),
+                AnyElement(qname="d"),
+                AnyElement(qname="e"),
+                AnyElement(qname="d"),
+            ],
+        )
+
+        actual = ElementMapper.build_class(element, "target")
+        expected = ClassFactory.create(
+            tag=Tag.ELEMENT,
+            qname="root",
+            namespace="",
+            location="",
+            module=None,
+            ns_map={},
+            attrs=[
+                AttrFactory.native(
+                    DataType.ANY_SIMPLE_TYPE,
+                    tag=Tag.ELEMENT,
+                    name="a",
+                    namespace="",
+                    index=0,
+                    restrictions=Restrictions(
+                        min_occurs=1,
+                        max_occurs=sys.maxsize,
+                        path=[("s", 1, 1, sys.maxsize)],
+                    ),
+                ),
+                AttrFactory.native(
+                    DataType.ANY_SIMPLE_TYPE,
+                    tag=Tag.ELEMENT,
+                    name="b",
+                    namespace="",
+                    index=1,
+                    restrictions=Restrictions(
+                        min_occurs=1,
+                        max_occurs=sys.maxsize,
+                        path=[("s", 1, 1, sys.maxsize)],
+                    ),
+                ),
+                AttrFactory.native(
+                    DataType.ANY_SIMPLE_TYPE,
+                    tag=Tag.ELEMENT,
+                    name="c",
+                    namespace="",
+                    index=2,
+                    restrictions=Restrictions(min_occurs=1, max_occurs=1),
+                ),
+                AttrFactory.native(
+                    DataType.ANY_SIMPLE_TYPE,
+                    tag=Tag.ELEMENT,
+                    name="d",
+                    namespace="",
+                    index=3,
+                    restrictions=Restrictions(
+                        min_occurs=1,
+                        max_occurs=sys.maxsize,
+                        path=[("s", 2, 1, sys.maxsize)],
+                    ),
+                ),
+                AttrFactory.native(
+                    DataType.ANY_SIMPLE_TYPE,
+                    tag=Tag.ELEMENT,
+                    name="e",
+                    namespace="",
+                    index=4,
+                    restrictions=Restrictions(
+                        min_occurs=1,
+                        max_occurs=sys.maxsize,
+                        path=[("s", 2, 1, sys.maxsize)],
+                    ),
+                ),
+            ],
+        )
+        self.assertEqual(expected, actual)
+
     def test_build_class_mixed_content(self):
         element = AnyElement(
             qname="{xsdata}root",
@@ -217,13 +301,10 @@ class ElementMapperTests(FactoryTestCase):
 
         self.assertEqual(2, len(target.attrs))
         self.assertEqual(sys.maxsize, target.attrs[0].restrictions.max_occurs)
-        self.assertEqual(2, len(target.attrs[0].types))
-        self.assertIsNone(target.attrs[0].restrictions.sequence)
+        self.assertEqual(1, len(target.attrs[0].types))
 
         attr = target.attrs[1].clone()
-        attr.restrictions.sequence = 1
         ElementMapper.add_attribute(target, attr)
-        self.assertEqual(1, target.attrs[1].restrictions.sequence)
 
         attr = AttrFactory.create()
         ElementMapper.add_attribute(target, attr)
@@ -238,25 +319,25 @@ class ElementMapperTests(FactoryTestCase):
         self.assertEqual("b", ElementMapper.select_namespace("b", "a", Tag.ATTRIBUTE))
         self.assertIsNone(ElementMapper.select_namespace(None, "a", Tag.ATTRIBUTE))
 
-    def test_sequence_names(self):
+    def test_sequential_groups(self):
         a = AnyElement(qname="a")
         b = AnyElement(qname="b")
         c = AnyElement(qname="c")
         d = AnyElement(qname="d")
         e = AnyElement(qname="e")
 
-        p = AnyElement(children=[a, b])
-        actual = ElementMapper.sequence_names(p)
-        self.assertEqual(0, len(actual))
+        p = AnyElement(children=[a, b, c, d])
+        actual = ElementMapper.sequential_groups(p)
+        self.assertEqual([], actual)
 
         p = AnyElement(children=[a, b, a])
-        actual = ElementMapper.sequence_names(p)
-        self.assertEqual({"a", "b"}, actual)
+        actual = ElementMapper.sequential_groups(p)
+        self.assertEqual([[0, 1, 2]], actual)
 
         p = AnyElement(children=[a, b, a, c])
-        actual = ElementMapper.sequence_names(p)
-        self.assertEqual({"a", "b"}, actual)
+        actual = ElementMapper.sequential_groups(p)
+        self.assertEqual([[0, 1, 2]], actual)
 
         p = AnyElement(children=[a, b, a, c, d, e, d])
-        actual = ElementMapper.sequence_names(p)
-        self.assertEqual({"a", "b", "d", "e"}, actual)
+        actual = ElementMapper.sequential_groups(p)
+        self.assertEqual([[0, 1, 2], [4, 5, 6]], actual)
diff --git a/tests/codegen/mappers/test_schema.py b/tests/codegen/mappers/test_schema.py
index 77380c18..95e83d21 100644
--- a/tests/codegen/mappers/test_schema.py
+++ b/tests/codegen/mappers/test_schema.py
@@ -238,7 +238,7 @@ class SchemaMapperTests(FactoryTestCase):
 
         child, restrictions = next(children)
         expected = Restrictions(
-            min_occurs=0, max_occurs=3, sequence=2, choice=str(id(choice))
+            path=[("s", id(complex_type.sequence), 0, 3), ("c", id(choice), 1, 1)]
         )
         self.assertEqual(expected, restrictions)
 
diff --git a/tests/codegen/models/test_attr.py b/tests/codegen/models/test_attr.py
index 6e20eb24..1e957cb9 100644
--- a/tests/codegen/models/test_attr.py
+++ b/tests/codegen/models/test_attr.py
@@ -29,6 +29,10 @@ class AttrTests(FactoryTestCase):
         attr.namespace = __file__
         self.assertNotEqual(attr, clone)
 
+    def test_property_key(self):
+        attr = AttrFactory.attribute(name="a", namespace="b")
+        self.assertEqual("Attribute.b.a", attr.key)
+
     def test_property_is_property(self):
         self.assertTrue(AttrFactory.attribute().is_attribute)
         self.assertTrue(AttrFactory.any_attribute().is_attribute)
diff --git a/tests/codegen/models/test_restrictions.py b/tests/codegen/models/test_restrictions.py
index b820ac03..7532a47a 100644
--- a/tests/codegen/models/test_restrictions.py
+++ b/tests/codegen/models/test_restrictions.py
@@ -22,6 +22,7 @@ class RestrictionsTests(TestCase):
             pattern=r"[A-Z]",
             explicit_timezone="+1",
             nillable=True,
+            path=[("s", 0, 1, 1)],
         )
 
     def test_property_is_list(self):
@@ -60,6 +61,21 @@ class RestrictionsTests(TestCase):
         child.merge(parent)
         self.assertFalse(child.nillable)
 
+    def test_merge_occurs(self):
+        a = Restrictions()
+        b = Restrictions()
+
+        a.merge(b)
+
+        self.assertIsNone(a.min_occurs)
+        self.assertIsNone(a.max_occurs)
+
+        b.min_occurs = 0
+        b.max_occurs = 1
+        a.merge(b)
+        self.assertEqual(0, a.min_occurs)
+        self.assertEqual(1, a.max_occurs)
+
     def test_asdict(self):
         expected = {
             "explicit_timezone": "+1",
diff --git a/tests/codegen/test_container.py b/tests/codegen/test_container.py
index bfe3d613..890579ad 100644
--- a/tests/codegen/test_container.py
+++ b/tests/codegen/test_container.py
@@ -41,17 +41,23 @@ class ClassContainerTests(FactoryTestCase):
                 "FlattenAttributeGroups",
             ],
             20: [
+                "CalculateAttributePaths",
                 "FlattenClassExtensions",
                 "SanitizeEnumerationClass",
+                "UpdateAttributesEffectiveChoice",
                 "UnnestInnerClasses",
                 "AddAttributeSubstitutions",
                 "ProcessAttributeTypes",
                 "MergeAttributes",
                 "ProcessMixedContentClass",
             ],
-            30: ["UpdateAttributesEffectiveChoice", "SanitizeAttributesDefaultValue"],
+            30: ["ResetAttributeSequences", "SanitizeAttributesDefaultValue"],
             40: ["ValidateAttributesOverrides", "RenameDuplicateAttributes"],
-            50: ["VacuumInnerClasses", "CreateCompoundFields"],
+            50: [
+                "VacuumInnerClasses",
+                "CreateCompoundFields",
+                "ResetAttributeSequenceNumbers",
+            ],
         }
 
         self.assertEqual(expected, actual)
diff --git a/tests/codegen/test_utils.py b/tests/codegen/test_utils.py
index 822998f8..84c7c965 100644
--- a/tests/codegen/test_utils.py
+++ b/tests/codegen/test_utils.py
@@ -122,8 +122,6 @@ class ClassUtilsTests(FactoryTestCase):
         self.assertEqual(source.attrs[0], target.attrs[1])
         self.assertEqual(source.attrs[1], target.attrs[2])
 
-        self.assertEqual(target.attrs[1].restrictions.group, id(attr))
-        self.assertEqual(target.attrs[2].restrictions.group, id(attr))
         mock_copy_inner_classes.assert_has_calls(
             [
                 mock.call(source, target, source.attrs[0]),
diff --git a/tests/fixtures/models.py b/tests/fixtures/models.py
index f42275a4..6dfd0a6d 100644
--- a/tests/fixtures/models.py
+++ b/tests/fixtures/models.py
@@ -147,7 +147,10 @@ class SequentialType:
         default_factory=list, metadata=dict(type="Element", sequence=1)
     )
     x3: List[int] = field(
-        default_factory=list, metadata=dict(type="Element", sequence=1)
+        default_factory=list, metadata=dict(type="Element", sequence=2)
+    )
+    x4: Optional[int] = field(
+        default=None, metadata=dict(type="Element", sequence=2)
     )
 
 
diff --git a/tests/formats/dataclass/serializers/test_code.py b/tests/formats/dataclass/serializers/test_code.py
index c9cb04aa..cf339f12 100644
--- a/tests/formats/dataclass/serializers/test_code.py
+++ b/tests/formats/dataclass/serializers/test_code.py
@@ -64,3 +64,10 @@ class PycodeSerializerTests(TestCase):
     def test_write_object_with_enum(self):
         iterator = self.serializer.write_object(Namespace.SOAP11, 0, set())
         self.assertEqual("Namespace.SOAP11", "".join(iterator))
+
+    def test_build_imports_ignores_nested_types(self):
+        class Foo:
+            pass
+
+        actual = self.serializer.build_imports({Foo})
+        self.assertEqual("", actual)
diff --git a/tests/formats/dataclass/serializers/test_xml.py b/tests/formats/dataclass/serializers/test_xml.py
index 46f30147..45af76cd 100644
--- a/tests/formats/dataclass/serializers/test_xml.py
+++ b/tests/formats/dataclass/serializers/test_xml.py
@@ -570,22 +570,24 @@ class XmlSerializerTests(TestCase):
         self.assertEqual(expected, list(result))
 
     def test_next_value(self):
-        obj = SequentialType(x0=1, x1=[2, 3, 4, None], x2=[6, 7], x3=[9])
+        obj = SequentialType(x0=1, x1=[2, 3, 4, None], x2=[6, 7], x3=[9], x4=10)
         meta = self.serializer.context.build(SequentialType)
         x0 = meta.text
         x1 = next(meta.find_children("x1"))
         x2 = next(meta.find_children("x2"))
         x3 = next(meta.find_children("x3"))
+        x4 = next(meta.find_children("x4"))
 
         actual = self.serializer.next_value(obj, meta)
         expected = [
             (x0, 1),
             (x1, 2),
             (x2, 6),
-            (x3, 9),
             (x1, 3),
             (x2, 7),
             (x1, 4),
+            (x3, 9),
+            (x4, 10),
         ]
 
         self.assertIsInstance(actual, Generator)
diff --git a/tests/integration/benchmarks/conftest.py b/tests/integration/benchmarks/conftest.py
index 762c5c02..850041c2 100644
--- a/tests/integration/benchmarks/conftest.py
+++ b/tests/integration/benchmarks/conftest.py
@@ -1,12 +1,5 @@
 from tests import xsdata_temp_dir
-from tests.fixtures.books import BookForm
-from tests.fixtures.books import Books
 from xsdata.formats.dataclass.context import XmlContext
-from xsdata.formats.dataclass.parsers import JsonParser
-from xsdata.formats.dataclass.parsers import XmlParser
-from xsdata.formats.dataclass.serializers import JsonSerializer
-from xsdata.formats.dataclass.serializers import XmlSerializer
-from xsdata.models.datatype import XmlDate
 
 xsdata_temp_dir.mkdir(parents=True, exist_ok=True)
 context = XmlContext()
@@ -17,101 +10,3 @@ def pytest_unconfigure(config):
         tmp_file.unlink()
 
     xsdata_temp_dir.rmdir()
-
-
-def make_books(how_many: int):
-    return Books(
-        book=[
-            BookForm(
-                author="Arne Dahl",
-                title="Misterioso: A Crime Novel",
-                genre="Thrillers & Suspense",
-                price=15.95,
-                pub_date=XmlDate(1999, 10, 5),
-                review=(
-                    "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
-                    "Integer at erat sagittis, accumsan mauris eu, egestas "
-                    "quam. Nam tristique felis justo, vel iaculis ipsum cursus "
-                    "at. Praesent varius enim ac nunc interdum placerat. "
-                    "Integer porttitor, nibh at viverra vehicula, leo dui "
-                    "suscipit nisi, ornare laoreet eros neque nec mi. Proin."
-                ),
-                id="9788831796781",
-            )
-            for _ in range(how_many)
-        ]
-    )
-
-
-def parse(source, handler):
-    parser = XmlParser(context=context, handler=handler)
-    parser.from_bytes(source, Books)
-
-
-def parse_json(source):
-    parser = JsonParser(context=context)
-    parser.from_bytes(source, Books)
-
-
-def write(size, obj, writer):
-    with xsdata_temp_dir.joinpath(f"benchmark_{size}.xml").open("w") as f:
-        serializer = XmlSerializer(writer=writer, context=context)
-        serializer.write(f, obj)
-
-
-def write_json(size, obj):
-    with xsdata_temp_dir.joinpath(f"benchmark_{size}.json").open("w") as f:
-        serializer = JsonSerializer(context=context)
-        serializer.write(f, obj)
-
-
-if __name__ == "__main__":
-    import argparse
-    import statistics
-    from xsdata.formats.dataclass.serializers import writers
-    from xsdata.formats.dataclass.parsers import handlers
-    from timeit import Timer
-
-    components = [
-        "LxmlEventHandler",
-        "XmlEventHandler",
-        "LxmlEventWriter",
-        "XmlEventWriter",
-        "JsonParser",
-        "JsonSerializer",
-    ]
-
-    parser = argparse.ArgumentParser()
-    parser.add_argument("-c", "--component", choices=components, required=True)
-    parser.add_argument("-n", "--number", default=1000, type=int)
-    parser.add_argument("-r", "--repeat", default=10, type=int)
-    args = parser.parse_args()
-
-    if args.component in writers.__all__:
-        component = getattr(writers, args.component)
-        books = make_books(args.number)
-        t = Timer(lambda: write(args.number, books, component))
-    elif args.component in handlers.__all__:
-        fixture = xsdata_temp_dir.joinpath(f"benchmark_{args.number}.xml")
-        if not fixture.exists():
-            write(args.number, make_books(args.number), writers.XmlEventWriter)
-
-        component = getattr(handlers, args.component)
-        t = Timer(lambda: parse(fixture.read_bytes(), component))
-    elif args.component == "JsonParser":
-        component = JsonParser
-        fixture = xsdata_temp_dir.joinpath(f"benchmark_{args.number}.json")
-        if not fixture.exists():
-            write_json(args.number, make_books(args.number))
-
-        t = Timer(lambda: parse_json(fixture.read_bytes()))
-    elif args.component == "JsonSerializer":
-        component = JsonSerializer
-        books = make_books(args.number)
-        t = Timer(lambda: write_json(args.number, books))
-
-    print(f"Benchmark {component.__name__} - n{args.number}/r{args.repeat}")
-    result = t.repeat(repeat=args.repeat, number=1)
-    print(f"avg {statistics.mean(result)}")
-    print(f"med {statistics.median(result)}")
-    print(f"stdev {statistics.stdev(result)}")
diff --git a/tests/integration/benchmarks/test_handlers.py b/tests/integration/benchmarks/test_handlers.py
index 0bbe051f..d1935c60 100644
--- a/tests/integration/benchmarks/test_handlers.py
+++ b/tests/integration/benchmarks/test_handlers.py
@@ -3,9 +3,9 @@ import random
 import pytest
 
 from tests import xsdata_temp_dir
-from tests.integration.benchmarks.conftest import make_books
-from tests.integration.benchmarks.conftest import parse
-from tests.integration.benchmarks.conftest import write
+from tests.integration.benchmarks.utils import make_books
+from tests.integration.benchmarks.utils import parse
+from tests.integration.benchmarks.utils import write
 from xsdata.formats.dataclass.context import XmlContext
 from xsdata.formats.dataclass.parsers import handlers as readers
 from xsdata.formats.dataclass.serializers import writers
diff --git a/tests/integration/benchmarks/utils.py b/tests/integration/benchmarks/utils.py
new file mode 100644
index 00000000..12c66f7e
--- /dev/null
+++ b/tests/integration/benchmarks/utils.py
@@ -0,0 +1,55 @@
+from tests.fixtures.books import BookForm
+from tests.fixtures.books import Books
+from tests.integration.benchmarks.conftest import context
+from tests.integration.benchmarks.conftest import xsdata_temp_dir
+from xsdata.formats.dataclass.parsers import JsonParser
+from xsdata.formats.dataclass.parsers import XmlParser
+from xsdata.formats.dataclass.serializers import JsonSerializer
+from xsdata.formats.dataclass.serializers import XmlSerializer
+from xsdata.models.datatype import XmlDate
+
+
+def make_books(how_many: int):
+    return Books(
+        book=[
+            BookForm(
+                author="Arne Dahl",
+                title="Misterioso: A Crime Novel",
+                genre="Thrillers & Suspense",
+                price=15.95,
+                pub_date=XmlDate(1999, 10, 5),
+                review=(
+                    "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
+                    "Integer at erat sagittis, accumsan mauris eu, egestas "
+                    "quam. Nam tristique felis justo, vel iaculis ipsum cursus "
+                    "at. Praesent varius enim ac nunc interdum placerat. "
+                    "Integer porttitor, nibh at viverra vehicula, leo dui "
+                    "suscipit nisi, ornare laoreet eros neque nec mi. Proin."
+                ),
+                id="9788831796781",
+            )
+            for _ in range(how_many)
+        ]
+    )
+
+
+def parse(source, handler):
+    parser = XmlParser(context=context, handler=handler)
+    parser.from_bytes(source, Books)
+
+
+def parse_json(source):
+    parser = JsonParser(context=context)
+    parser.from_bytes(source, Books)
+
+
+def write(size, obj, writer):
+    with xsdata_temp_dir.joinpath(f"benchmark_{size}.xml").open("w") as f:
+        serializer = XmlSerializer(writer=writer, context=context)
+        serializer.write(f, obj)
+
+
+def write_json(size, obj):
+    with xsdata_temp_dir.joinpath(f"benchmark_{size}.json").open("w") as f:
+        serializer = JsonSerializer(context=context)
+        serializer.write(f, obj)
diff --git a/tests/models/xsd/test_all.py b/tests/models/xsd/test_all.py
index 898e8ef2..94385686 100644
--- a/tests/models/xsd/test_all.py
+++ b/tests/models/xsd/test_all.py
@@ -6,4 +6,4 @@ from xsdata.models.xsd import All
 class AllTests(TestCase):
     def test_get_restrictions(self):
         obj = All(min_occurs=1, max_occurs=2)
-        self.assertEqual({"max_occurs": 2, "min_occurs": 1}, obj.get_restrictions())
+        self.assertEqual({"path": [("a", id(obj), 1, 2)]}, obj.get_restrictions())
diff --git a/tests/models/xsd/test_alternative.py b/tests/models/xsd/test_alternative.py
index 60eb3d2f..c9917fea 100644
--- a/tests/models/xsd/test_alternative.py
+++ b/tests/models/xsd/test_alternative.py
@@ -23,5 +23,4 @@ class AlternativeTests(TestCase):
 
     def test_get_restrictions(self):
         obj = Alternative()
-        expected = {"min_occurs": 0, "choice": str(id(obj))}
-        self.assertEqual(expected, obj.get_restrictions())
+        self.assertEqual({"path": [("alt", id(obj), 0, 1)]}, obj.get_restrictions())
diff --git a/tests/models/xsd/test_choice.py b/tests/models/xsd/test_choice.py
index d7159b39..b990cda3 100644
--- a/tests/models/xsd/test_choice.py
+++ b/tests/models/xsd/test_choice.py
@@ -7,18 +7,9 @@ from xsdata.models.xsd import Choice
 class ChoiceTests(TestCase):
     def test_get_restrictions(self):
         obj = Choice(min_occurs=1, max_occurs=2)
-
-        expected = {
-            "choice": str(id(obj)),
-            "min_occurs": 0,
-            "max_occurs": 2,
-        }
-        self.assertEqual(expected, obj.get_restrictions())
+        self.assertEqual({"path": [("c", id(obj), 1, 2)]}, obj.get_restrictions())
 
         obj = Choice(max_occurs="unbounded")
-        expected = {
-            "choice": str(id(obj)),
-            "min_occurs": 0,
-            "max_occurs": sys.maxsize,
-        }
-        self.assertEqual(expected, obj.get_restrictions())
+        self.assertEqual(
+            {"path": [("c", id(obj), 1, sys.maxsize)]}, obj.get_restrictions()
+        )
diff --git a/tests/models/xsd/test_group.py b/tests/models/xsd/test_group.py
index 0536261b..6b3cd0c4 100644
--- a/tests/models/xsd/test_group.py
+++ b/tests/models/xsd/test_group.py
@@ -17,4 +17,4 @@ class GroupTests(TestCase):
 
     def test_get_restrictions(self):
         obj = Group(min_occurs=1, max_occurs=2)
-        self.assertEqual({"max_occurs": 2, "min_occurs": 1}, obj.get_restrictions())
+        self.assertEqual({"path": [("g", id(obj), 1, 2)]}, obj.get_restrictions())
diff --git a/tests/models/xsd/test_sequence.py b/tests/models/xsd/test_sequence.py
new file mode 100644
index 00000000..bfee3bee
--- /dev/null
+++ b/tests/models/xsd/test_sequence.py
@@ -0,0 +1,15 @@
+import sys
+from unittest import TestCase
+
+from xsdata.models.xsd import Sequence
+
+
+class SequenceTests(TestCase):
+    def test_get_restrictions(self):
+        obj = Sequence(min_occurs=1, max_occurs=2)
+        self.assertEqual({"path": [("s", id(obj), 1, 2)]}, obj.get_restrictions())
+
+        obj = Sequence(min_occurs=1, max_occurs="unbounded")
+        self.assertEqual(
+            {"path": [("s", id(obj), 1, sys.maxsize)]}, obj.get_restrictions()
+        )
diff --git a/tests/utils/test_collections.py b/tests/utils/test_collections.py
index fb899faa..5190abc2 100644
--- a/tests/utils/test_collections.py
+++ b/tests/utils/test_collections.py
@@ -1,4 +1,5 @@
 from collections import namedtuple
+from typing import Iterator
 from unittest import TestCase
 
 from xsdata.utils import collections
@@ -30,3 +31,26 @@ class CollectionsTests(TestCase):
         self.assertFalse(collections.is_array(fixture(1, 2)))
         self.assertTrue(collections.is_array([]))
         self.assertTrue(collections.is_array(tuple()))
+        self.assertTrue(collections.is_array(frozenset()))
+
+    def test_connected_components(self):
+        lists = [[1, 2, 3], [4], [3, 4], [6]]
+        actual = collections.connected_components(lists)
+
+        self.assertIsInstance(actual, Iterator)
+        self.assertEqual([[1, 2, 3, 4], [6]], list(actual))
+
+    def test_find_connected_component(self):
+        groups = [[1, 2, 3], [4, 5, 6]]
+
+        actual = collections.find_connected_component(groups, 1)
+        self.assertEqual(0, actual)
+
+        actual = collections.find_connected_component(groups, 2)
+        self.assertEqual(0, actual)
+
+        actual = collections.find_connected_component(groups, 4)
+        self.assertEqual(1, actual)
+
+        actual = collections.find_connected_component(groups, 100)
+        self.assertEqual(-1, actual)