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: Add abstract suffixes to resolve class name conflicts #985

Merged
merged 1 commit into from
Mar 21, 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
35 changes: 27 additions & 8 deletions tests/codegen/handlers/test_rename_duplicate_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ def test_update_class_references(self):
self.processor.update_class_references(target, replacements, 5)
self.assertEqual([5, 5, 5, 5], list(target.references))

@mock.patch.object(RenameDuplicateClasses, "rename_class")
def test_rename_classes(self, mock_rename_class):
@mock.patch.object(RenameDuplicateClasses, "add_numeric_suffix")
def test_rename_classes(self, mock_add_numeric_suffix):
classes = [
ClassFactory.create(qname="_a", tag=Tag.ELEMENT),
ClassFactory.create(qname="_A", tag=Tag.ELEMENT),
Expand All @@ -153,7 +153,7 @@ def test_rename_classes(self, mock_rename_class):
self.processor.rename_classes(classes, False)
self.processor.rename_classes(classes, True)

mock_rename_class.assert_has_calls(
mock_add_numeric_suffix.assert_has_calls(
[
mock.call(classes[1], False),
mock.call(classes[0], False),
Expand All @@ -164,7 +164,17 @@ def test_rename_classes(self, mock_rename_class):
]
)

@mock.patch.object(RenameDuplicateClasses, "rename_class")
@mock.patch.object(RenameDuplicateClasses, "add_abstract_suffix")
def test_rename_classes_with_abstract_type(self, mock_add_abstract_suffix):
classes = [
ClassFactory.create(qname="_a", tag=Tag.ELEMENT),
ClassFactory.create(qname="_A", tag=Tag.ELEMENT, abstract=True),
]
self.processor.rename_classes(classes, True)

mock_add_abstract_suffix.assert_called_once_with(classes[1])

@mock.patch.object(RenameDuplicateClasses, "add_numeric_suffix")
def test_rename_classes_protects_single_element(self, mock_rename_class):
classes = [
ClassFactory.create(qname="_a", tag=Tag.ELEMENT),
Expand All @@ -175,13 +185,13 @@ def test_rename_classes_protects_single_element(self, mock_rename_class):
mock_rename_class.assert_called_once_with(classes[1], False)

@mock.patch.object(RenameDuplicateClasses, "rename_class_dependencies")
def test_rename_class(self, mock_rename_class_dependencies):
def test_add_numeric_suffix_by_slug(self, mock_rename_class_dependencies):
target = ClassFactory.create(qname="{foo}_a")
self.processor.container.add(target)
self.processor.container.add(ClassFactory.create(qname="{foo}a_1"))
self.processor.container.add(ClassFactory.create(qname="{foo}A_2"))
self.processor.container.add(ClassFactory.create(qname="{bar}a_3"))
self.processor.rename_class(target, False)
self.processor.add_numeric_suffix(target, False)

self.assertEqual("{foo}_a_3", target.qname)
self.assertEqual("_a", target.meta_name)
Expand All @@ -195,13 +205,13 @@ def test_rename_class(self, mock_rename_class_dependencies):
self.assertEqual([], self.container.data["{foo}_a"])

@mock.patch.object(RenameDuplicateClasses, "rename_class_dependencies")
def test_rename_class_by_name(self, mock_rename_class_dependencies):
def test_add_numeric_suffix_by_name(self, mock_rename_class_dependencies):
target = ClassFactory.create(qname="{foo}_a")
self.processor.container.add(target)
self.processor.container.add(ClassFactory.create(qname="{bar}a_1"))
self.processor.container.add(ClassFactory.create(qname="{thug}A_2"))
self.processor.container.add(ClassFactory.create(qname="{bar}a_3"))
self.processor.rename_class(target, True)
self.processor.add_numeric_suffix(target, True)

self.assertEqual("{foo}_a_4", target.qname)
self.assertEqual("_a", target.meta_name)
Expand All @@ -214,6 +224,15 @@ def test_rename_class_by_name(self, mock_rename_class_dependencies):
self.assertEqual([target], self.container.data["{foo}_a_4"])
self.assertEqual([], self.container.data["{foo}_a"])

def test_add_abstract_suffix(self):
target = ClassFactory.create(qname="{xsdata}line", abstract=True)
self.processor.container.add(target)

self.processor.add_abstract_suffix(target)

self.assertEqual("{xsdata}line_abstract", target.qname)
self.assertEqual("line", target.meta_name)

def test_rename_class_dependencies(self):
attr_type = AttrTypeFactory.create(qname="{foo}bar", reference=1)

Expand Down
46 changes: 34 additions & 12 deletions xsdata/codegen/handlers/rename_duplicate_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,33 @@ def merge_classes(self, classes: List[Class]):
self.update_class_references(item, search, replace)

def rename_classes(self, classes: List[Class], use_name: bool):
"""Rename all the classes in the list.
"""Rename the classes in the list.

Protect classes derived from xs:element if there is only one in
the list.
Cases:
- Two classes, one abstract append the abstract suffix
- One element, add numeric suffixes to the rest of the classes
- Add numeric suffixes to all classes.

Args:
classes: A list of classes with duplicate names
use_name: Whether simple or qualified names should be used
during renaming
"""
total_elements = sum(x.is_element for x in classes)
for target in sorted(classes, key=get_name):
if not target.is_element or total_elements > 1:
self.rename_class(target, use_name)
abstract = [x for x in classes if x.abstract]
if len(classes) == 2 and len(abstract) == 1:
self.add_abstract_suffix(abstract[0])
else:
total_elements = sum(x.is_element for x in classes)
for target in sorted(classes, key=get_name):
if not target.is_element or total_elements > 1:
self.add_numeric_suffix(target, use_name)

def add_abstract_suffix(self, target: Class):
"""Add the abstract suffix to class name."""
new_qname = f"{target.qname}_abstract"
self.rename_class(target, new_qname)

def rename_class(self, target: Class, use_name: bool):
def add_numeric_suffix(self, target: Class, use_name: bool):
"""Find the next available class name.

Save the original name in the class metadata and update
Expand All @@ -90,14 +101,25 @@ def rename_class(self, target: Class, use_name: bool):
use_name: Whether simple or qualified names should be
used during renaming
"""
qname = target.qname
namespace, name = namespaces.split_qname(target.qname)
target.qname = self.next_qname(namespace, name, use_name)
target.meta_name = name
new_qname = self.next_qname(namespace, name, use_name)
self.rename_class(target, new_qname)

def rename_class(self, target: Class, new_qname: str):
"""Update the class qualified name and update references.

Args:
target: The target class to update
new_qname: The new class qualified name
"""
qname = target.qname
target.qname = new_qname
target.meta_name = namespaces.local_name(qname)

self.container.reset(target, qname)

for item in self.container:
self.rename_class_dependencies(item, id(target), target.qname)
self.rename_class_dependencies(item, id(target), new_qname)

def next_qname(self, namespace: str, name: str, use_name: bool) -> str:
"""Use int suffixes to get the next available qualified name.
Expand Down
Loading