Skip to content

Commit

Permalink
Support subcriptable types and UnionType (#801)
Browse files Browse the repository at this point in the history
  • Loading branch information
tefra authored Jun 11, 2023
1 parent 4c4cf29 commit 11dbca8
Show file tree
Hide file tree
Showing 13 changed files with 427 additions and 201 deletions.
16 changes: 16 additions & 0 deletions docs/models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
==============

Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions tests/fixtures/artists/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ class Meta:
"required": True,
}
)
type: Optional[str] = field(
type_value: Optional[str] = field(
default=None,
metadata={
"name": "type",
"type": "Attribute",
}
)
Expand Down Expand Up @@ -303,9 +304,10 @@ class Meta:
"required": True,
}
)
type: Optional[str] = field(
type_value: Optional[str] = field(
default=None,
metadata={
"name": "type",
"type": "Attribute",
"required": True,
}
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/series/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,10 @@ class Meta:
"required": True,
}
)
type: Optional[str] = field(
type_value: Optional[str] = field(
default=None,
metadata={
"name": "type",
"type": "Element",
"required": True,
}
Expand Down
73 changes: 72 additions & 1 deletion tests/formats/dataclass/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,9 @@ def test_field_type_with_default_value(self):
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"))

Expand All @@ -551,6 +554,9 @@ def test_field_type_with_optional_value(self):
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)
Expand All @@ -569,6 +575,13 @@ def test_field_type_with_forward_reference(self):
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)
Expand All @@ -595,6 +608,18 @@ def test_field_type_with_array_type(self):
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"),
Expand All @@ -614,6 +639,11 @@ def test_field_type_with_token_attr(self):
"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(
Expand All @@ -640,19 +670,39 @@ def test_field_type_with_multiple_types(self):
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))
Expand All @@ -664,6 +714,10 @@ def test_choice_type(self):
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)]
Expand All @@ -678,11 +732,23 @@ def test_choice_type_with_circular_reference(self):
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
Expand All @@ -699,6 +765,11 @@ def test_choice_type_with_restrictions_tokens_true(self):
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"

Expand Down
Loading

0 comments on commit 11dbca8

Please sign in to comment.