# 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: ```python 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 {class}`~pyxsd.extensions.Extensions` instance is a standalone, per-compile registry — there is no global registry state. Register mixins on it, then pass it to `Schema.compile`: ```python 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: ```python @ext.for_type("Patient") class PatientMixin: ... ``` With `mixin=` it registers directly and returns nothing, which suits mixins that live elsewhere: ```python 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](#path-selectors)). | Selector | Matches | | -------- | ------- | | `for_type(name)` | The type declaration named *name* — a plain local name (`"Code"`) or a Clark name (`"{uri}Code"`). | | `for_namespace(uri)` | Every type declared in namespace *uri*. | | `for_kind(kind, derived_from=...)` | Types of *kind* — `simpleType` or `complexType` (case-insensitive). With *derived_from*, only types whose XSD base is the named type; the base name is matched by local spelling (a prefixed or Clark spelling contributes its local part). | | `for_path(path)` | The type of the element at an instance *path* from the document root down (see below). | | `where(predicate)` | Every type declaration whose element representative satisfies *predicate* — the escape hatch for anything the named selectors cannot say. | ```python @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: ```python 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). ```python 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 {class}`~pyxsd.schema_base.SchemaBase`. Two consequences, both deliberate: - **Mixin methods override inherited members.** A mixin defining `kind()` sits in front of the XSD base chain, and `super().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 declares `id = "MIXIN"` on a type with an `id` attribute 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 `str` or single `ValidationIssue` counts as that one finding): - a plain `str` becomes an **error** with code `extension-validation`; - a {class}`~pyxsd.validation.ValidationIssue` keeps its own severity, code, and message — its `phase` is forced to `"instance"`, and `element` is 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-error` naming 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-error` issue; binding continues. - **Where findings land:** the active parse's {class}`~pyxsd.validation.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. ```python from io import StringIO from pyxsd import Extensions, Schema from pyxsd.validation import IssueSeverity, ValidationIssue 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( 'https://host/x' ) ) 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 {doc}`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:string` and 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 {doc}`validation` for the report the hook feeds, {doc}`binding` for the parse modes that decide what a failed value binds as, and {doc}`api` for the `Extensions` reference.