Mixin extensions¶
The classes pyxsd generates are dynamic: they live in schema.classes,
built at compile time from the schema’s type declarations. Mixin
extensions fold your own classes into those generated classes at
compile time, so your methods ride on every bound instance of the
selected types — and a convention method can add validation findings to
the report without touching base XSD validation.
The motivating case is a method the schema cannot express. A generated
Patient class knows how to bind name and diagnosis; a mixin
teaches it to label itself:
from io import StringIO
from pyxsd import Extensions, Schema
ext = Extensions()
@ext.for_type("Patient")
class PatientMixin:
def label(self):
return f"{self.name} — {self.diagnosis}"
schema = Schema.compile(StringIO(XSD), extensions=ext)
document = schema.parse(StringIO(XML))
document.root.label() # a plain method, callable on the bound instance
Passing a registry to compile¶
An Extensions instance is a standalone,
per-compile registry — there is no global registry state. Register
mixins on it, then pass it to Schema.compile:
ext = Extensions()
ext.for_type("Code", mixin=CodeMixin)
schema = Schema.compile("schema.xsd", extensions=ext)
Registration is compile-time only: the registry is read while the classes are built. Mutating the registry after a compile leaves that compile’s classes untouched, and pyxsd never patches classes at runtime. Recompiling with the same registry rebuilds the classes with the mixins folded in (every compile produces fresh classes anyway). That includes path selectors: their resolution cache is per compile — keyed by the compile’s schema, weakly, so a reused registry re-resolves each path against the new declaration tree instead of answering with a stale predicate from the previous one.
Registering a mixin¶
Every registration method has two equivalent forms. Without a mixin
argument it returns a decorator that registers the decorated class:
@ext.for_type("Patient")
class PatientMixin: ...
With mixin= it registers directly and returns nothing, which suits
mixins that live elsewhere:
ext.for_type("Patient", mixin=PatientMixin)
The five selectors¶
Each selector compiles to a predicate over the schema’s type
declarations immediately, so an instance can be built before any schema
exists. A selector that matches nothing is silent — a schema
compiles fine when for_type("DoesNotExist", ...) matches nothing.
Only path selectors fail loudly (see Path selectors).
Selector |
Matches |
|---|---|
|
The type declaration named name — a plain local name ( |
|
Every type declared in namespace uri. |
|
Types of kind — |
|
The type of the element at an instance path from the document root down (see below). |
|
Every type declaration whose element representative satisfies predicate — the escape hatch for anything the named selectors cannot say. |
@ext.for_namespace("http://example.com/cda")
class CdaMixin: ...
@ext.for_kind("complexType", derived_from="Patient")
class PatientLikeMixin: ...
@ext.where(lambda er: er.name in ("Patient", "Dead"))
class LongNameMixin: ...
Path selectors¶
for_path names element declarations from the document root down,
every segment a local element name:
ext.for_path("/ClinicalDocument/recordTarget/patient", mixin=PatientMixin)
The final segment’s declaration contributes the selected type: its
inline anonymous type when one is declared, else the referenced global
type. Matching is by declaration identity, not by name — two same-named
local elements (/Root/a/shared and /Root/b/shared) select different
mixins even though their anonymous types collide in name.
Paths resolve at class-building time, once per compile, against the
composed declaration tree — so the registry can hold paths before the
schema is compiled, and a registry reused for another compile resolves
its paths again against that compile’s tree. A path that cannot resolve
fails the compile with PyXSDError, naming the offending path:
a segment that matches no element declaration in the traversal so far (a typo dangles loudly);
a path continuing into a child declared on the base of an extension-typed element — traversal stays within each element’s own content model and does not follow the base chain;
a final segment typed by a builtin (
type="xs:string"supplies no schema-declared type for a mixin to ride on), or an empty or ambiguous path.
Ordering and dedup¶
Registration order is precedence: a later registration outranks an earlier one. When several mixins match one type, they fold in with the most recently registered first, and that order is their position in the class’s MRO — on a member both mixins define, the later-registered one wins.
A mixin matched by several selectors appears once, at its highest-precedence position; dedup is by identity (two same-named distinct classes are two mixins).
ext.for_type("Code", mixin=SharedMixin)
ext.for_namespace("http://example.com/a", mixin=SharedMixin)
# SharedMixin is in the generated class's MRO exactly once.
Where mixins sit in the MRO¶
A generated class’s MRO runs: the generated class first, then the
mixins (highest precedence first), then the XSD derivation bases, then
SchemaBase. Two consequences, both
deliberate:
Mixin methods override inherited members. A mixin defining
kind()sits in front of the XSD base chain, andsuper().kind()reaches the base’s implementation — mixins compose with XSD derivation the way any Python class does.Mixins never shadow generated descriptors. The generated class is first in the MRO, so its own
__dict__— the attribute and element descriptors the binder routes through — always wins. A mixin that declaresid = "MIXIN"on a type with anidattribute does not break binding; the mixin’s member stays buried in its base and the descriptor keeps working.
Derived types see their bases’ mixins through the MRO: a mixin
registered for Patient is a base of the generated Inpatient too.
A union simple type’s class carries its mixins the same way: the
mixins sit between the union class and the union machinery
(XsdDataType), so a mixin’s methods are callable on the union’s
validated values and its hook runs on them, while the union’s own
member-trying construction stays in charge.
The __pyxsd_validate__ hook¶
A mixin may define the convention method __pyxsd_validate__(self),
returning findings that pyxsd appends to the instance report. The
contract, exactly as implemented:
The hook runs after base validation, on successfully constructed values only. It adds findings on top of the XSD report, never replaces it. A value that fails its XSD type (or is dropped, or is bound as a raw/unvalidated fallback under a lax parse mode) runs no hook.
Call sites: element instances (complex, simple-content, and bare simple-typed elements alike) and converted attribute values. A defaulted or fixed attribute value flows through the attribute descriptor too, so the hook fires for it as well.
Findings. The hook returns an iterable of findings (a bare
stror singleValidationIssuecounts as that one finding):a plain
strbecomes an error with codeextension-validation;a
ValidationIssuekeeps its own severity, code, and message — itsphaseis forced to"instance", andelementis filled from the declaring name (the element or attribute the value belongs to) when the issue carries none;any other finding type is recorded as one
extension-errornaming the unsupported type, and the rest of the findings are still processed.
Exception isolation. A hook that raises — or raises while its findings are being consumed — becomes exactly one
extension-errorissue; binding continues.Where findings land: the active parse’s
ValidationReport(Document.report). Outside a parse — for instance on a post-parse attribute write — findings are logged, like every other assignment-time diagnostic.
The hook costs nothing when absent: a type without
__pyxsd_validate__ pays one getattr per constructed value.
Example: a URI type with a method and a hook¶
A ServiceUri simple type restricting xs:anyURI — base validation
still owns the lexical space — carrying a plain fetch() method and a
hook that flags relative URIs as errors and plain http as a warning.
The mixin is registered once and applies to the type wherever it
appears: element text and attribute values.
from io import StringIO
from pyxsd import Extensions, Schema
from pyxsd.validation import IssueSeverity, ValidationIssue
SCHEMA = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:m="http://example.com/u" targetNamespace="http://example.com/u"
elementFormDefault="qualified">
<xs:simpleType name="ServiceUri">
<xs:restriction base="xs:anyURI">
<xs:pattern value="\\S*"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="Resource">
<xs:simpleContent>
<xs:extension base="m:ServiceUri">
<xs:attribute name="ref" type="m:ServiceUri"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:element name="resource" type="m:Resource"/>
</xs:schema>"""
ext = Extensions()
@ext.for_type("ServiceUri")
class UriMixin:
def fetch(self):
return f"fetched:{self}"
def __pyxsd_validate__(self):
parsed = self.to_parsed_uri()
findings = []
if not parsed.scheme and not parsed.netloc:
findings.append(f"the service URI {str(self)!r} has no host")
if parsed.scheme == "http":
findings.append(
ValidationIssue(
IssueSeverity.WARNING,
"insecure-scheme",
f"the service URI {str(self)!r} uses plain http",
)
)
return findings
schema = Schema.compile(StringIO(SCHEMA), extensions=ext)
document = schema.parse(
StringIO(
'<m:resource xmlns:m="http://example.com/u" ref="relative/path">https://host/x</m:resource>'
)
)
print(document.root.fetch()) # fetched:https://host/x
print(document.root.ref.fetch()) # fetched:relative/path
print(document.report.has_errors) # True — the ref value has no host
The report carries the hook’s error (extension-validation,
element="ref", phase instance) while base xs:anyURI validation
runs untouched. The https://host/x text produces no finding, and a
plain-http value would produce a warning with code
insecure-scheme instead.
What extensions are not¶
No post-compile extension. Registration is compile-time; there is no runtime class patching and no way to attach a mixin to classes a compile already built.
No overlay folding. The overlay mechanism (
Schema.compile(..., overlay=...), see Architecture) is untouched: overlays do not fold mixin members, and a mixin’s members are not overlay code.No builtin-type extension. Selectors match schema-declared type declarations only;
xs:stringand its siblings cannot carry a mixin.Stubs do not emit mixin members.
Schema.stub()describes the generated surface; mixin members are absent from emitted stubs, so code relying on autocomplete against a stub will not see your mixin’s methods.
See Validation for the report the hook feeds, Parse modes and binding
for the parse modes that decide what a failed value binds as, and
API reference for the Extensions reference.