Skip to content

Commit

Permalink
feat: Rewrite types analyze process
Browse files Browse the repository at this point in the history
  • Loading branch information
tefra committed Mar 30, 2024
1 parent e403f85 commit 5683a5e
Show file tree
Hide file tree
Showing 15 changed files with 518 additions and 547 deletions.
4 changes: 4 additions & 0 deletions tests/formats/dataclass/models/cases/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import sys

PY39 = sys.version_info[:2] >= (3, 9)
PY310 = sys.version_info[:2] >= (3, 10)
51 changes: 51 additions & 0 deletions tests/formats/dataclass/models/cases/attribute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from typing import Dict, List, Literal, Set, Tuple, Union

from tests.formats.dataclass.models.cases import PY39, PY310
from xsdata.models.enums import Mode

tokens = [
(int, False),
(Dict[int, int], False),
(Dict, False),
(Literal["foo"], False),
(Set[str], False),
(List[Union[List[int], int]], False),
(List[List[int]], False),
(Tuple[int, ...], ((int,), None, tuple)),
(List[int], ((int,), None, list)),
(List[Union[str, int]], ((str, int), None, list)),
]

not_tokens = [
(List[int], False),
(Dict[int, str], False),
(int, ((int,), None, None)),
(str, ((str,), None, None)),
(Union[str, Mode], ((str, Mode), None, None)),
]

if PY39:
tokens.extend(
[
(list[int, int], False),
(dict[str, str], False),
(dict, False),
(set[str], False),
(tuple[int, ...], ((int,), None, tuple)),
(list[int], ((int,), None, list)),
(list[Union[str, int]], ((str, int), None, list)),
]
)

if PY310:
tokens.extend(
[
(tuple[int | str], ((int, str), None, tuple)),
]
)

not_tokens.extend(
[
(int | str, ((int, str), None, None)),
]
)
20 changes: 20 additions & 0 deletions tests/formats/dataclass/models/cases/attributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import Dict, List, Set, Tuple

from tests.formats.dataclass.models.cases import PY39

cases = [
(int, False),
(Set, False),
(List, False),
(Tuple, False),
(Dict[str, int], False),
(Dict, ((str,), dict, None)),
(Dict[str, str], ((str,), dict, None)),
]

if PY39:
cases.extend(
[
(dict[str, str], ((str,), dict, None)),
]
)
45 changes: 45 additions & 0 deletions tests/formats/dataclass/models/cases/element.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from typing import Dict, List, Set, Tuple, Union

from tests.formats.dataclass.models.cases import PY39

tokens = [
(Set, False),
(Dict[str, int], False),
(Tuple[str, str], False),
(List[str], ((str,), None, list)),
(Tuple[str, ...], ((str,), None, tuple)),
(List[List[str]], ((str,), list, list)),
(List[Tuple[str, ...]], ((str,), list, tuple)),
(Tuple[List[str], ...], ((str,), tuple, list)),
]

not_tokens = [
(Set, False),
(Dict[str, int], False),
(Tuple[str, int], False),
(List[List[str]], False),
(List[Tuple[str, ...]], False),
(Tuple[List[str], ...], False),
(str, ((str,), None, None)),
(List[str], ((str,), list, None)),
(List[Union[str, int]], ((str, int), list, None)),
(Tuple[str, ...], ((str,), tuple, None)),
]

if PY39:
tokens.extend(
[
(list[str], ((str,), None, list)),
(tuple[str, ...], ((str,), None, tuple)),
(list[list[str]], ((str,), list, list)),
(list[tuple[str, ...]], ((str,), list, tuple)),
(tuple[list[str], ...], ((str,), tuple, list)),
]
)

not_tokens.extend(
[
(list[str], ((str,), list, None)),
(tuple[str, ...], ((str,), tuple, None)),
]
)
33 changes: 33 additions & 0 deletions tests/formats/dataclass/models/cases/elements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Dict, List, Optional, Tuple, Union

from tests.formats.dataclass.models.cases import PY39, PY310

cases = [
(Dict, False),
(str, ((object,), None, None)),
(List[str], ((object,), list, None)),
(Tuple[str, ...], ((object,), tuple, None)),
(Optional[Union[str, int]], ((object,), None, None)),
(Union[str, int, None], ((object,), None, None)),
(List[Union[List[str], Tuple[str, ...]]], ((object,), list, None)),
]


if PY39:
cases.extend(
[
(dict, False),
(list[str], ((object,), list, None)),
(tuple[str, ...], ((object,), tuple, None)),
(list[Union[list[str], tuple[str, ...]]], ((object,), list, None)),
]
)


if PY310:
cases.extend(
[
(str | int | None, ((object,), None, None)),
(list[list[str] | tuple[str, ...]], ((object,), list, None)),
]
)
24 changes: 24 additions & 0 deletions tests/formats/dataclass/models/cases/wildcard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import Dict, List, Literal, Optional, Set, Tuple

from tests.formats.dataclass.models.cases import PY310

cases = [
(int, False),
(Dict[int, int], False),
(Dict, False),
(Set, False),
(Literal["foo"], False),
(object, ((object,), None, None)),
(List[object], ((object,), list, None)),
(Tuple[object, ...], ((object,), tuple, None)),
(Optional[object], ((object,), None, None)),
]

if PY310:
cases.extend(
[
(list[object], ((object,), list, None)),
(tuple[object, ...], ((object,), tuple, None)),
(object | None, ((object,), None, None)),
]
)
75 changes: 1 addition & 74 deletions tests/formats/dataclass/models/test_builders.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import functools
import sys
import uuid
from dataclasses import dataclass, field, fields, make_dataclass
from decimal import Decimal
from typing import Dict, Iterator, List, Union, get_type_hints
from typing import Iterator, List, get_type_hints
from unittest import TestCase, mock
from xml.etree.ElementTree import QName

Expand Down Expand Up @@ -448,74 +446,3 @@ def test_resolve_namespaces(self):

actual = func(XmlType.WILDCARD, "##targetNamespace foo", "p")
self.assertEqual(("foo", "p"), tuple(sorted(actual)))

def test_analyze_types(self):
func = functools.partial(self.builder.analyze_types, BookForm, "foo")

actual = func(List[List[Union[str, int]]], None)
self.assertEqual((list, list, (int, str)), actual)

actual = func(Union[str, int], None)
self.assertEqual((None, None, (int, str)), actual)

actual = func(Dict[str, int], None)
self.assertEqual((dict, None, (int, str)), actual)

with self.assertRaises(XmlContextError) as cm:
func(List[List[List[int]]], None)

self.assertEqual(
"Error on BookForm::foo: Unsupported field typing `typing.List[typing.List[typing.List[int]]]`",
str(cm.exception),
)

def test_is_valid(self):
# Attributes need origin dict
self.assertFalse(
self.builder.is_valid(XmlType.ATTRIBUTES, None, None, (), False, True)
)

# Attributes don't support any origin
self.assertFalse(
self.builder.is_valid(XmlType.ATTRIBUTES, dict, list, (), False, True)
)

# Attributes don't support xs:NMTOKENS
self.assertFalse(
self.builder.is_valid(XmlType.ATTRIBUTES, dict, None, (), True, True)
)

self.assertTrue(
self.builder.is_valid(
XmlType.ATTRIBUTES, dict, None, (str, str), False, True
)
)

# xs:NMTOKENS need origin list
self.assertFalse(
self.builder.is_valid(XmlType.TEXT, dict, None, (), True, True)
)

# xs:NMTOKENS need origin list
self.assertFalse(self.builder.is_valid(XmlType.TEXT, set, None, (), True, True))

# Any type object is a superset, it's only supported alone
self.assertFalse(
self.builder.is_valid(
XmlType.ELEMENT, None, None, (object, int), False, True
)
)

# Type is not registered in converter.
self.assertFalse(
self.builder.is_valid(
XmlType.TEXT, None, None, (int, uuid.UUID), False, True
)
)

# init false vars are ignored!
self.assertTrue(
self.builder.is_valid(
XmlType.TEXT, None, None, (int, uuid.UUID), False, False
)
)
79 changes: 79 additions & 0 deletions tests/formats/dataclass/models/test_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import pytest

from tests.formats.dataclass.models.cases import (
attribute,
attributes,
element,
elements,
wildcard,
)
from xsdata.formats.dataclass.models.validators import (
validate_attribute,
validate_attributes,
validate_element,
validate_elements,
validate_wildcard,
)


@pytest.mark.parametrize("case,expected", attribute.tokens)
def test_validate_attribute_with_tokens(case, expected):
if expected:
assert expected == validate_attribute(case, tokens=True)
else:
with pytest.raises(TypeError):
validate_attribute(case, tokens=True)


@pytest.mark.parametrize("case,expected", attribute.not_tokens)
def test_validate_attribute_without_tokens(case, expected):
if expected:
assert expected == validate_attribute(case, tokens=False)
else:
with pytest.raises(TypeError):
validate_attribute(case, tokens=False)


@pytest.mark.parametrize("case,expected", attributes.cases)
def test_validate_attributes(case, expected):
if expected:
assert expected == validate_attributes(case)
else:
with pytest.raises(TypeError):
validate_attributes(case)


@pytest.mark.parametrize("case,expected", element.tokens)
def test_validate_element_with_tokens(case, expected):
if expected:
assert expected == validate_element(case, tokens=True)
else:
with pytest.raises(TypeError):
validate_element(case, tokens=True)


@pytest.mark.parametrize("case,expected", element.not_tokens)
def test_validate_element_without_tokens(case, expected):
if expected:
assert expected == validate_element(case, tokens=False)
else:
with pytest.raises(TypeError):
validate_element(case, tokens=False)


@pytest.mark.parametrize("case,expected", elements.cases)
def test_validate_elements(case, expected):
if expected:
assert expected == validate_elements(case)
else:
with pytest.raises(TypeError):
validate_element(case)


@pytest.mark.parametrize("case,expected", wildcard.cases)
def test_validate_wildcard(case, expected):
if expected:
assert expected == validate_wildcard(case)
else:
with pytest.raises(TypeError):
validate_wildcard(case)
7 changes: 5 additions & 2 deletions tests/formats/dataclass/parsers/test_dict.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from dataclasses import asdict, make_dataclass
from dataclasses import asdict, dataclass, field
from decimal import Decimal
from typing import List, Optional, Union
from xml.etree.ElementTree import QName
Expand Down Expand Up @@ -381,7 +381,10 @@ def test_bind_any_type_with_derived_dataclass(self):
self.assertEqual("Unable to locate xsi:type `notexists`", str(cm.exception))

def test_bind_text_with_unions(self):
Fixture = make_dataclass("Fixture", [("x", List[Union[int, float, str, bool]])])
@dataclass
class Fixture:
x: List[Union[int, float, str, bool]] = field(metadata={"tokens": True})

values = ["foo", 12.2, "12.2", 12, "12", True, "false"]

result = self.decoder.decode({"x": values}, Fixture)
Expand Down
Loading

0 comments on commit 5683a5e

Please sign in to comment.