Skip to content

Click Python Codegen

ladder_to_pyrung() and ladder_to_pyrung_project() convert Click ladder data back into executable pyrung Python source. This is the reverse of pyrung_to_ladder() — import from Click instead of export to Click.

Single-file codegen

ladder_to_pyrung() accepts a file path (to a CSV or directory) or a LadderBundle for in-memory round-trip without disk I/O.

from pyrung.click import ladder_to_pyrung

code = ladder_to_pyrung("main.csv")                    # from CSV file
code = ladder_to_pyrung("ladder_dir/")                  # from directory with subroutines/*.csv
code = ladder_to_pyrung(bundle)                         # from LadderBundle (no disk)
code = ladder_to_pyrung("main.csv", output_path="generated.py")  # write to file

Round-trip

from pyrung.click import pyrung_to_ladder, ladder_to_pyrung

bundle = pyrung_to_ladder(logic, mapping)
code = ladder_to_pyrung(bundle)          # no CSV files needed

Nickname substitution

Three ways to provide nicknames for readable variable names:

  1. nickname_csv= — path to a Click nickname CSV (Address.csv). Recommended, because it also enables structured type inference (see below).
  2. nicknames= — pre-parsed {operand: nickname} dict (e.g. {"X001": "start_button"}).
  3. Neither — raw operand names used as-is (X001, DS1, etc.).

Cannot provide both nickname_csv and nicknames.

code = ladder_to_pyrung("main.csv", nickname_csv="Address.csv")

code = ladder_to_pyrung("main.csv", nicknames={"X001": "start_button", "Y001": "motor"})

Structured type inference

When nickname_csv= is provided, codegen calls TagMap.from_nickname_file() internally. It reconstructs semantic metadata only from explicit markers such as :block, :udt, and :named_array(...). Bare tags remain grouping-only, so the generated code keeps them as flat tags or raw bank ranges instead of inventing pyrung structures.

Without nickname_csv, a named-array group comes back flat:

Channel1_Id = Int("Channel1_Id")
Channel1_Val = Int("Channel1_Val")
Channel2_Id = Int("Channel2_Id")
Channel2_Val = Int("Channel2_Val")

# in the program:
copy(Channel1_Id, Channel2_Val)

# in TagMap:
mapping = TagMap({
    Channel1_Id: ds[101],
    Channel1_Val: ds[102],
    ...
})

With nickname_csv= pointing to a CSV that has named-array markers:

@named_array(Int, count=2)
class Channel:
    Id = 0
    Val = 0

# in the program:
copy(Channel[1].Id, Channel[2].Val)

# in TagMap:
mapping = TagMap([
    *Channel.map_to(ds.select(101, 104)),
], include_system=False)

Named-array instance windows round-trip as whole-instance selects when the ladder uses an exact aligned span:

blockcopy(RecipeProfile.instance(2), WorkingRecipe.select(1, 3))
fill(0, RecipeProfile.instance_select(1, 2))

This works for both dense and sparse layouts — the range length must be an exact multiple of the stride and start at an instance boundary.

For UDTs (fields spanning different memory banks), per-field map_to is emitted:

@udt(count=2)
class Motor:
    Running: Bool = False
    Speed: Int = 0

mapping = TagMap([
    Motor.Running.map_to(c.select(101, 102)),
    Motor.Speed.map_to(ds.select(1001, 1002)),
], include_system=False)

Singleton structures (count=1) use dotted access without indexing: Config.Timeout, not Config[1].Timeout. If the CSV uses numbered names for a singleton (e.g. Config1_Timeout), the importer infers always_number=True and emits @named_array(Int, always_number=True).

For details on @named_array and @udt syntax, see the Tag Structures guide. For the CSV marker format, see CSV marker format.

What codegen infers

Tag types from operand prefixes (X→Bool, DS→Int, etc.), block ranges from DS100..DS102 notation, OR expansion via Or(), branch conditions, timer/counter pin chains, for/next loops, and comments.

For the CSV format that codegen reads, see the laddercodec CSV format guide.

Round-trip guarantee

The generated code is designed to round-trip: exec() the output, then pyrung_to_ladder(logic, mapping) reproduces the original CSV. This is tested extensively.

Multi-file project codegen

ladder_to_pyrung_project() generates a complete Python project instead of a single file. Each subroutine gets its own file with a @subroutine decorator, tags and the TagMap live in tags.py, and main.py ties everything together.

from pyrung.click import ladder_to_pyrung_project

files = ladder_to_pyrung_project("ladder_dir/")
files = ladder_to_pyrung_project("ladder_dir/", nickname_csv="Address.csv")
files = ladder_to_pyrung_project("ladder_dir/", output_dir="pump_project_py/")

The return value is a dict[str, str] mapping relative paths to content:

tags.py                  # tag declarations, structures, TagMap
main.py                  # Program context, main rungs, call() statements
subroutines/
  __init__.py
  startup.py             # @subroutine("startup") decorated function
  alarm_handler.py

How subroutines are represented

Each subroutine file defines a decorated function that auto-registers with the Program when called:

# subroutines/startup.py
from pyrung import Rung, subroutine, out
from tags import SubLight

@subroutine("startup")
def startup():
    with Rung():
        out(SubLight)

main.py imports and calls it by reference (not by string name):

from subroutines.startup import startup

with Program() as logic:
    with Rung(Button):
        call(startup)

Per-file imports

Each generated file imports only what it uses. A subroutine that touches X001 and Y001 won't import X002 or DS1. tags.py is the single source of truth for all tag declarations; other files import from it.

Nickname and structure support

Same as ladder_to_pyrung() — pass nickname_csv= for readable variable names and automatic @named_array / @udt inference, or nicknames= for a pre-parsed dict. tags.py suppresses the inline # X001 address comments since the TagMap and nickname CSV already provide that mapping.