Skip to content

Commit

Permalink
Add compatibility layer for dataclasses aternatives (#521)
Browse files Browse the repository at this point in the history
* Add compatibility layer for dataclasses aternatives

* Split xml nodes

* Add class type registry for plugin hooks

* Allow cli output arg to bypass config

* Add helper to reset xml context

* Add plugins entry point for custom class types

* ClassType methods accept an instance or a type
  • Loading branch information
tefra authored Jun 13, 2021
1 parent 7c63cf7 commit d10e751
Show file tree
Hide file tree
Showing 46 changed files with 2,198 additions and 2,125 deletions.
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ exclude: tests/fixtures

repos:
- repo: https://github.com/asottile/pyupgrade
rev: v2.19.1
rev: v2.19.4
hooks:
- id: pyupgrade
args: [--py37-plus]
Expand All @@ -11,7 +11,7 @@ repos:
hooks:
- id: reorder-python-imports
- repo: https://github.com/ambv/black
rev: 21.5b2
rev: 21.6b0
hooks:
- id: black
- repo: https://gitlab.com/pycqa/flake8
Expand All @@ -37,10 +37,10 @@ repos:
- id: docformatter
args: ["--in-place", "--pre-summary-newline"]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.812
rev: v0.902
hooks:
- id: mypy
additional_dependencies: [tokenize-rt]
additional_dependencies: [tokenize-rt, types-requests, types-Jinja2, types-click]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.17.0
hooks:
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ project_urls =
packages = xsdata
install_requires =
dataclasses;python_version<"3.7"
importlib-metadata;python_version<"3.8"
python_requires = >=3.6
include_package_data = True

Expand All @@ -50,7 +51,6 @@ cli =
docformatter
jinja2>=2.10
toposort>=1.5
importlib-metadata;python_version<"3.8"
docs =
sphinx
sphinx-autobuild
Expand Down
5 changes: 3 additions & 2 deletions tests/fixtures/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from typing import Any

from xsdata.exceptions import ConverterError
from xsdata.formats.converter import Converter, converter
from xsdata.formats.converter import Converter
from xsdata.formats.converter import converter

Telephone = namedtuple('Telephone', ['country_code', 'area_code', 'number'])

Expand All @@ -21,4 +22,4 @@ def serialize(self, value: Telephone, **kwargs: Any) -> str:
return "-".join(map(str, value))


converter.register_converter(Telephone, PhoneConverter())
converter.register_converter(Telephone, PhoneConverter())
10 changes: 9 additions & 1 deletion tests/fixtures/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class TypeC:
x: int
y: str
z: float
fixed: str = field(init=False, default="ignored")


@dataclass
Expand All @@ -46,6 +47,11 @@ class ExtendedType:
wildcard: Optional[object] = field(default=None, metadata={"type": "Wildcard"})


@dataclass
class ExtendedListType:
wildcard: List[object] = field(default_factory=list, metadata={"type": "Wildcard"})


@dataclass
class ChoiceType:
choice: List[object] = field(
Expand All @@ -71,7 +77,9 @@ class UnionType:

@dataclass
class AttrsType:
attrs: Dict[str, str] = field(metadata={"type": "Attributes"})
index: int = field(metadata={"type": "Attribute"})
attrs: Dict[str, str] = field(metadata={"type": "Attributes", "namespace": "##any"})
fixed: str = field(init=False, default="ignored", metadata={"type": "Attribute"})


@dataclass
Expand Down
46 changes: 27 additions & 19 deletions tests/formats/dataclass/models/test_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from tests.fixtures.models import TypeB
from tests.fixtures.series import Country
from xsdata.exceptions import XmlContextError
from xsdata.formats.dataclass.compat import class_types
from xsdata.formats.dataclass.models.builders import XmlMetaBuilder
from xsdata.formats.dataclass.models.builders import XmlVarBuilder
from xsdata.formats.dataclass.models.elements import XmlType
Expand All @@ -32,14 +33,22 @@


class XmlMetaBuilderTests(FactoryTestCase):
def setUp(self):
super().setUp()
self.builder = XmlMetaBuilder(
class_type=class_types.get_type("dataclasses"),
element_name_generator=return_input,
attribute_name_generator=return_input,
)

@mock.patch.object(XmlMetaBuilder, "build_vars")
def test_build(self, mock_build_vars):
var = XmlVarFactory.create(
xml_type=XmlType.ELEMENT, name="foo", qname="{foo}bar", types=(int,)
)
mock_build_vars.return_value = [var]

result = XmlMetaBuilder.build(Artist, None, return_input, return_input)
result = self.builder.build(Artist, None)
expected = XmlMetaFactory.create(
clazz=Artist,
qname="{http://musicbrainz.org/ns/mmd-2.0#}artist",
Expand All @@ -53,9 +62,7 @@ def test_build(self, mock_build_vars):

@mock.patch.object(XmlMetaBuilder, "build_vars", return_value=[])
def test_build_with_parent_namespace(self, mock_build_vars):
result = XmlMetaBuilder.build(
Country, "http://xsdata", return_input, return_input
)
result = self.builder.build(Country, "http://xsdata")

self.assertEqual(build_qname("http://xsdata", "country"), result.qname)
mock_build_vars.assert_called_once_with(
Expand All @@ -64,7 +71,8 @@ def test_build_with_parent_namespace(self, mock_build_vars):

@mock.patch.object(XmlMetaBuilder, "build_vars", return_value=[])
def test_build_with_no_meta_name_and_name_generator(self, *args):
result = XmlMetaBuilder.build(BookForm, None, text.snake_case, return_input)
self.builder.element_name_generator = text.snake_case
result = self.builder.build(BookForm, None)

self.assertEqual("book_form", result.qname)

Expand All @@ -83,20 +91,20 @@ class Thug(Bar):
class Meta:
name = "thug"

result = XmlMetaBuilder.build(Foo, None, return_input, return_input)
result = self.builder.build(Foo, None)
self.assertEqual("Foo", result.qname)

result = XmlMetaBuilder.build(Thug, None, return_input, return_input)
result = self.builder.build(Thug, None)
self.assertEqual("thug", result.qname)

def test_build_with_no_dataclass_raises_exception(self, *args):
with self.assertRaises(XmlContextError) as cm:
XmlMetaBuilder.build(int, None, return_input, return_input)
self.builder.build(int, None)

self.assertEqual(f"Type '{int}' is not a dataclass.", str(cm.exception))

def test_build_vars(self):
result = XmlMetaBuilder.build_vars(BookForm, None, text.pascal_case, str.upper)
result = self.builder.build_vars(BookForm, None, text.pascal_case, str.upper)
self.assertIsInstance(result, Iterator)

expected = [
Expand Down Expand Up @@ -162,28 +170,28 @@ def test_build_vars(self):
self.assertIsNone(var.clazz)

def test_build_vars_with_ignore_types(self):
result = XmlMetaBuilder.build_vars(TypeB, None, return_input, return_input)
result = self.builder.build_vars(TypeB, None, return_input, return_input)
self.assertIsInstance(result, Iterator)

actual = list(result)
self.assertEqual(2, len(actual))

def test_default_xml_type(self):
cls = make_dataclass("a", [("x", int)])
self.assertEqual(XmlType.TEXT, XmlMetaBuilder.default_xml_type(cls))
self.assertEqual(XmlType.TEXT, self.builder.default_xml_type(cls))

cls = make_dataclass("b", [("x", int), ("y", int)])
self.assertEqual(XmlType.ELEMENT, XmlMetaBuilder.default_xml_type(cls))
self.assertEqual(XmlType.ELEMENT, self.builder.default_xml_type(cls))

cls = make_dataclass(
"c", [("x", int), ("y", int, field(metadata=dict(type="Text")))]
)
self.assertEqual(XmlType.ELEMENT, XmlMetaBuilder.default_xml_type(cls))
self.assertEqual(XmlType.ELEMENT, self.builder.default_xml_type(cls))

cls = make_dataclass(
"d", [("x", int), ("y", int, field(metadata=dict(type="Element")))]
)
self.assertEqual(XmlType.TEXT, XmlMetaBuilder.default_xml_type(cls))
self.assertEqual(XmlType.TEXT, self.builder.default_xml_type(cls))

with self.assertRaises(XmlContextError) as cm:
cls = make_dataclass(
Expand All @@ -193,7 +201,7 @@ def test_default_xml_type(self):
("y", int, field(metadata=dict(type="Text"))),
],
)
XmlMetaBuilder.default_xml_type(cls)
self.builder.default_xml_type(cls)

self.assertEqual(
"Dataclass `e` includes more than one text node!", str(cm.exception)
Expand All @@ -203,8 +211,9 @@ def test_default_xml_type(self):
class XmlVarBuilderTests(TestCase):
def setUp(self) -> None:
self.builder = XmlVarBuilder(
default_xml_type=XmlType.ELEMENT,
class_type=class_types.get_type("dataclasses"),
parent_ns=None,
default_xml_type=XmlType.ELEMENT,
element_name_generator=return_input,
attribute_name_generator=return_input,
)
Expand All @@ -215,10 +224,9 @@ def test_build_with_choice_field(self):
globalns = sys.modules[CompoundFieldExample.__module__].__dict__
type_hints = get_type_hints(CompoundFieldExample)
class_field = fields(CompoundFieldExample)[0]
self.builder.parent_ns = "bar"

builder = XmlVarBuilder(XmlType.ELEMENT, "bar", return_input, return_input)

actual = builder.build(
actual = self.builder.build(
66,
"compound",
type_hints["compound"],
Expand Down
2 changes: 1 addition & 1 deletion tests/formats/dataclass/parsers/handlers/test_lxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from tests.fixtures.books.fixtures import events
from tests.fixtures.books.fixtures import events_default_ns
from xsdata.exceptions import XmlHandlerError
from xsdata.formats.dataclass.parsers.bases import RecordParser
from xsdata.formats.dataclass.parsers.handlers import LxmlEventHandler
from xsdata.formats.dataclass.parsers.handlers import LxmlSaxHandler
from xsdata.formats.dataclass.parsers.nodes import RecordParser


class LxmlEventHandlerTests(TestCase):
Expand Down
2 changes: 1 addition & 1 deletion tests/formats/dataclass/parsers/handlers/test_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from tests.fixtures.books.fixtures import events
from tests.fixtures.books.fixtures import events_default_ns
from xsdata.exceptions import XmlHandlerError
from xsdata.formats.dataclass.parsers.bases import RecordParser
from xsdata.formats.dataclass.parsers.handlers import XmlEventHandler
from xsdata.formats.dataclass.parsers.handlers import XmlSaxHandler
from xsdata.formats.dataclass.parsers.nodes import RecordParser


class XmlEventHandlerTests(TestCase):
Expand Down
Empty file.
Loading

0 comments on commit d10e751

Please sign in to comment.