Skip to content

Commit

Permalink
fix: detect proper xml attributs's namespace
Browse files Browse the repository at this point in the history
fixes #11

Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
  • Loading branch information
jkowalleck committed Sep 28, 2023
1 parent f1d5d9e commit de1773a
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 22 deletions.
9 changes: 4 additions & 5 deletions serializable/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,10 +460,7 @@ def _from_xml(cls: Type[_T], data: Union[TextIOWrapper, Element],
_namespaces = dict([node for _, node in
SafeElementTree.iterparse(StringIO(SafeElementTree.tostring(data, 'unicode')),
events=['start-ns'])])
if 'ns0' in _namespaces:
default_namespace = _namespaces['ns0']
else:
default_namespace = ''
default_namespace = (re.compile(r'^\{(.*?)\}.').search(data.tag) or (None, _namespaces.get('')))[1]

_data: Dict[str, Any] = {}

Expand Down Expand Up @@ -501,7 +498,9 @@ def _from_xml(cls: Type[_T], data: Union[TextIOWrapper, Element],

# Handle Sub-Elements
for child_e in data:
child_e_tag_name = str(child_e.tag).replace('{' + default_namespace + '}', '')
child_e_tag_name = str(child_e.tag)
if default_namespace is not None:
child_e_tag_name = child_e_tag_name.replace(f'{{{default_namespace}}}', '')

decoded_k = CurrentFormatter.formatter.decode(property_name=child_e_tag_name)
if decoded_k in klass.ignore_during_deserialization:
Expand Down
28 changes: 27 additions & 1 deletion tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@

import json
import os
from typing import Any
from typing import Any, Union
from unittest import TestCase

import lxml # type: ignore
from defusedxml import ElementTree as SafeElementTree # type: ignore
from sortedcontainers import SortedSet
from xmldiff import main # type: ignore
from xmldiff.actions import MoveNode # type: ignore

Expand Down Expand Up @@ -59,3 +60,28 @@ def assertEqualXml(self, a: str, b: str) -> None:
diff_results = main.diff_texts(a, b, diff_options={'F': 0.5})
diff_results = list(filter(lambda o: not isinstance(o, MoveNode), diff_results))
self.assertEqual(len(diff_results), 0, f'There are XML differences: {diff_results}\n- {a}\n+ {b}')


class DeepCompareMixin(object):
def assertDeepEqual(self, first: Any, second: Any, msg=None) -> None:
"""costly compare, but very verbose"""
self: Union[TestCase, 'DeepCompareMixin']
_omd = self.maxDiff
try:
self.maxDiff = None
dd1 = self.__deepDict(first)
dd2 = self.__deepDict(second)
self.assertDictEqual(dd1, dd2, msg)
finally:
self.maxDiff = _omd

def __deepDict(self, o: Any) -> Any:
if isinstance(o, (SortedSet, list, tuple)):
return tuple(self.__deepDict(i) for i in o)
if isinstance(o, dict):
return {k: self.__deepDict(v) for k, v in o}
if isinstance(o, set):
return tuple(sorted((self.__deepDict(i) for i in o), key=repr))
if hasattr(o, '__dict__'):
return {k: self.__deepDict(v) for k, v in vars(o).items() if not (k.startswith('__') and k.endswith('__'))}
return o
43 changes: 43 additions & 0 deletions tests/fixtures/the-phoenix-project-defaultNS-v4.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version='1.0' encoding='utf-8'?>
<book xmlns="http://the.phoenix.project/testing/defaultNS" isbn_number="978-1942788294">
<id>f3758bf0-0ff7-4366-a5e5-c209d4352b2d</id>
<title>The Phoenix Project</title>
<edition number="5">5th Anniversary Limited Edition</edition>
<publish_date>2018-04-16</publish_date>
<author>Gene Kim</author>
<author>George Spafford</author>
<author>Kevin Behr</author>
<type>fiction</type>
<publisher>
<address>10 Downing Street</address>
<name>IT Revolution Press LLC</name>
</publisher>
<references>
<reference ref="my-ref-1"/>
<reference ref="my-ref-3">
<reference ref="sub-ref-2"/>
</reference>
<reference ref="my-ref-2">
<reference ref="sub-ref-1"/>
<reference ref="sub-ref-3"/>
</reference>
</references>
<chapters>
<chapter>
<number>1</number>
<title>Tuesday, September 2</title>
</chapter>
<chapter>
<number>2</number>
<title>Tuesday, September 2</title>
</chapter>
<chapter>
<number>3</number>
<title>Tuesday, September 2</title>
</chapter>
<chapter>
<number>4</number>
<title>Wednesday, September 3</title>
</chapter>
</chapters>
</book>
2 changes: 1 addition & 1 deletion tests/fixtures/the-phoenix-project-defaultNS.xml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
<?xml version='1.0' encoding='utf-8'?>
<book xmlns="http://the.phoenix.project/testing/defaultNS" isbn_number="978-1942788294"><id>f3758bf0-0ff7-4366-a5e5-c209d4352b2d</id><title>The Phoenix Project</title><edition number="5">5th Anniversary Limited Edition</edition><publish_date>2018-04-16</publish_date><author>Gene Kim</author><author>George Spafford</author><author>Kevin Behr</author><type>fiction</type><publisher><name>IT Revolution Press LLC</name></publisher><chapters><chapter><number>1</number><title>Tuesday, September 2</title></chapter><chapter><number>2</number><title>Tuesday, September 2</title></chapter><chapter><number>3</number><title>Tuesday, September 2</title></chapter><chapter><number>4</number><title>Wednesday, September 3</title></chapter></chapters></book>
<book xmlns="http://the.phoenix.project/testing/defaultNS" isbn_number="978-1942788294"><id>f3758bf0-0ff7-4366-a5e5-c209d4352b2d</id><title>The Phoenix Project</title><edition number="5">5th Anniversary Limited Edition</edition><publish_date>2018-04-16</publish_date><author>Karl Ranseier</author><type>fiction</type><publisher><name>IT Revolution Press LLC</name></publisher><chapters><chapter><number>1</number><title>Tuesday, September 2</title></chapter><chapter><number>2</number><title>Tuesday, September 2</title></chapter><chapter><number>3</number><title>Tuesday, September 2</title></chapter><chapter><number>4</number><title>Wednesday, September 3</title></chapter></chapters></book>
6 changes: 3 additions & 3 deletions tests/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,17 +206,17 @@ class Book:
def __init__(self, title: str, isbn: str, publish_date: date, authors: Iterable[str],
publisher: Optional[Publisher] = None, chapters: Optional[Iterable[Chapter]] = None,
edition: Optional[BookEdition] = None, type: BookType = BookType.FICTION,
id: Optional[UUID] = None, references: Optional[List[BookReference]] = None) -> None:
id: Optional[UUID] = None, references: Optional[Iterable[BookReference]] = None) -> None:
self._id = id or uuid4()
self._title = title
self._isbn = isbn
self._edition = edition
self._publish_date = publish_date
self._authors = SortedSet(authors)
self._authors = set(authors)
self._publisher = publisher
self.chapters = list(chapters or [])
self._type = type
self.references = set(references or {})
self.references = set(references or [])

@property # type: ignore[misc]
@serializable.xml_sequence(1)
Expand Down
35 changes: 23 additions & 12 deletions tests/test_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import logging
import os
from copy import deepcopy

from defusedxml import ElementTree as SafeElementTree

Expand All @@ -28,15 +29,15 @@
KebabCasePropertyNameFormatter,
SnakeCasePropertyNameFormatter,
)
from tests.base import FIXTURES_DIRECTORY, BaseTestCase
from tests.base import FIXTURES_DIRECTORY, BaseTestCase, DeepCompareMixin
from tests.model import Book, SchemaVersion2, SchemaVersion3, SchemaVersion4, ThePhoenixProject, ThePhoenixProject_v1

logger = logging.getLogger('serializable')
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')


class TestXml(BaseTestCase):
class TestXml(BaseTestCase, DeepCompareMixin):

# region test_serialize

Expand Down Expand Up @@ -80,16 +81,16 @@ def test_serializable_with_defaultNS(self) -> None:
from xml.etree import ElementTree
xmlns = 'http://the.phoenix.project/testing/defaultNS'
with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-defaultNS.xml')) as expected_xml:
self.maxDiff = None
self.assertEqual(
expected_xml.read(),
ElementTree.tostring(
ThePhoenixProject.as_xml(as_string=False, xmlns=xmlns),
method='xml',
encoding='unicode', xml_declaration=True,
default_namespace=xmlns,
)
)
expected = expected_xml.read()
data = deepcopy(ThePhoenixProject_v1)
data._authors = {'Karl Ranseier', } # only one item, so order is no issue
actual = ElementTree.tostring(
data.as_xml(SchemaVersion4, as_string=False, xmlns=xmlns),
method='xml',
encoding='unicode', xml_declaration=True,
default_namespace=xmlns,
)
self.assertEqual(expected, actual)

# endregion test_serialize

Expand Down Expand Up @@ -185,4 +186,14 @@ def test_deserialize_tfp_sc1(self) -> None:
self.assertEqual(ThePhoenixProject_v1.authors, book.authors)
self.assertEqual(ThePhoenixProject_v1.chapters, book.chapters)

def test_deserializable_with_defaultNS_from_element(self) -> None:
"""regression test for https://github.com/madpah/serializable/issues/11"""
from xml.etree import ElementTree
expected = ThePhoenixProject
with open(os.path.join(FIXTURES_DIRECTORY, 'the-phoenix-project-defaultNS-v4.xml')) as fixture_xml:
fixture = fixture_xml.read()
fixture_element = ElementTree.XML(fixture)
actual = Book.from_xml(fixture_element)
self.assertDeepEqual(expected, actual)

# region test_deserialize

0 comments on commit de1773a

Please sign in to comment.