pyxsd 1.0.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 (88) hide show
  1. pyxsd/__init__.py +74 -0
  2. pyxsd/__main__.py +6 -0
  3. pyxsd/alternatives.py +516 -0
  4. pyxsd/assertions.py +592 -0
  5. pyxsd/binding.py +104 -0
  6. pyxsd/cli.py +653 -0
  7. pyxsd/compositors.py +17 -0
  8. pyxsd/content_model.py +1205 -0
  9. pyxsd/derivation.py +356 -0
  10. pyxsd/dict_export.py +95 -0
  11. pyxsd/document.py +237 -0
  12. pyxsd/element_representatives/__init__.py +37 -0
  13. pyxsd/element_representatives/all.py +198 -0
  14. pyxsd/element_representatives/annotation.py +39 -0
  15. pyxsd/element_representatives/any.py +53 -0
  16. pyxsd/element_representatives/any_attribute.py +48 -0
  17. pyxsd/element_representatives/attribute.py +661 -0
  18. pyxsd/element_representatives/attribute_group.py +158 -0
  19. pyxsd/element_representatives/choice.py +70 -0
  20. pyxsd/element_representatives/complex_content.py +37 -0
  21. pyxsd/element_representatives/complex_type.py +543 -0
  22. pyxsd/element_representatives/documentation.py +58 -0
  23. pyxsd/element_representatives/element.py +759 -0
  24. pyxsd/element_representatives/element_representative.py +1658 -0
  25. pyxsd/element_representatives/enumeration.py +19 -0
  26. pyxsd/element_representatives/explicit_timezone.py +18 -0
  27. pyxsd/element_representatives/extension.py +86 -0
  28. pyxsd/element_representatives/fraction_digits.py +17 -0
  29. pyxsd/element_representatives/group.py +270 -0
  30. pyxsd/element_representatives/identity.py +532 -0
  31. pyxsd/element_representatives/length.py +17 -0
  32. pyxsd/element_representatives/list.py +122 -0
  33. pyxsd/element_representatives/max_exclusive.py +17 -0
  34. pyxsd/element_representatives/max_inclusive.py +17 -0
  35. pyxsd/element_representatives/max_length.py +17 -0
  36. pyxsd/element_representatives/min_exclusive.py +17 -0
  37. pyxsd/element_representatives/min_inclusive.py +17 -0
  38. pyxsd/element_representatives/min_length.py +17 -0
  39. pyxsd/element_representatives/notation.py +94 -0
  40. pyxsd/element_representatives/pattern.py +19 -0
  41. pyxsd/element_representatives/restriction.py +181 -0
  42. pyxsd/element_representatives/schema.py +133 -0
  43. pyxsd/element_representatives/sequence.py +47 -0
  44. pyxsd/element_representatives/simple_content.py +59 -0
  45. pyxsd/element_representatives/simple_type.py +79 -0
  46. pyxsd/element_representatives/total_digits.py +17 -0
  47. pyxsd/element_representatives/union.py +113 -0
  48. pyxsd/element_representatives/white_space.py +17 -0
  49. pyxsd/element_representatives/xsd_type.py +1255 -0
  50. pyxsd/exceptions.py +45 -0
  51. pyxsd/facets.py +1229 -0
  52. pyxsd/identity.py +1093 -0
  53. pyxsd/instance_binding.py +619 -0
  54. pyxsd/namespaces.py +194 -0
  55. pyxsd/nodes.py +39 -0
  56. pyxsd/open_content.py +706 -0
  57. pyxsd/particle_derivation.py +2007 -0
  58. pyxsd/regex_charset.py +176 -0
  59. pyxsd/schema.py +667 -0
  60. pyxsd/schema_base.py +2536 -0
  61. pyxsd/schema_checks.py +3528 -0
  62. pyxsd/schema_composition.py +1701 -0
  63. pyxsd/schema_context.py +160 -0
  64. pyxsd/schema_hints.py +164 -0
  65. pyxsd/transforms/__init__.py +18 -0
  66. pyxsd/transforms/displayer.py +40 -0
  67. pyxsd/transforms/print_data.py +27 -0
  68. pyxsd/transforms/to_dict.py +26 -0
  69. pyxsd/transforms/transform.py +215 -0
  70. pyxsd/tree.py +37 -0
  71. pyxsd/upa.py +322 -0
  72. pyxsd/validation.py +237 -0
  73. pyxsd/version_gates.py +103 -0
  74. pyxsd/versioning.py +268 -0
  75. pyxsd/wildcards.py +1120 -0
  76. pyxsd/writers/__init__.py +4 -0
  77. pyxsd/writers/xml_tag_writer.py +191 -0
  78. pyxsd/writers/xml_tree_writer.py +258 -0
  79. pyxsd/xpath_api.py +93 -0
  80. pyxsd/xpath_assertions.py +571 -0
  81. pyxsd/xpath_subset.py +227 -0
  82. pyxsd/xsd_data_types.py +1351 -0
  83. pyxsd/xsi.py +144 -0
  84. pyxsd-1.0.0.dist-info/METADATA +156 -0
  85. pyxsd-1.0.0.dist-info/RECORD +88 -0
  86. pyxsd-1.0.0.dist-info/WHEEL +4 -0
  87. pyxsd-1.0.0.dist-info/entry_points.txt +2 -0
  88. pyxsd-1.0.0.dist-info/licenses/LICENSE +29 -0
pyxsd/__init__.py ADDED
@@ -0,0 +1,74 @@
1
+ """pyxsd: schema-guided XML-to-Python object mapping with a transform pipeline."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ __version__ = "1.0.0"
6
+
7
+ __all__ = [
8
+ "BindingPolicy",
9
+ "Document",
10
+ "NamespaceError",
11
+ "ParseModes",
12
+ "PyXSDError",
13
+ "PyXSDWarning",
14
+ "Schema",
15
+ "ValidationError",
16
+ "XMLNode",
17
+ "XPathError",
18
+ "__version__",
19
+ "compile",
20
+ "parse",
21
+ ]
22
+
23
+ if TYPE_CHECKING:
24
+ from pyxsd.binding import BindingPolicy, ParseModes
25
+ from pyxsd.document import Document
26
+ from pyxsd.exceptions import (
27
+ NamespaceError,
28
+ PyXSDError,
29
+ PyXSDWarning,
30
+ ValidationError,
31
+ XPathError,
32
+ )
33
+ from pyxsd.nodes import XMLNode
34
+ from pyxsd.schema import Schema, compile, parse
35
+
36
+
37
+ def __getattr__(name: str):
38
+ # Lazy imports so that ``import pyxsd`` does not pull the parser
39
+ # stack (and the ER tag registry) unless the API is actually used.
40
+ if name in ("BindingPolicy", "ParseModes"):
41
+ from pyxsd import binding
42
+
43
+ return getattr(binding, name)
44
+ if name == "Document":
45
+ from pyxsd.document import Document
46
+
47
+ return Document
48
+ if name in (
49
+ "NamespaceError",
50
+ "PyXSDError",
51
+ "PyXSDWarning",
52
+ "ValidationError",
53
+ "XPathError",
54
+ ):
55
+ from pyxsd import exceptions
56
+
57
+ return getattr(exceptions, name)
58
+ if name == "Schema":
59
+ from pyxsd.schema import Schema
60
+
61
+ return Schema
62
+ if name == "XMLNode":
63
+ from pyxsd.nodes import XMLNode
64
+
65
+ return XMLNode
66
+ if name == "compile":
67
+ from pyxsd.schema import compile
68
+
69
+ return compile
70
+ if name == "parse":
71
+ from pyxsd.schema import parse
72
+
73
+ return parse
74
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
pyxsd/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Command-line entry point for pyxsd."""
2
+
3
+ from pyxsd.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
pyxsd/alternatives.py ADDED
@@ -0,0 +1,516 @@
1
+ """XSD 1.1 ``xs:alternative`` type alternatives (conditional type assignment).
2
+
3
+ An element declaration may carry ``xs:alternative`` children that
4
+ associate an XPath test with a type. The alternatives are evaluated in
5
+ declaration order at instance phase, and the first whose test is true
6
+ supplies the element's governing type (XSD 1.1 §3.3.4.1, §3.12). This
7
+ module owns the schema-phase half:
8
+
9
+ * :class:`Alternative`, one compiled type alternative (its ``test``, the
10
+ parsed expression, the declaration-site namespace bindings and the
11
+ resolved type);
12
+ * :class:`AlternativeER`, the element representative for the
13
+ ``xs:alternative`` tag, so the declaration walk can see it and collect
14
+ it on the owning element in declaration order;
15
+ * :func:`compile_alternatives`, the schema-phase step that parses every
16
+ ``test`` with the CTA XPath subset and reports ``alternative-invalid``;
17
+ * :func:`check_element_alternatives`, the pass that runs once generated
18
+ classes exist: it resolves each alternative's type and enforces the
19
+ XSD 1.1 conditional-type-assignment legality rule that each type must
20
+ be validly derived from the element's declared type (``xs:error`` and
21
+ the ``xs:anyType`` ur-type are exempt, XSD 1.1 §3.3.6.1 clause 7).
22
+
23
+ There is deliberately no "required derivation ordering": a later
24
+ alternative's type may be validly derived from an earlier one's. XSD 1.1
25
+ §3.3.2.1/§3.12 impose no such rule, and the corpus
26
+ (IBM ``S3_12/s3_12v08``) declares exactly that broad-then-narrow shape
27
+ as valid.
28
+
29
+ The instance-phase selection (first true test's type governs, with
30
+ ``xsi:type`` taking precedence) is layered on top of the ordered
31
+ :class:`Alternative` list this module collects.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from typing import Any
37
+
38
+ from pyxsd.derivation import is_validly_derived
39
+ from pyxsd.element_representatives.element_representative import ElementRepresentative
40
+ from pyxsd.element_representatives.identity import _DeclarationSite
41
+ from pyxsd.namespaces import XSD_NS, clark, local_name, namespace_of
42
+ from pyxsd.xpath_assertions import CompiledXPath, evaluate, parse_cta_xpath
43
+ from pyxsd.xpath_subset import XPathError
44
+
45
+ __all__ = [
46
+ "ERROR_TYPE",
47
+ "Alternative",
48
+ "AlternativeER",
49
+ "check_element_alternatives",
50
+ "compile_alternatives",
51
+ "select_alternative_type",
52
+ ]
53
+
54
+
55
+ class _ErrorTypeSentinel:
56
+ """Sentinel for a selected alternative whose type is ``xs:error``.
57
+
58
+ ``xs:error`` has an empty value space, so an element it governs can
59
+ never be valid; the instance phase must be able to tell that apart
60
+ from "no alternative matched" (where the declared type governs).
61
+ """
62
+
63
+ __slots__ = ()
64
+
65
+ def __repr__(self) -> str: # pragma: no cover - debugging aid
66
+ return "xs:error"
67
+
68
+
69
+ #: Returned by :func:`select_alternative_type` when the selected
70
+ #: alternative's type is ``xs:error`` (never valid).
71
+ ERROR_TYPE = _ErrorTypeSentinel()
72
+
73
+ #: Sentinel marking an ``AlternativeER`` whose ``test`` has not been
74
+ #: compiled yet (a test-free alternative legitimately compiles to ``None``).
75
+ _UNSET = object()
76
+
77
+ #: The ``xs:error`` type definition, which an alternative may name even
78
+ #: though it is not derived from the declared type (XSD 1.1 §3.3.6.1
79
+ #: clause 7.2).
80
+ _XSD_ERROR = clark(XSD_NS, "error")
81
+
82
+
83
+ class Alternative:
84
+ """A compiled ``xs:alternative`` (a type alternative component).
85
+
86
+ Attributes are plain slots so the instance phase can read the
87
+ ordered table off the element (``Element.compiledAlternatives``)
88
+ without re-parsing anything.
89
+ """
90
+
91
+ __slots__ = (
92
+ "compiled",
93
+ "er",
94
+ "inline_type",
95
+ "is_error",
96
+ "namespaces",
97
+ "resolved_name",
98
+ "test",
99
+ "type_class",
100
+ "type_name",
101
+ "xpath_default_namespace",
102
+ )
103
+
104
+ def __init__(
105
+ self,
106
+ *,
107
+ er: Any,
108
+ test: str | None,
109
+ compiled: CompiledXPath | None,
110
+ namespaces: dict[str, str],
111
+ xpath_default_namespace: str | None,
112
+ type_name: str | None,
113
+ inline_type: Any,
114
+ ) -> None:
115
+ self.er = er
116
+ self.test = test
117
+ self.compiled = compiled
118
+ self.namespaces = namespaces
119
+ self.xpath_default_namespace = xpath_default_namespace
120
+ self.type_name = type_name
121
+ self.inline_type = inline_type
122
+ #: The type reference resolved to a Clark name (filled by
123
+ #: :func:`check_element_alternatives`).
124
+ self.resolved_name: str | None = None
125
+ #: The Python class standing for the alternative's type.
126
+ self.type_class: type | None = None
127
+ #: Whether the type is ``xs:error`` (legal, always admissible).
128
+ self.is_error: bool = False
129
+
130
+
131
+ class AlternativeER(_DeclarationSite, ElementRepresentative):
132
+ """The element representative for the ``xs:alternative`` tag.
133
+
134
+ ``xs:alternative`` appears inside an element declaration, after the
135
+ inline type (if any) and before the identity constraints. It carries
136
+ an optional XPath 2.0 ``test`` and exactly one of a ``type``
137
+ attribute or an inline ``simpleType``/``complexType``. The
138
+ ``_DeclarationSite`` mixin resolves the declaration-site prefix
139
+ bindings and ``xpathDefaultNamespace`` for the test.
140
+ """
141
+
142
+ #: Only an annotation and (at most) one inline type may appear.
143
+ _ALLOWED_CHILDREN = ("annotation", "simpleType", "complexType")
144
+ _MAX_ONE_CHILDREN = ("annotation", "simpleType", "complexType")
145
+ _CHILD_ORDER = (("annotation",), ("simpleType", "complexType"))
146
+ #: The inline type slot holds mutually exclusive alternatives.
147
+ _ONE_OF_SLOTS = frozenset({1})
148
+
149
+ #: Unqualified attributes the XML representation allows; foreign
150
+ #: namespace attributes are always admissible.
151
+ _ALLOWED_ATTRIBUTES = ("id", "test", "type", "xpathDefaultNamespace")
152
+
153
+ def __init__(self, xsdElement, parent) -> None:
154
+ self.test = xsdElement.get("test")
155
+ self.type_name = xsdElement.get("type")
156
+ # Sentinel: a compiled alternative may legitimately hold a
157
+ # ``None`` expression (a test-free default alternative).
158
+ self._compiled: Any = _UNSET
159
+ super().__init__(xsdElement, parent)
160
+ # The inline type (if any) is built on demand: the derived-type
161
+ # check and the instance-phase selection need it. Building every
162
+ # alternative's inline type eagerly in the global class-building
163
+ # loop would surface an inline type whose base is not yet
164
+ # implemented as a schema error even when the element declares
165
+ # the ur-type and the alternative is never selected.
166
+ inline_type = self._inline_type()
167
+ if inline_type is not None:
168
+ inline_type._alternativeInline = True
169
+ # Record on the owning element in declaration order. The element
170
+ # allocates ``alternatives`` before its children are factored.
171
+ container = self.parent
172
+ alternatives = getattr(container, "alternatives", None)
173
+ if alternatives is not None:
174
+ alternatives.append(self)
175
+
176
+ def getName(self) -> str:
177
+ """Returns a bookkeeping name for the alternative."""
178
+ return f"{self.getContainingTypeName()}|alternative"
179
+
180
+ def _inline_type(self) -> Any:
181
+ """Returns the alternative's inline type representative, if any."""
182
+ for child in getattr(self, "processedChildren", None) or ():
183
+ if child is not None and type(child).__name__ in ("SimpleType", "ComplexType"):
184
+ return child
185
+ return None
186
+
187
+ def checkDeclarationLegality(self) -> None:
188
+ """Reports the alternative's XML-representation constraints.
189
+
190
+ Covers the allowed attribute set, the requirement that exactly
191
+ one of a ``type`` attribute / inline type is present (XSD 1.1
192
+ §3.12.3), and the compilation of the ``test`` (an out-of-subset
193
+ or statically invalid expression is ``alternative-invalid``).
194
+ """
195
+ allowed = frozenset(self._ALLOWED_ATTRIBUTES)
196
+ for raw in self.xsdElement.attrib:
197
+ if raw.startswith("{"):
198
+ continue
199
+ if raw not in allowed:
200
+ self._reportSchemaError(
201
+ f"<alternative> does not allow the '{raw}' attribute",
202
+ code="declaration-attribute",
203
+ )
204
+ inline = self._inline_type()
205
+ supplied = (1 if self.type_name is not None else 0) + (1 if inline is not None else 0)
206
+ if supplied != 1:
207
+ self._reportSchemaError(
208
+ "<alternative> must carry exactly one of a 'type' attribute, "
209
+ "a simpleType child or a complexType child",
210
+ code="alternative-invalid",
211
+ )
212
+ self.compile()
213
+
214
+ def compile(self) -> Alternative | None:
215
+ """Parses the ``test`` once, reporting ``alternative-invalid``.
216
+
217
+ Returns the compiled :class:`Alternative`; idempotent, because the
218
+ declaration sweep and the derivation pass may both consult it.
219
+ A test-free alternative is legal (it becomes the default) and
220
+ compiles to an :class:`Alternative` with ``compiled=None``. An
221
+ empty or unusable test still returns an ``Alternative`` (so the
222
+ type can be checked) after reporting, so the single report is not
223
+ lost.
224
+ """
225
+ if self._compiled is not _UNSET:
226
+ return self._compiled
227
+ namespaces: dict[str, str] = {}
228
+ default_namespace: str | None = None
229
+ compiled: CompiledXPath | None = None
230
+ if self.test is not None:
231
+ text = self.test.strip()
232
+ try:
233
+ namespaces = dict(self._declarationNamespaces() or {})
234
+ default_namespace = self._xpathDefaultNamespace()
235
+ compiled = parse_cta_xpath(
236
+ text,
237
+ namespaces,
238
+ default_namespace=default_namespace,
239
+ )
240
+ except XPathError as exc:
241
+ self._reportSchemaError(
242
+ f"<alternative> test {text!r} is outside the "
243
+ f"conditional-type-assignment XPath subset: {exc}",
244
+ code="alternative-invalid",
245
+ )
246
+ else:
247
+ try:
248
+ namespaces = dict(self._declarationNamespaces() or {})
249
+ default_namespace = self._xpathDefaultNamespace()
250
+ except XPathError:
251
+ namespaces = {}
252
+ default_namespace = None
253
+ self._compiled = Alternative(
254
+ er=self,
255
+ test=self.test,
256
+ compiled=compiled,
257
+ namespaces=namespaces,
258
+ xpath_default_namespace=default_namespace,
259
+ type_name=self.type_name,
260
+ inline_type=self._inline_type(),
261
+ )
262
+ return self._compiled
263
+
264
+ def resolveTypeClass(self, host: Any) -> type | None:
265
+ """Resolves the alternative's type to a Python class.
266
+
267
+ An inline type builds its class directly; a ``type`` attribute
268
+ resolves through the declaration-site namespace context. A
269
+ reference to ``xs:error`` sets :attr:`Alternative.is_error` and
270
+ returns ``None``. An unresolved reference returns ``None``.
271
+ """
272
+ if self._compiled is _UNSET:
273
+ self.compile()
274
+ alternative: Alternative = self._compiled
275
+ inline = alternative.inline_type
276
+ if inline is not None and alternative.type_name is not None:
277
+ # Ambiguous: both a type reference and an inline type are
278
+ # present. The representation check already reported it;
279
+ # deriving from either would add a misleading second issue.
280
+ return None
281
+ if inline is not None:
282
+ try:
283
+ alternative.type_class = inline.clsFor(host)
284
+ except Exception:
285
+ alternative.type_class = None
286
+ return alternative.type_class
287
+ raw = alternative.type_name
288
+ if raw is None:
289
+ return None
290
+ resolved = self.resolveSchemaQName(raw, parser=host)
291
+ alternative.resolved_name = resolved
292
+ if _is_error_name(resolved):
293
+ alternative.is_error = True
294
+ return None
295
+ # A silent lookup: an unresolved alternative type is reported
296
+ # by the derivation pass (``alternative-invalid``), not here.
297
+ alternative.type_class = ElementRepresentative.typeFromName(resolved, host, warn=False)
298
+ return alternative.type_class
299
+
300
+
301
+ def _is_error_name(resolved: Any) -> bool:
302
+ """Whether *resolved* names ``xs:error`` (Clark or lexical form)."""
303
+ if not isinstance(resolved, str):
304
+ return False
305
+ if namespace_of(resolved) == XSD_NS and local_name(resolved) == "error":
306
+ return True
307
+ return resolved in ("xs:error", "xsd:error")
308
+
309
+
310
+ def compile_alternatives(element: Any) -> list[Alternative]:
311
+ """Compiles every alternative of *element* in declaration order.
312
+
313
+ Returns the compiled alternatives (a test-free default alternative is
314
+ included). Idempotent per element: the declaration sweep and the
315
+ derivation pass share one compile, so an unusable test is reported
316
+ once. Never raises.
317
+ """
318
+ cached = getattr(element, "compiledAlternatives", None)
319
+ if cached is not None:
320
+ return cached
321
+ compiled: list[Alternative] = []
322
+ for representative in getattr(element, "alternatives", None) or ():
323
+ result = representative.compile()
324
+ if result is not None:
325
+ compiled.append(result)
326
+ element.compiledAlternatives = compiled
327
+ return compiled
328
+
329
+
330
+ def _alternatives_owner(descriptor: Any) -> Any:
331
+ """Returns the declaration that owns an element's alternatives.
332
+
333
+ A ``ref`` site delegates to the global declaration it resolves to
334
+ (the alternatives live on the global element's representative); any
335
+ other descriptor owns its own list.
336
+ """
337
+ if descriptor is None:
338
+ return None
339
+ if getattr(descriptor, "isElementRef", False):
340
+ return getattr(descriptor, "referredElement", None) or descriptor
341
+ return descriptor
342
+
343
+
344
+ def _alternative_usable(alternative: Alternative, host: Any) -> bool:
345
+ """Whether a selected alternative's type can actually be built.
346
+
347
+ A named type or an inline type whose base cannot be resolved (for
348
+ example an alternative based on a not-yet-implemented built-in such
349
+ as ``xs:dateTimeStamp``) must not be selected: building it would
350
+ surface a spurious type error and false-reject an instance the
351
+ declared type accepts. The declaration check reports the underlying
352
+ schema problem; selection falls back to the declared type, matching
353
+ the lax-validator doctrine. This is checked without building the
354
+ class, so no report issue is produced here.
355
+ """
356
+ if alternative.is_error or alternative.type_class is not None:
357
+ return True
358
+ if alternative.type_name is not None and alternative.inline_type is None:
359
+ resolved = alternative.er.resolveSchemaQName(alternative.type_name, parser=host)
360
+ return ElementRepresentative.typeFromName(resolved, host, warn=False) is not None
361
+ inline = alternative.inline_type
362
+ if inline is None:
363
+ return False
364
+ for raw in getattr(inline, "superClassNames", ()) or ():
365
+ resolved = inline.resolveSchemaQName(raw, parser=host)
366
+ if ElementRepresentative.typeFromName(resolved, host, warn=False) is None:
367
+ return False
368
+ return True
369
+
370
+
371
+ def _selected_class(alternative: Alternative, host: Any) -> Any:
372
+ """Returns the selected alternative's governing type.
373
+
374
+ ``ERROR_TYPE`` for ``xs:error``; otherwise the resolved Python class
375
+ or ``None`` when it cannot be resolved (the class is resolved lazily
376
+ for an element whose declared type is the ur-type, where the schema
377
+ phase deliberately skips resolution).
378
+ """
379
+ if alternative.is_error:
380
+ return ERROR_TYPE
381
+ try:
382
+ usable = _alternative_usable(alternative, host)
383
+ except Exception:
384
+ usable = True
385
+ if not usable:
386
+ return None
387
+ if alternative.type_class is None and host is not None:
388
+ try:
389
+ alternative.er.resolveTypeClass(host)
390
+ except Exception:
391
+ return None
392
+ return alternative.type_class
393
+
394
+
395
+ def select_alternative_type(descriptor: Any, node: Any, host: Any) -> Any:
396
+ """Selects the conditional type assignment governing *node*.
397
+
398
+ Evaluates the declaration's alternatives in declaration order against
399
+ the element's attribute context and returns the governing type class
400
+ of the first alternative whose ``test`` is true (or of the test-free
401
+ default alternative). A test whose evaluation raises a dynamic error
402
+ is treated as false and the next alternative is tried (XSD 1.1
403
+ §3.12.6; Saxon CTA cta0016). Returns:
404
+
405
+ * ``None`` when the declaration carries no alternatives or no test
406
+ matches and there is no default — the caller keeps the declared
407
+ type;
408
+ * :data:`ERROR_TYPE` when the selected alternative names ``xs:error``;
409
+ * the selected type's Python class otherwise.
410
+
411
+ ``xsi:type`` precedence is the caller's responsibility: do not call
412
+ this when an ``xsi:type`` is present (XSD 1.1 §3.3.4.1).
413
+ """
414
+ owner = _alternatives_owner(descriptor)
415
+ alternatives = getattr(owner, "compiledAlternatives", None) if owner is not None else None
416
+ if not alternatives:
417
+ return None
418
+ for alternative in alternatives:
419
+ if alternative.test is None:
420
+ # Only the final alternative may omit its test; it is the
421
+ # default type (XSD 1.1 §3.3.2.1).
422
+ return _selected_class(alternative, host)
423
+ compiled = alternative.compiled
424
+ if compiled is None:
425
+ # The schema phase already reported the unusable test; it can
426
+ # never be true, so treat it as false and keep going.
427
+ continue
428
+ try:
429
+ result = evaluate(compiled, node)
430
+ except XPathError:
431
+ continue
432
+ if bool(result):
433
+ return _selected_class(alternative, host)
434
+ return None
435
+
436
+
437
+ def _is_ur_type(declared: Any) -> bool:
438
+ """Whether *declared* is ``xs:anyType`` (or its stand-in)."""
439
+ from pyxsd import xsd_data_types
440
+ from pyxsd.schema_base import SchemaBase
441
+
442
+ return declared is SchemaBase or declared is xsd_data_types.AnyType
443
+
444
+
445
+ def check_element_alternatives(element: Any) -> None:
446
+ """Enforces the CTA legality rules for *element*'s alternatives.
447
+
448
+ Runs after generated classes exist. For every alternative whose type
449
+ resolves, the type must be validly derived from the element's
450
+ declared type (``xs:error`` and the ``xs:anyType`` ur-type are
451
+ exempt, XSD 1.1 §3.3.6.1 clause 7). An unresolvable type is reported
452
+ ``alternative-invalid``.
453
+
454
+ Note: a later alternative's type *may* be validly derived from an
455
+ earlier alternative's type. The corpus (IBM ``S3_12/s3_12v08``)
456
+ declares a broad first alternative followed by narrower ones, which
457
+ is legal; there is no "required derivation ordering" rule in XSD 1.1
458
+ §3.3.2.1/§3.12, so none is enforced.
459
+ """
460
+ alternatives = getattr(element, "compiledAlternatives", None)
461
+ if not alternatives:
462
+ return
463
+ try:
464
+ declared = element.getType()
465
+ except Exception:
466
+ declared = None
467
+ if declared is None or _is_ur_type(declared):
468
+ # The declared type is the ur-type: every type is validly derived
469
+ # from it, so there is nothing to enforce. Resolving the
470
+ # alternatives here would force their inline types to build,
471
+ # surfacing unrelated gaps (for example a not-yet-implemented
472
+ # built-in base) as schema errors; skip it.
473
+ return
474
+ host = getattr(element, "host", None) or getattr(element.getSchema(), "host", None)
475
+ if host is None:
476
+ return
477
+
478
+ for alternative in alternatives:
479
+ alternative.er.resolveTypeClass(host)
480
+ if alternative.is_error:
481
+ continue
482
+ type_class = alternative.type_class
483
+ if type_class is None:
484
+ # An inline type build failure has already been reported by
485
+ # the class builder, and a missing type by the representation
486
+ # check. Only an unresolved *named* type is reported here so
487
+ # the alternative does not silently pass.
488
+ if alternative.type_name is not None and alternative.inline_type is None:
489
+ element._reportSchemaError(
490
+ f"the type '{alternative.type_name}' of a type alternative "
491
+ f"of element '{element.name}' could not be resolved",
492
+ code="alternative-invalid",
493
+ )
494
+ continue
495
+ reason = is_validly_derived(type_class, declared)
496
+ if reason is not None:
497
+ element._reportSchemaError(
498
+ f"the type '{alternative.type_name or _inline_label(alternative)}' "
499
+ f"of a type alternative of element '{element.name}' is not "
500
+ f"validly derived from the declared type "
501
+ f"'{_class_label(declared)}'",
502
+ code="alternative-invalid",
503
+ )
504
+
505
+
506
+ def _inline_label(alternative: Alternative) -> str:
507
+ """A readable label for an inline-typed alternative."""
508
+ return "<inline type>"
509
+
510
+
511
+ def _class_label(cls: Any) -> str:
512
+ """A readable label for a resolved type class."""
513
+ name = cls.__dict__.get("name")
514
+ if isinstance(name, str) and name:
515
+ return name
516
+ return getattr(cls, "__name__", str(cls))