Examples
SVG-to-Turtle compiler
This example reads a small subset of SVG and compiles it into a Python
module that reproduces the drawing with the standard-library
turtle module. It demonstrates several fluent-codegen features:
Building a
Modulewith multiple functions.Using the E-objects system (
enames,.e) for natural-looking math and method calls.Automatic name deduplication — the same local name (
pos,heading) is used for each<use>element, and the scope manager appends suffixes automatically.
Supported SVG elements:
<line x1=… y1=… x2=… y2=…>— a straight line segment.<defs>containing<line>elements withidattributes — compiled into helper functions.<use href="#id" x="..." y="...">— compiled into code that calls the helper.
The compiler script
Points to note:
Variables of type
Efrom the E-objects system have a suffix_eas a convention.Note the use of E-objects in
_emit_line, so that the code written mirrors the output code almost exactly, but without it being a string or subject to the problems of string-based generation.
docs/examples/svg_to_turtle/svg_to_turtle.py#!/usr/bin/env python3
"""SVG-to-Turtle compiler.
Reads a subset of SVG (straight-line segments) and emits a Python module
that reproduces the drawing using the standard-library ``turtle`` module.
This is an example of using **fluent-codegen** to compile one language
into Python.
Supported SVG elements
----------------------
* ``<line x1=… y1=… x2=… y2=…>`` – a single line segment
* ``<defs>`` containing ``<line>`` elements with ``id`` attributes
* ``<use href="#id" x="tx" y="ty">``
Usage::
python svg_to_turtle.py drawing.svg
Produces ``drawing.py`` with a ``draw(t)`` function that accepts a
``turtle.Turtle``.
"""
from __future__ import annotations
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
from fluent_codegen import codegen
SVG_NS = "http://www.w3.org/2000/svg"
XLINK_NS = "http://www.w3.org/1999/xlink"
# ---------------------------------------------------------------------------
# Code generation
# ---------------------------------------------------------------------------
def compile_svg(svg_path: str | Path) -> str:
"""Compile an SVG file into Python source code."""
tree = ET.parse(svg_path)
root = tree.getroot()
module = codegen.Module()
module.add_comment(f"Generated from {Path(svg_path).name} by svg_to_turtle.py")
module.add_comment("Do not edit — regenerate from the SVG source.")
# Import turtle so the generated module is self-contained.
_, turtle_mod = module.create_import("turtle")
# Type annotation for the turtle parameter: turtle.Turtle
turtle_type = turtle_mod.attr("Turtle")
none_type = codegen.constants.None_
pos_type = module.enames.tuple[module.enames.int, module.enames.int]
# We'll use the E-object for the turtle parameter throughout.
# First, collect <defs> definitions (Phase 2).
defs: dict[str, ET.Element] = {}
for defs_elem in root.iter(f"{{{SVG_NS}}}defs"):
for child in defs_elem:
tag = child.tag.removeprefix(f"{{{SVG_NS}}}")
elem_id = child.get("id")
if elem_id and tag == "line":
defs[elem_id] = child
# Create a helper function for each definition.
def_func_names: dict[str, codegen.Name] = {}
for def_id, elem in defs.items():
func, func_name = module.create_function(
f"_draw_{def_id}",
args=[
codegen.FunctionArg.standard("t", annotation=turtle_type),
codegen.FunctionArg.standard("start", annotation=pos_type),
],
return_type=none_type,
)
_emit_line(
func.body,
func.enames.t,
func.enames.start,
_float(elem, "x1"),
_float(elem, "y1"),
_float(elem, "x2"),
_float(elem, "y2"),
)
def_func_names[def_id] = func_name
# Main draw function.
draw_func, draw_name = module.create_function(
"draw",
args=[codegen.FunctionArg.standard("t", annotation=turtle_type)],
return_type=none_type,
)
turtle_e = draw_func.enames.t
start = draw_func.body.assign("start", codegen.auto((0, 0)))
for child in root:
tag = child.tag.removeprefix(f"{{{SVG_NS}}}")
if tag == "defs":
continue # already handled above
if tag == "line":
_emit_line(
draw_func.body,
turtle_e,
start.e,
_float(child, "x1"),
_float(child, "y1"),
_float(child, "x2"),
_float(child, "y2"),
)
elif tag == "use":
href = child.get("href") or child.get(f"{{{XLINK_NS}}}href") or ""
ref_id = href.lstrip("#")
if ref_id not in def_func_names:
raise ValueError(f"<use> references unknown id {ref_id!r}")
tx, ty = _parse_x_y(child)
# Save turtle state
pos = draw_func.body.assign("pos", turtle_e.position())
heading = draw_func.body.assign("heading", turtle_e.heading())
# Call the helper
draw_func.body.add_statement(def_func_names[ref_id].e(turtle_e, (tx, ty)))
# Restore
draw_func.body.add_statements(
[
turtle_e.penup(),
turtle_e.goto(pos.e),
turtle_e.setheading(heading.e),
]
)
# if __name__ == "__main__" block
dunder_name = module.scope.name("__name__")
if_main = module.create_if()
main_block = if_main.create_if_branch(dunder_name.e == "__main__")
t_var = main_block.assign("t", turtle_mod.e.Turtle())
main_block.add_statements([draw_name.e(t_var.e), turtle_mod.e.done()])
return module.as_python_source()
def _emit_line(
block: codegen.Block,
turtle_e: codegen.E,
start_e: codegen.E,
x1: float,
y1: float,
x2: float,
y2: float,
) -> None:
"""Emit turtle commands to draw a single line from (x1,y1) to (x2,y2)."""
block.add_statements(
[
turtle_e.penup(),
turtle_e.goto(start_e[0] + x1, start_e[1] + y1),
turtle_e.pendown(),
turtle_e.goto(start_e[0] + x2, start_e[1] + y2),
]
)
# ---------------------------------------------------------------------------
# SVG parsing helpers
# ---------------------------------------------------------------------------
def _float(element: ET.Element, attr: str) -> float:
"""Extract a float attribute from an SVG element."""
return float(element.attrib[attr])
def _parse_x_y(element: ET.Element) -> tuple[float, float]:
return (int(element.attrib.get("x", "0")), int(element.attrib.get("y", "0")))
# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------
def main() -> None:
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <input.svg>", file=sys.stderr)
sys.exit(1)
svg_path = Path(sys.argv[1])
if not svg_path.exists():
print(f"File not found: {svg_path}", file=sys.stderr)
sys.exit(1)
output = svg_path.with_suffix(".py")
source = compile_svg(svg_path)
output.write_text(source + "\n")
print(f"Wrote {output}")
if __name__ == "__main__":
main()
Sample input
A simple house shape built from <line> elements and <use>
references to reusable beams/walls defined in <defs>:
house.svg<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="200"
height="200"
version="1.1"
id="svg17"
sodipodi:docname="house.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview19"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="2.655"
inkscape:cx="117.3258"
inkscape:cy="67.231638"
inkscape:window-width="1920"
inkscape:window-height="1024"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<!-- A simple house shape made of line segments -->
<defs>
<!-- A horizontal beam -->
<line
id="beam"
x1="0"
y1="0"
x2="80"
y2="0"
style="fill:#000000;stroke:#000000;stroke-opacity:1" />
<!-- A vertical wall -->
<line
id="wall"
x1="0"
y1="0"
x2="0"
y2="80"
style="fill:#000000;stroke:#000000;stroke-opacity:1" />
</defs>
<!-- Walls -->
<use
href="#wall"
x="40" y="60"
id="use1" />
<use
href="#wall"
x="120" y="60"
id="use2" />
<!-- Roof -->
<line
x1="30"
y1="60"
x2="80"
y2="20"
id="line9"
style="fill:#000000;stroke:#000000;stroke-opacity:1" />
<line
x1="80"
y1="20"
x2="130"
y2="60"
id="line11"
style="fill:#000000;stroke:#000000;stroke-opacity:1" />
<!-- Floor and ceiling as reused beams -->
<use
href="#beam"
x="40" y="60"
id="use3" />
<use
href="#beam"
x="40" y="140"
id="use4" />
</svg>
Generated output
Running python svg_to_turtle.py house.svg produces:
house.py (generated)# Generated from house.svg by svg_to_turtle.py
# Do not edit — regenerate from the SVG source.
import turtle
def _draw_beam(t: turtle.Turtle, start: tuple[int, int]) -> None:
t.penup()
t.goto(start[0] + 0.0, start[1] + 0.0)
t.pendown()
t.goto(start[0] + 80.0, start[1] + 0.0)
def _draw_wall(t: turtle.Turtle, start: tuple[int, int]) -> None:
t.penup()
t.goto(start[0] + 0.0, start[1] + 0.0)
t.pendown()
t.goto(start[0] + 0.0, start[1] + 80.0)
def draw(t: turtle.Turtle) -> None:
start = (0, 0)
pos = t.position()
heading = t.heading()
_draw_wall(t, (40, 60))
t.penup()
t.goto(pos)
t.setheading(heading)
pos_2 = t.position()
heading_2 = t.heading()
_draw_wall(t, (120, 60))
t.penup()
t.goto(pos_2)
t.setheading(heading_2)
t.penup()
t.goto(start[0] + 30.0, start[1] + 60.0)
t.pendown()
t.goto(start[0] + 80.0, start[1] + 20.0)
t.penup()
t.goto(start[0] + 80.0, start[1] + 20.0)
t.pendown()
t.goto(start[0] + 130.0, start[1] + 60.0)
pos_3 = t.position()
heading_3 = t.heading()
_draw_beam(t, (40, 60))
t.penup()
t.goto(pos_3)
t.setheading(heading_3)
pos_4 = t.position()
heading_4 = t.heading()
_draw_beam(t, (40, 140))
t.penup()
t.goto(pos_4)
t.setheading(heading_4)
if __name__ == "__main__":
t = turtle.Turtle()
draw(t)
turtle.done()
Key points to note in the generated code:
_draw_beam()and_draw_wall()are helper functions compiled from the<defs>element, with names based on the element names.Each
<use>saves the turtle position, calls the helper, and restores.The local variables
pos,headingare auto-suffixed topos_2,heading_2for the second<use>.