Repository: tefra/xsdata
Pull request #670 (2022-03-27 08:15:43)

Add option to move inner types to upper level
## 📒 Description

In some schemas the inner types are very annoying to work with because they are too many, xsdata should provide an option to move all inner types to upper level.

Hint, pydantic classes don't behave very well with inner classes, this should be a nice workaround till they do...


Resolves #xxxx

## 🔗 What I've Done

- Fix an issue where inner types didn't inherit the parent class target namespace
- Add a new class meta option global_type with default True, to distinquish between global types (discoverable) vs inner anonymous typees.
- Add mew generator config to unnest inner types.


## 💬 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 dba49d5a..d5a37cfc 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -37,7 +37,7 @@ repos:
       - id: docformatter
         args: ["--in-place", "--pre-summary-newline"]
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v0.941
+    rev: v0.942
     hooks:
       - id: mypy
         additional_dependencies: [tokenize-rt, types-requests, types-Jinja2, types-click, types-docutils]
diff --git a/docs/models.rst b/docs/models.rst
index 2a6aec3a..c34d0f6c 100644
--- a/docs/models.rst
+++ b/docs/models.rst
@@ -97,6 +97,10 @@ procedures.
      - str
      - Specify the element target namespace for auto type locator, if module namespace
        is not available or the type is not qualified.
+   * - global_type
+     - bool
+     - Specify if the class represents a global discoverable type or that it can be used
+       only as another class dependency, default: True
    * - element_name_generator
      - Callable
      - Element name generator
diff --git a/xsdata/codegen/container.py b/xsdata/codegen/container.py
index ed195ae5..61b9bcaf 100644
--- a/xsdata/codegen/container.py
+++ b/xsdata/codegen/container.py
@@ -19,6 +19,7 @@ from xsdata.codegen.handlers import ClassEnumerationHandler
 from xsdata.codegen.handlers import ClassExtensionHandler
 from xsdata.codegen.handlers import ClassInnersHandler
 from xsdata.codegen.handlers import ClassNameConflictHandler
+from xsdata.codegen.handlers import ClassUnnestHandler
 from xsdata.codegen.mixins import ContainerInterface
 from xsdata.codegen.models import Class
 from xsdata.codegen.models import get_qname
@@ -42,7 +43,7 @@ class ClassContainer(ContainerInterface):
 
     def __init__(self, config: GeneratorConfig):
         """Initialize a class container instance with its processors based on
-        the the provided configuration."""
+        the provided configuration."""
         super().__init__(config)
 
         self.data: Dict = {}
@@ -52,6 +53,7 @@ class ClassContainer(ContainerInterface):
                 AttributeGroupHandler(self),
                 ClassExtensionHandler(self),
                 ClassEnumerationHandler(self),
+                ClassUnnestHandler(self),
                 AttributeSubstitutionHandler(self),
                 AttributeTypeHandler(self),
                 AttributeMergeHandler(),
@@ -127,9 +129,6 @@ class ClassContainer(ContainerInterface):
             if obj.status < step:
                 self.process_class(obj, step)
 
-        if any(obj.status < step for obj in self):
-            return self.process_classes(step)
-
     def process_class(self, target: Class, step: int):
         target.status = Status(step)
         for processor in self.processors.get(step, []):
diff --git a/xsdata/codegen/handlers/__init__.py b/xsdata/codegen/handlers/__init__.py
index bc813443..35908d11 100644
--- a/xsdata/codegen/handlers/__init__.py
+++ b/xsdata/codegen/handlers/__init__.py
@@ -13,6 +13,7 @@ from .class_enumeration import ClassEnumerationHandler
 from .class_extension import ClassExtensionHandler
 from .class_inners import ClassInnersHandler
 from .class_name_conflict import ClassNameConflictHandler
+from .class_unnest import ClassUnnestHandler
 
 __all__ = [
     "AttributeCompoundChoiceHandler",
@@ -30,4 +31,5 @@ __all__ = [
     "ClassEnumerationHandler",
     "ClassExtensionHandler",
     "ClassNameConflictHandler",
+    "ClassUnnestHandler",
 ]
diff --git a/xsdata/codegen/handlers/class_enumeration.py b/xsdata/codegen/handlers/class_enumeration.py
index f280f89c..4744a70e 100644
--- a/xsdata/codegen/handlers/class_enumeration.py
+++ b/xsdata/codegen/handlers/class_enumeration.py
@@ -2,10 +2,8 @@ from typing import Any
 from typing import List
 
 from xsdata.codegen.mixins import RelativeHandlerInterface
-from xsdata.codegen.models import Attr
 from xsdata.codegen.models import Class
 from xsdata.models.enums import Tag
-from xsdata.utils.namespaces import build_qname
 
 
 class ClassEnumerationHandler(RelativeHandlerInterface):
@@ -20,11 +18,9 @@ class ClassEnumerationHandler(RelativeHandlerInterface):
         Steps:
             1. Filter attrs not derived from xs:enumeration
             2. Flatten attrs derived from xs:union of enumerations
-            3. Promote inner enumeration classes to root classes
         """
         self.filter(target)
         self.flatten(target)
-        self.promote(target)
 
     @classmethod
     def filter(cls, target: Class):
@@ -59,33 +55,3 @@ class ClassEnumerationHandler(RelativeHandlerInterface):
             target.inner.clear()
 
             target.attrs.extend(attr.clone() for enum in enums for attr in enum.attrs)
-
-    def promote(self, target: Class):
-        """
-        Promote inner enumeration classes to root classes.
-
-        Steps:
-            1. Find inner enumerations
-            2. Clone and update their qualified name
-            3. Update attributes types
-        """
-        for inner in list(target.inner):
-            if inner.is_enumeration:
-                target.inner.remove(inner)
-                clone = self.clone_enumeration(inner, target.name)
-                self.container.add(clone)
-                for attr in target.attrs:
-                    self.update_types(attr, inner.qname, clone.qname)
-
-    @classmethod
-    def clone_enumeration(cls, inner: Class, name: str) -> Class:
-        clone = inner.clone()
-        clone.qname = build_qname(clone.target_namespace, f"{name}_{clone.name}")
-        return clone
-
-    @classmethod
-    def update_types(cls, attr: Attr, search: str, replace: str):
-        for attr_type in attr.types:
-            if attr_type.qname == search and attr_type.forward:
-                attr_type.qname = replace
-                attr_type.forward = False
diff --git a/xsdata/codegen/handlers/class_unnest.py b/xsdata/codegen/handlers/class_unnest.py
new file mode 100644
index 00000000..eadb923e
--- /dev/null
+++ b/xsdata/codegen/handlers/class_unnest.py
@@ -0,0 +1,55 @@
+from typing import Optional
+
+from xsdata.codegen.mixins import RelativeHandlerInterface
+from xsdata.codegen.models import Attr
+from xsdata.codegen.models import Class
+from xsdata.utils.namespaces import build_qname
+
+
+class ClassUnnestHandler(RelativeHandlerInterface):
+    """Unnest class processor."""
+
+    __slots__ = ()
+
+    def process(self, target: Class):
+        """
+        Promote enumeration classes to root classes.
+
+        Candidates
+            - Enumerations
+            - All if config is enabled
+        """
+        for inner in list(target.inner):
+            if inner.is_enumeration or self.container.config.output.unnest_classes:
+                self.promote(target, inner)
+
+    def promote(self, target: Class, inner: Class):
+        target.inner.remove(inner)
+        attr = self.find_forward_attr(target, inner.qname)
+        if attr:
+            clone = self.clone_class(inner, target.name)
+            self.update_types(attr, inner.qname, clone.qname)
+            self.container.add(clone)
+
+    @classmethod
+    def clone_class(cls, inner: Class, name: str) -> Class:
+        clone = inner.clone()
+        clone.local_type = True
+        clone.qname = build_qname(inner.target_namespace, f"{name}_{inner.name}")
+        return clone
+
+    @classmethod
+    def update_types(cls, attr: Attr, search: str, replace: str):
+        for attr_type in attr.types:
+            if attr_type.qname == search and attr_type.forward:
+                attr_type.qname = replace
+                attr_type.forward = False
+
+    @classmethod
+    def find_forward_attr(cls, target: Class, qname: str) -> Optional[Attr]:
+        for attr in target.attrs:
+            for attr_type in attr.types:
+                if attr_type.forward and attr_type.qname == qname:
+                    return attr
+
+        return None
diff --git a/xsdata/codegen/mappers/definitions.py b/xsdata/codegen/mappers/definitions.py
index 9a70adea..b0cc0a0c 100644
--- a/xsdata/codegen/mappers/definitions.py
+++ b/xsdata/codegen/mappers/definitions.py
@@ -33,7 +33,7 @@ class DefinitionsMapper:
     """
     Map a definitions instance to message and service classes.
 
-    Currently only SOAP 1.1 bindings with rpc/document style is
+    Currently, only SOAP 1.1 bindings with rpc/document style is
     supported.
     """
 
@@ -280,7 +280,7 @@ class DefinitionsMapper:
         inner = collections.first(inner for inner in target.inner if inner.name == name)
         if not inner:
             inner = Class(
-                qname=namespaces.build_qname(name),
+                qname=namespaces.build_qname(target.target_namespace, name),
                 tag=Tag.BINDING_MESSAGE,
                 location=target.location,
                 ns_map=target.ns_map.copy(),
@@ -317,7 +317,7 @@ class DefinitionsMapper:
         cls, definitions: Definitions, message: str, extended: AnyElement, ns_map: Dict
     ) -> Iterator[Attr]:
         """Find a Message instance and map its parts to attributes according to
-        the the extensible element.."""
+        the extensible element.."""
         parts = []
         if "part" in extended.attributes:
             parts.append(extended.attributes["part"])
diff --git a/xsdata/codegen/mappers/schema.py b/xsdata/codegen/mappers/schema.py
index d729316a..2fe4fa74 100644
--- a/xsdata/codegen/mappers/schema.py
+++ b/xsdata/codegen/mappers/schema.py
@@ -102,8 +102,8 @@ class SchemaMapper:
         """Build the target class attributes from the given ElementBase
         children."""
 
-        restrictions = Restrictions.from_element(obj)
-        for child, restrictions in cls.element_children(obj, restrictions):
+        base_restrictions = Restrictions.from_element(obj)
+        for child, restrictions in cls.element_children(obj, base_restrictions):
             cls.build_class_attribute(target, child, restrictions)
 
         target.attrs.sort(key=lambda x: x.index)
diff --git a/xsdata/codegen/models.py b/xsdata/codegen/models.py
index d1dd9b01..ae129e3a 100644
--- a/xsdata/codegen/models.py
+++ b/xsdata/codegen/models.py
@@ -417,6 +417,7 @@ class Class:
     :param mixed:
     :param abstract:
     :param nillable:
+    :param local_type:
     :param status:
     :param container:
     :param package:
@@ -439,6 +440,7 @@ class Class:
     mixed: bool = field(default=False)
     abstract: bool = field(default=False)
     nillable: bool = field(default=False)
+    local_type: bool = field(default=False)
     status: Status = field(default=Status.RAW)
     container: Optional[str] = field(default=None)
     package: Optional[str] = field(default=None)
@@ -496,7 +498,7 @@ class Class:
 
     @property
     def is_global_type(self) -> bool:
-        """Return whether this instance is a non abstract element, wsdl binding
+        """Return whether this instance is a non-abstract element, wsdl binding
         class or a complex type without simple content."""
         return (not self.abstract and self.tag in GLOBAL_TYPES) or (
             self.tag == Tag.COMPLEX_TYPE and not self.is_simple_type
diff --git a/xsdata/formats/dataclass/context.py b/xsdata/formats/dataclass/context.py
index ac6aa9fa..16fb4319 100644
--- a/xsdata/formats/dataclass/context.py
+++ b/xsdata/formats/dataclass/context.py
@@ -85,12 +85,17 @@ class XmlContext:
             return
 
         self.xsi_cache.clear()
-
-        name_generator = self.element_name_generator
+        builder = XmlMetaBuilder(
+            class_type=self.class_type,
+            element_name_generator=self.element_name_generator,
+            attribute_name_generator=self.attribute_name_generator,
+        )
         for clazz in self.get_subclasses(object):
             if self.class_type.is_model(clazz):
-                qname = XmlMetaBuilder.build_target_qname(clazz, name_generator)
-                self.xsi_cache[qname].append(clazz)
+                meta = builder.build_class_meta(clazz, None)
+
+                if meta.target_qname:
+                    self.xsi_cache[meta.target_qname].append(clazz)
 
         self.sys_modules = len(sys.modules)
 
diff --git a/xsdata/formats/dataclass/models/builders.py b/xsdata/formats/dataclass/models/builders.py
index 040f97be..166b0e5f 100644
--- a/xsdata/formats/dataclass/models/builders.py
+++ b/xsdata/formats/dataclass/models/builders.py
@@ -8,6 +8,7 @@ from typing import get_type_hints
 from typing import Iterator
 from typing import List
 from typing import Mapping
+from typing import NamedTuple
 from typing import Optional
 from typing import Sequence
 from typing import Set
@@ -28,6 +29,16 @@ from xsdata.utils.constants import return_input
 from xsdata.utils.namespaces import build_qname
 
 
+class ClassMeta(NamedTuple):
+    element_name_generator: Callable
+    attribute_name_generator: Callable
+    qname: str
+    local_name: str
+    nillable: bool
+    namespace: Optional[str]
+    target_qname: Optional[str]
+
+
 class XmlMetaBuilder:
     __slots__ = "class_type", "element_name_generator", "attribute_name_generator"
 
@@ -47,33 +58,12 @@ class XmlMetaBuilder:
 
         self.class_type.verify_model(clazz)
 
-        # Fetch the dataclass meta settings and make sure we don't inherit
-        # the parent class meta.
-        meta = clazz.Meta if "Meta" in clazz.__dict__ else None
-        element_name_generator = getattr(
-            meta, "element_name_generator", self.element_name_generator
-        )
-        attribute_name_generator = getattr(
-            meta, "attribute_name_generator", self.attribute_name_generator
-        )
-        local_name = getattr(meta, "name", None)
-        local_name = local_name or element_name_generator(clazz.__name__)
-        nillable = getattr(meta, "nillable", False)
-        namespace = getattr(meta, "namespace", parent_namespace)
-        module = sys.modules[clazz.__module__]
-        qname = build_qname(namespace, local_name)
-
-        if self.is_inner_class(clazz):
-            target_qname = None
-        else:
-            target_namespace = self.target_namespace(module, meta)
-            target_qname = build_qname(target_namespace, local_name)
-
+        meta = self.build_class_meta(clazz, parent_namespace)
         class_vars = self.build_vars(
             clazz,
-            namespace,
-            element_name_generator,
-            attribute_name_generator,
+            meta.namespace,
+            meta.element_name_generator,
+            meta.attribute_name_generator,
         )
 
         attributes = {}
@@ -99,9 +89,9 @@ class XmlMetaBuilder:
 
         return XmlMeta(
             clazz=clazz,
-            qname=qname,
-            target_qname=target_qname,
-            nillable=nillable,
+            qname=meta.qname,
+            target_qname=meta.target_qname,
+            nillable=meta.nillable,
             text=text,
             attributes=attributes,
             elements=elements,
@@ -147,6 +137,45 @@ class XmlMetaBuilder:
             if var is not None:
                 yield var
 
+    def build_class_meta(
+        self, clazz: Type, parent_namespace: Optional[str]
+    ) -> ClassMeta:
+        """
+        Fetch the class meta options and merge defaults.
+
+        Metaclass is not inheritable
+        """
+        meta = clazz.Meta if "Meta" in clazz.__dict__ else None
+        element_name_generator = getattr(
+            meta, "element_name_generator", self.element_name_generator
+        )
+        attribute_name_generator = getattr(
+            meta, "attribute_name_generator", self.attribute_name_generator
+        )
+        global_type = getattr(meta, "global_type", True)
+        local_name = getattr(meta, "name", None)
+        local_name = local_name or element_name_generator(clazz.__name__)
+        nillable = getattr(meta, "nillable", False)
+        namespace = getattr(meta, "namespace", parent_namespace)
+        qname = build_qname(namespace, local_name)
+
+        if self.is_inner_class(clazz) or not global_type:
+            target_qname = None
+        else:
+            module = sys.modules[clazz.__module__]
+            target_namespace = self.target_namespace(module, meta)
+            target_qname = build_qname(target_namespace, local_name)
+
+        return ClassMeta(
+            element_name_generator,
+            attribute_name_generator,
+            qname,
+            local_name,
+            nillable,
+            namespace,
+            target_qname,
+        )
+
     @classmethod
     def find_declared_class(cls, clazz: Type, name: str) -> Type:
         for base in clazz.__mro__:
@@ -161,20 +190,6 @@ class XmlMetaBuilder:
         """Return whether the given type is nested inside another type."""
         return "." in clazz.__qualname__
 
-    @classmethod
-    def build_target_qname(cls, clazz: Type, element_name_generator: Callable) -> str:
-        """Build the source qualified name of a model based on the module
-        namespace."""
-        meta = clazz.Meta if "Meta" in clazz.__dict__ else None
-        local_name = getattr(meta, "name", None)
-        element_name_generator = getattr(
-            meta, "element_name_generator", element_name_generator
-        )
-        local_name = local_name or element_name_generator(clazz.__name__)
-        module = sys.modules[clazz.__module__]
-        target_namespace = cls.target_namespace(module, meta)
-        return build_qname(target_namespace, local_name)
-
     @classmethod
     def target_namespace(cls, module: Any, meta: Any) -> Optional[str]:
         """The target namespace this class metadata was defined in."""
diff --git a/xsdata/formats/dataclass/templates/class.jinja2 b/xsdata/formats/dataclass/templates/class.jinja2
index 246622cf..de3f8dc2 100644
--- a/xsdata/formats/dataclass/templates/class.jinja2
+++ b/xsdata/formats/dataclass/templates/class.jinja2
@@ -5,18 +5,22 @@
 {% set parent_namespace = obj.namespace if obj.namespace is not none else parent_namespace|default(None) -%}
 {% set parents = parents|default([obj.name]) -%}
 {% set class_name =  obj.name|class_name -%}
+{% set global_type = level == 0 and not obj.local_type -%}
 {% set local_name =  obj.meta_name or obj.name -%}
-{% set local_name = None if class_name == local_name or parents|length != 1 else local_name -%}
+{% set local_name = None if class_name == local_name or not global_type else local_name -%}
 {% set base_classes = obj.extensions|map(attribute='type')|map('type_name')|join(', ') -%}
-{% set target_namespace = obj.target_namespace if level == 0 and module_namespace != obj.target_namespace else None %}
+{% set target_namespace = obj.target_namespace if global_type and module_namespace != obj.target_namespace else None %}
 
 {{ class_annotation }}
 class {{ class_name }}{{"({})".format(base_classes) if base_classes }}:
 {%- if help %}
 {{ help|indent(4, first=True) }}
 {%- endif -%}
-{%- if local_name or obj.is_nillable or obj.namespace is not none or target_namespace %}
+{%- if local_name or obj.is_nillable or obj.namespace is not none or target_namespace or obj.local_type %}
     class Meta:
+        {%- if obj.local_type %}
+        global_type = False
+        {%- endif -%}
         {%- if local_name %}
         name = "{{ local_name }}"
         {%- endif -%}
diff --git a/xsdata/models/config.py b/xsdata/models/config.py
index d87f84b1..c3614804 100644
--- a/xsdata/models/config.py
+++ b/xsdata/models/config.py
@@ -228,6 +228,7 @@ class GeneratorOutput:
     :param max_line_length: Adjust the maximum line length, default: 79
     :param postponed_annotations: Enable postponed evaluation of annotations,
         default: false, python>=3.7 Only
+    :param unnest_classes: Move inner classes to upper level, default: false
     """
 
     package: str = element(default="generated")
@@ -240,6 +241,7 @@ class GeneratorOutput:
     compound_fields: CompoundFields = element(default_factory=CompoundFields)
     max_line_length: int = attribute(default=79)
     postponed_annotations: bool = element(default=False)
+    unnest_classes: bool = element(default=False)
 
     def __post_init__(self):
         self.validate()


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 2bcef8b8..234fc725 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -21,7 +21,6 @@ jobs:
           - {name: Python 3.8, python: '3.8', os: ubuntu}
           - {name: Python 3.9, python: '3.9', os: ubuntu}
           - {name: Python 3.10, python: '3.10', os: ubuntu}
-          - {name: Python 3.11, python: '3.11-dev', os: ubuntu}
           - {name: Windows py37, python: '3.7', os: windows}
           - {name: MacOS py37, python: '3.7', os: macos}
     steps:
@@ -31,7 +30,7 @@ jobs:
         with:
           python-version: ${{ matrix.python }}
       - name: Install libxml2
-        if: startsWith(matrix.python, 'pypy') || startsWith(matrix.python, '3.11-dev')
+        if: startsWith(matrix.python, 'pypy')
         run: |
           sudo apt-get install libxml2-dev libxslt-dev
       - name: Install dependencies
@@ -51,7 +50,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python: ['3.7', '3.8', '3.9', '3.10', '3.11-dev']
+        python: ['3.7', '3.8', '3.9', '3.10']
         mode: [xsd, json, xml]
     steps:
       - uses: actions/checkout@v2
@@ -65,10 +64,6 @@ jobs:
         uses: actions/setup-python@v2
         with:
           python-version: ${{ matrix.python }}
-      - name: Install libxml2
-        if: startsWith(matrix.python, '3.11-dev')
-        run: |
-          sudo apt-get install libxml2-dev libxslt-dev
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip setuptools pytest pytest-xdist xmlschema
diff --git a/tests/codegen/handlers/test_class_enumeration.py b/tests/codegen/handlers/test_class_enumeration.py
index 4a43f975..45e6a2cd 100644
--- a/tests/codegen/handlers/test_class_enumeration.py
+++ b/tests/codegen/handlers/test_class_enumeration.py
@@ -3,7 +3,6 @@ from xsdata.codegen.handlers import ClassEnumerationHandler
 from xsdata.models.config import GeneratorConfig
 from xsdata.models.enums import DataType
 from xsdata.models.enums import Tag
-from xsdata.utils.namespaces import build_qname
 from xsdata.utils.testing import AttrFactory
 from xsdata.utils.testing import AttrTypeFactory
 from xsdata.utils.testing import ClassFactory
@@ -71,32 +70,3 @@ class ClassEnumerationHandlerTests(FactoryTestCase):
         expected = self.root_enum.attrs + self.inner_enum.attrs
         self.assertEqual(expected, self.target.attrs)
         self.assertEqual(0, len(self.target.inner))
-
-    def test_promote(self):
-        target = ClassFactory.elements(2)
-        inner = ClassFactory.enumeration(3)
-
-        target.inner.append(inner)
-        target.inner.append(ClassFactory.simple_type())  # Irrelevant
-        attr_type = AttrTypeFactory.create(qname=inner.qname, forward=True)
-
-        target.attrs[0].types.append(attr_type.clone())
-        target.attrs[1].types.append(attr_type.clone())
-
-        self.container.add(target)
-        self.assertEqual(3, len(self.container.data))
-
-        self.processor.process(target)
-
-        new_qname = build_qname(inner.target_namespace, f"{target.name}_{inner.name}")
-
-        self.assertEqual(4, len(self.container.data))
-        new_inner = self.container.find(new_qname)
-
-        self.assertEqual(1, len(target.inner))
-        self.assertNotEqual(new_inner.qname, inner.qname)
-        self.assertEqual(new_inner.attrs, inner.attrs)
-        self.assertEqual(new_inner.qname, target.attrs[0].types[1].qname)
-        self.assertEqual(new_inner.qname, target.attrs[1].types[1].qname)
-        self.assertFalse(target.attrs[0].types[1].forward)
-        self.assertFalse(target.attrs[1].types[1].forward)
diff --git a/tests/codegen/handlers/test_class_unnest.py b/tests/codegen/handlers/test_class_unnest.py
new file mode 100644
index 00000000..83fbbd0c
--- /dev/null
+++ b/tests/codegen/handlers/test_class_unnest.py
@@ -0,0 +1,117 @@
+from xsdata.codegen.container import ClassContainer
+from xsdata.codegen.handlers import ClassEnumerationHandler
+from xsdata.codegen.handlers import ClassUnnestHandler
+from xsdata.models.config import GeneratorConfig
+from xsdata.models.enums import DataType
+from xsdata.models.enums import Tag
+from xsdata.utils.testing import AttrFactory
+from xsdata.utils.testing import AttrTypeFactory
+from xsdata.utils.testing import ClassFactory
+from xsdata.utils.testing import FactoryTestCase
+
+
+class ClassUnnestHandlerTests(FactoryTestCase):
+    def setUp(self):
+        super().setUp()
+        self.container = ClassContainer(config=GeneratorConfig())
+        self.processor = ClassUnnestHandler(container=self.container)
+
+    def test_process(self):
+        self.container.config.output.unnest_classes = False
+
+        enumeration = ClassFactory.enumeration(2)
+        local_type = ClassFactory.elements(2)
+        target = ClassFactory.create()
+
+        target.inner.append(enumeration)
+        target.inner.append(local_type)
+        self.container.add(target)
+
+        self.processor.process(target)
+
+        self.assertEqual(1, len(target.inner))
+        self.assertTrue(local_type in target.inner)
+
+        self.container.config.output.unnest_classes = True
+        self.processor.process(target)
+        self.assertEqual(0, len(target.inner))
+
+    def test_promote_with_orphan_inner(self):
+        inner = ClassFactory.elements(2)
+        target = ClassFactory.create()
+        target.inner.append(inner)
+        self.container.add(target)
+
+        self.processor.promote(target, inner)
+
+        self.assertEqual(0, len(target.inner))
+        self.assertEqual(1, len(self.container.data))
+
+    def test_promote_updates_forward_attr_types(self):
+        inner = ClassFactory.elements(2)
+        attr = AttrFactory.reference(inner.qname, forward=True)
+        target = ClassFactory.create()
+        target.attrs.append(attr)
+        target.inner.append(inner)
+        self.container.add(target)
+
+        self.processor.promote(target, inner)
+
+        self.assertEqual(0, len(target.inner))
+        self.assertEqual(2, len(self.container.data))
+        self.assertFalse(attr.types[0].forward)
+        self.assertEqual("{xsdata}class_C_class_B", attr.types[0].qname)
+
+    def test_clone_attr(self):
+        target = ClassFactory.create(qname="{a}b")
+        actual = self.processor.clone_class(target, "parent")
+
+        self.assertIsNot(target, actual)
+        self.assertTrue(actual.local_type)
+        self.assertEqual("{a}parent_b", actual.qname)
+
+    def test_update_types(self):
+        attr = AttrFactory.create(
+            types=[
+                AttrTypeFactory.create(qname="a", forward=True),
+                AttrTypeFactory.create(qname="a", forward=False),
+                AttrTypeFactory.create(qname="b", forward=False),
+            ]
+        )
+
+        self.processor.update_types(attr, "a", "c")
+
+        self.assertEqual("c", attr.types[0].qname)
+        self.assertFalse(attr.types[0].forward)
+        self.assertEqual("a", attr.types[1].qname)
+        self.assertFalse(attr.types[1].forward)
+        self.assertEqual("b", attr.types[2].qname)
+        self.assertFalse(attr.types[2].forward)
+
+    def test_find_forward_attr(self):
+        target = ClassFactory.create(
+            attrs=[
+                AttrFactory.create(
+                    types=[
+                        AttrTypeFactory.create("a", forward=False),
+                        AttrTypeFactory.create("b", forward=False),
+                    ]
+                ),
+                AttrFactory.create(
+                    types=[
+                        AttrTypeFactory.create("a", forward=True),
+                        AttrTypeFactory.create("b", forward=True),
+                    ]
+                ),
+                AttrFactory.create(),
+            ]
+        )
+
+        actual = self.processor.find_forward_attr(target, "a")
+        self.assertEqual(target.attrs[1], actual)
+
+        actual = self.processor.find_forward_attr(target, "b")
+        self.assertEqual(target.attrs[1], actual)
+
+        actual = self.processor.find_forward_attr(target, "c")
+        self.assertIsNone(actual)
diff --git a/tests/codegen/mappers/test_definitions.py b/tests/codegen/mappers/test_definitions.py
index e89333e4..4886d8de 100644
--- a/tests/codegen/mappers/test_definitions.py
+++ b/tests/codegen/mappers/test_definitions.py
@@ -732,7 +732,7 @@ class DefinitionsMapperTests(FactoryTestCase):
 
         actual = DefinitionsMapper.build_inner_class(target, "body")
         expected = ClassFactory.create(
-            qname="body",
+            qname="{xsdata}body",
             tag=Tag.BINDING_MESSAGE,
             location=target.location,
             module=None,
diff --git a/tests/codegen/test_container.py b/tests/codegen/test_container.py
index 9d84ff96..6fb6064c 100644
--- a/tests/codegen/test_container.py
+++ b/tests/codegen/test_container.py
@@ -47,6 +47,7 @@ class ClassContainerTests(FactoryTestCase):
                 "AttributeGroupHandler",
                 "ClassExtensionHandler",
                 "ClassEnumerationHandler",
+                "ClassUnnestHandler",
                 "AttributeSubstitutionHandler",
                 "AttributeTypeHandler",
                 "AttributeMergeHandler",
@@ -121,7 +122,7 @@ class ClassContainerTests(FactoryTestCase):
 
     def test_process_classes(self):
         target = ClassFactory.create(
-            attrs=[AttrFactory.reference("enumeration")],
+            attrs=[AttrFactory.reference("enumeration", forward=True)],
             inner=[ClassFactory.enumeration(2, qname="enumeration")],
         )
 
diff --git a/tests/fixtures/models.py b/tests/fixtures/models.py
index d686dc42..675fa976 100644
--- a/tests/fixtures/models.py
+++ b/tests/fixtures/models.py
@@ -9,6 +9,8 @@ from xml.etree.ElementTree import QName
 
 from xsdata.utils.constants import return_true
 
+__NAMESPACE__ = "xsdata"
+
 
 @dataclass
 class TypeA:
@@ -170,6 +172,9 @@ class Paragraph:
 
 @dataclass
 class Parent:
+    class Meta:
+        global_type = False
+
     @dataclass
     class Inner:
         pass
diff --git a/tests/formats/dataclass/models/test_builders.py b/tests/formats/dataclass/models/test_builders.py
index b6395cb7..9c3efc00 100644
--- a/tests/formats/dataclass/models/test_builders.py
+++ b/tests/formats/dataclass/models/test_builders.py
@@ -125,6 +125,10 @@ class XmlMetaBuilderTests(FactoryTestCase):
         actual = self.builder.build(Parent.Inner, None)
         self.assertIsNone(actual.target_qname)
 
+    def test_build_local_type_has_no_target_qname(self):
+        actual = self.builder.build(Parent, None)
+        self.assertIsNone(actual.target_qname)
+
     def test_target_namespace(self):
         class Meta:
             namespace = "bar"
diff --git a/tests/formats/dataclass/parsers/test_json.py b/tests/formats/dataclass/parsers/test_json.py
index bb8ef91f..20b6ed95 100644
--- a/tests/formats/dataclass/parsers/test_json.py
+++ b/tests/formats/dataclass/parsers/test_json.py
@@ -335,7 +335,7 @@ class JsonParserTests(FactoryTestCase):
         data = {
             "wildcard": {
                 "qname": "b",
-                "type": "TypeB",
+                "type": "{xsdata}TypeB",
                 "value": {
                     "x": "1",
                     "y": "a",
@@ -343,7 +343,9 @@ class JsonParserTests(FactoryTestCase):
             }
         }
         expected = ExtendedType(
-            wildcard=DerivedElement(qname="b", value=TypeB(x=1, y="a"), type="TypeB")
+            wildcard=DerivedElement(
+                qname="b", value=TypeB(x=1, y="a"), type="{xsdata}TypeB"
+            )
         )
         self.assertEqual(expected, self.parser.bind_dataclass(data, ExtendedType))
 
diff --git a/tests/models/test_config.py b/tests/models/test_config.py
index 5540c0a0..4aea0c4f 100644
--- a/tests/models/test_config.py
+++ b/tests/models/test_config.py
@@ -36,6 +36,7 @@ class GeneratorConfigTests(TestCase):
             "    <RelativeImports>false</RelativeImports>\n"
             '    <CompoundFields defaultName="choice" forceDefaultName="false">false</CompoundFields>\n'
             "    <PostponedAnnotations>false</PostponedAnnotations>\n"
+            "    <UnnestClasses>false</UnnestClasses>\n"
             "  </Output>\n"
             "  <Conventions>\n"
             '    <ClassName case="pascalCase" safePrefix="type"/>\n'
@@ -92,6 +93,7 @@ class GeneratorConfigTests(TestCase):
             "    <RelativeImports>false</RelativeImports>\n"
             '    <CompoundFields defaultName="choice" forceDefaultName="false">false</CompoundFields>\n'
             "    <PostponedAnnotations>false</PostponedAnnotations>\n"
+            "    <UnnestClasses>false</UnnestClasses>\n"
             "  </Output>\n"
             "  <Conventions>\n"
             '    <ClassName case="pascalCase" safePrefix="type"/>\n'