Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use the event writer to build element trees #1032

Merged
merged 3 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 12 additions & 27 deletions docs/data_binding/tree_serializing.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,26 @@
# Element Tree Serializer

The element tree serializers will render an object into an element tree, that you can
use to run XPATH evaluations or XSLT transformations.
The tree serializer will render an object into an element tree, that you can use to run
XPATH evaluations or XSLT transformations.

There are two implementations based on lxml
[LxmlTreeSerializer][xsdata.formats.dataclass.serializers.LxmlTreeSerializer] and native
python [XmlTreeSerializer][xsdata.formats.dataclass.serializers.XmlTreeSerializer].
The [TreeSerializer][xsdata.formats.dataclass.serializers.TreeSerializer] depends on
lxml. There is no native python ElementTree implementation because of limitations with
namespaces.

## xml.etree.ElementTree.Element

```python
>>> from xml.etree import ElementTree
>>> from tests.fixtures.books.fixtures import books
>>> from xsdata.formats.dataclass.serializers import XmlTreeSerializer
...
>>> serializer = XmlTreeSerializer()
>>> result = serializer.render(books)
...
>>> result.find(".//title").text
'The First Book'

```

## lxml.etree.Element
## Example

```python
>>> from lxml import etree
>>> from tests.fixtures.books.fixtures import books
>>> from xsdata.formats.dataclass.serializers import LxmlTreeSerializer
>>> from xsdata.formats.dataclass.serializers import TreeSerializer
...
>>> serializer = LxmlTreeSerializer()
>>> result = serializer.render(books)
>>> serializer = TreeSerializer()
>>> serializer.config.indent = " "
>>> result = serializer.render(books, ns_map={'bk': "urn:books"})
...
>>> etree.indent(result)
>>> actual = etree.tostring(result)
>>> print(actual.decode())
<ns0:books xmlns:ns0="urn:books">
<bk:books xmlns:bk="urn:books">
<book id="bk001" lang="en">
<author>Hightower, Kim</author>
<title>The First Book</title>
Expand All @@ -52,6 +37,6 @@ python [XmlTreeSerializer][xsdata.formats.dataclass.serializers.XmlTreeSerialize
<pub_date>2001-01-10</pub_date>
<review>A masterpiece of the fine art of gossiping.</review>
</book>
</ns0:books>
</bk:books>

```
Empty file.
36 changes: 0 additions & 36 deletions tests/formats/dataclass/serializers/tree/test_lxml.py

This file was deleted.

27 changes: 0 additions & 27 deletions tests/formats/dataclass/serializers/tree/test_mixins.py

This file was deleted.

39 changes: 0 additions & 39 deletions tests/formats/dataclass/serializers/tree/test_native.py

This file was deleted.

34 changes: 33 additions & 1 deletion tests/formats/dataclass/serializers/writers/test_lxml.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from dataclasses import make_dataclass
from unittest import TestCase

import lxml

from tests import fixtures_dir
from tests.fixtures.books.fixtures import books
from xsdata.formats.dataclass.serializers import XmlSerializer
from xsdata.formats.dataclass.serializers.config import SerializerConfig
from xsdata.formats.dataclass.serializers.writers import LxmlEventWriter
from xsdata.formats.dataclass.serializers.tree import TreeSerializer
from xsdata.formats.dataclass.serializers.writers import (
LxmlEventWriter,
)


class LxmlEventWriterTests(TestCase):
Expand Down Expand Up @@ -58,3 +63,30 @@ def test_no_indent(self):
_, actual = actual.split("\n", 1)
_, expected = expected.split("\n", 1)
self.assertEqual(expected.replace(" ", "").replace("\n", ""), actual)


class LxmlTreeBuilderTests(TestCase):
def setUp(self):
super().setUp()
self.serializer = TreeSerializer()
self.serializer.config.indent = " "

def test_render(self):
tree = self.serializer.render(books)

actual = lxml.etree.tostring(tree).decode()

expected = fixtures_dir.joinpath("books/books_auto_ns.xml").read_text()
expected = "\n".join(expected.splitlines()[1:])
self.assertEqual(expected, actual)

def test_render_with_no_indent(self):
self.serializer.config.indent = ""
tree = self.serializer.render(books)

lxml.etree.indent(tree, " ")
actual = lxml.etree.tostring(tree).decode()

expected = fixtures_dir.joinpath("books/books_auto_ns.xml").read_text()
expected = "\n".join(expected.splitlines()[1:])
self.assertEqual(expected, actual)
21 changes: 12 additions & 9 deletions tests/integration/benchmarks/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
from tests import xsdata_temp_dir
from tests.integration.benchmarks.utils import make_books, parse, write
from xsdata.formats.dataclass.context import XmlContext
from xsdata.formats.dataclass.parsers import handlers as readers
from xsdata.formats.dataclass.serializers import writers
from xsdata.formats.dataclass.parsers.handlers import LxmlEventHandler, XmlEventHandler
from xsdata.formats.dataclass.serializers.writers import LxmlEventWriter, XmlEventWriter

context = XmlContext()
readers_list = list(readers.__all__)
writers_list = list(writers.__all__)

readers_list.remove("default_handler")
writers_list.remove("default_writer")
readers_list = [
LxmlEventHandler,
XmlEventHandler,
]
writers_list = [
XmlEventWriter,
LxmlEventWriter,
]

random.shuffle(readers_list)
random.shuffle(writers_list)
Expand All @@ -27,12 +30,12 @@
@pytest.mark.parametrize("number", numbers)
@pytest.mark.parametrize("writer", writers_list)
def test_serialize(benchmark, writer, number):
benchmark(write, number, fixtures[number], getattr(writers, writer))
benchmark(write, number, fixtures[number], writer)


@pytest.mark.benchmark(disable_gc=True, group="Parse")
@pytest.mark.parametrize("number", numbers)
@pytest.mark.parametrize("handler", readers_list)
def test_parse(benchmark, handler, number):
src = xsdata_temp_dir.joinpath(f"benchmark_{number}.xml").read_bytes()
benchmark(parse, src, getattr(readers, handler))
benchmark(parse, src, handler)
2 changes: 1 addition & 1 deletion xsdata/formats/dataclass/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from dataclasses import dataclass, fields
from typing import Any, Dict, NamedTuple, Optional, Type, Union
from typing import Any, Dict, Optional, Type, Union

from xsdata.exceptions import ClientValueError
from xsdata.formats.dataclass.context import XmlContext
Expand Down
7 changes: 3 additions & 4 deletions xsdata/formats/dataclass/serializers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from xsdata.formats.dataclass.serializers.code import PycodeSerializer
from xsdata.formats.dataclass.serializers.dict import DictEncoder, DictFactory
from xsdata.formats.dataclass.serializers.json import JsonSerializer
from xsdata.formats.dataclass.serializers.tree.native import XmlTreeSerializer
from xsdata.formats.dataclass.serializers.xml import XmlSerializer

__all__ = [
Expand All @@ -13,8 +12,8 @@
]

try:
from xsdata.formats.dataclass.serializers.tree.lxml import LxmlTreeSerializer
from xsdata.formats.dataclass.serializers.tree import TreeSerializer

__all__.append("LxmlTreeSerializer")
__all__.append("TreeSerializer")
except ImportError: # pragma: no cover
pass # pragma: no cover
pass
Loading
Loading