Skip to content

Commit

Permalink
Process inherited fields recursively
Browse files Browse the repository at this point in the history
  • Loading branch information
tefra committed May 3, 2021
1 parent 3a174b1 commit 79cd0a2
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 60 deletions.
23 changes: 22 additions & 1 deletion tests/codegen/handlers/test_attribute_sanitizer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from xsdata.codegen.container import ClassContainer
from xsdata.codegen.handlers import AttributeSanitizerHandler
from xsdata.codegen.models import Restrictions
from xsdata.codegen.models import Status
from xsdata.models.enums import DataType
from xsdata.models.enums import Tag
from xsdata.utils import collections
Expand Down Expand Up @@ -160,7 +161,7 @@ def test_remove_inherited_fields(self):
self.processor.process(target)
self.assertEqual(1, len(target.attrs))

def test_remove_inherited_fields_with_lists_type(self):
def test_process_inherited_fields(self):
target = ClassFactory.elements(2)
target.attrs[0].restrictions.min_occurs = 1
target.attrs[0].restrictions.max_occurs = 3
Expand All @@ -187,6 +188,26 @@ def test_remove_inherited_fields_with_lists_type(self):
self.processor.process(target)
self.assertEqual(0, len(target.attrs))

target = ClassFactory.create(
attrs=AttrFactory.list(1, name="foo", tag=Tag.ELEMENT)
)
source_one = ClassFactory.create(status=Status.PROCESSED)
source_two = ClassFactory.create(status=Status.PROCESSED)
source_one.extensions.append(ExtensionFactory.reference(source_two.qname))
target.extensions.append(ExtensionFactory.reference(source_one.qname))

source_two.attrs.append(target.attrs[0].clone())
source_two.attrs[0].tag = Tag.ATTRIBUTE

self.processor.container = ClassContainer()
self.processor.container.add(target)
self.processor.container.add(source_two)
self.processor.container.add(source_one)
self.processor.process(target)

self.assertEqual("foo", target.attrs[0].name)
self.assertEqual("foo_Attribute", source_two.attrs[0].name)

def test_set_effective_choices(self):
target = ClassFactory.create()
attrs = [
Expand Down
27 changes: 27 additions & 0 deletions tests/codegen/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from xsdata.codegen.utils import ClassUtils
from xsdata.exceptions import CodeGenerationError
from xsdata.models.enums import DataType
from xsdata.models.enums import Tag
from xsdata.utils.testing import AttrFactory
from xsdata.utils.testing import AttrTypeFactory
from xsdata.utils.testing import ClassFactory
Expand Down Expand Up @@ -271,3 +272,29 @@ def test_merge_attributes(self):
self.assertEqual(names, [x.name for x in target.attrs])
self.assertEqual(min_occurs, [x.restrictions.min_occurs for x in target.attrs])
self.assertEqual(max_occurs, [x.restrictions.max_occurs for x in target.attrs])

def test_rename_attribute_by_preference(self):
one = AttrFactory.create(name="a", tag=Tag.ELEMENT)
two = AttrFactory.create(name="a", tag=Tag.ATTRIBUTE)

ClassUtils.rename_attribute_by_preference(one, two)
self.assertEqual("a", one.name)
self.assertEqual("a_Attribute", two.name)

one = AttrFactory.create(name="a", tag=Tag.ELEMENT)
two = AttrFactory.create(name="a", tag=Tag.ELEMENT, namespace="foo")
ClassUtils.rename_attribute_by_preference(one, two)
self.assertEqual("a", one.name)
self.assertEqual("foo_a", two.name)

one = AttrFactory.create(name="a", tag=Tag.ELEMENT, namespace="foo")
two = AttrFactory.create(name="a", tag=Tag.ELEMENT)
ClassUtils.rename_attribute_by_preference(one, two)
self.assertEqual("foo_a", one.name)
self.assertEqual("a", two.name)

one = AttrFactory.create(name="a", tag=Tag.ELEMENT)
two = AttrFactory.create(name="a", tag=Tag.ELEMENT)
ClassUtils.rename_attribute_by_preference(one, two)
self.assertEqual("a_Element", one.name)
self.assertEqual("a", two.name)
8 changes: 0 additions & 8 deletions tests/fixtures/defxmlschema/chapter13.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,6 @@ class SmallSizeType(SizeType):

@dataclass
class RestrictedProductType(ProductType):
name: Optional[str] = field(
default=None,
metadata={
"type": "Element",
"namespace": "",
"required": True,
}
)
routing_num: Optional[int] = field(
default=None,
metadata={
Expand Down
73 changes: 45 additions & 28 deletions xsdata/codegen/handlers/attribute_sanitizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
from xsdata.codegen.models import Attr
from xsdata.codegen.models import AttrType
from xsdata.codegen.models import Class
from xsdata.codegen.models import Extension
from xsdata.codegen.utils import ClassUtils
from xsdata.formats.converter import converter
from xsdata.models.enums import DataType
from xsdata.utils import collections
from xsdata.utils.text import alnum


@dataclass
Expand All @@ -28,36 +31,50 @@ class AttributeSanitizerHandler(HandlerInterface):
def process(self, target: Class):
self.cascade_default_value(target)
self.reset_unsupported_types(target)
self.remove_inherited_fields(target)

for extension in target.extensions:
self.process_inherited_fields(target, extension)

self.set_effective_choices(target)

def remove_inherited_fields(self, target: Class):
"""Compare all override fields and if they mach the parent definition
remove them."""

if len(target.extensions) == 1:
source = self.container.find(target.extensions[0].type.qname)

# All dummy extensions have been removed at this stage.
assert source is not None

choices = self.container.config.output.compound_fields
for attr in list(target.attrs):
# Quick match with attr types
pos = collections.find(source.attrs, attr)
if pos > -1:
cmp = source.attrs[pos]
res = attr.restrictions
cmp_res = cmp.restrictions
with_occurrences = not all((choices, res.choice, cmp_res.choice))

if (
attr.default == cmp.default
and attr.fixed == cmp.fixed
and attr.mixed == cmp.mixed
and res.is_compatible(cmp_res, with_occurrences)
):
target.attrs.remove(attr)
def process_inherited_fields(self, target: Class, extension: Extension):
source = self.container.find(extension.type.qname)
assert source is not None

for attr in list(target.attrs):
search = alnum(attr.name)
source_attr = collections.first(
source_attr
for source_attr in source.attrs
if alnum(source_attr.name) == search
)

if not source_attr:
continue

if attr.tag == source_attr.tag:
self.process_inherited_field(target, attr, source_attr)
else:
ClassUtils.rename_attribute_by_preference(attr, source_attr)

for extension in source.extensions:
self.process_inherited_fields(target, extension)

def process_inherited_field(self, target: Class, attr: Attr, source_attr: Attr):
choices = self.container.config.output.compound_fields
with_occurrences = not all(
(choices, attr.restrictions.choice, source_attr.restrictions.choice)
)

if (
attr.default == source_attr.default
and attr.fixed == source_attr.fixed
and attr.mixed == source_attr.mixed
and attr.restrictions.is_compatible(
source_attr.restrictions, with_occurrences
)
):
target.attrs.remove(attr)

@classmethod
def cascade_default_value(cls, target: Class):
Expand Down
2 changes: 1 addition & 1 deletion xsdata/codegen/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ class Attr:
"""

tag: str
name: str
name: str = field(compare=False)
local_name: str = field(init=False)
index: int = field(compare=False, default_factory=int)
default: Optional[str] = field(default=None, compare=False)
Expand Down
24 changes: 2 additions & 22 deletions xsdata/codegen/sanitizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from xsdata.codegen.models import AttrType
from xsdata.codegen.models import Class
from xsdata.codegen.models import Restrictions
from xsdata.codegen.utils import ClassUtils
from xsdata.logger import logger
from xsdata.models.config import StructureStyle
from xsdata.models.enums import DataType
Expand All @@ -16,7 +17,6 @@
from xsdata.utils import text
from xsdata.utils.collections import group_by
from xsdata.utils.namespaces import build_qname
from xsdata.utils.namespaces import clean_uri
from xsdata.utils.namespaces import split_qname
from xsdata.utils.text import alnum

Expand Down Expand Up @@ -329,7 +329,7 @@ def process_duplicate_attribute_names(cls, attrs: List[Attr]) -> None:
for items in grouped.values():
total = len(items)
if total == 2 and not items[0].is_enumeration:
cls.rename_attribute_by_preference(*items)
ClassUtils.rename_attribute_by_preference(*items)
elif total > 1:
cls.rename_attributes_with_index(attrs, items)

Expand All @@ -346,26 +346,6 @@ def rename_attributes_with_index(cls, attrs: List[Attr], rename: List[Attr]):

rename[index].name = f"{name}_{num}"

@classmethod
def rename_attribute_by_preference(cls, a: Attr, b: Attr):
"""
Decide and rename one of the two given attributes.
When both attributes are derived from the same xs:tag and one of the two fields
has a specific namespace prepend it to the name. Preferable rename the second
attribute.
Otherwise append the derived from tag to the name of one of the two attributes.
Preferably rename the second field or the field derived from xs:attribute.
"""
if a.tag == b.tag and (a.namespace or b.namespace):
change = b if b.namespace else a
assert change.namespace is not None
change.name = f"{clean_uri(change.namespace)}_{change.name}"
else:
change = b if b.is_attribute else a
change.name = f"{change.name}_{change.tag}"

@classmethod
def build_attr_choice(cls, attr: Attr) -> Attr:
"""
Expand Down
21 changes: 21 additions & 0 deletions xsdata/codegen/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from xsdata.exceptions import CodeGenerationError
from xsdata.utils import collections
from xsdata.utils.namespaces import build_qname
from xsdata.utils.namespaces import clean_uri
from xsdata.utils.namespaces import split_qname


Expand Down Expand Up @@ -187,3 +188,23 @@ def merge_attributes(cls, target: Class, source: Class):
existing.types.extend(attr.types)

target.attrs.sort(key=lambda x: x.index)

@classmethod
def rename_attribute_by_preference(cls, a: Attr, b: Attr):
"""
Decide and rename one of the two given attributes.
When both attributes are derived from the same xs:tag and one of the two fields
has a specific namespace prepend it to the name. Preferable rename the second
attribute.
Otherwise append the derived from tag to the name of one of the two attributes.
Preferably rename the second field or the field derived from xs:attribute.
"""
if a.tag == b.tag and (a.namespace or b.namespace):
change = b if b.namespace else a
assert change.namespace is not None
change.name = f"{clean_uri(change.namespace)}_{change.name}"
else:
change = b if b.is_attribute else a
change.name = f"{change.name}_{change.tag}"

0 comments on commit 79cd0a2

Please sign in to comment.