From 4cf57e75f5fbe99800429432bfdf535aa946ddc4 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 8 Jan 2022 12:07:07 +0200 Subject: [PATCH] Add cfg options to change/force compound field names Closes #639 --- docs/api/codegen.rst | 1 + .../test_attribute_compound_choice.py | 12 +++++-- tests/models/test_config.py | 4 +-- tox.ini | 2 +- .../handlers/attribute_compound_choice.py | 14 ++++++--- xsdata/models/config.py | 31 +++++++++++++++---- xsdata/models/mixins.py | 29 ++++++++++------- xsdata/utils/click.py | 11 +++++-- 8 files changed, 74 insertions(+), 30 deletions(-) diff --git a/docs/api/codegen.rst b/docs/api/codegen.rst index e707651bb..027cae69b 100644 --- a/docs/api/codegen.rst +++ b/docs/api/codegen.rst @@ -34,6 +34,7 @@ like naming conventions and substitutions. GeneratorSubstitutions StructureStyle DocstringStyle + CompoundFields ObjectType GeneratorSubstitution NameConvention diff --git a/tests/codegen/handlers/test_attribute_compound_choice.py b/tests/codegen/handlers/test_attribute_compound_choice.py index 87cc40106..eaf411678 100644 --- a/tests/codegen/handlers/test_attribute_compound_choice.py +++ b/tests/codegen/handlers/test_attribute_compound_choice.py @@ -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) @@ -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 @@ -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)) diff --git a/tests/models/test_config.py b/tests/models/test_config.py index 71371a700..fec443a29 100644 --- a/tests/models/test_config.py +++ b/tests/models/test_config.py @@ -33,7 +33,7 @@ def test_create(self): " filenames\n" " reStructuredText\n" " false\n" - " false\n" + ' false\n' " \n" " \n" ' \n' @@ -88,7 +88,7 @@ def test_read(self): " filenames\n" " reStructuredText\n" " false\n" - " false\n" + ' false\n' " \n" " \n" ' \n' diff --git a/tox.ini b/tox.ini index 2da9f9a36..f9513db93 100644 --- a/tox.ini +++ b/tox.ini @@ -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 diff --git a/xsdata/codegen/handlers/attribute_compound_choice.py b/xsdata/codegen/handlers/attribute_compound_choice.py index 6739c4775..b319f950e 100644 --- a/xsdata/codegen/handlers/attribute_compound_choice.py +++ b/xsdata/codegen/handlers/attribute_compound_choice.py @@ -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): @@ -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) diff --git a/xsdata/models/config.py b/xsdata/models/config.py index 87e4cd8ca..7ac09aed0 100644 --- a/xsdata/models/config.py +++ b/xsdata/models/config.py @@ -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 @@ -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 @@ -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) @@ -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: """ @@ -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): @@ -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: diff --git a/xsdata/models/mixins.py b/xsdata/models/mixins.py index 7caa6b905..e9e109c09 100644 --- a/xsdata/models/mixins.py +++ b/xsdata/models/mixins.py @@ -187,12 +187,18 @@ 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) @@ -200,13 +206,19 @@ def attribute(optional: bool = True, **kwargs: Any) -> Any: 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) @@ -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", diff --git a/xsdata/utils/click.py b/xsdata/utils/click.py index 099b561a7..f9a962cb9 100644 --- a/xsdata/utils/click.py +++ b/xsdata/utils/click.py @@ -38,6 +38,11 @@ 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): @@ -45,19 +50,19 @@ def build_options(obj: Any, parent: str) -> Iterator[Callable[[FC], FC]]: 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}"]