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: Disambiguate choices #946

Merged
merged 7 commits into from
Mar 2, 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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ repos:
- id: end-of-file-fixer
- id: debug-statements
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.1
rev: v0.3.0
hooks:
- id: ruff
args: [ --fix, --show-fixes ]
Expand Down
1 change: 1 addition & 0 deletions docs/codegen/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ pass through each step before next one starts. The order of the steps is very im

- [VacuumInnerClasses][xsdata.codegen.handlers.VacuumInnerClasses]
- [CreateCompoundFields][xsdata.codegen.handlers.CreateCompoundFields]
- [DisambiguateChoices][xsdata.codegen.handlers.DisambiguateChoices]
- [ResetAttributeSequenceNumbers][xsdata.codegen.handlers.ResetAttributeSequenceNumbers]

### Step: Designate
Expand Down
9 changes: 6 additions & 3 deletions docs/models/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,12 @@ Elements type represents repeatable choice elements. It's more commonly referred

!!! Warning

If a compound field includes ambiguous types, you need to use
`~xsdata.formats.dataclass.models.generics.DerivedElement` to wrap
your values, otherwise your object can be assigned to the wrong element.
A compound field can not contain ambigous types because it's impossible to infer the
element from the actual value.

The xml contenxt will raise an error. The solution is to introduce intermediate
simple types or subclasses per element. This will resolve xml roundtrips but
it will not work for certain json roundtrips.

#### Wildcard

Expand Down
251 changes: 251 additions & 0 deletions tests/codegen/handlers/test_disambiguate_choices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
from dataclasses import replace

from xsdata.codegen.container import ClassContainer
from xsdata.codegen.handlers import DisambiguateChoices
from xsdata.codegen.models import Restrictions, Status
from xsdata.models.config import GeneratorConfig
from xsdata.models.enums import DataType, Tag
from xsdata.utils.testing import (
AttrFactory,
AttrTypeFactory,
ClassFactory,
FactoryTestCase,
)


class DisambiguateChoicesTest(FactoryTestCase):
maxDiff = None

def setUp(self):
super().setUp()

self.container = ClassContainer(config=GeneratorConfig())
self.handler = DisambiguateChoices(self.container)

def test_process_with_duplicate_wildcards(self):
compound = AttrFactory.create(tag=Tag.CHOICE, types=[])
target = ClassFactory.create()
target.attrs.append(compound)
compound.choices.append(AttrFactory.native(DataType.STRING))
compound.choices.append(AttrFactory.any(namespace="foo"))
compound.choices.append(
AttrFactory.any(
namespace="bar", restrictions=Restrictions(min_occurs=1, max_occurs=1)
)
)
compound.choices.append(
AttrFactory.any(
namespace="bar", restrictions=Restrictions(max_occurs=3, min_occurs=0)
)
)
self.container.add(target)
self.handler.process(target)

self.assertEqual(2, len(compound.choices))

wildcard = compound.choices[-1]
self.assertEqual("content", wildcard.name)
self.assertEqual([AttrTypeFactory.native(DataType.ANY_TYPE)], wildcard.types)
self.assertEqual("foo bar", wildcard.namespace)
self.assertEqual(1, wildcard.restrictions.min_occurs)
self.assertEqual(4, wildcard.restrictions.max_occurs)

def test_process_with_duplicate_simple_types(self):
compound = AttrFactory.create(tag=Tag.CHOICE, types=[])
target = ClassFactory.create()
target.attrs.append(compound)
compound.choices.append(AttrFactory.native(DataType.STRING, name="a"))
compound.choices.append(
AttrFactory.native(DataType.STRING, name="b", namespace="xs")
)
self.container.add(target)

self.handler.process(target)
self.assertEqual(2, len(compound.choices))

self.assertEqual("a", compound.choices[0].types[0].qname)
self.assertEqual("{xs}b", compound.choices[1].types[0].qname)

self.assertEqual(2, len(target.inner))
self.assertEqual("a", target.inner[0].qname)
self.assertEqual("{xs}b", target.inner[1].qname)

self.assertEqual(["a", "{xs}b"], [x.qname for x in compound.types])

def test_process_with_duplicate_any_types(self):
compound = AttrFactory.create(tag=Tag.CHOICE, types=[])
target = ClassFactory.create()
target.attrs.append(compound)
compound.choices.append(AttrFactory.native(DataType.ANY_TYPE, name="a"))
compound.choices.append(
AttrFactory.native(DataType.ANY_TYPE, name="b", namespace="xs")
)
self.container.add(target)

self.handler.process(target)
self.assertEqual(2, len(compound.choices))

self.assertEqual("a", compound.choices[0].types[0].qname)
self.assertEqual("{xs}b", compound.choices[1].types[0].qname)

self.assertEqual(2, len(target.inner))
self.assertEqual("a", target.inner[0].qname)
self.assertEqual("{xs}b", target.inner[1].qname)

def test_process_with_duplicate_complex_types(self):
compound = AttrFactory.any()
target = ClassFactory.create()
target.attrs.append(compound)
compound.choices.append(AttrFactory.reference(name="a", qname="myint"))
compound.choices.append(AttrFactory.reference(name="b", qname="myint"))
self.container.add(target)

self.handler.process(target)
self.assertEqual(2, len(compound.choices))

self.assertEqual("attr_C", compound.choices[0].types[0].qname)
self.assertEqual("attr_D", compound.choices[1].types[0].qname)

self.assertEqual(2, len(target.inner))
self.assertEqual("attr_C", target.inner[0].qname)
self.assertEqual("attr_D", target.inner[1].qname)

for inner in target.inner:
self.assertEqual("myint", inner.extensions[0].type.qname)
self.assertEqual("myint", inner.extensions[0].type.qname)

self.assertEqual(DataType.ANY_TYPE, compound.types[0].datatype)

def test_disambiguate_choice_with_unnest_true(self):
target = ClassFactory.create()
attr = AttrFactory.reference(qname="a")

config = GeneratorConfig()
config.output.unnest_classes = True
container = ClassContainer(config=config)
handler = DisambiguateChoices(container)

container.add(target)
handler.disambiguate_choice(target, attr)

self.assertIsNotNone(container.find(attr.qname))

def test_disambiguate_choice_with_circular_ref(self):
target = ClassFactory.create()
attr = AttrFactory.reference(qname="a")
attr.types[0].circular = True

self.container.add(target)
self.handler.disambiguate_choice(target, attr)

self.assertTrue(attr.types[0].circular)
self.assertIsNotNone(self.container.find(attr.qname))

def test_find_ambiguous_choices_ignore_wildcards(self):
"""Wildcards are merged."""

attr = AttrFactory.create()
attr.choices.append(AttrFactory.any())
attr.choices.append(AttrFactory.any())
attr.choices.append(
AttrFactory.create(
name="this", types=[AttrTypeFactory.native(DataType.ANY_TYPE)]
)
)

result = self.handler.find_ambiguous_choices(attr)
self.assertEqual(["this"], [x.name for x in result])

def test_is_simple_type(self):
attr = AttrFactory.native(DataType.STRING)
self.assertTrue(self.handler.is_simple_type(attr))

enumeration = ClassFactory.enumeration(2)
self.container.add(enumeration)
attr = AttrFactory.reference(qname=enumeration.qname)
self.assertTrue(self.handler.is_simple_type(attr))

complex = ClassFactory.create()
self.container.add(complex)
attr = AttrFactory.reference(qname=complex.qname)
self.assertFalse(self.handler.is_simple_type(attr))

def test_create_ref_class(self):
source = ClassFactory.create(
status=Status.RESOLVED,
location="here.xsd",
ns_map={"foo": "bar"},
)
attr = AttrFactory.create(
namespace="test",
restrictions=Restrictions(nillable=True),
)

result = self.handler.create_ref_class(source, attr, inner=True)

self.assertTrue(result.local_type)
self.assertEqual("{test}attr_B", result.qname)
self.assertEqual(source.status, result.status)
self.assertEqual(Tag.ELEMENT, result.tag)
self.assertEqual(source.location, result.location)
self.assertEqual(source.ns_map, result.ns_map)
self.assertEqual(attr.restrictions.nillable, result.nillable)

def test_create_ref_class_creates_unique_inner_names(self):
source = ClassFactory.create(
status=Status.RESOLVED,
location="here.xsd",
ns_map={"foo": "bar"},
)
attr = AttrFactory.create(name="a")
source.inner.append(ClassFactory.create(qname="{xs}a"))
result = self.handler.create_ref_class(source, attr, inner=True)

self.assertEqual("a_1", result.name)

def test_add_any_type_value(self):
target = ClassFactory.elements(2)
source = AttrFactory.any()
self.handler.add_any_type_value(target, source)

last = target.attrs[-1]
self.assertEqual("content", last.name)
self.assertEqual(Tag.ANY, last.tag)
self.assertEqual(source.namespace, last.namespace)
self.assertEqual([AttrTypeFactory.native(DataType.ANY_TYPE)], last.types)
self.assertFalse(last.restrictions.is_optional)
self.assertFalse(last.restrictions.is_list)

def test_add_simply_type_value(self):
target = ClassFactory.elements(2)
source = AttrFactory.native(
DataType.STRING,
restrictions=Restrictions(
max_length=2, nillable=True, path=[("s", 1, 1, 1)]
),
)
self.handler.add_simple_type_value(target, source)

last = target.attrs[-1]
self.assertEqual("value", last.name)
self.assertEqual(Tag.EXTENSION, last.tag)
self.assertIsNone(last.namespace)
self.assertEqual(source.types, last.types)
self.assertFalse(last.restrictions.is_optional)
self.assertFalse(last.restrictions.is_list)
self.assertEqual([], last.restrictions.path)
self.assertFalse(last.restrictions.nillable)

def test_add_extension(self):
target = ClassFactory.create()
source = AttrFactory.reference("{xs}type")
source.types[0].forward = True
source.types[0].circular = True
self.handler.add_extension(target, source)

last = target.extensions[-1]
self.assertEqual(Tag.EXTENSION, last.tag)

expected = replace(source.types[0], forward=False, circular=False)
self.assertEqual(expected, last.type)
self.assertEqual(Restrictions(), last.restrictions)
Loading
Loading