Source-to-source transforms

Everything so far has instrumented code in order to run it. But the same machinery can hand you the rewritten source without executing anything — which is what a linter, a formatter, a source-map generator, or a compile-to-plain Python tool needs. pyccolo.transform() desugars, and pyccolo.untransform() resugars.

Desugar and resugar

Under a tracer that defines syntax augmentation, pyc.transform returns the instrumented / desugared source as a string, and pyc.untransform reverses an augmentation — turning valid Python back into the augmented surface syntax. Using the shipped optional-chaining example, a?.b?.c desugars to plain a.b.c and back:

import pyccolo as pyc
from pyccolo.examples.optional_chaining import ScriptOptionalChainer as OC

with OC:
    assert pyc.transform("y = a?.b?.c") == "y = a.b.c"

    tree = pyc.parse("y = a?.b?.c", instrument=False)
    assert pyc.untransform(tree) == "y = a?.b?.c"

pyc.parse(code, instrument=False) gives you the augmented syntax parsed into an AST without adding instrumentation, which is the right input to untransform.

Analysis-only (pure) transforms

Some rewrites keep bookkeeping state as a side effect of running — fine when you are about to execute the result, wrong when you only want to inspect it. Pass pure=True to mark a transform as analysis-only, so cooperating rewrites know not to disturb execution-relevant state:

with OC:
    assert pyc.transform("y = a?.b", pure=True) == "y = a.b"

A rewrite can consult pyccolo.is_pure_transform() to branch on this. It is thread- and async-safe, backed by a ContextVar, so a pure transform on one thread won’t perturb a live tracer on another.

Source maps

For tooling that must map positions between the original and transformed source — underlining an error at the right column, say — both transform and untransform accept a positions=[(line, col), ...] argument and return the corresponding positions in the transformed (or untransformed) coordinate space alongside the source:

with OC:
    transformed, mapped = pyc.transform(
        "y = a?.b?.c", positions=[(1, 4)]
    )
    # `mapped` holds the (line, col) of that point in `transformed`

The precise signatures — including what mapped contains — are in Source-to-source API.