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.
Files changed (138) hide show
  1. pyxsdata/__init__.py +1 -0
  2. pyxsdata/__main__.py +16 -0
  3. pyxsdata/cli.py +169 -0
  4. pyxsdata/codegen/__init__.py +6 -0
  5. pyxsdata/codegen/container.py +291 -0
  6. pyxsdata/codegen/exceptions.py +23 -0
  7. pyxsdata/codegen/handlers/__init__.py +53 -0
  8. pyxsdata/codegen/handlers/add_attribute_substitutions.py +122 -0
  9. pyxsdata/codegen/handlers/calculate_attribute_paths.py +87 -0
  10. pyxsdata/codegen/handlers/create_compound_fields.py +258 -0
  11. pyxsdata/codegen/handlers/create_wrapper_fields.py +140 -0
  12. pyxsdata/codegen/handlers/designate_class_packages.py +233 -0
  13. pyxsdata/codegen/handlers/detect_circular_references.py +93 -0
  14. pyxsdata/codegen/handlers/disambiguate_choices.py +293 -0
  15. pyxsdata/codegen/handlers/filter_classes.py +41 -0
  16. pyxsdata/codegen/handlers/flatten_attribute_groups.py +55 -0
  17. pyxsdata/codegen/handlers/flatten_class_extensions.py +387 -0
  18. pyxsdata/codegen/handlers/merge_attributes.py +93 -0
  19. pyxsdata/codegen/handlers/merge_duplicate_classes.py +73 -0
  20. pyxsdata/codegen/handlers/process_attributes_types.py +339 -0
  21. pyxsdata/codegen/handlers/process_mixed_content_class.py +47 -0
  22. pyxsdata/codegen/handlers/rename_duplicate_attributes.py +17 -0
  23. pyxsdata/codegen/handlers/rename_duplicate_classes.py +161 -0
  24. pyxsdata/codegen/handlers/reset_attribute_sequence_numbers.py +51 -0
  25. pyxsdata/codegen/handlers/reset_attribute_sequences.py +54 -0
  26. pyxsdata/codegen/handlers/sanitize_attributes_default_value.py +284 -0
  27. pyxsdata/codegen/handlers/sanitize_enumeration_class.py +63 -0
  28. pyxsdata/codegen/handlers/unnest_inner_classes.py +104 -0
  29. pyxsdata/codegen/handlers/update_attributes_effective_choice.py +169 -0
  30. pyxsdata/codegen/handlers/vacuum_inner_classes.py +97 -0
  31. pyxsdata/codegen/handlers/validate_attributes_overrides.py +202 -0
  32. pyxsdata/codegen/handlers/validate_references.py +108 -0
  33. pyxsdata/codegen/mappers/__init__.py +13 -0
  34. pyxsdata/codegen/mappers/definitions.py +576 -0
  35. pyxsdata/codegen/mappers/dict.py +75 -0
  36. pyxsdata/codegen/mappers/dtd.py +332 -0
  37. pyxsdata/codegen/mappers/element.py +172 -0
  38. pyxsdata/codegen/mappers/mixins.py +128 -0
  39. pyxsdata/codegen/mappers/schema.py +399 -0
  40. pyxsdata/codegen/mixins.py +193 -0
  41. pyxsdata/codegen/models.py +762 -0
  42. pyxsdata/codegen/parsers/__init__.py +9 -0
  43. pyxsdata/codegen/parsers/definitions.py +53 -0
  44. pyxsdata/codegen/parsers/dtd.py +134 -0
  45. pyxsdata/codegen/parsers/schema.py +412 -0
  46. pyxsdata/codegen/resolver.py +170 -0
  47. pyxsdata/codegen/stopwatch.py +16 -0
  48. pyxsdata/codegen/transformer.py +449 -0
  49. pyxsdata/codegen/utils.py +524 -0
  50. pyxsdata/codegen/validator.py +201 -0
  51. pyxsdata/codegen/writer.py +96 -0
  52. pyxsdata/exceptions.py +30 -0
  53. pyxsdata/formats/__init__.py +0 -0
  54. pyxsdata/formats/converter.py +871 -0
  55. pyxsdata/formats/dataclass/__init__.py +0 -0
  56. pyxsdata/formats/dataclass/client.py +198 -0
  57. pyxsdata/formats/dataclass/compat.py +212 -0
  58. pyxsdata/formats/dataclass/context.py +317 -0
  59. pyxsdata/formats/dataclass/filters.py +987 -0
  60. pyxsdata/formats/dataclass/generator.py +273 -0
  61. pyxsdata/formats/dataclass/models/__init__.py +0 -0
  62. pyxsdata/formats/dataclass/models/builders.py +598 -0
  63. pyxsdata/formats/dataclass/models/elements.py +588 -0
  64. pyxsdata/formats/dataclass/models/generics.py +46 -0
  65. pyxsdata/formats/dataclass/parsers/__init__.py +12 -0
  66. pyxsdata/formats/dataclass/parsers/bases.py +256 -0
  67. pyxsdata/formats/dataclass/parsers/config.py +48 -0
  68. pyxsdata/formats/dataclass/parsers/dict.py +423 -0
  69. pyxsdata/formats/dataclass/parsers/handlers/__init__.py +22 -0
  70. pyxsdata/formats/dataclass/parsers/handlers/lxml.py +83 -0
  71. pyxsdata/formats/dataclass/parsers/handlers/native.py +171 -0
  72. pyxsdata/formats/dataclass/parsers/json.py +98 -0
  73. pyxsdata/formats/dataclass/parsers/mixins.py +291 -0
  74. pyxsdata/formats/dataclass/parsers/nodes/__init__.py +17 -0
  75. pyxsdata/formats/dataclass/parsers/nodes/element.py +682 -0
  76. pyxsdata/formats/dataclass/parsers/nodes/primitive.py +71 -0
  77. pyxsdata/formats/dataclass/parsers/nodes/skip.py +21 -0
  78. pyxsdata/formats/dataclass/parsers/nodes/standard.py +97 -0
  79. pyxsdata/formats/dataclass/parsers/nodes/union.py +190 -0
  80. pyxsdata/formats/dataclass/parsers/nodes/wildcard.py +117 -0
  81. pyxsdata/formats/dataclass/parsers/nodes/wrapper.py +57 -0
  82. pyxsdata/formats/dataclass/parsers/tree.py +73 -0
  83. pyxsdata/formats/dataclass/parsers/utils.py +258 -0
  84. pyxsdata/formats/dataclass/parsers/xml.py +123 -0
  85. pyxsdata/formats/dataclass/serializers/__init__.py +19 -0
  86. pyxsdata/formats/dataclass/serializers/code.py +199 -0
  87. pyxsdata/formats/dataclass/serializers/config.py +60 -0
  88. pyxsdata/formats/dataclass/serializers/dict.py +109 -0
  89. pyxsdata/formats/dataclass/serializers/json.py +43 -0
  90. pyxsdata/formats/dataclass/serializers/mixins.py +1049 -0
  91. pyxsdata/formats/dataclass/serializers/tree.py +34 -0
  92. pyxsdata/formats/dataclass/serializers/writers/__init__.py +18 -0
  93. pyxsdata/formats/dataclass/serializers/writers/lxml.py +100 -0
  94. pyxsdata/formats/dataclass/serializers/writers/native.py +100 -0
  95. pyxsdata/formats/dataclass/serializers/xml.py +53 -0
  96. pyxsdata/formats/dataclass/transports.py +92 -0
  97. pyxsdata/formats/dataclass/typing.py +280 -0
  98. pyxsdata/formats/mixins.py +102 -0
  99. pyxsdata/formats/types.py +3 -0
  100. pyxsdata/logger.py +3 -0
  101. pyxsdata/models/__init__.py +0 -0
  102. pyxsdata/models/config.py +590 -0
  103. pyxsdata/models/datatype.py +712 -0
  104. pyxsdata/models/dtd.py +143 -0
  105. pyxsdata/models/enums.py +344 -0
  106. pyxsdata/models/mixins.py +225 -0
  107. pyxsdata/models/wsdl.py +314 -0
  108. pyxsdata/models/xsd.py +1050 -0
  109. pyxsdata/py.typed +0 -0
  110. pyxsdata/pydantic/__init__.py +27 -0
  111. pyxsdata/pydantic/bindings.py +156 -0
  112. pyxsdata/pydantic/compat.py +143 -0
  113. pyxsdata/pydantic/fields.py +28 -0
  114. pyxsdata/pydantic/generator.py +71 -0
  115. pyxsdata/pydantic/hooks/__init__.py +0 -0
  116. pyxsdata/pydantic/hooks/class_type.py +4 -0
  117. pyxsdata/pydantic/hooks/cli.py +4 -0
  118. pyxsdata/pydantic/py.typed +0 -0
  119. pyxsdata/utils/__init__.py +0 -0
  120. pyxsdata/utils/click.py +202 -0
  121. pyxsdata/utils/collections.py +116 -0
  122. pyxsdata/utils/constants.py +21 -0
  123. pyxsdata/utils/dates.py +280 -0
  124. pyxsdata/utils/debug.py +35 -0
  125. pyxsdata/utils/downloader.py +113 -0
  126. pyxsdata/utils/graphs.py +42 -0
  127. pyxsdata/utils/hooks.py +14 -0
  128. pyxsdata/utils/namespaces.py +188 -0
  129. pyxsdata/utils/objects.py +29 -0
  130. pyxsdata/utils/package.py +30 -0
  131. pyxsdata/utils/testing.py +628 -0
  132. pyxsdata/utils/text.py +217 -0
  133. pyxsdata-26.3.0.dist-info/METADATA +151 -0
  134. pyxsdata-26.3.0.dist-info/RECORD +138 -0
  135. pyxsdata-26.3.0.dist-info/WHEEL +5 -0
  136. pyxsdata-26.3.0.dist-info/entry_points.txt +2 -0
  137. pyxsdata-26.3.0.dist-info/licenses/LICENSE +21 -0
  138. 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,6 @@
1
+ from urllib.request import build_opener
2
+
3
+ from pyxsdata import __version__
4
+
5
+ opener = build_opener()
6
+ opener.addheaders = [("User-agent", f"xsdata/{__version__}")]
@@ -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
+ )