Jinja2

Jinja2 is the code-rendering layer. Every generator class the tool emits, every fallback generator, and every team-contribution template is a Jinja2 template rendered to a string of Python (or YAML, or Markdown) source. This chapter is about that specific, slightly unusual use of a templating engine: rendering code, not markup.

Inline Template, not a loader environment

Both src/test_data_workbench/adaptation/generator_factory.py and src/test_data_workbench/core/template_generator.py use Jinja2’s Template class directly, constructed from an inline triple-quoted string, rather than a configured Environment with a FileSystemLoader pointed at template files on disk:

from jinja2 import Template

template = Template('''
from faker import Faker
...
class {{ class_name }}Generator:
    ...
''')
return template.render(class_name=..., columns=..., ...)

Every generator-producing method (_create_user_generator, _create_product_generator, _create_order_generator, _create_review_generator, _create_generic_generator, _create_fallback_generator) follows this same shape: build the template string, compute a small context dict from the table’s columns, render, return the string. Keeping the templates as string literals next to the Python that computes their context keeps each generator strategy self-contained in one method, at the cost of losing syntax highlighting for the embedded template and of Jinja2 parsing the template fresh on every call rather than once at import time. For templates this small and infrequently invoked (once per table, per tdw deploy), that cost is negligible.

Whitespace control shapes the emitted Python

Because the output has to be syntactically valid Python, not just plausible text, whitespace-trimming tags ({%- %} / {%- -%}) do real work throughout these templates. A block that should not appear at all when its condition is false, like the sequential primary-key counter, is written so its absence leaves no stray blank line or dangling comma:

def __init__(self, seed: Optional[int] = None):
    self.fake = Faker()
    self._generated_emails = set()
{%- if has_sequential_pk %}
    self._pk_counter = 0
{%- endif %}
    if seed is not None:
        Faker.seed(seed)
        random.seed(seed)

Without the trim markers, Jinja2’s default block behavior leaves the newline around {% if %}/{% endif %} tags in the output, which would still be syntactically valid here but would accumulate into visibly uneven generated code across the many conditional sections. Since the entire point of generated-code-as-product (Design Principles) is that a person reads this output, tidy whitespace is not cosmetic, it is part of the “inspectable” property the docs claim.

Optional constructor shape via conditional blocks

The generic generator template (_create_generic_generator) is the clearest example of the templates doing real structural work, not just value substitution. Whether the generated class’s __init__ takes a reference_data parameter at all, whether it needs a self-reference tracking dict, and whether the class gets a _get_reference_id or _self_reference_id helper method are each decided by a boolean computed in Python (uses_reference_helper, uses_self_reference_helper) and threaded into the template as conditionals. A table with no foreign keys gets a generator with a plain no-argument constructor; a table with a self-referential foreign key (categories.parent_id -> categories.id) gets a materially different class shape. The template is doing dispatch on structure, not just filling in blanks in one fixed skeleton.

Rendered code is validated after the fact, not trusted

Nothing about Jinja2 guarantees the rendered text is valid Python; it is a text templating engine and has no idea what Python syntax is. That check happens downstream, in the AST-based validators described in Architecture, which parse every generated file before it is trusted. Jinja2’s job ends at producing the string; correctness of that string is someone else’s concern by design, which keeps the template code itself simple string substitution rather than tangled with validation logic.

Three-tier contribution templates

core/template_generator.py’s TeamTemplateGenerator uses the same inline-Template approach for a different purpose: producing copy-paste-and-edit starting points for team members at three explicit skill levels (beginner, intermediate, advanced), each with a different generated class shape, a plain generate() with TODO markers for beginners, explicit relationship handling for intermediates, batching and caching scaffolding for advanced contributors. It also renders the project’s generated CONTRIBUTING.md and scenario YAML through the same mechanism, which is why Jinja2 is listed as a general code/config renderer for the project rather than narrowly “the thing that makes generator classes.”