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:
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.
Inherit from
Transform(frompyxsd.transforms), directly or via a base class.TransformandDisplayerare abstract —__init__must be defined.__init__(self, rootInstance)takes the parsed root as its only positional argument.__call__(self, *args, **kwargs)does the work; its parameters are the arguments given in the transform call.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 & 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.