Architecture¶
pyxsd’s core idea, unchanged since 2006: an XSD schema compiles into a set of Python classes, and an XML document compiles into an instance tree of those classes. Everything else — validation, transforms, the writers — operates on those two artifacts.
Pipeline¶
Schema compile.
Schema.compile()reads the schema (a file, path, orET.Element), resolvesxs:include/xs:import/xs:redefineby splicing the composed documents into the main tree before any class generation, and registers every top-level element as a descriptor in the element-representative registry. Dependencies are resolved against the including document and read through the active resource policy (see below).Class generation. Every schema component named in the registry generates a Python class:
Simple types resolve to one of the ~45 built-in types in
pyxsd.xsd_data_types(validation happens in__new__) or to a generated class derived throughxs:restriction/xs:list/xs:union.Complex types / elements generate subclasses of
SchemaBasethroughtypes.new_class, withElementandAttributedescriptors wired up via__set_name__and class metadata (_elementNames_,_attributeNames_) computed bySchemaBase.__init_subclass__.
The generated class tree mirrors the derivation tree in your schema:
xs:extensionproduces real subclassing, soisinstancechecks and inherited descriptors work the way you’d expect.Instance binding.
Schema.parse()parses the instance under the selected parser policy and limits, matches the document root against the global element declarations (honoringxsi:typedispatch and abstract-element rejection), then recursively builds instances. Text and attributes are validated against the declared types; undeclared children fall through wildcard (xs:any) declarations as generic nodes. Substitution-group members are accepted wherever their head is declared. Order/occurrence/identity checks run as post-passes, and the bound tree comes back as aDocumentcarrying the merged report.Transforms.
Document.transform()applies a callable — a plain function, or a user class derived frompyxsd.transforms.Transform(the CLI resolves-t 'Name(args)'strings to the same callables) — to the root instance. Transforms see a stable node shape (_name_,_attribs_,_children_,_value_; see Data model).Writers.
Document.write()serializes the (possibly transformed) tree withXmlTreeWriter, preserving document order and xsi declarations. Serialization is deterministic: the writer emits no generated timestamp, so writing the same tree twice produces the same bytes.writeHeaderInfo()is a no-op subclass hook, not a default header (#48).
Resource resolution and the parser boundary¶
Composition and instance loading share one reference model,
pyxsd.resource_refs. A source is classified as a local path, a <memory>
stream, or an absolute URI. resource_base() gives a document’s canonical
identity (resolved local path, remote URI, or <memory>), and
resolve_reference() resolves a dependency against the including document,
not its directory: a remote base follows RFC 3986 URI semantics, a local base
uses filesystem semantics, and a file: URI stays a remote reference that the
default loader refuses. The canonical base also drives composition-cycle
detection.
A ResourcePolicy sits between that model and the filesystem or network:
read() supplies original bytes plus a canonical source ID, and parse()
returns the ElementTree and records namespace bindings. The default loader is
local-only. RemoteResourcePolicy is the opt-in HTTPS implementation; it
authorizes each root, same-origin/cross-origin dependency, instance hint, and
redirect against finite per-compilation budgets. Python overlays are never
retrieved remotely.
There are two parser roles:
Semantic parsing.
parse_with_namespacesusesxml.etree.ElementTree.iterparsewith the selectedXmlParserPolicy. The default is the stdlib parser;protected()and.strict()switch to defusedxml and the shared Expat declaration guards.XmlLimitsbounds the bytes, element depth, and start-tag count. This tree is what binding, validation, transforms, XPath, and the writer use.Source indexing.
pyxsd.source_spans.SourceIndexruns a separate rawxml.parsers.expatpass over the original bytes to record element paths and exact half-open byte spans. It never installs an external-resource fetcher, builds no second tree, and applies the same declaration policy and, when supplied, structural limits. It rejects every entity reference it cannot expand, so it never accepts a document the semantic parser rejected. The MCP server uses it forresult: "subtree"so the returned bytes are an exact slice of the captured source rather than a serialization of the bound tree.
Neither parser role is a sandbox. Path confinement, authentication, and timeouts belong to the application policy or the surrounding process boundary; see XML security and untrusted input.
The ElementRepresentative system¶
While the schema is being processed, every xs:* tag (element,
complexType, sequence, attributeGroup, …) becomes an
ElementRepresentative (ER) — a lightweight object that collects the
schema’s own metadata (names, occurrence limits, type references, facets)
and links to its children ERs. ERs are the compiler front end: they
decide what class each component should get, resolve references (element
refs, group refs, substitution groups), and report schema-level errors.
The registry they populate is consulted during class generation and again
during instance binding (root matching, xsi:type resolution).
Key methods of the ER system:
ElementRepresentative.factory— creates the right ER class for a tag, recursively processing children.ElementRepresentative.registry— global map of component name → ERs.XsdType.clsFor— the class-generation entry point for a type.
Validation reporting¶
All non-fatal issues — from schema composition, instance validation, or
identity constraints — are recorded in a ValidationReport: the
schema’s own findings at Schema.report, and the merged run (schema
issues plus the document’s) at Document.report. Codes (e.g. order,
occurrence-min, identity-key) are stable strings suitable for
grep-based CI checks; see Validation.
Overlay classes (experimental)¶
-c file.py (or Schema.compile(..., overlay=...)) loads a user module
whose classes extend the generated ones by subclassing SchemaBase —
see pyxsd.schema.load_overlay_classes. This mechanism is unchanged in
spirit from 0.1 and remains experimental.