Writing transforms

Writing a transform means writing a small Python class with a fixed shape. pyxsd loads transforms dynamically from just a name and a call string, so the shape matters.

The rules

Every transform must follow these rules:

  1. Give the entry-point class a matching module. CLI discovery matches the class name to the module name (case and underscores are normalized, but match exactly to be safe). One transform per file avoids ambiguity.

  2. Inherit from Transform (from pyxsd.transforms), directly or via a base class. Transform and Displayer are abstract — __init__ must be defined.

  3. __init__(self, rootInstance) takes the parsed root as its only positional argument.

  4. __call__(self, *args, **kwargs) does the work; its parameters are the arguments given in the transform call.

  5. Return the root instance from __call__ if you want the writer to serialize it (or the next transform to consume it).

Minimal example

from pyxsd.transforms import Transform, iter_tree


class UpperValues(Transform):
    """Uppercase all string values in the tree."""

    def __init__(self, rootInstance):
        super().__init__(rootInstance)

    def __call__(self):
        for node in iter_tree(self.root):
            if node._value_:
                node._value_ = [v.upper() for v in node._value_]
        return self.root

iter_tree(instance) is a generator yielding every node in pre-order, descending into lists, tuples and dicts; non-node objects are skipped.

A runnable in-memory example

The transform above can be exercised without touching the filesystem: compile the schema and parse the instance into a Document, run the transform through Document.transform, and revalidate the changed tree with Document.revalidate so its report can be inspected.

import io

from pyxsd.schema import Schema
from pyxsd.transforms import Transform, iter_tree

SCHEMA = """\
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="note">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="body" type="xs:string"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>
"""

INSTANCE = "<note><body>hello &amp; goodbye</body></note>"


class UpperValues(Transform):
    """Uppercase every string value in the tree."""

    def __init__(self, rootInstance):
        super().__init__(rootInstance)

    def __call__(self):
        for node in iter_tree(self.root):
            if node._value_:
                node._value_ = [value.upper() for value in node._value_]
        return self.root


def main():
    document = Schema.compile(io.StringIO(SCHEMA)).parse(io.StringIO(INSTANCE))

    print("body before:", document.root.body)

    transformed = document.transform(lambda root: UpperValues(root)())

    print(transformed.to_string().strip())

    # Revalidate the changed tree against the same schema; the new
    # document carries a fresh report for the tree's current shape.
    again = transformed.revalidate()
    print("revalidation errors:", [issue.code for issue in again.report.issues])


if __name__ == "__main__":
    main()

Returning self.root hands the tree to the next stage. Other values are returned unchanged; a final None produces no CLI transform output, but does not stop later calls in a chain. See Using transforms for library return semantics.

Using the visitor helpers

Transform.walk(instance, visitor, *args, **kwargs) traverses the tree and calls visitor(node, attrNames, elemNames, *args, **kwargs) on every node. Stock visitors (classCollector, tagCollector, tagFinder) power the getInstancesByClassName, getAllSubElements, and getElementsByName helpers; see The Transform class for the full API.

Making new nodes

Use makeElemObj(name) to mint a node with the correct _name_/_attribs_/_children_/_value_ structure, and makeCommentElem(text) for comments. Anything you assemble this way is writable by XmlTreeWriter. The node shape is documented in Data model.

Revalidating after structural changes

If your transform changes the tree’s shape so much that it no longer corresponds to the schema, call Document.revalidate() on the transformed document — it re-parses the serialized tree against the same schema and returns a fresh document whose report reflects the new structure:

again = transformed.revalidate()
print(again.report)

Distributing transforms

  • Keep your transform (and any helper library) in its own directory; pyxsd resolves sibling imports of transform modules automatically.

  • Name the entry-point module after the class.

  • License your work under terms of your choice (BSD/MIT recommended); only transforms contributed into the pyxsd package fall under pyxsd’s license.

  • Document what the transform expects the data to look like — the bundled examples each carry a module docstring you can use as a template.