Skip to content

Commit

Permalink
Add cfg options to change/force compound field names
Browse files Browse the repository at this point in the history
Closes #639
  • Loading branch information
tefra committed Jan 8, 2022
1 parent c98ea6f commit 4cf57e7
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 30 deletions.
1 change: 1 addition & 0 deletions docs/api/codegen.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ like naming conventions and substitutions.
GeneratorSubstitutions
StructureStyle
DocstringStyle
CompoundFields
ObjectType
GeneratorSubstitution
NameConvention
Expand Down
12 changes: 10 additions & 2 deletions tests/codegen/handlers/test_attribute_compound_choice.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def setUp(self):
super().setUp()

self.config = GeneratorConfig()
self.config.output.compound_fields = True
self.config.output.compound_fields.enabled = True
self.container = ClassContainer(config=self.config)
self.processor = AttributeCompoundChoiceHandler(container=self.container)

Expand Down Expand Up @@ -116,6 +116,14 @@ def test_choose_name(self):
actual = self.processor.choose_name(target, ["a", "b", "c", "d"])
self.assertEqual("choice_1", actual)

self.processor.config.default_name = "ThisOrThat"
actual = self.processor.choose_name(target, ["a", "b", "c", "d"])
self.assertEqual("ThisOrThat", actual)

self.processor.config.force_default_name = True
actual = self.processor.choose_name(target, ["a", "b", "c"])
self.assertEqual("ThisOrThat", actual)

def test_build_attr_choice(self):
attr = AttrFactory.create(
name="a", namespace="xsdata", default="123", help="help", fixed=True
Expand Down Expand Up @@ -170,7 +178,7 @@ def len_sequential(target: Class):

attrs_clone = [attr.clone() for attr in target.attrs]

self.processor.compound_fields = False
self.processor.config.enabled = False
self.processor.reset_sequential(target, 0)
self.assertEqual(2, len_sequential(target))

Expand Down
4 changes: 2 additions & 2 deletions tests/models/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def test_create(self):
" <Structure>filenames</Structure>\n"
" <DocstringStyle>reStructuredText</DocstringStyle>\n"
" <RelativeImports>false</RelativeImports>\n"
" <CompoundFields>false</CompoundFields>\n"
' <CompoundFields defaultName="choice" forceDefaultName="false">false</CompoundFields>\n'
" </Output>\n"
" <Conventions>\n"
' <ClassName case="pascalCase" safePrefix="type"/>\n'
Expand Down Expand Up @@ -88,7 +88,7 @@ def test_read(self):
" <Structure>filenames</Structure>\n"
" <DocstringStyle>reStructuredText</DocstringStyle>\n"
" <RelativeImports>false</RelativeImports>\n"
" <CompoundFields>false</CompoundFields>\n"
' <CompoundFields defaultName="choice" forceDefaultName="false">false</CompoundFields>\n'
" </Output>\n"
" <Conventions>\n"
' <ClassName case="pascalCase" safePrefix="type"/>\n'
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ commands =

[testenv:docs]
basepython = python3.8
extras = docs,cli
extras = docs,cli,lxml
changedir = docs
commands =
sphinx-build -b html . _build
Expand Down
14 changes: 9 additions & 5 deletions xsdata/codegen/handlers/attribute_compound_choice.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ class AttributeCompoundChoiceHandler(RelativeHandlerInterface):
"""Group attributes that belong in the same choice and replace them by
compound fields."""

__slots__ = "compound_fields"
__slots__ = "config"

def __init__(self, container: ContainerInterface):
super().__init__(container)

self.compound_fields = container.config.output.compound_fields
self.config = container.config.output.compound_fields

def process(self, target: Class):
if self.compound_fields:
if self.config.enabled:
groups = group_by(target.attrs, get_restriction_choice)
for choice, attrs in groups.items():
if choice and len(attrs) > 1 and any(attr.is_list for attr in attrs):
Expand Down Expand Up @@ -74,8 +74,12 @@ def choose_name(self, target: Class, names: List[str]) -> str:
reserved = set(map(get_slug, self.base_attrs(target)))
reserved.update(map(get_slug, target.attrs))

if len(names) > 3 or len(names) != len(set(names)):
name = "choice"
if (
self.config.force_default_name
or len(names) > 3
or len(names) != len(set(names))
):
name = self.config.default_name
else:
name = "_Or_".join(names)

Expand Down
31 changes: 25 additions & 6 deletions xsdata/models/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import sys
import warnings
from dataclasses import dataclass
from dataclasses import field
from enum import Enum
from pathlib import Path
from typing import Any
Expand All @@ -25,6 +24,7 @@
from xsdata.models.mixins import array_element
from xsdata.models.mixins import attribute
from xsdata.models.mixins import element
from xsdata.models.mixins import text_node
from xsdata.utils import objects
from xsdata.utils import text

Expand Down Expand Up @@ -165,7 +165,7 @@ class OutputFormat:
:param kw_only: Enable keyword only arguments, default: false, python>=3.10 Only
"""

value: str = field(default="dataclasses")
value: str = text_node(default="dataclasses", cli="output")
repr: bool = attribute(default=True)
eq: bool = attribute(default=True)
order: bool = attribute(default=False)
Expand Down Expand Up @@ -197,6 +197,23 @@ def validate(self):
)


@dataclass
class CompoundFields:
"""
Compound fields options.
:param enabled: Use compound fields for repeatable elements, default: false
:param default_name: Default compound field name, default: choice
:param force_default_name: Always use the default compound field, otherwise
if the number of elements is less than 4 the generator will try to dynamically
create the field name eg. hat_or_dress_or_something.
"""

enabled: bool = text_node(default=False, cli="compound-fields")
default_name: str = attribute(default="choice", cli=False)
force_default_name: bool = attribute(default=False, cli=False)


@dataclass
class GeneratorOutput:
"""
Expand All @@ -218,7 +235,7 @@ class GeneratorOutput:
)
docstring_style: DocstringStyle = element(default=DocstringStyle.RST)
relative_imports: bool = element(default=False)
compound_fields: bool = element(default=False)
compound_fields: CompoundFields = element(default_factory=CompoundFields)
max_line_length: int = attribute(default=79)

def update(self, **kwargs: Any):
Expand Down Expand Up @@ -350,9 +367,11 @@ class GeneratorConfig:
Generator configuration binding model.
:cvar version: xsdata version number the config was created/updated
:param output: output options
:param conventions: generator conventions
:param aliases: generator aliases
:param output: Output options
:param conventions: Generator conventions
:param aliases: Generator aliases, Deprecated since v21.12, use substitutions
:param substitutions: Generator search and replace substitutions for classes,
fields, packages and modules names.
"""

class Meta:
Expand Down
29 changes: 18 additions & 11 deletions xsdata/models/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,26 +187,38 @@ def children(self, condition: Callable = return_true) -> Iterator["ElementBase"]
yield value


def text_node(**kwargs: Any) -> Any:
"""Shortcut method for text node fields."""
metadata = extract_metadata(kwargs, type=XmlType.TEXT)
add_default_value(kwargs, optional=False)

return field(metadata=metadata, **kwargs)


def attribute(optional: bool = True, **kwargs: Any) -> Any:
"""Shortcut method for attribute fields."""
metadata = extract_metadata(kwargs, type=XmlType.ATTRIBUTE)

if not has_default(kwargs) and optional:
kwargs["default"] = None
add_default_value(kwargs, optional=optional)

return field(metadata=metadata, **kwargs)


def element(optional: bool = True, **kwargs: Any) -> Any:
"""Shortcut method for element fields."""
metadata = extract_metadata(kwargs, type=XmlType.ELEMENT)

if not has_default(kwargs) and optional:
kwargs["default"] = None
add_default_value(kwargs, optional=optional)

return field(metadata=metadata, **kwargs)


def add_default_value(params: Dict, optional: bool):
"""Add default value to the params if it's missing and its marked as
optional."""

if optional and not ("default" in params or "default_factory" in params):
params["default"] = None


def array_element(**kwargs: Any) -> Any:
"""Shortcut method for list element fields."""
metadata = extract_metadata(kwargs, type=XmlType.ELEMENT)
Expand All @@ -231,11 +243,6 @@ def extract_metadata(params: Dict, **kwargs: Any) -> Dict:
return metadata


def has_default(params: Dict) -> bool:
"""Chek if default value or factory exists in the given params."""
return "default" in params or "default_factory" in params


FIELD_PARAMS = (
"default",
"default_factory",
Expand Down
11 changes: 8 additions & 3 deletions xsdata/utils/click.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,31 @@ def build_options(obj: Any, parent: str) -> Iterator[Callable[[FC], FC]]:
for field in fields(obj):
type_hint = type_hints[field.name]
doc_hint = doc_hints[field.name]
name = field.metadata.get("cli", field.name)

if not name:
continue

qname = f"{parent}.{field.name}".strip(".")

if is_dataclass(type_hint):
yield from build_options(type_hint, qname)
else:
is_flag = False
opt_type = type_hint
if field.name == "value":
if name == "output":
opt_type = click.Choice(CodeWriter.generators.keys())
names = ["-o", "--output"]
elif type_hint is bool:
is_flag = True
opt_type = None
name = text.kebab_case(field.name)
name = text.kebab_case(name)
names = [f"--{name}/--no-{name}"]
else:
if issubclass(type_hint, enum.Enum):
opt_type = EnumChoice(type_hint)

parts = text.split_words(field.name)
parts = text.split_words(name)
name = "-".join(parts)
name_short = "".join(part[0] for part in parts)
names = [f"--{name}", f"-{name_short}"]
Expand Down

0 comments on commit 4cf57e7

Please sign in to comment.