docx4j-xsdata 26.2.1__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 (153) hide show
  1. docx4j_xsdata/__init__.py +1 -0
  2. docx4j_xsdata/__main__.py +16 -0
  3. docx4j_xsdata/cli.py +169 -0
  4. docx4j_xsdata/codegen/__init__.py +6 -0
  5. docx4j_xsdata/codegen/class_names.py +217 -0
  6. docx4j_xsdata/codegen/container.py +296 -0
  7. docx4j_xsdata/codegen/exceptions.py +23 -0
  8. docx4j_xsdata/codegen/handlers/__init__.py +55 -0
  9. docx4j_xsdata/codegen/handlers/add_attribute_substitutions.py +122 -0
  10. docx4j_xsdata/codegen/handlers/apply_class_names.py +191 -0
  11. docx4j_xsdata/codegen/handlers/calculate_attribute_paths.py +87 -0
  12. docx4j_xsdata/codegen/handlers/create_compound_fields.py +263 -0
  13. docx4j_xsdata/codegen/handlers/create_wrapper_fields.py +140 -0
  14. docx4j_xsdata/codegen/handlers/designate_class_packages.py +233 -0
  15. docx4j_xsdata/codegen/handlers/detect_circular_references.py +93 -0
  16. docx4j_xsdata/codegen/handlers/disambiguate_choices.py +301 -0
  17. docx4j_xsdata/codegen/handlers/filter_classes.py +41 -0
  18. docx4j_xsdata/codegen/handlers/flatten_attribute_groups.py +55 -0
  19. docx4j_xsdata/codegen/handlers/flatten_class_extensions.py +387 -0
  20. docx4j_xsdata/codegen/handlers/merge_attributes.py +93 -0
  21. docx4j_xsdata/codegen/handlers/merge_duplicate_classes.py +73 -0
  22. docx4j_xsdata/codegen/handlers/process_attributes_types.py +339 -0
  23. docx4j_xsdata/codegen/handlers/process_mixed_content_class.py +47 -0
  24. docx4j_xsdata/codegen/handlers/rename_duplicate_attributes.py +17 -0
  25. docx4j_xsdata/codegen/handlers/rename_duplicate_classes.py +164 -0
  26. docx4j_xsdata/codegen/handlers/reset_attribute_sequence_numbers.py +51 -0
  27. docx4j_xsdata/codegen/handlers/reset_attribute_sequences.py +54 -0
  28. docx4j_xsdata/codegen/handlers/sanitize_attributes_default_value.py +284 -0
  29. docx4j_xsdata/codegen/handlers/sanitize_enumeration_class.py +63 -0
  30. docx4j_xsdata/codegen/handlers/unnest_inner_classes.py +107 -0
  31. docx4j_xsdata/codegen/handlers/update_attributes_effective_choice.py +169 -0
  32. docx4j_xsdata/codegen/handlers/vacuum_inner_classes.py +97 -0
  33. docx4j_xsdata/codegen/handlers/validate_attributes_overrides.py +202 -0
  34. docx4j_xsdata/codegen/handlers/validate_references.py +108 -0
  35. docx4j_xsdata/codegen/mappers/__init__.py +13 -0
  36. docx4j_xsdata/codegen/mappers/definitions.py +576 -0
  37. docx4j_xsdata/codegen/mappers/dict.py +75 -0
  38. docx4j_xsdata/codegen/mappers/dtd.py +332 -0
  39. docx4j_xsdata/codegen/mappers/element.py +172 -0
  40. docx4j_xsdata/codegen/mappers/mixins.py +128 -0
  41. docx4j_xsdata/codegen/mappers/schema.py +399 -0
  42. docx4j_xsdata/codegen/mixins.py +193 -0
  43. docx4j_xsdata/codegen/models.py +771 -0
  44. docx4j_xsdata/codegen/parsers/__init__.py +9 -0
  45. docx4j_xsdata/codegen/parsers/definitions.py +53 -0
  46. docx4j_xsdata/codegen/parsers/dtd.py +134 -0
  47. docx4j_xsdata/codegen/parsers/schema.py +412 -0
  48. docx4j_xsdata/codegen/resolver.py +279 -0
  49. docx4j_xsdata/codegen/stopwatch.py +16 -0
  50. docx4j_xsdata/codegen/transformer.py +449 -0
  51. docx4j_xsdata/codegen/utils.py +524 -0
  52. docx4j_xsdata/codegen/validator.py +201 -0
  53. docx4j_xsdata/codegen/writer.py +88 -0
  54. docx4j_xsdata/exceptions.py +30 -0
  55. docx4j_xsdata/formats/__init__.py +0 -0
  56. docx4j_xsdata/formats/converter.py +878 -0
  57. docx4j_xsdata/formats/dataclass/__init__.py +0 -0
  58. docx4j_xsdata/formats/dataclass/client.py +198 -0
  59. docx4j_xsdata/formats/dataclass/compat.py +205 -0
  60. docx4j_xsdata/formats/dataclass/context.py +311 -0
  61. docx4j_xsdata/formats/dataclass/filters.py +1138 -0
  62. docx4j_xsdata/formats/dataclass/generator.py +410 -0
  63. docx4j_xsdata/formats/dataclass/models/__init__.py +0 -0
  64. docx4j_xsdata/formats/dataclass/models/builders.py +600 -0
  65. docx4j_xsdata/formats/dataclass/models/elements.py +687 -0
  66. docx4j_xsdata/formats/dataclass/models/generics.py +46 -0
  67. docx4j_xsdata/formats/dataclass/parsers/__init__.py +12 -0
  68. docx4j_xsdata/formats/dataclass/parsers/bases.py +293 -0
  69. docx4j_xsdata/formats/dataclass/parsers/config.py +75 -0
  70. docx4j_xsdata/formats/dataclass/parsers/dict.py +421 -0
  71. docx4j_xsdata/formats/dataclass/parsers/handlers/__init__.py +22 -0
  72. docx4j_xsdata/formats/dataclass/parsers/handlers/lxml.py +98 -0
  73. docx4j_xsdata/formats/dataclass/parsers/handlers/native.py +171 -0
  74. docx4j_xsdata/formats/dataclass/parsers/json.py +98 -0
  75. docx4j_xsdata/formats/dataclass/parsers/mixins.py +301 -0
  76. docx4j_xsdata/formats/dataclass/parsers/nodes/__init__.py +17 -0
  77. docx4j_xsdata/formats/dataclass/parsers/nodes/element.py +722 -0
  78. docx4j_xsdata/formats/dataclass/parsers/nodes/primitive.py +71 -0
  79. docx4j_xsdata/formats/dataclass/parsers/nodes/skip.py +21 -0
  80. docx4j_xsdata/formats/dataclass/parsers/nodes/standard.py +97 -0
  81. docx4j_xsdata/formats/dataclass/parsers/nodes/union.py +190 -0
  82. docx4j_xsdata/formats/dataclass/parsers/nodes/wildcard.py +117 -0
  83. docx4j_xsdata/formats/dataclass/parsers/nodes/wrapper.py +55 -0
  84. docx4j_xsdata/formats/dataclass/parsers/skipped.py +136 -0
  85. docx4j_xsdata/formats/dataclass/parsers/tree.py +73 -0
  86. docx4j_xsdata/formats/dataclass/parsers/utils.py +315 -0
  87. docx4j_xsdata/formats/dataclass/parsers/xml.py +123 -0
  88. docx4j_xsdata/formats/dataclass/ruff.toml +12 -0
  89. docx4j_xsdata/formats/dataclass/serializers/__init__.py +21 -0
  90. docx4j_xsdata/formats/dataclass/serializers/code.py +199 -0
  91. docx4j_xsdata/formats/dataclass/serializers/config.py +79 -0
  92. docx4j_xsdata/formats/dataclass/serializers/dict.py +109 -0
  93. docx4j_xsdata/formats/dataclass/serializers/json.py +43 -0
  94. docx4j_xsdata/formats/dataclass/serializers/mixins.py +1259 -0
  95. docx4j_xsdata/formats/dataclass/serializers/tree.py +34 -0
  96. docx4j_xsdata/formats/dataclass/serializers/writers/__init__.py +18 -0
  97. docx4j_xsdata/formats/dataclass/serializers/writers/lxml.py +103 -0
  98. docx4j_xsdata/formats/dataclass/serializers/writers/native.py +100 -0
  99. docx4j_xsdata/formats/dataclass/serializers/xml.py +53 -0
  100. docx4j_xsdata/formats/dataclass/templates/class.jinja2 +55 -0
  101. docx4j_xsdata/formats/dataclass/templates/docstrings.accessible.jinja2 +1 -0
  102. docx4j_xsdata/formats/dataclass/templates/docstrings.blank.jinja2 +0 -0
  103. docx4j_xsdata/formats/dataclass/templates/docstrings.google.jinja2 +8 -0
  104. docx4j_xsdata/formats/dataclass/templates/docstrings.numpy.jinja2 +12 -0
  105. docx4j_xsdata/formats/dataclass/templates/docstrings.rst.jinja2 +9 -0
  106. docx4j_xsdata/formats/dataclass/templates/enum.jinja2 +20 -0
  107. docx4j_xsdata/formats/dataclass/templates/imports.jinja2 +11 -0
  108. docx4j_xsdata/formats/dataclass/templates/module.jinja2 +17 -0
  109. docx4j_xsdata/formats/dataclass/templates/package.jinja2 +54 -0
  110. docx4j_xsdata/formats/dataclass/templates/service.jinja2 +4 -0
  111. docx4j_xsdata/formats/dataclass/transports.py +92 -0
  112. docx4j_xsdata/formats/dataclass/typing.py +280 -0
  113. docx4j_xsdata/formats/mixins.py +102 -0
  114. docx4j_xsdata/formats/types.py +3 -0
  115. docx4j_xsdata/logger.py +3 -0
  116. docx4j_xsdata/models/__init__.py +0 -0
  117. docx4j_xsdata/models/config.py +660 -0
  118. docx4j_xsdata/models/datatype.py +712 -0
  119. docx4j_xsdata/models/dtd.py +143 -0
  120. docx4j_xsdata/models/enums.py +344 -0
  121. docx4j_xsdata/models/mixins.py +225 -0
  122. docx4j_xsdata/models/wsdl.py +319 -0
  123. docx4j_xsdata/models/xsd.py +1050 -0
  124. docx4j_xsdata/py.typed +0 -0
  125. docx4j_xsdata/schemas/mathml3-common.xsd +99 -0
  126. docx4j_xsdata/schemas/mathml3-content.xsd +684 -0
  127. docx4j_xsdata/schemas/mathml3-presentation.xsd +2151 -0
  128. docx4j_xsdata/schemas/mathml3-strict-content.xsd +186 -0
  129. docx4j_xsdata/schemas/mathml3.xsd +9 -0
  130. docx4j_xsdata/schemas/soapenc.xsd +536 -0
  131. docx4j_xsdata/schemas/xlink.xsd +270 -0
  132. docx4j_xsdata/schemas/xml.xsd +34 -0
  133. docx4j_xsdata/schemas/xsi.xsd +9 -0
  134. docx4j_xsdata/utils/__init__.py +0 -0
  135. docx4j_xsdata/utils/click.py +200 -0
  136. docx4j_xsdata/utils/collections.py +116 -0
  137. docx4j_xsdata/utils/constants.py +21 -0
  138. docx4j_xsdata/utils/dates.py +280 -0
  139. docx4j_xsdata/utils/debug.py +35 -0
  140. docx4j_xsdata/utils/downloader.py +113 -0
  141. docx4j_xsdata/utils/graphs.py +49 -0
  142. docx4j_xsdata/utils/hooks.py +14 -0
  143. docx4j_xsdata/utils/namespaces.py +188 -0
  144. docx4j_xsdata/utils/objects.py +29 -0
  145. docx4j_xsdata/utils/package.py +30 -0
  146. docx4j_xsdata/utils/testing.py +628 -0
  147. docx4j_xsdata/utils/text.py +217 -0
  148. docx4j_xsdata-26.2.1.dist-info/METADATA +167 -0
  149. docx4j_xsdata-26.2.1.dist-info/RECORD +153 -0
  150. docx4j_xsdata-26.2.1.dist-info/WHEEL +5 -0
  151. docx4j_xsdata-26.2.1.dist-info/entry_points.txt +2 -0
  152. docx4j_xsdata-26.2.1.dist-info/licenses/LICENSE +21 -0
  153. docx4j_xsdata-26.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ __version__ = "26.2.1"
@@ -0,0 +1,16 @@
1
+ import sys
2
+
3
+
4
+ def main() -> None:
5
+ """Cli entry point."""
6
+ try:
7
+ from docx4j_xsdata.cli import cli
8
+
9
+ cli()
10
+ except ImportError:
11
+ print('Install cli requirements "pip install docx4j-xsdata[cli]"')
12
+ sys.exit(1)
13
+
14
+
15
+ if __name__ == "__main__":
16
+ main()
docx4j_xsdata/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 docx4j_xsdata import __version__
12
+ from docx4j_xsdata.codegen.transformer import ResourceTransformer
13
+ from docx4j_xsdata.logger import logger
14
+ from docx4j_xsdata.models.config import GeneratorConfig, GeneratorOutput
15
+ from docx4j_xsdata.utils.click import URL, LogFormatter, LogHandler, model_options
16
+ from docx4j_xsdata.utils.downloader import Downloader
17
+ from docx4j_xsdata.utils.hooks import load_entry_points
18
+
19
+ # Load cli plugins
20
+ load_entry_points("docx4j_xsdata.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
+ """Xsdata command line interface."""
43
+ logger.setLevel(logging.INFO)
44
+ formatwarning_orig = warnings.formatwarning
45
+
46
+ logger.info(
47
+ "========= docx4j-xsdata 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=".xsdata.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=".xsdata.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 docx4j_xsdata import __version__
4
+
5
+ opener = build_opener()
6
+ opener.addheaders = [("User-agent", f"docx4j-xsdata/{__version__}")]
@@ -0,0 +1,217 @@
1
+ """The docx4j fork's explicit class name table.
2
+
3
+ `Substitutions` are regular expressions, which cannot express a thousand
4
+ entry name table like docx4j's `org.docx4j.wml.ObjectFactory`. This is
5
+ the table: one JSON file per namespace in a directory, named by the
6
+ generator config's `<Output><ClassNames>` element.
7
+
8
+ ```json
9
+ {
10
+ "namespace": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
11
+ "java_package": "org.docx4j.wml",
12
+ "types": {"CT_PPr": "PPr", "CT_Settings": "CTSettings", "ST_Jc": "STJc"},
13
+ "elements": {"document": "Document", "ins": "RunIns", "t@CT_R": "RT"}
14
+ }
15
+ ```
16
+
17
+ `types` maps the name of a complexType or simpleType in that namespace,
18
+ `elements` the name of a global or local element, to the name of the
19
+ class the generator emits for it. Names that are not in the table keep
20
+ the naming conventions. A name from the table is emitted verbatim: it is
21
+ applied before the case convention, not through it.
22
+
23
+ An `elements` key may carry a **scope**, `element@EnclosingType`, the
24
+ analogue of JAXB's `@XmlElementDecl(scope=...)`. The enclosing type is
25
+ read in the namespace of the file the entry is written in, so `t@CT_R`
26
+ in the wml file is wml's `CT_R` and not the `CT_R` of another namespace
27
+ that happens to hold a wml element. It addresses the one
28
+ element of the one compound field the generator invented an intermediate
29
+ class for (`DisambiguateChoices`, `w:t` of `CT_R` where `w:t`,
30
+ `w:instrText` and `w:delInstrText` all have the type `CT_Text`). Those
31
+ classes have no xml identity of their own -- the element name lives in the
32
+ `choices` metadata of the field -- and JAXB had no class for them either,
33
+ it had one `JAXBElement` per name over the shared type. An unscoped entry
34
+ therefore never renames them; only a scoped one does.
35
+ """
36
+
37
+ import json
38
+ from dataclasses import dataclass, field
39
+ from keyword import iskeyword
40
+ from pathlib import Path
41
+
42
+ from docx4j_xsdata.codegen.exceptions import CodegenError
43
+ from docx4j_xsdata.logger import logger
44
+ from docx4j_xsdata.utils.namespaces import build_qname
45
+
46
+ TYPES = "types"
47
+ ELEMENTS = "elements"
48
+ SCOPE_SEPARATOR = "@"
49
+
50
+
51
+ @dataclass
52
+ class ClassNames:
53
+ """An explicit schema name to class name table, by namespace.
54
+
55
+ Attributes:
56
+ types: The (namespace, type name) to class name map
57
+ elements: The (namespace, element name) to class name map
58
+ scoped: The (namespace, element name, enclosing type qualified
59
+ name) to class name map, from the `element@EnclosingType`
60
+ keys; the enclosing type is qualified with the namespace of
61
+ the file the entry came from
62
+ java_packages: The namespace to java package map, informative
63
+ """
64
+
65
+ types: dict[tuple[str, str], str] = field(default_factory=dict)
66
+ elements: dict[tuple[str, str], str] = field(default_factory=dict)
67
+ scoped: dict[tuple[str, str, str], str] = field(default_factory=dict)
68
+ java_packages: dict[str, str] = field(default_factory=dict)
69
+
70
+ def __len__(self) -> int:
71
+ """Return the total number of entries."""
72
+ return len(self.types) + len(self.elements) + len(self.scoped)
73
+
74
+ def find(self, namespace: str | None, name: str, is_element: bool) -> str | None:
75
+ """Return the class name for a schema name, or None.
76
+
77
+ Args:
78
+ namespace: The target namespace of the schema that declares it
79
+ name: The complexType, simpleType or element name
80
+ is_element: Whether the class was generated for an element
81
+
82
+ Returns:
83
+ The class name to emit, or None to keep the conventions.
84
+ """
85
+ table = self.elements if is_element else self.types
86
+ return table.get((namespace or "", name))
87
+
88
+ def find_scoped(self, namespace: str | None, name: str, scope: str) -> str | None:
89
+ """Return the class name for an element in one enclosing type.
90
+
91
+ Args:
92
+ namespace: The target namespace of the schema that declares it
93
+ name: The element name
94
+ scope: The qualified name of the type whose compound field
95
+ declares it
96
+
97
+ Returns:
98
+ The class name to emit, or None to keep the conventions.
99
+ """
100
+ return self.scoped.get((namespace or "", name, scope))
101
+
102
+ def names(self) -> set[str]:
103
+ """Return every class name the table can produce."""
104
+ return (
105
+ set(self.types.values())
106
+ | set(self.elements.values())
107
+ | set(self.scoped.values())
108
+ )
109
+
110
+ @classmethod
111
+ def load(cls, directory: Path) -> "ClassNames":
112
+ """Load every json file of a directory into one table.
113
+
114
+ Args:
115
+ directory: The directory with one json file per namespace
116
+
117
+ Returns:
118
+ The class name table.
119
+
120
+ Raises:
121
+ CodegenError: If the directory or one of the files is unusable.
122
+ """
123
+ if not directory.is_dir():
124
+ raise CodegenError(f"ClassNames directory not found: {directory}")
125
+
126
+ obj = cls()
127
+ for path in sorted(directory.glob("*.json")):
128
+ obj.load_file(path)
129
+
130
+ return obj
131
+
132
+ def load_file(self, path: Path) -> None:
133
+ """Load one namespace file into the table.
134
+
135
+ Args:
136
+ path: The json file path
137
+
138
+ Raises:
139
+ CodegenError: If the file is not a json object.
140
+ """
141
+ try:
142
+ data = json.loads(path.read_text())
143
+ except ValueError as e:
144
+ raise CodegenError(f"ClassNames file is not valid json: {path}") from e
145
+
146
+ if not isinstance(data, dict):
147
+ raise CodegenError(f"ClassNames file is not a json object: {path}")
148
+
149
+ namespace = data.get("namespace") or ""
150
+ java_package = data.get("java_package")
151
+ if java_package:
152
+ self.java_packages[namespace] = java_package
153
+
154
+ self.update(namespace, data.get(TYPES), TYPES, path)
155
+ self.update(namespace, data.get(ELEMENTS), ELEMENTS, path)
156
+
157
+ def update(
158
+ self,
159
+ namespace: str,
160
+ entries: dict | None,
161
+ kind: str,
162
+ path: Path,
163
+ ) -> None:
164
+ """Merge one section of one file into the table.
165
+
166
+ Args:
167
+ namespace: The namespace the file is for
168
+ entries: The schema name to class name map, if any
169
+ kind: The section name, for the messages
170
+ path: The json file path, for the messages
171
+
172
+ Raises:
173
+ CodegenError: If the section is not a json object.
174
+ """
175
+ if entries is None:
176
+ return
177
+
178
+ if not isinstance(entries, dict):
179
+ raise CodegenError(f"ClassNames {kind} is not a json object: {path}")
180
+
181
+ for name, class_name in entries.items():
182
+ if not isinstance(class_name, str) or not self.is_valid(class_name):
183
+ logger.warning(
184
+ "ClassNames: `%s` is not a valid class name, ignoring %s %s",
185
+ class_name,
186
+ kind,
187
+ name,
188
+ )
189
+ continue
190
+
191
+ table: dict[tuple, str]
192
+ key: tuple
193
+ if kind == ELEMENTS and SCOPE_SEPARATOR in name:
194
+ local, _, scope = name.partition(SCOPE_SEPARATOR)
195
+ table = self.scoped
196
+ key = (namespace, local, build_qname(namespace, scope))
197
+ elif kind == ELEMENTS:
198
+ table, key = self.elements, (namespace, name)
199
+ else:
200
+ table, key = self.types, (namespace, name)
201
+
202
+ current = table.get(key)
203
+ if current is not None and current != class_name:
204
+ logger.warning(
205
+ "ClassNames: %s %s is mapped twice, `%s` wins over `%s`",
206
+ kind,
207
+ name,
208
+ class_name,
209
+ current,
210
+ )
211
+
212
+ table[key] = class_name
213
+
214
+ @classmethod
215
+ def is_valid(cls, name: str) -> bool:
216
+ """Return whether the name can be a python class name."""
217
+ return name.isidentifier() and not iskeyword(name)