pyxsdata 26.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyxsdata/__init__.py +1 -0
- pyxsdata/__main__.py +16 -0
- pyxsdata/cli.py +169 -0
- pyxsdata/codegen/__init__.py +6 -0
- pyxsdata/codegen/container.py +291 -0
- pyxsdata/codegen/exceptions.py +23 -0
- pyxsdata/codegen/handlers/__init__.py +53 -0
- pyxsdata/codegen/handlers/add_attribute_substitutions.py +122 -0
- pyxsdata/codegen/handlers/calculate_attribute_paths.py +87 -0
- pyxsdata/codegen/handlers/create_compound_fields.py +258 -0
- pyxsdata/codegen/handlers/create_wrapper_fields.py +140 -0
- pyxsdata/codegen/handlers/designate_class_packages.py +233 -0
- pyxsdata/codegen/handlers/detect_circular_references.py +93 -0
- pyxsdata/codegen/handlers/disambiguate_choices.py +293 -0
- pyxsdata/codegen/handlers/filter_classes.py +41 -0
- pyxsdata/codegen/handlers/flatten_attribute_groups.py +55 -0
- pyxsdata/codegen/handlers/flatten_class_extensions.py +387 -0
- pyxsdata/codegen/handlers/merge_attributes.py +93 -0
- pyxsdata/codegen/handlers/merge_duplicate_classes.py +73 -0
- pyxsdata/codegen/handlers/process_attributes_types.py +339 -0
- pyxsdata/codegen/handlers/process_mixed_content_class.py +47 -0
- pyxsdata/codegen/handlers/rename_duplicate_attributes.py +17 -0
- pyxsdata/codegen/handlers/rename_duplicate_classes.py +161 -0
- pyxsdata/codegen/handlers/reset_attribute_sequence_numbers.py +51 -0
- pyxsdata/codegen/handlers/reset_attribute_sequences.py +54 -0
- pyxsdata/codegen/handlers/sanitize_attributes_default_value.py +284 -0
- pyxsdata/codegen/handlers/sanitize_enumeration_class.py +63 -0
- pyxsdata/codegen/handlers/unnest_inner_classes.py +104 -0
- pyxsdata/codegen/handlers/update_attributes_effective_choice.py +169 -0
- pyxsdata/codegen/handlers/vacuum_inner_classes.py +97 -0
- pyxsdata/codegen/handlers/validate_attributes_overrides.py +202 -0
- pyxsdata/codegen/handlers/validate_references.py +108 -0
- pyxsdata/codegen/mappers/__init__.py +13 -0
- pyxsdata/codegen/mappers/definitions.py +576 -0
- pyxsdata/codegen/mappers/dict.py +75 -0
- pyxsdata/codegen/mappers/dtd.py +332 -0
- pyxsdata/codegen/mappers/element.py +172 -0
- pyxsdata/codegen/mappers/mixins.py +128 -0
- pyxsdata/codegen/mappers/schema.py +399 -0
- pyxsdata/codegen/mixins.py +193 -0
- pyxsdata/codegen/models.py +762 -0
- pyxsdata/codegen/parsers/__init__.py +9 -0
- pyxsdata/codegen/parsers/definitions.py +53 -0
- pyxsdata/codegen/parsers/dtd.py +134 -0
- pyxsdata/codegen/parsers/schema.py +412 -0
- pyxsdata/codegen/resolver.py +170 -0
- pyxsdata/codegen/stopwatch.py +16 -0
- pyxsdata/codegen/transformer.py +449 -0
- pyxsdata/codegen/utils.py +524 -0
- pyxsdata/codegen/validator.py +201 -0
- pyxsdata/codegen/writer.py +96 -0
- pyxsdata/exceptions.py +30 -0
- pyxsdata/formats/__init__.py +0 -0
- pyxsdata/formats/converter.py +871 -0
- pyxsdata/formats/dataclass/__init__.py +0 -0
- pyxsdata/formats/dataclass/client.py +198 -0
- pyxsdata/formats/dataclass/compat.py +212 -0
- pyxsdata/formats/dataclass/context.py +317 -0
- pyxsdata/formats/dataclass/filters.py +987 -0
- pyxsdata/formats/dataclass/generator.py +273 -0
- pyxsdata/formats/dataclass/models/__init__.py +0 -0
- pyxsdata/formats/dataclass/models/builders.py +598 -0
- pyxsdata/formats/dataclass/models/elements.py +588 -0
- pyxsdata/formats/dataclass/models/generics.py +46 -0
- pyxsdata/formats/dataclass/parsers/__init__.py +12 -0
- pyxsdata/formats/dataclass/parsers/bases.py +256 -0
- pyxsdata/formats/dataclass/parsers/config.py +48 -0
- pyxsdata/formats/dataclass/parsers/dict.py +423 -0
- pyxsdata/formats/dataclass/parsers/handlers/__init__.py +22 -0
- pyxsdata/formats/dataclass/parsers/handlers/lxml.py +83 -0
- pyxsdata/formats/dataclass/parsers/handlers/native.py +171 -0
- pyxsdata/formats/dataclass/parsers/json.py +98 -0
- pyxsdata/formats/dataclass/parsers/mixins.py +291 -0
- pyxsdata/formats/dataclass/parsers/nodes/__init__.py +17 -0
- pyxsdata/formats/dataclass/parsers/nodes/element.py +682 -0
- pyxsdata/formats/dataclass/parsers/nodes/primitive.py +71 -0
- pyxsdata/formats/dataclass/parsers/nodes/skip.py +21 -0
- pyxsdata/formats/dataclass/parsers/nodes/standard.py +97 -0
- pyxsdata/formats/dataclass/parsers/nodes/union.py +190 -0
- pyxsdata/formats/dataclass/parsers/nodes/wildcard.py +117 -0
- pyxsdata/formats/dataclass/parsers/nodes/wrapper.py +57 -0
- pyxsdata/formats/dataclass/parsers/tree.py +73 -0
- pyxsdata/formats/dataclass/parsers/utils.py +258 -0
- pyxsdata/formats/dataclass/parsers/xml.py +123 -0
- pyxsdata/formats/dataclass/serializers/__init__.py +19 -0
- pyxsdata/formats/dataclass/serializers/code.py +199 -0
- pyxsdata/formats/dataclass/serializers/config.py +60 -0
- pyxsdata/formats/dataclass/serializers/dict.py +109 -0
- pyxsdata/formats/dataclass/serializers/json.py +43 -0
- pyxsdata/formats/dataclass/serializers/mixins.py +1049 -0
- pyxsdata/formats/dataclass/serializers/tree.py +34 -0
- pyxsdata/formats/dataclass/serializers/writers/__init__.py +18 -0
- pyxsdata/formats/dataclass/serializers/writers/lxml.py +100 -0
- pyxsdata/formats/dataclass/serializers/writers/native.py +100 -0
- pyxsdata/formats/dataclass/serializers/xml.py +53 -0
- pyxsdata/formats/dataclass/transports.py +92 -0
- pyxsdata/formats/dataclass/typing.py +280 -0
- pyxsdata/formats/mixins.py +102 -0
- pyxsdata/formats/types.py +3 -0
- pyxsdata/logger.py +3 -0
- pyxsdata/models/__init__.py +0 -0
- pyxsdata/models/config.py +590 -0
- pyxsdata/models/datatype.py +712 -0
- pyxsdata/models/dtd.py +143 -0
- pyxsdata/models/enums.py +344 -0
- pyxsdata/models/mixins.py +225 -0
- pyxsdata/models/wsdl.py +314 -0
- pyxsdata/models/xsd.py +1050 -0
- pyxsdata/py.typed +0 -0
- pyxsdata/pydantic/__init__.py +27 -0
- pyxsdata/pydantic/bindings.py +156 -0
- pyxsdata/pydantic/compat.py +143 -0
- pyxsdata/pydantic/fields.py +28 -0
- pyxsdata/pydantic/generator.py +71 -0
- pyxsdata/pydantic/hooks/__init__.py +0 -0
- pyxsdata/pydantic/hooks/class_type.py +4 -0
- pyxsdata/pydantic/hooks/cli.py +4 -0
- pyxsdata/pydantic/py.typed +0 -0
- pyxsdata/utils/__init__.py +0 -0
- pyxsdata/utils/click.py +202 -0
- pyxsdata/utils/collections.py +116 -0
- pyxsdata/utils/constants.py +21 -0
- pyxsdata/utils/dates.py +280 -0
- pyxsdata/utils/debug.py +35 -0
- pyxsdata/utils/downloader.py +113 -0
- pyxsdata/utils/graphs.py +42 -0
- pyxsdata/utils/hooks.py +14 -0
- pyxsdata/utils/namespaces.py +188 -0
- pyxsdata/utils/objects.py +29 -0
- pyxsdata/utils/package.py +30 -0
- pyxsdata/utils/testing.py +628 -0
- pyxsdata/utils/text.py +217 -0
- pyxsdata-26.3.0.dist-info/METADATA +151 -0
- pyxsdata-26.3.0.dist-info/RECORD +138 -0
- pyxsdata-26.3.0.dist-info/WHEEL +5 -0
- pyxsdata-26.3.0.dist-info/entry_points.txt +2 -0
- pyxsdata-26.3.0.dist-info/licenses/LICENSE +21 -0
- pyxsdata-26.3.0.dist-info/top_level.txt +1 -0
pyxsdata/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "26.3.0"
|
pyxsdata/__main__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def main() -> None:
|
|
5
|
+
"""Cli entry point."""
|
|
6
|
+
try:
|
|
7
|
+
from pyxsdata.cli import cli
|
|
8
|
+
|
|
9
|
+
cli()
|
|
10
|
+
except ImportError:
|
|
11
|
+
print('Install cli requirements "pip install xsdata[cli]"')
|
|
12
|
+
sys.exit(1)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
main()
|
pyxsdata/cli.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import platform
|
|
3
|
+
import sys
|
|
4
|
+
import warnings
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from pyxsdata import __version__
|
|
12
|
+
from pyxsdata.codegen.transformer import ResourceTransformer
|
|
13
|
+
from pyxsdata.logger import logger
|
|
14
|
+
from pyxsdata.models.config import GeneratorConfig, GeneratorOutput
|
|
15
|
+
from pyxsdata.utils.click import URL, LogFormatter, LogHandler, model_options
|
|
16
|
+
from pyxsdata.utils.downloader import Downloader
|
|
17
|
+
from pyxsdata.utils.hooks import load_entry_points
|
|
18
|
+
|
|
19
|
+
# Load cli plugins
|
|
20
|
+
load_entry_points("pyxsdata.plugins.cli")
|
|
21
|
+
|
|
22
|
+
# Setup xsdata logger to print records to stdout/stderr
|
|
23
|
+
handler = LogHandler()
|
|
24
|
+
handler.formatter = LogFormatter()
|
|
25
|
+
|
|
26
|
+
logger.handlers = [handler]
|
|
27
|
+
logger.propagate = False
|
|
28
|
+
|
|
29
|
+
# Attach the cli handler to the python warnings logger
|
|
30
|
+
py_warnings = logging.getLogger("py.warnings")
|
|
31
|
+
py_warnings.handlers = [handler]
|
|
32
|
+
py_warnings.propagate = False
|
|
33
|
+
|
|
34
|
+
# Log warnings as well
|
|
35
|
+
logging.captureWarnings(True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@click.group()
|
|
39
|
+
@click.pass_context
|
|
40
|
+
@click.version_option(__version__)
|
|
41
|
+
def cli(ctx: click.Context, **kwargs: Any) -> None:
|
|
42
|
+
"""Pyxsdata command line interface."""
|
|
43
|
+
logger.setLevel(logging.INFO)
|
|
44
|
+
formatwarning_orig = warnings.formatwarning
|
|
45
|
+
|
|
46
|
+
logger.info(
|
|
47
|
+
"========= pyxsdata v%s / Python %s / Platform %s =========\n",
|
|
48
|
+
__version__,
|
|
49
|
+
platform.python_version(),
|
|
50
|
+
sys.platform,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def format_warning(message: Any, category: Any, *args: Any) -> str:
|
|
54
|
+
return (
|
|
55
|
+
f"{category.__name__}: {message}" if category else message
|
|
56
|
+
) # pragma: no cover
|
|
57
|
+
|
|
58
|
+
def format_warning_restore() -> None:
|
|
59
|
+
warnings.formatwarning = formatwarning_orig
|
|
60
|
+
|
|
61
|
+
warnings.formatwarning = format_warning # type: ignore
|
|
62
|
+
|
|
63
|
+
ctx.call_on_close(format_warning_restore)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@cli.command("init-config")
|
|
67
|
+
@click.argument("output", type=click.Path(), default=".pyxsdata.xml")
|
|
68
|
+
def init_config(**kwargs: Any) -> None:
|
|
69
|
+
"""Create or update a configuration file."""
|
|
70
|
+
file_path = Path(kwargs["output"])
|
|
71
|
+
if file_path.exists():
|
|
72
|
+
config = GeneratorConfig.read(file_path)
|
|
73
|
+
logger.info("Updating configuration file %s", kwargs["output"])
|
|
74
|
+
else:
|
|
75
|
+
logger.info("Initializing configuration file %s", kwargs["output"])
|
|
76
|
+
config = GeneratorConfig.create()
|
|
77
|
+
|
|
78
|
+
with file_path.open("w") as fp:
|
|
79
|
+
config.write(fp, config)
|
|
80
|
+
|
|
81
|
+
handler.emit_warnings()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@cli.command("download")
|
|
85
|
+
@click.argument("source", type=URL(), required=True)
|
|
86
|
+
@click.option(
|
|
87
|
+
"-o",
|
|
88
|
+
"--output",
|
|
89
|
+
type=click.Path(),
|
|
90
|
+
default="./",
|
|
91
|
+
help="Output directory, default cwd",
|
|
92
|
+
)
|
|
93
|
+
def download(source: str, output: str) -> None:
|
|
94
|
+
"""Download a schema or a definition locally with all its dependencies."""
|
|
95
|
+
downloader = Downloader(output=Path(output).resolve())
|
|
96
|
+
downloader.wget(source)
|
|
97
|
+
|
|
98
|
+
handler.emit_warnings()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
_SUPPORTED_EXTENSIONS = ("wsdl", "xsd", "dtd", "xml", "json")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@cli.command("generate")
|
|
105
|
+
@click.argument("source", required=True)
|
|
106
|
+
@click.option(
|
|
107
|
+
"-r",
|
|
108
|
+
"--recursive",
|
|
109
|
+
is_flag=True,
|
|
110
|
+
default=False,
|
|
111
|
+
help="Search files recursively in the source directory",
|
|
112
|
+
)
|
|
113
|
+
@click.option("-c", "--config", default=".pyxsdata.xml", help="Project configuration")
|
|
114
|
+
@click.option("--cache", is_flag=True, default=False, help="Cache sources loading")
|
|
115
|
+
@click.option("--debug", is_flag=True, default=False, help="Show debug messages")
|
|
116
|
+
@click.option(
|
|
117
|
+
"--extensions",
|
|
118
|
+
default=",".join(_SUPPORTED_EXTENSIONS),
|
|
119
|
+
help="Comma-separated list of extensions to filter",
|
|
120
|
+
)
|
|
121
|
+
@model_options(GeneratorOutput)
|
|
122
|
+
def generate(**kwargs: Any) -> None:
|
|
123
|
+
"""Generate code from xsd, dtd, wsdl, xml and json files.
|
|
124
|
+
|
|
125
|
+
The input source can be either a filepath, uri or a directory
|
|
126
|
+
containing xml, json, xsd and wsdl files.
|
|
127
|
+
"""
|
|
128
|
+
debug = kwargs.pop("debug")
|
|
129
|
+
if debug:
|
|
130
|
+
logger.setLevel(logging.DEBUG)
|
|
131
|
+
|
|
132
|
+
source = kwargs.pop("source")
|
|
133
|
+
cache = kwargs.pop("cache")
|
|
134
|
+
recursive = kwargs.pop("recursive")
|
|
135
|
+
config_file = Path(kwargs.pop("config")).resolve()
|
|
136
|
+
|
|
137
|
+
# Parse the comma-separated extensions string into a tuple
|
|
138
|
+
extensions_str = kwargs.pop("extensions")
|
|
139
|
+
extensions = tuple(ext.strip() for ext in extensions_str.split(",") if ext.strip())
|
|
140
|
+
|
|
141
|
+
params = {k.replace("__", "."): v for k, v in kwargs.items() if v is not None}
|
|
142
|
+
config = GeneratorConfig.read(config_file)
|
|
143
|
+
config.output.update(**params)
|
|
144
|
+
|
|
145
|
+
transformer = ResourceTransformer(config=config)
|
|
146
|
+
uris = sorted(resolve_source(source, recursive=recursive, extensions=extensions))
|
|
147
|
+
transformer.process(uris, cache=cache)
|
|
148
|
+
|
|
149
|
+
handler.emit_warnings()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def resolve_source(
|
|
153
|
+
source: str, recursive: bool, extensions: tuple[str, ...] = _SUPPORTED_EXTENSIONS
|
|
154
|
+
) -> Iterator[str]:
|
|
155
|
+
"""Yields all supported resource URIs."""
|
|
156
|
+
if source.find("://") > -1 and not source.startswith("file://"):
|
|
157
|
+
yield source
|
|
158
|
+
else:
|
|
159
|
+
path = Path(source).resolve()
|
|
160
|
+
match = "**/*" if recursive else "*"
|
|
161
|
+
if path.is_dir():
|
|
162
|
+
for ext in extensions:
|
|
163
|
+
yield from (x.as_uri() for x in path.glob(f"{match}.{ext}"))
|
|
164
|
+
else: # is a file
|
|
165
|
+
yield path.as_uri()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__": # pragma: no cover
|
|
169
|
+
cli()
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
from collections.abc import Callable, Iterator
|
|
2
|
+
|
|
3
|
+
from pyxsdata.codegen.handlers import (
|
|
4
|
+
AddAttributeSubstitutions,
|
|
5
|
+
CalculateAttributePaths,
|
|
6
|
+
CreateCompoundFields,
|
|
7
|
+
CreateWrapperFields,
|
|
8
|
+
DesignateClassPackages,
|
|
9
|
+
DetectCircularReferences,
|
|
10
|
+
DisambiguateChoices,
|
|
11
|
+
FilterClasses,
|
|
12
|
+
FlattenAttributeGroups,
|
|
13
|
+
FlattenClassExtensions,
|
|
14
|
+
MergeAttributes,
|
|
15
|
+
MergeDuplicateClasses,
|
|
16
|
+
ProcessAttributeTypes,
|
|
17
|
+
ProcessMixedContentClass,
|
|
18
|
+
RenameDuplicateAttributes,
|
|
19
|
+
RenameDuplicateClasses,
|
|
20
|
+
ResetAttributeSequenceNumbers,
|
|
21
|
+
ResetAttributeSequences,
|
|
22
|
+
SanitizeAttributesDefaultValue,
|
|
23
|
+
SanitizeEnumerationClass,
|
|
24
|
+
UnnestInnerClasses,
|
|
25
|
+
UpdateAttributesEffectiveChoice,
|
|
26
|
+
VacuumInnerClasses,
|
|
27
|
+
ValidateAttributesOverrides,
|
|
28
|
+
ValidateReferences,
|
|
29
|
+
)
|
|
30
|
+
from pyxsdata.codegen.mixins import ContainerInterface
|
|
31
|
+
from pyxsdata.codegen.models import Class, Status
|
|
32
|
+
from pyxsdata.codegen.stopwatch import stopwatch
|
|
33
|
+
from pyxsdata.codegen.utils import ClassUtils
|
|
34
|
+
from pyxsdata.codegen.validator import ClassValidator
|
|
35
|
+
from pyxsdata.models.config import GeneratorConfig
|
|
36
|
+
from pyxsdata.utils import collections
|
|
37
|
+
from pyxsdata.utils.constants import return_true
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Steps:
|
|
41
|
+
"""Process steps."""
|
|
42
|
+
|
|
43
|
+
UNGROUP = 10
|
|
44
|
+
FLATTEN = 20
|
|
45
|
+
SANITIZE = 30
|
|
46
|
+
RESOLVE = 40
|
|
47
|
+
CLEANUP = 50
|
|
48
|
+
FINALIZE = 60
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ClassContainer(ContainerInterface):
|
|
52
|
+
"""A class list wrapper with an easy access api.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
config: The generator configuration instance
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
processors: A step-processors mapping
|
|
59
|
+
data: The class qname map
|
|
60
|
+
step: The current process step
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
__slots__ = ("processors", "step")
|
|
64
|
+
|
|
65
|
+
def __init__(self, config: GeneratorConfig):
|
|
66
|
+
"""Initialize the container and all the class processors.
|
|
67
|
+
|
|
68
|
+
The order of the steps and the processors is the secret
|
|
69
|
+
recipe of the xsdata code generator.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
config: The generator configuration instance
|
|
73
|
+
"""
|
|
74
|
+
super().__init__(config)
|
|
75
|
+
self.step: int = 0
|
|
76
|
+
self.processors: dict[int, list] = {
|
|
77
|
+
Steps.UNGROUP: [
|
|
78
|
+
FlattenAttributeGroups(self),
|
|
79
|
+
],
|
|
80
|
+
Steps.FLATTEN: [
|
|
81
|
+
CalculateAttributePaths(),
|
|
82
|
+
FlattenClassExtensions(self),
|
|
83
|
+
SanitizeEnumerationClass(self),
|
|
84
|
+
UpdateAttributesEffectiveChoice(),
|
|
85
|
+
UnnestInnerClasses(self),
|
|
86
|
+
AddAttributeSubstitutions(self),
|
|
87
|
+
ProcessAttributeTypes(self),
|
|
88
|
+
MergeAttributes(),
|
|
89
|
+
ProcessMixedContentClass(),
|
|
90
|
+
],
|
|
91
|
+
Steps.SANITIZE: [
|
|
92
|
+
ResetAttributeSequences(),
|
|
93
|
+
RenameDuplicateAttributes(),
|
|
94
|
+
],
|
|
95
|
+
Steps.RESOLVE: [
|
|
96
|
+
ValidateAttributesOverrides(self),
|
|
97
|
+
],
|
|
98
|
+
Steps.CLEANUP: [
|
|
99
|
+
VacuumInnerClasses(),
|
|
100
|
+
],
|
|
101
|
+
Steps.FINALIZE: [
|
|
102
|
+
DetectCircularReferences(self),
|
|
103
|
+
CreateCompoundFields(self),
|
|
104
|
+
CreateWrapperFields(self),
|
|
105
|
+
DisambiguateChoices(self),
|
|
106
|
+
SanitizeAttributesDefaultValue(self),
|
|
107
|
+
ResetAttributeSequenceNumbers(self),
|
|
108
|
+
],
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
def __iter__(self) -> Iterator[Class]:
|
|
112
|
+
"""Yield an iterator for the class map values."""
|
|
113
|
+
for items in list(self.data.values()):
|
|
114
|
+
yield from items
|
|
115
|
+
|
|
116
|
+
def find(self, qname: str, condition: Callable = return_true) -> Class | None:
|
|
117
|
+
"""Find class that matches the given qualified name and condition callable.
|
|
118
|
+
|
|
119
|
+
Classes are allowed to have the same qualified name, e.g. xsd:Element
|
|
120
|
+
extending xsd:ComplexType with the same name, you can provide and additional
|
|
121
|
+
callback to filter the classes like the tag.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
qname: The qualified name of the class
|
|
125
|
+
condition: A user callable to filter further
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
A class instance or None if no match found.
|
|
129
|
+
"""
|
|
130
|
+
for row in self.data.get(qname, []):
|
|
131
|
+
if condition(row):
|
|
132
|
+
if row.status < self.step:
|
|
133
|
+
self.process_class(row, self.step)
|
|
134
|
+
return self.find(qname, condition)
|
|
135
|
+
|
|
136
|
+
return row
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def find_inner(self, source: Class, qname: str) -> Class:
|
|
140
|
+
"""Search by qualified name for a specific inner class or fail.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
source: The source class to search for the inner class
|
|
144
|
+
qname: The qualified name of the inner class to look up
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
The inner class instance
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
CodeGenerationError: If the inner class is not found.
|
|
151
|
+
"""
|
|
152
|
+
inner = ClassUtils.find_nested(source, qname)
|
|
153
|
+
if inner.status < self.step:
|
|
154
|
+
self.process_class(inner, self.step)
|
|
155
|
+
|
|
156
|
+
return inner
|
|
157
|
+
|
|
158
|
+
def first(self, qname: str) -> Class:
|
|
159
|
+
"""Return the first class that matches the qualified name.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
qname: The qualified name of the class
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
The first matching class
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
KeyError: If no class matches the qualified name
|
|
169
|
+
"""
|
|
170
|
+
classes = self.data.get(qname)
|
|
171
|
+
if not classes:
|
|
172
|
+
raise KeyError(f"Class {qname} not found")
|
|
173
|
+
|
|
174
|
+
return classes[0]
|
|
175
|
+
|
|
176
|
+
def process(self) -> None:
|
|
177
|
+
"""Run the processor and filter steps."""
|
|
178
|
+
self.validate_classes()
|
|
179
|
+
self.process_classes(Steps.UNGROUP)
|
|
180
|
+
self.remove_groups()
|
|
181
|
+
self.process_classes(Steps.FLATTEN)
|
|
182
|
+
self.filter_classes()
|
|
183
|
+
self.process_classes(Steps.SANITIZE)
|
|
184
|
+
self.process_classes(Steps.RESOLVE)
|
|
185
|
+
self.process_classes(Steps.CLEANUP)
|
|
186
|
+
self.process_classes(Steps.FINALIZE)
|
|
187
|
+
self.designate_classes()
|
|
188
|
+
|
|
189
|
+
def validate_classes(self) -> None:
|
|
190
|
+
"""Merge redefined classes."""
|
|
191
|
+
with stopwatch("ClassValidator"):
|
|
192
|
+
ClassValidator(self).process()
|
|
193
|
+
|
|
194
|
+
def process_classes(self, step: int) -> None:
|
|
195
|
+
"""Run the given step processors for all classes.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
step: The step reference number
|
|
199
|
+
"""
|
|
200
|
+
self.step = step
|
|
201
|
+
for obj in self:
|
|
202
|
+
if obj.status < step:
|
|
203
|
+
self.process_class(obj, step)
|
|
204
|
+
|
|
205
|
+
def process_class(self, target: Class, step: int) -> None:
|
|
206
|
+
"""Run the step processors for the given class.
|
|
207
|
+
|
|
208
|
+
Process recursively any inner classes as well.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
target: The target class to process
|
|
212
|
+
step: The step reference number
|
|
213
|
+
"""
|
|
214
|
+
target.status = Status(step)
|
|
215
|
+
for processor in self.processors.get(step, []):
|
|
216
|
+
with stopwatch(processor.__class__.__name__):
|
|
217
|
+
processor.process(target)
|
|
218
|
+
|
|
219
|
+
for inner in target.inner:
|
|
220
|
+
if inner.status < step:
|
|
221
|
+
self.process_class(inner, step)
|
|
222
|
+
|
|
223
|
+
target.status = Status(step + 1)
|
|
224
|
+
|
|
225
|
+
def designate_classes(self) -> None:
|
|
226
|
+
"""Designate the final class names, packages and modules."""
|
|
227
|
+
designators = [
|
|
228
|
+
MergeDuplicateClasses(self),
|
|
229
|
+
RenameDuplicateClasses(self),
|
|
230
|
+
ValidateReferences(self),
|
|
231
|
+
DesignateClassPackages(self),
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
for designator in designators:
|
|
235
|
+
with stopwatch(designator.__class__.__name__):
|
|
236
|
+
designator.run()
|
|
237
|
+
|
|
238
|
+
def filter_classes(self) -> None:
|
|
239
|
+
"""Filter the classes to be generated."""
|
|
240
|
+
with stopwatch(FilterClasses.__name__):
|
|
241
|
+
FilterClasses(self).run()
|
|
242
|
+
|
|
243
|
+
def remove_groups(self) -> None:
|
|
244
|
+
"""Remove xs:groups and xs:attributeGroups from the container."""
|
|
245
|
+
self.set([x for x in iter(self) if not x.is_group])
|
|
246
|
+
|
|
247
|
+
def add(self, item: Class) -> None:
|
|
248
|
+
"""Add class item to the container.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
item: The class instance to add
|
|
252
|
+
"""
|
|
253
|
+
self.data.setdefault(item.qname, []).append(item)
|
|
254
|
+
|
|
255
|
+
def remove(self, *items: Class) -> None:
|
|
256
|
+
"""Safely remove classes from the container.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
items: The classes to remove
|
|
260
|
+
"""
|
|
261
|
+
for item in items:
|
|
262
|
+
self.data[item.qname] = [
|
|
263
|
+
c for c in self.data[item.qname] if c.ref != item.ref
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
def reset(self, item: Class, qname: str) -> None:
|
|
267
|
+
"""Update the given class qualified name.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
item: The target class instance to update
|
|
271
|
+
qname: The new qualified name of the class
|
|
272
|
+
"""
|
|
273
|
+
self.data[qname] = [c for c in self.data[qname] if c.ref != item.ref]
|
|
274
|
+
self.add(item)
|
|
275
|
+
|
|
276
|
+
def set(self, items: list[Class]) -> None:
|
|
277
|
+
"""Set the list of classes to the container.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
items: The list of classes
|
|
281
|
+
"""
|
|
282
|
+
self.data.clear()
|
|
283
|
+
self.extend(items)
|
|
284
|
+
|
|
285
|
+
def extend(self, items: list[Class]) -> None:
|
|
286
|
+
"""Add a list of classes to the container.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
items: The list of class instances to add
|
|
290
|
+
"""
|
|
291
|
+
collections.apply(items, self.add)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import IO, Any
|
|
2
|
+
|
|
3
|
+
from click import ClickException, echo
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CodegenWarning(Warning):
|
|
7
|
+
"""Recovered errors during code generation recovered errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CodegenError(ClickException):
|
|
11
|
+
"""Unexpected state during code generation related errors."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message: str, **kwargs: Any):
|
|
14
|
+
"""Click exception constructor with metadata."""
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.meta = kwargs
|
|
17
|
+
|
|
18
|
+
def show(self, file: IO[Any] | None = None) -> None:
|
|
19
|
+
"""Echo codegen error message and details."""
|
|
20
|
+
echo("=========")
|
|
21
|
+
super().show(file)
|
|
22
|
+
for key, value in self.meta.items():
|
|
23
|
+
echo(f"{key}: {value}")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from .add_attribute_substitutions import AddAttributeSubstitutions
|
|
2
|
+
from .calculate_attribute_paths import CalculateAttributePaths
|
|
3
|
+
from .create_compound_fields import CreateCompoundFields
|
|
4
|
+
from .create_wrapper_fields import CreateWrapperFields
|
|
5
|
+
from .designate_class_packages import DesignateClassPackages
|
|
6
|
+
from .detect_circular_references import DetectCircularReferences
|
|
7
|
+
from .disambiguate_choices import DisambiguateChoices
|
|
8
|
+
from .filter_classes import FilterClasses
|
|
9
|
+
from .flatten_attribute_groups import FlattenAttributeGroups
|
|
10
|
+
from .flatten_class_extensions import FlattenClassExtensions
|
|
11
|
+
from .merge_attributes import MergeAttributes
|
|
12
|
+
from .merge_duplicate_classes import MergeDuplicateClasses
|
|
13
|
+
from .process_attributes_types import ProcessAttributeTypes
|
|
14
|
+
from .process_mixed_content_class import ProcessMixedContentClass
|
|
15
|
+
from .rename_duplicate_attributes import RenameDuplicateAttributes
|
|
16
|
+
from .rename_duplicate_classes import RenameDuplicateClasses
|
|
17
|
+
from .reset_attribute_sequence_numbers import ResetAttributeSequenceNumbers
|
|
18
|
+
from .reset_attribute_sequences import ResetAttributeSequences
|
|
19
|
+
from .sanitize_attributes_default_value import SanitizeAttributesDefaultValue
|
|
20
|
+
from .sanitize_enumeration_class import SanitizeEnumerationClass
|
|
21
|
+
from .unnest_inner_classes import UnnestInnerClasses
|
|
22
|
+
from .update_attributes_effective_choice import UpdateAttributesEffectiveChoice
|
|
23
|
+
from .vacuum_inner_classes import VacuumInnerClasses
|
|
24
|
+
from .validate_attributes_overrides import ValidateAttributesOverrides
|
|
25
|
+
from .validate_references import ValidateReferences
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AddAttributeSubstitutions",
|
|
29
|
+
"CalculateAttributePaths",
|
|
30
|
+
"CreateCompoundFields",
|
|
31
|
+
"CreateWrapperFields",
|
|
32
|
+
"DesignateClassPackages",
|
|
33
|
+
"DetectCircularReferences",
|
|
34
|
+
"DisambiguateChoices",
|
|
35
|
+
"FilterClasses",
|
|
36
|
+
"FlattenAttributeGroups",
|
|
37
|
+
"FlattenClassExtensions",
|
|
38
|
+
"MergeAttributes",
|
|
39
|
+
"MergeDuplicateClasses",
|
|
40
|
+
"ProcessAttributeTypes",
|
|
41
|
+
"ProcessMixedContentClass",
|
|
42
|
+
"RenameDuplicateAttributes",
|
|
43
|
+
"RenameDuplicateClasses",
|
|
44
|
+
"ResetAttributeSequenceNumbers",
|
|
45
|
+
"ResetAttributeSequences",
|
|
46
|
+
"SanitizeAttributesDefaultValue",
|
|
47
|
+
"SanitizeEnumerationClass",
|
|
48
|
+
"UnnestInnerClasses",
|
|
49
|
+
"UpdateAttributesEffectiveChoice",
|
|
50
|
+
"VacuumInnerClasses",
|
|
51
|
+
"ValidateAttributesOverrides",
|
|
52
|
+
"ValidateReferences",
|
|
53
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
3
|
+
from pyxsdata.codegen.mixins import ContainerInterface, RelativeHandlerInterface
|
|
4
|
+
from pyxsdata.codegen.models import Attr, AttrType, Class
|
|
5
|
+
from pyxsdata.codegen.utils import ClassUtils
|
|
6
|
+
from pyxsdata.models.enums import Tag
|
|
7
|
+
from pyxsdata.utils import collections
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AddAttributeSubstitutions(RelativeHandlerInterface):
|
|
11
|
+
"""Apply substitution attributes to the given class recursively.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
container: The class container instance
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
substitutions: Mapping of type names to attr values
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__slots__ = "substitutions"
|
|
21
|
+
|
|
22
|
+
def __init__(self, container: ContainerInterface):
|
|
23
|
+
"""Initialize the class."""
|
|
24
|
+
super().__init__(container)
|
|
25
|
+
self.substitutions: dict[str, list[Attr]] | None = None
|
|
26
|
+
|
|
27
|
+
def process(self, target: Class) -> None:
|
|
28
|
+
"""Process the given class attrs for substitution groups.
|
|
29
|
+
|
|
30
|
+
This method will ignore attrs in the class derived from
|
|
31
|
+
a xs:enumeration, xs:anyType and xs:any. If this is the
|
|
32
|
+
first time we call the method, build the substitution
|
|
33
|
+
map.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
target: The target class instance
|
|
37
|
+
"""
|
|
38
|
+
if self.substitutions is None:
|
|
39
|
+
self.create_substitutions()
|
|
40
|
+
|
|
41
|
+
for attr in target.attrs.copy():
|
|
42
|
+
if not (attr.is_enumeration or attr.is_wildcard):
|
|
43
|
+
self.process_attribute(target, attr)
|
|
44
|
+
|
|
45
|
+
def process_attribute(self, target: Class, attr: Attr) -> None:
|
|
46
|
+
"""Add substitution attrs that refer to the attr type.
|
|
47
|
+
|
|
48
|
+
If the given attr is referenced in substitution groups
|
|
49
|
+
clone all substitution attrs and place them bellow
|
|
50
|
+
the original attr. Convert all the attrs of the group
|
|
51
|
+
to repeatable choice elements.
|
|
52
|
+
|
|
53
|
+
Guard against multiple substitutions in case of xs:groups.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
target: The target class instance
|
|
57
|
+
attr: The source attr instance to check and process
|
|
58
|
+
"""
|
|
59
|
+
index = target.attrs.index(attr)
|
|
60
|
+
assert self.substitutions is not None
|
|
61
|
+
|
|
62
|
+
for attr_type in attr.types:
|
|
63
|
+
if attr_type.substituted:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
attr_type.substituted = True
|
|
67
|
+
for substitution in self.substitutions.get(attr_type.qname, []):
|
|
68
|
+
self.prepare_substituted(attr)
|
|
69
|
+
|
|
70
|
+
clone = ClassUtils.clone_attribute(substitution, attr.restrictions)
|
|
71
|
+
clone.restrictions.min_occurs = 0
|
|
72
|
+
clone.restrictions.max_occurs = attr.restrictions.max_occurs
|
|
73
|
+
|
|
74
|
+
attr.substitution = clone.substitution = attr_type.name
|
|
75
|
+
|
|
76
|
+
pos = collections.find(target.attrs, clone)
|
|
77
|
+
index = pos + 1 if pos > -1 else index
|
|
78
|
+
target.attrs.insert(index, clone)
|
|
79
|
+
|
|
80
|
+
self.process_attribute(target, clone)
|
|
81
|
+
|
|
82
|
+
def create_substitutions(self) -> None:
|
|
83
|
+
"""Build the substitutions mapping of type names to attr values.
|
|
84
|
+
|
|
85
|
+
The values are simple reference attrs that we can easily
|
|
86
|
+
clone later on demand.
|
|
87
|
+
"""
|
|
88
|
+
self.substitutions = defaultdict(list)
|
|
89
|
+
for obj in self.container:
|
|
90
|
+
for qname in obj.substitutions:
|
|
91
|
+
attr = self.create_substitution(obj)
|
|
92
|
+
self.substitutions[qname].append(attr)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def prepare_substituted(cls, attr: Attr):
|
|
96
|
+
"""Prepare the original attr for substitutions.
|
|
97
|
+
|
|
98
|
+
Effectively place the attr inside a xs:choice container
|
|
99
|
+
with min occurs zero.
|
|
100
|
+
"""
|
|
101
|
+
attr.restrictions.min_occurs = 0
|
|
102
|
+
if not attr.restrictions.choice:
|
|
103
|
+
choice = id(attr)
|
|
104
|
+
attr.restrictions.choice = choice
|
|
105
|
+
attr.restrictions.path.append(("c", choice, 1, 1))
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def create_substitution(cls, source: Class) -> Attr:
|
|
109
|
+
"""Create a reference attr to the source class qname.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
source: The source class to reference
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
The reference to the source class attr.
|
|
116
|
+
"""
|
|
117
|
+
return Attr(
|
|
118
|
+
name=source.name,
|
|
119
|
+
types=[AttrType(qname=source.qname)],
|
|
120
|
+
tag=Tag.ELEMENT,
|
|
121
|
+
namespace=source.namespace,
|
|
122
|
+
)
|