Repository: tefra/xsdata (python)

Issue:
Support subcriptable types and new Union Type
The default generator has to remain compatible with all python versions, but someone could use https://github.com/asottile/pyupgrade to upgrade to 3.10 syntax.

```python
Foo | Bar instead of typing.Union[Foo, Bar]
FooBar | None instead of typing.Optional[FooBar]
dict[str, FooBar] instead of typing.Dict[str, FooBar]
list[FooBar] instead of typing.List[FooBar]
tuple[FooBar, ...] instead of typing.Tuple[FooBar, ...]
```

xsdata should be able to resolve the typing annotations but there are a couple of issues


- The new UnionType is not supported at all by xsdata
- The subscriptable types don't evaluate [forward references](https://github.com/python/cpython/pull/30900) correctly, the issue was fixed in 3.11 but it wasn't backported to 3.10


The first issue is easy to correct but the second I am not so sure.








Pull request description:
Support subscriptable types and UnionType
## 📒 Description

This pr adds support for subscriptable types (list[str], dict[str, str]) and the new union type (str | int).

Add generator options to subscriptable/union types


Resolves #681

## 🔗 What I've Done

> Write a description of the steps taken to resolve the issue


## 💬 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/docs/models.rst b/docs/models.rst
index 6ca5ce1b..49938ffc 100644
--- a/docs/models.rst
+++ b/docs/models.rst
@@ -120,6 +120,22 @@ Simply follow the Python lib :mod:`python:dataclasses` documentation.
     Everything else will raise an exception as unsupported.
 
 
+
+Support for generics in standard collections and the new union type was added
+in v23.6
+
+
+.. warning::
+
+    You will get false positive errors from mypy if you are using compound fields.
+
+    Mypy `issue <https://github.com/python/mypy/issues/13026>`_
+
+    .. code-block::
+
+        Value of type "Type[type]" is not indexable  [index]
+
+
 Field Metadata
 ==============
 
diff --git a/setup.cfg b/setup.cfg
index 3341ff6e..0d0fd042 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -34,6 +34,7 @@ project_urls =
 [options]
 packages = xsdata
 install_requires =
+    typing-extensions
     importlib-metadata;python_version<"3.8"
 python_requires = >=3.7
 include_package_data = True
diff --git a/xsdata/formats/dataclass/filters.py b/xsdata/formats/dataclass/filters.py
index 0f44a943..88011fac 100644
--- a/xsdata/formats/dataclass/filters.py
+++ b/xsdata/formats/dataclass/filters.py
@@ -53,6 +53,8 @@ class Filters:
         "module_safe_prefix",
         "docstring_style",
         "max_line_length",
+        "union_type",
+        "subscriptable_types",
         "relative_imports",
         "postponed_annotations",
         "format",
@@ -99,6 +101,8 @@ class Filters:
         self.module_safe_prefix: str = config.conventions.module_name.safe_prefix
         self.docstring_style: DocstringStyle = config.output.docstring_style
         self.max_line_length: int = config.output.max_line_length
+        self.union_type: bool = config.output.union_type
+        self.subscriptable_types: bool = config.output.subscriptable_types
         self.relative_imports: bool = config.output.relative_imports
         self.postponed_annotations: bool = config.output.postponed_annotations
         self.format = config.output.format
@@ -664,11 +668,16 @@ class Filters:
             self.field_type_name(x, parents) for x in attr.types
         )
 
-        result = ", ".join(type_names)
-        if len(type_names) > 1:
-            result = f"Union[{result}]"
+        if self.union_type:
+            result = " | ".join(type_names)
+        else:
+            result = ", ".join(type_names)
+            if len(type_names) > 1:
+                result = f"Union[{result}]"
 
         iterable = "Tuple[{}, ...]" if self.format.frozen else "List[{}]"
+        if self.subscriptable_types:
+            iterable = iterable.lower()
 
         if attr.is_tokens:
             result = iterable.format(result)
@@ -680,12 +689,12 @@ class Filters:
             return result
 
         if attr.is_dict:
-            return "Dict[str, str]"
+            return "dict[str, str]" if self.subscriptable_types else "Dict[str, str]"
 
         if attr.is_nillable or (
             attr.default is None and (attr.is_optional or not self.format.kw_only)
         ):
-            return f"Optional[{result}]"
+            return f"None | {result}" if self.union_type else f"Optional[{result}]"
 
         return result
 
@@ -704,14 +713,22 @@ class Filters:
             self.field_type_name(x, parents) for x in choice.types
         )
 
-        result = ", ".join(type_names)
-        if len(type_names) > 1:
-            result = f"Union[{result}]"
+        if self.union_type:
+            result = " | ".join(type_names)
+        else:
+            result = ", ".join(type_names)
+            if len(type_names) > 1:
+                result = f"Union[{result}]"
 
         if choice.is_tokens:
-            result = (
-                f"Tuple[{result}, ...]" if self.format.frozen else f"List[{result}]"
-            )
+            iterable = "Tuple[{}, ...]" if self.format.frozen else "List[{}]"
+            if self.subscriptable_types:
+                iterable = iterable.lower()
+
+            result = iterable.format(result)
+
+        if self.subscriptable_types:
+            return f"type[{result}]"
 
         return f"Type[{result}]"
 
@@ -727,6 +744,9 @@ class Filters:
         elif attr_type.circular:
             name = f'"{name}"'
 
+        if self.postponed_annotations:
+            name = name.strip('"')
+
         return name
 
     def constant_value(self, attr: Attr) -> str:
@@ -789,4 +809,13 @@ class Filters:
 
     @classmethod
     def build_type_patterns(cls, x: str) -> Tuple:
-        return f": {x} =", f"[{x}]", f"[{x},", f" {x},", f" {x}]", f" {x}("
+        return (
+            f": {x} =",
+            f"[{x}]",
+            f"[{x},",
+            f" {x},",
+            f" {x}]",
+            f" {x}(",
+            f" | {x}",
+            f"{x} |",
+        )
diff --git a/xsdata/formats/dataclass/parsers/json.py b/xsdata/formats/dataclass/parsers/json.py
index 19a66f3a..618512ce 100644
--- a/xsdata/formats/dataclass/parsers/json.py
+++ b/xsdata/formats/dataclass/parsers/json.py
@@ -74,7 +74,7 @@ class JsonParser(AbstractParser):
         try:
             origin = get_origin(clazz)
             list_type = False
-            if origin is List:
+            if origin is list:
                 list_type = True
                 args = get_args(clazz)
 
diff --git a/xsdata/formats/dataclass/typing.py b/xsdata/formats/dataclass/typing.py
index 6ac27bcf..446c7677 100644
--- a/xsdata/formats/dataclass/typing.py
+++ b/xsdata/formats/dataclass/typing.py
@@ -1,67 +1,74 @@
-from typing import _eval_type  # type: ignore
+import sys
 from typing import Any
-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 typing import TypeVar
 from typing import Union
 
-NONE_TYPE = type(None)
-EMPTY_ARGS = ()
+from typing_extensions import get_args
+from typing_extensions import get_origin
 
+NONE_TYPE = type(None)
 
-def get_origin(tp: Any) -> Any:
-    if tp is Dict:
-        return Dict
 
-    if tp in (Tuple, List, Union):
-        raise TypeError()
+try:
+    from types import UnionType  # type: ignore
+except ImportError:
+    UnionType = ()  # type: ignore
 
-    if isinstance(tp, TypeVar):
-        return TypeVar
 
-    origin = getattr(tp, "__origin__", None)
-    if origin:
-        if origin in (list, List):
-            return List
+if (3, 9) <= sys.version_info[:2] <= (3, 10):
+    # Backport this fix for python 3.9 and 3.10
+    # https://github.com/python/cpython/pull/30900
 
-        if origin in (tuple, Tuple):
-            return Tuple
+    from types import GenericAlias
+    from typing import ForwardRef
+    from typing import _eval_type as __eval_type  # type: ignore
 
-        if origin in (dict, Dict):
-            return Dict
+    def _eval_type(tp: Any, globalns: Any, localns: Any) -> Any:
+        if isinstance(tp, GenericAlias):
+            args = tuple(
+                ForwardRef(arg) if isinstance(arg, str) else arg for arg in tp.__args__
+            )
+            tp = tp.__origin__[args]  # type: ignore
 
-        if origin is Union:
-            return Union
+        return __eval_type(tp, globalns, localns)
 
-        if origin in (type, Type):
-            return Type
+else:
+    from typing import _eval_type  # type: ignore
 
-    if origin or str(tp).startswith("typing."):
-        raise TypeError()
 
-    return None
+intern_typing = sys.intern("typing.")
 
 
-def get_args(tp: Any) -> Tuple:
-    return getattr(tp, "__args__", EMPTY_ARGS) or EMPTY_ARGS
+def is_from_typing(tp: Any) -> bool:
+    return str(tp).startswith(intern_typing)
 
 
 def evaluate(
-    tp: Any, globalns: Optional[Any] = None, localns: Optional[Any] = None
+    tp: Any,
+    globalns: Any = None,
+    localns: Any = None,
 ) -> Tuple[Type, ...]:
     return tuple(_evaluate(_eval_type(tp, globalns, localns)))
 
 
 def _evaluate(tp: Any) -> Iterator[Type]:
-    origin = get_origin(tp)
+    if tp in (dict, list, tuple):
+        origin = tp
+    elif isinstance(tp, TypeVar):
+        origin = TypeVar
+    else:
+        origin = get_origin(tp)
 
-    func = __evaluations__.get(origin)
-    if func:
-        yield from func(tp)
+    if origin:
+        try:
+            yield from __evaluations__[origin](tp)
+        except KeyError:
+            raise TypeError()
+    elif is_from_typing(tp):
+        raise TypeError()
     else:
         yield tp
 
@@ -82,15 +89,14 @@ def _evaluate_mapping(tp: Any) -> Iterator[Type]:
         yield str
 
     for arg in args:
-        origin = get_origin(arg)
-        if origin is TypeVar:
+        if isinstance(arg, TypeVar):
             try:
                 next(_evaluate_typevar(arg))
             except TypeError:
                 yield str
             else:
                 raise TypeError()
-        elif origin is not None:
+        elif is_from_typing(arg) or get_origin(arg) is not None:
             raise TypeError()
         else:
             yield arg
@@ -100,12 +106,22 @@ def _evaluate_list(tp: Any) -> Iterator[Type]:
     yield list
 
     args = get_args(tp)
+    if not args:
+        yield str
+
     for arg in args:
+        yield from _evaluate_array_arg(arg)
+
+
+def _evaluate_array_arg(arg: Any) -> Iterator[Type]:
+    if isinstance(arg, TypeVar):
+        yield from _evaluate_typevar(arg)
+    else:
         origin = get_origin(arg)
 
-        if origin is None:
+        if origin is None and not is_from_typing(arg):
             yield arg
-        elif origin in (Union, List, Tuple, TypeVar):
+        elif origin in (Union, UnionType, list, tuple):
             yield from __evaluations__[origin](arg)
         else:
             raise TypeError()
@@ -115,17 +131,14 @@ def _evaluate_tuple(tp: Any) -> Iterator[Type]:
     yield tuple
 
     args = get_args(tp)
+    if not args:
+        yield str
+
     for arg in args:
         if arg is Ellipsis:
             continue
 
-        origin = get_origin(arg)
-        if origin is None:
-            yield arg
-        elif origin in (Union, List, Tuple, TypeVar):
-            yield from __evaluations__[origin](arg)
-        else:
-            raise TypeError()
+        yield from _evaluate_array_arg(arg)
 
 
 def _evaluate_union(tp: Any) -> Iterator[Type]:
@@ -134,14 +147,17 @@ def _evaluate_union(tp: Any) -> Iterator[Type]:
         if arg is NONE_TYPE:
             continue
 
-        origin = get_origin(arg)
-        if origin is None:
-            yield arg
-        elif origin is List and not origin_locked:
-            yield from _evaluate_list(arg)
-            origin_locked = True
+        if isinstance(arg, TypeVar):
+            yield from _evaluate_typevar(arg)
         else:
-            raise TypeError()
+            origin = get_origin(arg)
+            if origin is list and not origin_locked:
+                yield from _evaluate_list(arg)
+                origin_locked = True
+            elif origin is None and not is_from_typing(arg):
+                yield arg
+            else:
+                raise TypeError()
 
 
 def _evaluate_typevar(tp: TypeVar):
@@ -155,10 +171,11 @@ def _evaluate_typevar(tp: TypeVar):
 
 
 __evaluations__ = {
-    Tuple: _evaluate_tuple,
-    List: _evaluate_list,
-    Dict: _evaluate_mapping,
+    tuple: _evaluate_tuple,
+    list: _evaluate_list,
+    dict: _evaluate_mapping,
     Union: _evaluate_union,
-    Type: _evaluate_type,
+    UnionType: _evaluate_union,
+    type: _evaluate_type,
     TypeVar: _evaluate_typevar,
 }
diff --git a/xsdata/models/config.py b/xsdata/models/config.py
index 5c723a49..5b96194c 100644
--- a/xsdata/models/config.py
+++ b/xsdata/models/config.py
@@ -255,8 +255,12 @@ class GeneratorOutput:
     :param docstring_style: Docstring style, default: reStructuredText
     :param filter_strategy: Class filter strategy, default: globals
     :param relative_imports: Use relative imports, default: false
-    :param compound_fields: Use compound fields for repeatable elements, default: false
+    :param compound_fields: Use compound fields for repeatable elements,
+        default: false
     :param max_line_length: Adjust the maximum line length, default: 79
+    :param subscriptable_types: Use PEP-585 generics for standard collections,
+        default: false, python>=3.9 Only
+    :param union_type: Use PEP-604 union type, default: false, python>=3.10 Only
     :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
@@ -277,11 +281,38 @@ class GeneratorOutput:
     relative_imports: bool = element(default=False)
     compound_fields: CompoundFields = element(default_factory=CompoundFields)
     max_line_length: int = attribute(default=79)
+    subscriptable_types: bool = attribute(default=False)
+    union_type: bool = attribute(default=False)
     postponed_annotations: bool = element(default=False)
     unnest_classes: bool = element(default=False)
     ignore_patterns: bool = element(default=False)
     include_header: bool = element(default=False)
 
+    def __post_init__(self):
+        self.validate()
+
+    def validate(self):
+        if self.subscriptable_types and sys.version_info < (3, 9):
+            self.subscriptable_types = False
+            warnings.warn(
+                "Generics PEP 585 requires python >= 3.9, reverting...",
+                CodeGenerationWarning,
+            )
+
+        if self.union_type and sys.version_info < (3, 10):
+            self.union_type = False
+            warnings.warn(
+                "UnionType PEP 604 requires python >= 3.10, reverting...",
+                CodeGenerationWarning,
+            )
+
+        if self.union_type and not self.postponed_annotations:
+            self.postponed_annotations = True
+            warnings.warn(
+                "Enabling postponed annotations, because `union_type==True`",
+                CodeGenerationWarning,
+            )
+
     def update(self, **kwargs: Any):
         objects.update(self, **kwargs)
         self.format.validate()
diff --git a/xsdata/models/datatype.py b/xsdata/models/datatype.py
index 9c962471..4345237a 100644
--- a/xsdata/models/datatype.py
+++ b/xsdata/models/datatype.py
@@ -95,7 +95,7 @@ class XmlDate(NamedTuple):
 
         .. warning::
 
-        date instances don't have timezone information!
+            date instances don't have timezone information!
         """
         return cls(obj.year, obj.month, obj.day)
 
diff --git a/xsdata/utils/text.py b/xsdata/utils/text.py
index 779786e4..5f9ac680 100644
--- a/xsdata/utils/text.py
+++ b/xsdata/utils/text.py
@@ -58,6 +58,7 @@ stop_words = {
     "self",
     "str",
     "try",
+    "type",
     "while",
     "with",
     "yield",


Test patch:
diff --git a/tests/fixtures/artists/metadata.py b/tests/fixtures/artists/metadata.py
index d23e73f5..d5afed05 100644
--- a/tests/fixtures/artists/metadata.py
+++ b/tests/fixtures/artists/metadata.py
@@ -25,9 +25,10 @@ class Alias:
             "required": True,
         }
     )
-    type: Optional[str] = field(
+    type_value: Optional[str] = field(
         default=None,
         metadata={
+            "name": "type",
             "type": "Attribute",
         }
     )
@@ -303,9 +304,10 @@ class Artist:
             "required": True,
         }
     )
-    type: Optional[str] = field(
+    type_value: Optional[str] = field(
         default=None,
         metadata={
+            "name": "type",
             "type": "Attribute",
             "required": True,
         }
diff --git a/tests/fixtures/series/series.py b/tests/fixtures/series/series.py
index 3b397388..ad4088a7 100644
--- a/tests/fixtures/series/series.py
+++ b/tests/fixtures/series/series.py
@@ -215,9 +215,10 @@ class Series:
             "required": True,
         }
     )
-    type: Optional[str] = field(
+    type_value: Optional[str] = field(
         default=None,
         metadata={
+            "name": "type",
             "type": "Element",
             "required": True,
         }
diff --git a/tests/formats/dataclass/test_filters.py b/tests/formats/dataclass/test_filters.py
index 5058d6ef..32112d60 100644
--- a/tests/formats/dataclass/test_filters.py
+++ b/tests/formats/dataclass/test_filters.py
@@ -540,6 +540,9 @@ class FiltersTests(FactoryTestCase):
         attr.restrictions.nillable = True
         self.assertEqual("Optional[FooBar]", self.filters.field_type(attr, []))
 
+        self.filters.union_type = True
+        self.assertEqual("None | FooBar", self.filters.field_type(attr, []))
+
     def test_field_type_with_optional_value(self):
         attr = AttrFactory.create(types=AttrTypeFactory.list(1, qname="foo_bar"))
 
@@ -551,6 +554,9 @@ class FiltersTests(FactoryTestCase):
         attr.restrictions.min_occurs = 0
         self.assertEqual("Optional[FooBar]", self.filters.field_type(attr, []))
 
+        self.filters.union_type = True
+        self.assertEqual("None | FooBar", self.filters.field_type(attr, []))
+
     def test_field_type_with_circular_reference(self):
         attr = AttrFactory.create(
             types=AttrTypeFactory.list(1, qname="foo_bar", circular=True)
@@ -569,6 +575,13 @@ class FiltersTests(FactoryTestCase):
             self.filters.field_type(attr, ["Parent", "Inner"]),
         )
 
+        self.filters.postponed_annotations = True
+        self.filters.union_type = True
+        self.assertEqual(
+            "None | Parent.Inner.FooBar",
+            self.filters.field_type(attr, ["Parent", "Inner"]),
+        )
+
     def test_field_type_with_forward_and_circular_reference(self):
         attr = AttrFactory.create(
             types=AttrTypeFactory.list(1, qname="foo_bar", forward=True, circular=True)
@@ -595,6 +608,18 @@ class FiltersTests(FactoryTestCase):
             self.filters.field_type(attr, ["A", "Parent"]),
         )
 
+        self.filters.subscriptable_types = True
+        self.assertEqual(
+            'tuple["A.Parent.FooBar", ...]',
+            self.filters.field_type(attr, ["A", "Parent"]),
+        )
+
+        self.filters.format.frozen = False
+        self.assertEqual(
+            'list["A.Parent.FooBar"]',
+            self.filters.field_type(attr, ["A", "Parent"]),
+        )
+
     def test_field_type_with_token_attr(self):
         attr = AttrFactory.create(
             types=AttrTypeFactory.list(1, qname="foo_bar"),
@@ -614,6 +639,11 @@ class FiltersTests(FactoryTestCase):
             "Tuple[Tuple[FooBar, ...], ...]", self.filters.field_type(attr, [])
         )
 
+        self.filters.subscriptable_types = True
+        self.assertEqual(
+            "tuple[tuple[FooBar, ...], ...]", self.filters.field_type(attr, [])
+        )
+
     def test_field_type_with_alias(self):
         attr = AttrFactory.create(
             types=AttrTypeFactory.list(
@@ -640,19 +670,39 @@ class FiltersTests(FactoryTestCase):
             self.filters.field_type(attr, ["A", "Parent"]),
         )
 
+        self.filters.union_type = True
+        self.assertEqual(
+            'List["A.Parent.BossLife" | int]',
+            self.filters.field_type(attr, ["A", "Parent"]),
+        )
+        self.filters.subscriptable_types = True
+        self.assertEqual(
+            'list["A.Parent.BossLife" | int]',
+            self.filters.field_type(attr, ["A", "Parent"]),
+        )
+
     def test_field_type_with_any_attribute(self):
         attr = AttrFactory.any_attribute()
 
         self.assertEqual("Dict[str, str]", self.filters.field_type(attr, ["a", "b"]))
 
+        self.filters.subscriptable_types = True
+        self.assertEqual("dict[str, str]", self.filters.field_type(attr, ["a", "b"]))
+
     def test_field_type_with_native_type(self):
         attr = AttrFactory.create(
             types=[
                 AttrTypeFactory.native(DataType.INT),
                 AttrTypeFactory.native(DataType.POSITIVE_INTEGER),
+                AttrTypeFactory.native(DataType.STRING),
             ]
         )
-        self.assertEqual("Optional[int]", self.filters.field_type(attr, ["a", "b"]))
+        self.assertEqual(
+            "Optional[Union[int, str]]", self.filters.field_type(attr, ["a", "b"])
+        )
+
+        self.filters.union_type = True
+        self.assertEqual("None | int | str", self.filters.field_type(attr, ["a", "b"]))
 
     def test_field_type_with_prohibited_attr(self):
         attr = AttrFactory.create(restrictions=Restrictions(max_occurs=0))
@@ -664,6 +714,10 @@ class FiltersTests(FactoryTestCase):
         actual = self.filters.choice_type(choice, ["a", "b"])
         self.assertEqual("Type[Foobar]", actual)
 
+        self.filters.subscriptable_types = True
+        actual = self.filters.choice_type(choice, ["a", "b"])
+        self.assertEqual("type[Foobar]", actual)
+
     def test_choice_type_with_forward_reference(self):
         choice = AttrFactory.create(
             types=[AttrTypeFactory.create("foobar", forward=True)]
@@ -678,11 +732,23 @@ class FiltersTests(FactoryTestCase):
         actual = self.filters.choice_type(choice, ["a", "b"])
         self.assertEqual('Type["Foobar"]', actual)
 
+        self.filters.postponed_annotations = True
+        actual = self.filters.choice_type(choice, ["a", "b"])
+        self.assertEqual("Type[Foobar]", actual)
+
     def test_choice_type_with_multiple_types(self):
         choice = AttrFactory.create(types=[type_str, type_bool])
         actual = self.filters.choice_type(choice, ["a", "b"])
         self.assertEqual("Type[Union[str, bool]]", actual)
 
+        self.filters.subscriptable_types = True
+        actual = self.filters.choice_type(choice, ["a", "b"])
+        self.assertEqual("type[Union[str, bool]]", actual)
+
+        self.filters.union_type = True
+        actual = self.filters.choice_type(choice, ["a", "b"])
+        self.assertEqual("type[str | bool]", actual)
+
     def test_choice_type_with_list_types_are_ignored(self):
         choice = AttrFactory.create(types=[type_str, type_bool])
         choice.restrictions.max_occurs = 200
@@ -699,6 +765,11 @@ class FiltersTests(FactoryTestCase):
         actual = self.filters.choice_type(choice, ["a", "b"])
         self.assertEqual("Type[Tuple[Union[str, bool], ...]]", actual)
 
+        self.filters.union_type = True
+        self.filters.subscriptable_types = True
+        actual = self.filters.choice_type(choice, ["a", "b"])
+        self.assertEqual("type[tuple[str | bool, ...]]", actual)
+
     def test_default_imports_with_decimal(self):
         expected = "from decimal import Decimal"
 
diff --git a/tests/formats/dataclass/test_typing.py b/tests/formats/dataclass/test_typing.py
index e20eff5c..ec5c72b5 100644
--- a/tests/formats/dataclass/test_typing.py
+++ b/tests/formats/dataclass/test_typing.py
@@ -1,4 +1,5 @@
 import datetime
+import sys
 from decimal import Decimal
 from enum import Enum
 from typing import Any
@@ -26,141 +27,167 @@ from xsdata.models.enums import Namespace
 
 
 class TypingTests(TestCase):
-    def test_get_origin_list(self):
-        self.assertEqual(List, get_origin(List[int]))
-        self.assertEqual(List, get_origin(List[Union[int]]))
-
-        with self.assertRaises(TypeError):
-            get_origin(List)
-
-    def test_get_origin_tuple(self):
-        self.assertEqual(Tuple, get_origin(Tuple[int]))
-        self.assertEqual(Tuple, get_origin(Tuple[Union[int, str]]))
-        self.assertEqual(Tuple, get_origin(Tuple[int, ...]))
-
-        with self.assertRaises(TypeError):
-            get_origin(Tuple)
-
-    def test_get_origin_dict(self):
-        self.assertEqual(Dict, get_origin(Dict))
-        self.assertEqual(Dict, get_origin(Dict[int, str]))
-        self.assertEqual(Dict, get_origin(Dict[Union[int], Union[str]]))
-
-    def test_get_origin_union(self):
-        self.assertIsNone(get_origin(Union[int]))
-        self.assertEqual(Union, get_origin(Optional[int]))
-        self.assertEqual(Union, get_origin(Union[int, str]))
-
-        with self.assertRaises(TypeError):
-            get_origin(Union)
-
-        with self.assertRaises(TypeError):
-            get_origin(Optional)
-
-    def test_get_origin_type(self):
-        self.assertEqual(Type, get_origin(Type[str]))
-
-    def test_get_origin_types(self):
-        self.assertIsNone(get_origin(str))
-        self.assertIsNone(get_origin(int))
-        self.assertIsNone(get_origin(bool))
-        self.assertIsNone(get_origin(float))
-        self.assertIsNone(get_origin(bytes))
-        self.assertIsNone(get_origin(object))
-        self.assertIsNone(get_origin(datetime.time))
-        self.assertIsNone(get_origin(datetime.date))
-        self.assertIsNone(get_origin(datetime.datetime))
-        self.assertIsNone(get_origin(XmlTime))
-        self.assertIsNone(get_origin(XmlDate))
-        self.assertIsNone(get_origin(XmlDateTime))
-        self.assertIsNone(get_origin(XmlDuration))
-        self.assertIsNone(get_origin(XmlPeriod))
-        self.assertIsNone(get_origin(QName))
-        self.assertIsNone(get_origin(Decimal))
-        self.assertIsNone(get_origin(Enum))
-        self.assertIsNone(get_origin(Namespace))
-
-    def test_get_origin_unsupported(self):
-        unsupported = [Set, Tuple]
-        for check in unsupported:
-            with self.assertRaises(TypeError, msg=check):
-                get_origin(check)
-
-    def test_get_args(self):
-        self.assertEqual((), get_args(int))
-        self.assertEqual((int,), get_args(List[int]))
-        self.assertEqual((int, Ellipsis), get_args(Tuple[int, ...]))
-        self.assertEqual((int,), get_args(Tuple[int]))
-        self.assertEqual((int, str, float), get_args(Tuple[int, str, float]))
-        self.assertEqual((Union[str, int],), get_args(Tuple[Union[str, int]]))
-        self.assertEqual((List[int], type(None)), get_args(Optional[List[int]]))
-        self.assertEqual((int, str), get_args(Union[int, str]))
-        self.assertEqual((int, type(None)), get_args(Optional[int]))
-        self.assertEqual((int, type(None), str), get_args(Union[Optional[int], str]))
+    def assertCases(self, cases):
+        for tp, result in cases.items():
+            if result is False:
+                with self.assertRaises(TypeError):
+                    evaluate(tp)
+            else:
+                self.assertEqual(result, evaluate(tp), msg=tp)
 
     def test_evaluate_simple(self):
-        self.assertEqual((int,), evaluate(int))
-
-    def test_evaluate_dict(self):
-        self.assertEqual((dict, str, str), evaluate(Dict))
-        self.assertEqual((dict, str, int), evaluate(Dict[str, int]))
-
-        unsupported_cases = [
-            Dict[Any, Any],
-            Dict[Union[str, int], int],
-            Dict[int, Union[str, int]],
-            Dict[TypeVar("A", bound=int), str],
-        ]
-
-        for case in unsupported_cases:
-            with self.assertRaises(TypeError, msg=case):
-                evaluate(case)
-
-    def test_evaluate_union(self):
-        self.assertEqual((bool, str), evaluate(Optional[Union[bool, str]]))
-        self.assertEqual(
-            (list, int, float), evaluate(Optional[List[Union[int, float]]])
+        types = (
+            int,
+            str,
+            int,
+            bool,
+            float,
+            bytes,
+            object,
+            datetime.time,
+            datetime.date,
+            datetime.datetime,
+            XmlTime,
+            XmlDate,
+            XmlDateTime,
+            XmlDuration,
+            XmlPeriod,
+            QName,
+            Decimal,
+            Enum,
+            Namespace,
         )
+        cases = {tp: (tp,) for tp in types}
+        self.assertCases(cases)
 
-        unsupported_cases = [Optional[T], Union[List[int], Dict[str, str]]]
-        for case in unsupported_cases:
-            with self.assertRaises(TypeError, msg=case):
+    def test_evaluate_unsupported_typing(self):
+        cases = [Any, Set[str]]
+
+        for case in cases:
+            with self.assertRaises(TypeError):
                 evaluate(case)
 
+    def test_evaluate_dict(self):
+        cases = {
+            Dict: (dict, str, str),
+            Dict[str, int]: (dict, str, int),
+            Dict[Any, Any]: False,
+            Dict[Union[str, int], int]: False,
+            Dict[int, Union[str, int]]: False,
+            Dict[TypeVar("A", bound=int), str]: False,
+            Dict[TypeVar("A"), str]: (dict, str, str),
+        }
+
+        if sys.version_info[:2] >= (3, 9):
+            cases.update(
+                {
+                    dict: (dict, str, str),
+                    dict[str, int]: (dict, str, int),
+                    dict[Any, Any]: False,
+                    dict[Union[str, int], int]: False,
+                    dict[int, Union[str, int]]: False,
+                    dict[TypeVar("A", bound=int), str]: False,
+                    dict[TypeVar("A"), str]: (dict, str, str),
+                }
+            )
+
+        if sys.version_info[:2] >= (3, 10):
+            cases.update({dict[str | int, int]: False})
+
+        self.assertCases(cases)
+
     def test_evaluate_list(self):
         A = TypeVar("A", int, str)
 
-        self.assertEqual((list, int, str), evaluate(List[A]))
-        self.assertEqual((list, int), evaluate(List[int]))
-        self.assertEqual((list, float, str), evaluate(List[Union[float, str]]))
-        self.assertEqual((list, int), evaluate(List[Optional[int]]))
-        self.assertEqual((list, tuple, int), evaluate(List[Tuple[int]]))
-        self.assertEqual(
-            (list, list, bool, str), evaluate(List[List[Union[bool, str]]])
-        )
-
-        unsupported_cases = [List, List[Dict[str, str]]]
-        for case in unsupported_cases:
-            with self.assertRaises(TypeError, msg=case):
-                evaluate(case)
+        cases = {
+            List[A]: (list, int, str),
+            List[int]: (list, int),
+            List[Union[float, str]]: (list, float, str),
+            List[Optional[int]]: (list, int),
+            List[Tuple[int]]: (list, tuple, int),
+            List[List[Union[bool, str]]]: (list, list, bool, str),
+            List: (list, str),
+            List[Dict[str, str]]: False,
+            List[Any]: False,
+        }
+
+        if sys.version_info[:2] >= (3, 9):
+            cases.update(
+                {
+                    list[A]: (list, int, str),
+                    list[int]: (list, int),
+                    list[Union[float, str]]: (list, float, str),
+                    list[Optional[int]]: (list, int),
+                    list[Tuple[int]]: (list, tuple, int),
+                    list[list[Union[bool, str]]]: (list, list, bool, str),
+                    list: (list, str),
+                    list["str"]: (list, str),
+                    list[dict[str, str]]: False,
+                    list[Any]: False,
+                }
+            )
+
+        self.assertCases(cases)
 
     def test_evaluate_tuple(self):
         A = TypeVar("A", int, str)
 
-        self.assertEqual((tuple, int, str), evaluate(Tuple[A]))
-        self.assertEqual((tuple, int), evaluate(Tuple[int]))
-        self.assertEqual((tuple, int), evaluate(Tuple[int, ...]))
-        self.assertEqual((tuple, list, int), evaluate(Tuple[List[int], ...]))
-        self.assertEqual((tuple, float, str), evaluate(Tuple[Union[float, str]]))
-        self.assertEqual((tuple, int), evaluate(Tuple[Optional[int]]))
-        self.assertEqual(
-            (tuple, tuple, bool, str), evaluate(Tuple[Tuple[Union[bool, str]]])
-        )
+        cases = {
+            Tuple[A]: (tuple, int, str),
+            Tuple[int]: (tuple, int),
+            Tuple[int, ...]: (tuple, int),
+            Tuple[List[int], ...]: (tuple, list, int),
+            Tuple[Union[float, str]]: (tuple, float, str),
+            Tuple[Optional[int]]: (tuple, int),
+            Tuple[Tuple[int]]: (tuple, tuple, int),
+            Tuple[Tuple[Union[bool, str]]]: (tuple, tuple, bool, str),
+            Tuple: (tuple, str),
+            Tuple[Dict[str, str]]: False,
+            Tuple[Any, ...]: False,
+        }
+
+        if sys.version_info[:2] >= (3, 9):
+            cases.update(
+                {
+                    tuple[A]: (tuple, int, str),
+                    tuple[int]: (tuple, int),
+                    tuple[int, ...]: (tuple, int),
+                    tuple[List[int], ...]: (tuple, list, int),
+                    tuple[Union[float, str]]: (tuple, float, str),
+                    tuple[Optional[int]]: (tuple, int),
+                    tuple[tuple[int]]: (tuple, tuple, int),
+                    tuple[tuple[Union[bool, str]]]: (tuple, tuple, bool, str),
+                    tuple: (tuple, str),
+                    tuple[dict[str, str]]: False,
+                    tuple[Any, ...]: False,
+                }
+            )
+
+        self.assertCases(cases)
 
-        unsupported_cases = [Tuple, Tuple[Dict[str, str]]]
-        for case in unsupported_cases:
-            with self.assertRaises(TypeError, msg=case):
-                evaluate(case)
+    def test_evaluate_union(self):
+        A = TypeVar("A", int, str)
+
+        cases = {
+            Optional[Union[bool, str]]: (bool, str),
+            Optional[List[Union[int, float]]]: (list, int, float),
+            Optional[A]: (int, str),
+            Union[List[int], None]: (list, int),
+            Union[List[int], List[str]]: False,
+        }
+
+        if sys.version_info[:2] >= (3, 10):
+            cases.update(
+                {
+                    None | bool | str: (bool, str),
+                    None | List[int | float]: (list, int, float),
+                    None | A: (int, str),
+                    List[int] | None: (list, int),
+                    List[int] | List[str]: False,
+                }
+            )
+
+        self.assertCases(cases)
 
     def test_evaluate_type(self):
         self.assertEqual((str,), evaluate(Type["str"]))
diff --git a/tests/models/test_config.py b/tests/models/test_config.py
index 19d24193..9eab3570 100644
--- a/tests/models/test_config.py
+++ b/tests/models/test_config.py
@@ -30,7 +30,7 @@ class GeneratorConfigTests(TestCase):
         expected = (
             '<?xml version="1.0" encoding="UTF-8"?>\n'
             f'<Config xmlns="http://pypi.org/project/xsdata" version="{__version__}">\n'
-            '  <Output maxLineLength="79">\n'
+            '  <Output maxLineLength="79" subscriptableTypes="false" unionType="false">\n'
             "    <Package>generated</Package>\n"
             '    <Format repr="true" eq="true" order="false" unsafeHash="false" frozen="false" slots="false" kwOnly="false">dataclasses</Format>\n'
             "    <Structure>filenames</Structure>\n"
@@ -72,7 +72,7 @@ class GeneratorConfigTests(TestCase):
         existing = (
             '<?xml version="1.0" encoding="UTF-8"?>\n'
             '<Config xmlns="http://pypi.org/project/xsdata" version="20.8">\n'
-            '  <Output maxLineLength="79">\n'
+            '  <Output maxLineLength="79" subscriptableTypes="false" unionType="false">\n'
             "    <Package>foo.bar</Package>\n"
             "  </Output>\n"
             "  <Conventions>\n"
@@ -91,7 +91,7 @@ class GeneratorConfigTests(TestCase):
         expected = (
             '<?xml version="1.0" encoding="UTF-8"?>\n'
             f'<Config xmlns="http://pypi.org/project/xsdata" version="{__version__}">\n'
-            '  <Output maxLineLength="79">\n'
+            '  <Output maxLineLength="79" subscriptableTypes="false" unionType="false">\n'
             "    <Package>foo.bar</Package>\n"
             '    <Format repr="true" eq="true" order="false" unsafeHash="false"'
             ' frozen="false" slots="false" kwOnly="false">dataclasses</Format>\n'
@@ -139,6 +139,36 @@ class GeneratorConfigTests(TestCase):
 
         self.assertEqual("eq must be true if order is true", str(cm.exception))
 
+    def test_subscriptable_types_requires_390(self):
+        if sys.version_info < (3, 9):
+            with warnings.catch_warnings(record=True) as w:
+                self.assertFalse(
+                    GeneratorOutput(subscriptable_types=True).subscriptable_types
+                )
+
+            self.assertEqual(
+                "Generics PEP 585 requires python >= 3.9, reverting...",
+                str(w[-1].message),
+            )
+
+        else:
+            self.assertTrue(
+                GeneratorOutput(subscriptable_types=True).subscriptable_types
+            )
+
+    def test_use_union_type_requires_310(self):
+        if sys.version_info < (3, 10):
+            with warnings.catch_warnings(record=True) as w:
+                self.assertFalse(GeneratorOutput(union_type=True).union_type)
+
+            self.assertEqual(
+                "UnionType PEP 604 requires python >= 3.10, reverting...",
+                str(w[-1].message),
+            )
+
+        else:
+            self.assertTrue(GeneratorOutput(union_type=True).union_type)
+
     def test_format_slots_requires_310(self):
         if sys.version_info < (3, 10):
             self.assertTrue(OutputFormat(slots=True, value="attrs").slots)