Skip to content

Commit 46000d0

Browse files
committed
Check for name clashes
1 parent 6f2953c commit 46000d0

File tree

4 files changed

+62
-5
lines changed

4 files changed

+62
-5
lines changed

.pylintrc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ disable=
1414
bad-continuation, # clashes with black
1515
line-too-long, # clashes with black
1616
ungrouped-imports, # clashes with isort
17-
wrong-import-position, # clashes with isort
17+
wrong-import-position, # clashes with isort
1818
missing-docstring,
1919
fixme,
20+
too-few-public-methods, # this seems useless
21+
2022

2123
[BASIC]
2224

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ Most of these limitations/assumptions are enforced. They may make this project u
7676
* Text content is a string
7777
* It isn't possible to pass any parameters to the wrapped `@dataclass` decorator
7878
* Some properties of dataclass `field`s are not exposed: `default_factory`, `repr`, `hash`, `init`, `compare`. For most, it is because I don't understand the implications fully or how that would be useful for XML. `default_factory` is hard only because of [the overloaded type signatures](https://github.com/python/typeshed/blob/master/stdlib/3.7/dataclasses.pyi), and getting that to work with `mypy`
79-
* Deserialisation is strict; missing required attributes and child elements will cause an error
79+
* Deserialisation is strict; missing required attributes and child elements will cause an error. I want this to be the default behaviour, but it should be straightforward to add a parameter to `load` for lenient operation
8080
* Unions of types aren't yet supported
8181
* Dataclasses must be written by hand, no tools are provided to generate these from, DTDs, XML schema definitions, or RELAX NG schemas
8282

src/xml_dataclasses/structs.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,14 +219,31 @@ def text(
219219
return _xml_field(xml_type=XmlFieldType.Text, default=default, metadata=metadata)
220220

221221

222-
class XmlDataclass: # pylint: disable=too-few-public-methods
222+
class XmlDataclass:
223223
__ns__: Optional[str]
224224
__attributes__: Collection[AttrInfo[Any]]
225225
__children__: Collection[ChildInfo[Any]]
226226
__text_field__: Optional[TextInfo[Any]]
227227
__nsmap__: Optional[Mapping[Optional[str], str]]
228228

229229

230+
class _XmlNameTracker:
231+
def __init__(self, field_type: str):
232+
self.field_type = field_type
233+
self.seen: Dict[str, str] = {}
234+
235+
def add(self, xml_name: str, field_name: str) -> None:
236+
try:
237+
previous = self.seen[xml_name]
238+
except KeyError:
239+
self.seen[xml_name] = field_name
240+
else:
241+
raise ValueError(
242+
f"Duplicate {self.field_type} '{xml_name}' on '{field_name}', "
243+
f"previously declared on '{previous}'"
244+
)
245+
246+
230247
def xml_dataclass(cls: Type[Any]) -> Type[XmlDataclass]:
231248
new_cls = dataclass()(cls)
232249
try:
@@ -239,6 +256,8 @@ def xml_dataclass(cls: Type[Any]) -> Type[XmlDataclass]:
239256
except AttributeError:
240257
new_cls.__nsmap__ = None
241258

259+
seen_attrs = _XmlNameTracker("attribute")
260+
seen_children = _XmlNameTracker("child")
242261
attrs: List[AttrInfo[Any]] = []
243262
children: List[ChildInfo[Any]] = []
244263
text_field = None
@@ -249,9 +268,13 @@ def xml_dataclass(cls: Type[Any]) -> Type[XmlDataclass]:
249268
raise ValueError(f"Non-XML field '{f.name}' on XML dataclass") from None
250269

251270
if xml_type == XmlFieldType.Attr:
252-
attrs.append(AttrInfo.resolve(f))
271+
attr_info = AttrInfo.resolve(f)
272+
seen_attrs.add(attr_info.xml_name, f.name)
273+
attrs.append(attr_info)
253274
elif xml_type == XmlFieldType.Child:
254-
children.append(ChildInfo.resolve(f))
275+
child_info = ChildInfo.resolve(f)
276+
seen_children.add(child_info.xml_name, f.name)
277+
children.append(child_info)
255278
elif xml_type == XmlFieldType.Text:
256279
text_field = TextInfo.resolve(f)
257280
else:

tests/structs_test.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,35 @@ class Foo:
302302
msg = str(exc_info.value)
303303
assert "Unknown XML field type" in msg
304304
assert "'object'" in msg
305+
306+
307+
def test_xml_dataclass_child_name_clash():
308+
class Foo:
309+
__ns__ = None
310+
bar: XmlDt2 = child(rename="spam")
311+
baz: XmlDt2 = child(rename="spam")
312+
313+
with pytest.raises(ValueError) as exc_info:
314+
xml_dataclass(Foo)
315+
316+
msg = str(exc_info.value)
317+
assert "Duplicate child" in msg
318+
assert "'spam'" in msg
319+
assert "'bar'" in msg
320+
assert "'baz'" in msg
321+
322+
323+
def test_xml_dataclass_attr_name_clash():
324+
class Foo:
325+
__ns__ = None
326+
bar: str = attr(rename="spam")
327+
baz: str = attr(rename="spam")
328+
329+
with pytest.raises(ValueError) as exc_info:
330+
xml_dataclass(Foo)
331+
332+
msg = str(exc_info.value)
333+
assert "Duplicate attribute" in msg
334+
assert "'spam'" in msg
335+
assert "'bar'" in msg
336+
assert "'baz'" in msg

0 commit comments

Comments
 (0)