Skip to content

Commit

Permalink
Automate generate options
Browse files Browse the repository at this point in the history
- Generate the click options by the config model
- Allow to enable/disable any flag
- Removed -cf/-ri we can't have switches with short names
- Allow to bypass any value from the config
  • Loading branch information
tefra committed Aug 8, 2021
1 parent d3cbb81 commit 7dc04b0
Show file tree
Hide file tree
Showing 13 changed files with 198 additions and 215 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ exclude: tests/fixtures

repos:
- repo: https://github.com/asottile/pyupgrade
rev: v2.23.1
rev: v2.23.3
hooks:
- id: pyupgrade
args: [--py37-plus]
Expand Down
4 changes: 2 additions & 2 deletions docs/examples/dataclasses-features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ Dataclasses Features
====================

By default xsdata with generate :mod:`python:dataclasses` with the default features on
but you can use a :ref:`generator config <Generator Config>` to toggle almost all of
them.
but you can use the cli flags or a :ref:`generator config <Generator Config>` to toggle
all of them.


.. literalinclude:: /../tests/fixtures/stripe/.xsdata.xml
Expand Down
10 changes: 5 additions & 5 deletions tests/codegen/handlers/test_class_designate.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def test_group_by_namespace(self):
]

self.container.extend(classes)
self.config.output.structure = StructureStyle.NAMESPACES
self.config.output.structure_style = StructureStyle.NAMESPACES
self.config.output.package = "bar"

self.handler.run()
Expand All @@ -84,7 +84,7 @@ def test_group_all_together(self):
]

self.container.extend(classes)
self.config.output.structure = StructureStyle.SINGLE_PACKAGE
self.config.output.structure_style = StructureStyle.SINGLE_PACKAGE
self.config.output.package = "foo.bar.thug"

self.handler.run()
Expand Down Expand Up @@ -113,7 +113,7 @@ def test_group_by_strong_components(self):
classes[1].attrs.append(AttrFactory.reference(classes[2].qname))
classes[2].attrs.append(AttrFactory.reference(classes[3].qname))

self.config.output.structure = StructureStyle.CLUSTERS
self.config.output.structure_style = StructureStyle.CLUSTERS
self.config.output.package = "foo.bar"
self.container.extend(classes)

Expand Down Expand Up @@ -144,7 +144,7 @@ def test_group_by_namespace_clusters(self):
classes[2].attrs.append(AttrFactory.reference(classes[3].qname))
classes[3].attrs.append(AttrFactory.reference(classes[1].qname, circular=True))

self.config.output.structure = StructureStyle.NAMESPACE_CLUSTERS
self.config.output.structure_style = StructureStyle.NAMESPACE_CLUSTERS
self.config.output.package = "models"
self.container.extend(classes)

Expand Down Expand Up @@ -172,7 +172,7 @@ def test_group_by_namespace_clusters_raises_exception(self):
classes[2].attrs.append(AttrFactory.reference(classes[3].qname))
classes[3].attrs.append(AttrFactory.reference(classes[1].qname, circular=True))

self.config.output.structure = StructureStyle.NAMESPACE_CLUSTERS
self.config.output.structure_style = StructureStyle.NAMESPACE_CLUSTERS
self.config.output.package = "models"
self.container.extend(classes)

Expand Down
4 changes: 2 additions & 2 deletions tests/codegen/handlers/test_class_name_conflict.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def test_run_with_single_location_source(self, mock_rename_classes):
ClassFactory.create(qname="a"),
]

self.container.config.output.structure = StructureStyle.SINGLE_PACKAGE
self.container.config.output.structure_style = StructureStyle.SINGLE_PACKAGE
self.container.extend(classes)
self.processor.run()

Expand All @@ -71,7 +71,7 @@ def test_run_with_clusters_structure(self, mock_rename_classes):
ClassFactory.create(qname="{bar}a"),
ClassFactory.create(qname="a"),
]
self.container.config.output.structure = StructureStyle.CLUSTERS
self.container.config.output.structure_style = StructureStyle.CLUSTERS
self.container.extend(classes)
self.processor.run()

Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_compound.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_xml_documents():
package = "tests.fixtures.compound.models"
runner = CliRunner()
result = runner.invoke(
cli, [str(schema), "-p", package, "-ss", "single-package", "-cf"]
cli, [str(schema), "-p", package, "-ss", "single-package", "--compound-fields"]
)

if result.exception:
Expand Down
87 changes: 14 additions & 73 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from xsdata.codegen.writer import CodeWriter
from xsdata.formats.dataclass.generator import DataclassGenerator
from xsdata.logger import logger
from xsdata.models.config import DocstringStyle
from xsdata.models.config import GeneratorConfig
from xsdata.models.config import StructureStyle
from xsdata.utils.downloader import Downloader
Expand All @@ -37,7 +36,7 @@ def tearDownClass(cls):

@mock.patch.object(SchemaTransformer, "process")
@mock.patch.object(SchemaTransformer, "__init__", return_value=None)
def test_generate_with_default_output(self, mock_init, mock_process):
def test_generate(self, mock_init, mock_process):
source = fixtures_dir.joinpath("defxmlschema/chapter03.xsd")
result = self.runner.invoke(cli, [str(source), "--package", "foo"])
config = mock_init.call_args[1]["config"]
Expand All @@ -47,104 +46,46 @@ def test_generate_with_default_output(self, mock_init, mock_process):
self.assertEqual("foo", config.output.package)
self.assertEqual("dataclasses", config.output.format.value)
self.assertFalse(config.output.relative_imports)
self.assertEqual(StructureStyle.FILENAMES, config.output.structure)
self.assertEqual(StructureStyle.FILENAMES, config.output.structure_style)
self.assertEqual([source.as_uri()], mock_process.call_args[0][0])

@mock.patch.object(SchemaTransformer, "process")
@mock.patch.object(SchemaTransformer, "__init__", return_value=None)
def test_generate_with_print_mode(self, mock_init, mock_process):
source = fixtures_dir.joinpath("defxmlschema/chapter03.xsd")
result = self.runner.invoke(cli, [str(source), "--package", "foo", "--print"])

self.assertIsNone(result.exception)
self.assertEqual([source.as_uri()], mock_process.call_args[0][0])
self.assertEqual(logging.ERROR, logger.getEffectiveLevel())
self.assertTrue(mock_init.call_args[1]["print"])

@mock.patch.object(SchemaTransformer, "process")
@mock.patch.object(SchemaTransformer, "__init__", return_value=None)
def test_generate_with_structure_style_mode(self, mock_init, mock_process):
source = fixtures_dir.joinpath("defxmlschema/chapter03.xsd")
result = self.runner.invoke(
cli,
[str(source), "--package", "foo", "--structure-style", "single-package"],
)
config = mock_init.call_args[1]["config"]

self.assertIsNone(result.exception)
self.assertEqual([source.as_uri()], mock_process.call_args[0][0])
self.assertFalse(mock_init.call_args[1]["print"])
self.assertEqual("foo", config.output.package)
self.assertEqual("dataclasses", config.output.format.value)
self.assertEqual(StructureStyle.SINGLE_PACKAGE, config.output.structure)

@mock.patch.object(SchemaTransformer, "process")
@mock.patch.object(SchemaTransformer, "__init__", return_value=None)
def test_generate_with_docstring_style(self, mock_init, mock_process):
source = fixtures_dir.joinpath("defxmlschema/chapter03.xsd")
result = self.runner.invoke(
cli, [str(source), "--package", "foo", "--docstring-style", "Google"]
)
config = mock_init.call_args[1]["config"]

self.assertIsNone(result.exception)
self.assertEqual([source.as_uri()], mock_process.call_args[0][0])
self.assertEqual(DocstringStyle.GOOGLE, config.output.docstring_style)

@mock.patch.object(SchemaTransformer, "process")
@mock.patch.object(SchemaTransformer, "__init__", return_value=None)
def test_generate_with_configuration_file(self, mock_init, mock_process):
file_path = Path(tempfile.mktemp())
config = GeneratorConfig()
config.output.package = "foo.bar"
config.output.structure = StructureStyle.NAMESPACES
config.output.structure_style = StructureStyle.NAMESPACES
with file_path.open("w") as fp:
config.write(fp, config)

source = fixtures_dir.joinpath("defxmlschema/chapter03.xsd")
result = self.runner.invoke(
cli, [str(source), "--config", str(file_path)], catch_exceptions=False
cli,
[str(source), "--config", str(file_path), "--no-eq"],
catch_exceptions=False,
)
config = mock_init.call_args[1]["config"]

self.assertIsNone(result.exception)
self.assertFalse(mock_init.call_args[1]["print"])
self.assertEqual("foo.bar", config.output.package)
self.assertEqual("dataclasses", config.output.format.value)
self.assertEqual(StructureStyle.NAMESPACES, config.output.structure)
self.assertFalse(config.output.format.eq)
self.assertEqual(StructureStyle.NAMESPACES, config.output.structure_style)
self.assertEqual([source.as_uri()], mock_process.call_args[0][0])
file_path.unlink()

@mock.patch.object(SchemaTransformer, "process")
@mock.patch.object(SchemaTransformer, "__init__", return_value=None)
def test_generate_with_configuration_file_and_overriding_args(self, mock_init, _):
file_path = Path(tempfile.mktemp())
config = GeneratorConfig()
config.output.package = "foo.bar"
config.output.structure = StructureStyle.FILENAMES
with file_path.open("w") as fp:
config.write(fp, config)

def test_generate_with_print_mode(self, mock_init, mock_process):
source = fixtures_dir.joinpath("defxmlschema/chapter03.xsd")
result = self.runner.invoke(cli, [str(source), "--package", "foo", "--print"])

result = self.runner.invoke(
cli,
[
str(source),
f"--config={file_path}",
"--output=testing",
"--package=foo",
"--structure-style=namespaces",
"--relative-imports",
],
)
self.assertIsNone(result.exception, msg=result.output)
config = mock_init.call_args[1]["config"]

self.assertEqual("foo", config.output.package)
self.assertTrue(config.output.relative_imports)
self.assertEqual(StructureStyle.NAMESPACES, config.output.structure)
file_path.unlink()
self.assertIsNone(result.exception)
self.assertEqual([source.as_uri()], mock_process.call_args[0][0])
self.assertEqual(logging.ERROR, logger.getEffectiveLevel())
self.assertTrue(mock_init.call_args[1]["print"])

@mock.patch("xsdata.cli.logger.info")
def test_init_config(self, mock_info):
Expand Down
18 changes: 18 additions & 0 deletions tests/utils/test_objects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from types import SimpleNamespace
from unittest import TestCase

from xsdata.utils import objects


class ObjectsTests(TestCase):
def test_update(self):

obj = SimpleNamespace()
obj.foo = SimpleNamespace()
obj.foo.bar = 2
obj.bar = 1

kwargs = {"foo.bar": 1, "bar": 2}
objects.update(obj, **kwargs)
self.assertEqual(1, obj.foo.bar)
self.assertEqual(2, obj.bar)
121 changes: 15 additions & 106 deletions xsdata/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
from xsdata.logger import logger
from xsdata.models.config import DocstringStyle
from xsdata.models.config import GeneratorConfig
from xsdata.models.config import OutputFormat
from xsdata.models.config import GeneratorOutput
from xsdata.models.config import StructureStyle
from xsdata.utils.click import model_options
from xsdata.utils.downloader import Downloader
from xsdata.utils.hooks import load_entry_points

Expand Down Expand Up @@ -75,86 +76,9 @@ def download(source: str, output: str):

@cli.command("generate")
@click.argument("source", required=True)
@click.option(
"-c",
"--config",
default=".xsdata.xml",
help="Specify a configuration file with advanced options.",
)
@click.option(
"-p",
"--package",
required=False,
help=(
"Specify the target package to be created inside the current working directory "
"Default: generated"
),
default="generated",
)
@click.option(
"-o",
"--output",
type=outputs,
help=(
"Specify the output format from the builtin code generator and any third "
"party installed plugins. Default: dataclasses"
),
default="dataclasses",
)
@click.option(
"-ds",
"--docstring-style",
type=docstring_styles,
help=(
"Specify the docstring style for the default output format. "
"Default: reStructuredText"
),
default="reStructuredText",
)
@click.option(
"-ss",
"--structure-style",
type=structure_styles,
help=(
"Specify a structure style to organize classes "
"Default: filenames"
"\n\n"
"filenames: groups classes by the schema location"
"\n\n"
"namespaces: group classes by the target namespace"
"\n\n"
"clusters: group by strong connected dependencies"
"\n\n"
"namespace-clusters: group by strong connected dependencies and namespaces"
"\n\n"
"single-package: group all classes together"
),
default="filenames",
)
@click.option(
"-cf",
"--compound-fields",
is_flag=True,
default=False,
help=(
"Use compound fields for repeating choices in order to maintain elements "
"ordering between data binding operations."
),
)
@click.option(
"-ri",
"--relative-imports",
is_flag=True,
default=False,
help="Enable relative imports",
)
@click.option(
"-pp",
"--print",
is_flag=True,
default=False,
help="Print to console instead of writing the generated output to files",
)
@click.option("-c", "--config", default=".xsdata.xml", help="Project configuration")
@click.option("-pp", "--print", is_flag=True, default=False, help="Print output")
@model_options(GeneratorOutput)
def generate(**kwargs: Any):
"""
Generate code from xml schemas, webservice definitions and any xml or json
Expand All @@ -163,34 +87,19 @@ def generate(**kwargs: Any):
The input source can be either a filepath, uri or a directory
containing xml, json, xsd and wsdl files.
"""
if kwargs["print"]:
logger.setLevel(logging.ERROR)

config_file = Path(kwargs["config"])
if config_file.exists():
config = GeneratorConfig.read(config_file)
if kwargs["package"] != "generated":
config.output.package = kwargs["package"]
else:
config = GeneratorConfig()
config.output.format = OutputFormat(value=kwargs["output"])
config.output.package = kwargs["package"]
config.output.relative_imports = kwargs["relative_imports"]
config.output.compound_fields = kwargs["compound_fields"]
config.output.docstring_style = DocstringStyle(kwargs["docstring_style"])

if kwargs["structure_style"] != StructureStyle.FILENAMES.value:
config.output.structure = StructureStyle(kwargs["structure_style"])
source = kwargs.pop("source")
stdout = kwargs.pop("print")
config_file = Path(kwargs.pop("config")).resolve()

if kwargs["output"] != "dataclasses":
config.output.format.value = kwargs["output"]
if stdout:
logger.setLevel(logging.ERROR)

if kwargs["relative_imports"]:
config.output.relative_imports = True
params = {k.replace("__", "."): v for k, v in kwargs.items() if v is not None}
config = GeneratorConfig.read(config_file)
config.output.update(**params)

uris = resolve_source(kwargs["source"])
transformer = SchemaTransformer(config=config, print=kwargs["print"])
transformer.process(list(uris))
transformer = SchemaTransformer(config=config, print=stdout)
transformer.process(list(resolve_source(source)))


def resolve_source(source: str) -> Iterator[str]:
Expand Down
Loading

0 comments on commit 7dc04b0

Please sign in to comment.