ftemplatemodules lets you keep big f-strings (and >=3.14 t-strings) for LLM prompts, LaTeX docs, or any other long text in their own files, with a little compile-time transforming, then import them as normal Python functions.
It's for dev-authored templates that are mostly text with a bit of Python interpolation. The heavy lifting is done by the Python compiler, so syntax and error checking behave like ordinary Python.
This is not a full template engine (HTML escaping, macros/filters, sandboxing, etc.). But if you want to factor templates into an importable module that your IDE will better understand, we've got you.
So:
import ftemplatemodules.auto
from prompts import hello
print(hello("Alice"))[hello(name) -> str]
Hello {name}
I like f-strings. A lot. But once they get big, some issues start to show up:
- Long LLM prompts and LaTeX templates are mostly human language, not code. I want my IDE to do human-language things like spelling and grammar, for the text, and code suggestions for the code. Not the other way around!
- f-strings are great, but sometimes (e.g. with LaTeX) the braces
{}really need to be different characters. LaTeX uses braces a lot and constantly escaping them is annoying. - Comments that are only relevant to the template either have to be moved out of the string or end up in the output.
- Whitespace often has to be pre-normalized in the string, or normalized later with a runtime function that has to be careful not to touch the f-string expressions.
So fTemplateModules parses and transforms a .ftmpl file to address these while cheating by passing most of the hard work back to the Python compiler, and then presenting the result as a Python module from the outside.
The .ftmpl files have a fairly simple structure: Everything is free-form
text except for a few statements which are always a complete line starting and
ending in square braces: [some statement or command], which we'll call
square-lines.
In its simplest form, the file is a series of blocks where the first line is a square-line declaring a function signature and is followed by a free-form text block that continues to the end of the file or the next square-line:
[my_llm_promptA(s: str) -> str]
My LLM Prompt with an f-string formatting : {s}
[my_llm_promptB(x: float)-> str]
Some other prompt with a {x:.2f} number.
More completely: the file starts with one or more import square lines
( [import other_module] ), which are ordinary Python import lines wrapped
in square brackets. They're followed by one or more blocks of the form :
[function-signature ; optional-comma-separated-options] followed by
an optional square-line for a doc-string description, which is followed
by the associated free-form text. The doc-string square-line uses an
additional quote: ["A free-form text description"] (see example below).
If the comma separated list of optional transforms is given, these
transforms are applied to the template-string/doc-string pair for that
block in that list order.
For example:
[test_prompt_tex(sub: int) -> str ; remove_cpp_comments, latex_tmpl]
["A LaTeX test template"]
A string with some math in it $x=<sub>$ $\vec{x}=\mathbf{<sub>}$
and a c++ like comment removed /* comment */ something something
// Another c comment
As with any mixed-language parser, there are some edge cases. Square
braces [] were picked because they're not often used in human text and
don't clash with f-strings {}. The ; was picked as an option separator
because Python rarely uses it, and it means line-break anyway. Lastly,
"] at the end of a line ends a doc-string, so don't do that if you
don't want it to end.
Each block is rearranged into a single function. The above example becomes:
def test_prompt_tex(sub: int) -> str:
"""A LaTeX test template"""
return f'A string with some math in it $x={sub}$ $\\vec{{x}}=\\mathbf{{{sub}}}$\nand a c++ like comment removed thing\n\n'and is made available for import in the normal way.
You can import other modules using an [import line] at the
beginning of the file. This can be any valid module, either another
.ftmpl file or an ordinary Python module. One of the examples uses
this to import the json.dumps() function.
Once imported, everything should work as expected for any Python module,
including the help() function and, with a little hacking, pydoc() (see
the pydocftmpl.py example)
Current built-in transforms are :
| Option | Description |
|---|---|
| remove_cpp_comments | Use PyParsing's cpp_style_comment() to remove c++ style comments. |
| remove_python_comments | Use PyParsing's python_style_comment() to remove python style comments. |
| remove_html_comments | Use PyParsing's html_comment() to remove html style comments. |
| append_doc | Append the current template string to the current doc string. |
| unwrap_lines | Unwrap line-broken lines and normalize line white space. This transform |
| reduces the number of EOLs in a row and replaces an EOL with a space if | |
| it is the only one. | |
| latex_tmpl | Transform for LaTeX templates: escape {} to {{}} and map <> to {} |
Each transform is a function with signature (str, str) -> (str, str), and is
added to the possible options list with the @add_transform(NAME) decorator:
@add_transform("transform_name")
def _(tmpl: str, docs: str) -> (str, str):
...
return (tmpl, docs)See the source code in transforms.py for some examples.
After transforms are applied, the resulting string is parsed according to the return type declared in the function signature:
-> str(default): Parsed as an f-string (assembled immediately)-> Template(Python 3.14+): Parsed as a t-string (assembly deferred), returning astring.templatelib.Templateobject
Additional parsers can be registered with the @add_parser(NAME) decorator:
@add_parser("MyType")
def parse_as_mytype(tmpl: str) -> ast.expr:
...
return ast_nodeSee the source code in parsers.py for some examples.
The function set_debug_hook(callback: Callable) can be used to enable
debugging and set a function to be called when ever a template is used.
The callback function gets the name of the template and the result as returned
by the function as the first two arguments and the arguments the template
was called with as its keyword arguments.
The callback hook is only compiled into the template when debugging is
enabled, so the hook has to be set before a .ftmpl module is imported to
have an effect.
examples/testDebug.py wraps the test.py example with a JSON logger to
log everything going through the templates into a file.
(ToDo: better examples! A longer example is in the example directory)
The following Python :
import ftemplatemodules.auto # installs the .ftmpl import hook
from prompts import test_prompt
print(test_prompt(
data={"key1": "something-one", "key2": "something-two"},
action="Say Hello!"
))uses prompts.ftmpl :
[from basePrompts import *]
[from json import dumps]
[test_prompt(data, action) -> str]
["Prompt template function for a friendly LLM"]
You are a friendly LLM.
{action}
The JSON is {dumps(data)}
{jsonInstructions()}
and basePrompts.ftmpl:
[jsonInstructions() -> str]
Output as well-formed JSON, where the JSON is complete, should avoid using dictionaries, and has a line length of 70 characters.
to produce :
You are a friendly LLM.
Say Hello!
The JSON is {"key1": "something-one", "key2": "something-two"}
Output as well formed JSON, where the JSON is complete, should avoid using dictionaries, and has a line length of 70 characters.
T-strings provide deferred template assembly. Use -> Template as the return type:
[my_template(name) -> Template]
Hello, {name}!
The function returns a Template object from string.templatelib instead of a string.
See examples/test_tstring.py for a working example.
The example in examples/ double as functional tests for validation for now. They should all run cleanly:
python examples/test.py
python examples/testDebug.py
python examples/testPydoc.py
python3.14 examples/test_tstring.pyNo pytest test suite for now as i'm the only maintainer. Exercising the import machinery is in examples/import_test/test_imports.py. Pre-PyPI build verification is in scripts/verify-build.sh:
- Better examples.
- Some proper tests. For now the example also double as tests.