Skip to content

ijm/fTemplateModules

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fTemplateModules

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}

Why?

I like f-strings. A lot. But once they get big, some issues start to show up:

  1. 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!
  2. 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.
  3. Comments that are only relevant to the template either have to be moved out of the string or end up in the output.
  4. 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.

Grammar

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)

Transforms

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 {}

Custom Transforms

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.

Custom Parsers

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 a string.templatelib.Template object

Additional parsers can be registered with the @add_parser(NAME) decorator:

@add_parser("MyType")
def parse_as_mytype(tmpl: str) -> ast.expr:
    ...
    return ast_node

See the source code in parsers.py for some examples.

Debugging hook - an inspection hatch

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.

Example Templates

(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.

Python 3.14+ Template Strings

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.

Verification

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.py

No 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:

ToDo

  1. Better examples.
  2. Some proper tests. For now the example also double as tests.

About

Magically python importable f-string template files.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors