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.
- pyxsd/__init__.py +74 -0
- pyxsd/__main__.py +6 -0
- pyxsd/alternatives.py +516 -0
- pyxsd/assertions.py +592 -0
- pyxsd/binding.py +104 -0
- pyxsd/cli.py +653 -0
- pyxsd/compositors.py +17 -0
- pyxsd/content_model.py +1205 -0
- pyxsd/derivation.py +356 -0
- pyxsd/dict_export.py +95 -0
- pyxsd/document.py +237 -0
- pyxsd/element_representatives/__init__.py +37 -0
- pyxsd/element_representatives/all.py +198 -0
- pyxsd/element_representatives/annotation.py +39 -0
- pyxsd/element_representatives/any.py +53 -0
- pyxsd/element_representatives/any_attribute.py +48 -0
- pyxsd/element_representatives/attribute.py +661 -0
- pyxsd/element_representatives/attribute_group.py +158 -0
- pyxsd/element_representatives/choice.py +70 -0
- pyxsd/element_representatives/complex_content.py +37 -0
- pyxsd/element_representatives/complex_type.py +543 -0
- pyxsd/element_representatives/documentation.py +58 -0
- pyxsd/element_representatives/element.py +759 -0
- pyxsd/element_representatives/element_representative.py +1658 -0
- pyxsd/element_representatives/enumeration.py +19 -0
- pyxsd/element_representatives/explicit_timezone.py +18 -0
- pyxsd/element_representatives/extension.py +86 -0
- pyxsd/element_representatives/fraction_digits.py +17 -0
- pyxsd/element_representatives/group.py +270 -0
- pyxsd/element_representatives/identity.py +532 -0
- pyxsd/element_representatives/length.py +17 -0
- pyxsd/element_representatives/list.py +122 -0
- pyxsd/element_representatives/max_exclusive.py +17 -0
- pyxsd/element_representatives/max_inclusive.py +17 -0
- pyxsd/element_representatives/max_length.py +17 -0
- pyxsd/element_representatives/min_exclusive.py +17 -0
- pyxsd/element_representatives/min_inclusive.py +17 -0
- pyxsd/element_representatives/min_length.py +17 -0
- pyxsd/element_representatives/notation.py +94 -0
- pyxsd/element_representatives/pattern.py +19 -0
- pyxsd/element_representatives/restriction.py +181 -0
- pyxsd/element_representatives/schema.py +133 -0
- pyxsd/element_representatives/sequence.py +47 -0
- pyxsd/element_representatives/simple_content.py +59 -0
- pyxsd/element_representatives/simple_type.py +79 -0
- pyxsd/element_representatives/total_digits.py +17 -0
- pyxsd/element_representatives/union.py +113 -0
- pyxsd/element_representatives/white_space.py +17 -0
- pyxsd/element_representatives/xsd_type.py +1255 -0
- pyxsd/exceptions.py +45 -0
- pyxsd/facets.py +1229 -0
- pyxsd/identity.py +1093 -0
- pyxsd/instance_binding.py +619 -0
- pyxsd/namespaces.py +194 -0
- pyxsd/nodes.py +39 -0
- pyxsd/open_content.py +706 -0
- pyxsd/particle_derivation.py +2007 -0
- pyxsd/regex_charset.py +176 -0
- pyxsd/schema.py +667 -0
- pyxsd/schema_base.py +2536 -0
- pyxsd/schema_checks.py +3528 -0
- pyxsd/schema_composition.py +1701 -0
- pyxsd/schema_context.py +160 -0
- pyxsd/schema_hints.py +164 -0
- pyxsd/transforms/__init__.py +18 -0
- pyxsd/transforms/displayer.py +40 -0
- pyxsd/transforms/print_data.py +27 -0
- pyxsd/transforms/to_dict.py +26 -0
- pyxsd/transforms/transform.py +215 -0
- pyxsd/tree.py +37 -0
- pyxsd/upa.py +322 -0
- pyxsd/validation.py +237 -0
- pyxsd/version_gates.py +103 -0
- pyxsd/versioning.py +268 -0
- pyxsd/wildcards.py +1120 -0
- pyxsd/writers/__init__.py +4 -0
- pyxsd/writers/xml_tag_writer.py +191 -0
- pyxsd/writers/xml_tree_writer.py +258 -0
- pyxsd/xpath_api.py +93 -0
- pyxsd/xpath_assertions.py +571 -0
- pyxsd/xpath_subset.py +227 -0
- pyxsd/xsd_data_types.py +1351 -0
- pyxsd/xsi.py +144 -0
- pyxsd-1.0.0.dist-info/METADATA +156 -0
- pyxsd-1.0.0.dist-info/RECORD +88 -0
- pyxsd-1.0.0.dist-info/WHEEL +4 -0
- pyxsd-1.0.0.dist-info/entry_points.txt +2 -0
- 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
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))
|