trolskgen
Ergonomic codegen for Python - pip install trolskgen. Blog post with a motivating example.
Note on Python 3.14 template strings.
From Python 3.14 upwards, there are template strings, these make trolskgen significantly more succinct.
Where previously you'd do:
name = t("f") func = t( """ def {name}(): ... """, name=name, ) trolskgen.to_source(func)
As of Python 3.14, you can do:
name = t"f" func = t""" def {name}(): ... """ trolskgen.to_source(func)
There are some if sys.version_info >= (3, 14) flags around, but it should just work come release date.
trolskgen lets you easily build and compose ast.AST trees, and thereby easily generate source code. It doesn't handle any formatting concerns, just ruff format it afterwards. If you want comments, sorry, instead use a docstring or some Annotated[] wizardry.
Quick example:
import trolskgen from trolskgen import t func = t( """ def {name}(): ... """, name="f", ) trolskgen.to_source(func) trolskgen.to_ast(func)
Gives you the source str:
And the ast.AST:
ast.Module( body=[ ast.FunctionDef( name="f", args=ast.arguments(...), body=[ast.Expr(value=ast.Constant(value=Ellipsis))], decorator_list=[], type_params=[], ) ], )
A more complete example:
import datetime as dt name = "MySpecialClass" bases = [int, list] field_name = "d" fields = [ t( "a: {type_}", type_=str, ), t( "{field_name}: dt.date = {default}", field_name=field_name, default=dt.date(2000, 1, 1), ), ] method = t( """ def inc(self) -> None: self.{field_name} += dt.timedelta(days=1) """, field_name=field_name, ) my_special_class_source = t( """ class {name}({bases:*}, float): {fields:*} {method} """, name=name, bases=bases, fields=fields, method=method, ) trolskgen.to_source(my_special_class_source)
Gives you the source str:
class MySpecialClass(int, list, float): a: str d: dt.date = dt.date(2000, 1, 1) def inc(self) -> None: self.d += dt.timedelta(days=1)
API
| Building templates |
|---|
trolskgen.t(s: str, **kwargs: Any) -> trolskgen.templates.Template |
Creates source templates. If you use the format string :*, it will splat in place - see above: {bases:*}, {fields:*}
This is redundant as of Python 3.14 - see above.
| Converting to AST/source |
|---|
trolskgen.to_ast(o: Any, *, config: Config) -> ast.AST |
trolskgen.to_source(o: Any, *, config: Config, ruff_format: bool, ruff_line_length: int) -> str |
Try to convert o into an ast.AST/str representation.
The following are special cases for the value of o:
ast.ASTnodes - these just get passed straight back out.trolskgen.templates.Templateorstring.templatelib.Template- these get parsed as Python code.
trolskgen will generate sensible ASTs, for the following types:
Noneintfloatstrboollisttupledictsetclassesfunctionsdt.datetimedt.dateenum.EnumdataclassAnnotated,T | U, etc.pydantic.BaseModel
If you have ruff installed, you can call with ruff_format=True.
We can add our own classes/overrides using:
| Configuring/Overriding |
|---|
__trolskgen__ |
__trolskgen_cls__ |
trolskgen.Converter |
trolskgen.Config |
trolskgen.Config().prepend_converter(converter: Converter, *, before: Converter | None) -> Config |
If you own the class, you can just add a __trolskgen__ method:
For example:
class MyInterfaceClass: def __trolskgen__(self, f: trolskgen.F) -> ast.AST: return f(t("MyInterfaceClass({values:*})", values=[1, 2, 3])) trolskgen.to_source(MyInterfaceClass()) == "MyInterfaceClass(1, 2, 3)"
You can also add a __trolskgen_cls__ @classmethod:
@dataclass class MyJustName: a: str @classmethod def __trolskgen_cls__(cls, f: trolskgen.F) -> ast.AST: return f(t("Foo")) trolskgen.to_source(MyJustName("bar")) == "Foo(a='bar')"
Note that we use f to recursively call trolskgen.to_ast(...) while preserving the current Config.
If you don't own the class, you can build a trolskgen.Config with a custom Converter function.
For example, if you for some reason wanted to render all ints in the form x + 1, you could:
def custom_int_converter(o: Any, f: trolskgen.F) -> ast.AST | None: if not isinstance(o, int): return None return f(t(f"{o - 1} + 1")) config = trolskgen.Config().prepend_converter(custom_int_converter) trolskgen.to_source([6, 9], config=config) == "[5 + 1, 8 + 1]"
Development
uv pip install -e '.[dev]'
mypy .
pytest -vv
uv pip install build twine
python -m build
twine check dist/*
twine upload dist/*