Skip to content

Generating Python

Write a layout back out as the script that would build it. Useful when the layout already exists and the thing you are short of is source: open a .tosc someone else made, and read it as code.

to_python

to_python(
    target: Document | Control, *, variable: str = "doc"
) -> str

Write the Python that rebuilds a layout.

Parameters:

Name Type Description Default
target Document | Control

A Document or a Control to rebuild.

required
variable str

The name to bind the finished document to.

'doc'

Returns:

Type Description
str

A module's worth of source. Running it produces the same layout, with

str

fresh node ids and any type defaults the original file omitted.

Source code in src/py2tosc/codegen.py
def to_python(target: Document | Control, *, variable: str = "doc") -> str:
    """Write the Python that rebuilds a layout.

    Args:
        target: A [`Document`][py2tosc.Document] or a
            [`Control`][py2tosc.Control] to rebuild.
        variable: The name to bind the finished document to.

    Returns:
        A module's worth of source. Running it produces the same layout, with
        fresh node ids and any type defaults the original file omitted.
    """
    root = target if isinstance(target, Control) else target.root
    version = None if isinstance(target, Control) else target.version

    names: dict[str, str] = {}
    taken: set[str] = set()
    body: list[str] = []
    wiring: list[str] = []

    def emit(control: Control, parent: str | None) -> None:
        variable_name = _identifier(control, taken)
        names[control.id] = variable_name
        body.extend(_control_source(control, variable_name))
        if parent is not None:
            body.append(f"{parent}.add({variable_name})")
        body.append("")
        for child in control.children:
            emit(child, variable_name)

    emit(root, None)

    # Local bindings come last: they name their destination, which may be
    # anywhere in the tree, including somewhere not yet built.
    for control in root.walk():
        if not _defers(control):
            continue
        for message in control.messages:
            source = _dataclass_source(message)
            if isinstance(message, LocalMessage):
                target_name = names.get(message.dst_id)
                if target_name is not None:
                    source = source.replace(
                        f"dst_id={message.dst_id!r}", f"dst_id={target_name}.id"
                    )
            wiring.append(f"{names[control.id]}.messages.append({source})")

    header = [
        '"""Generated by py2tosc. Edit freely: this is ordinary Python."""',
        "",
        "import py2tosc",
        "from py2tosc import (",
        "    Color,",
        "    GamepadMessage,",
        "    LocalMessage,",
        "    MidiCommand,",
        "    MidiMessage,",
        "    MidiValue,",
        "    OscMessage,",
        "    Partial,",
        "    Trigger,",
        "    Value,",
        ")",
        "",
        "",
    ]
    lines = header + body
    if wiring:
        lines += [
            "# every binding that addresses another control by identity",
            *wiring,
            "",
        ]
    if version is not None:
        lines.append(
            f"{variable} = py2tosc.Document(root={names[root.id]}, version={version!r})"
        )
    else:
        lines.append(f"{variable} = {names[root.id]}")
    return "\n".join(lines) + "\n"