Template Files¶
This guide covers how to structure and write template files for Repolish.
Template Directory Structure¶
Provider templates are organized under a repolish/ directory within each
provider. This directory contains the project layout files that will be
processed and copied to your project:
my-provider/
├── repolish.py # Provider factory (required)
└── repolish/ # Template directory (required)
├── README.md
├── pyproject.toml
├── src/
│ └── __init__.py
└── tests/
└── test_example.py
All files under repolish/ will be processed with Jinja2 templating and copied
to your project when you run repolish.
Jinja Extension for Syntax Highlighting¶
Template files can optionally use a .jinja extension to enable proper syntax
highlighting in editors like VS Code. This is especially useful for
configuration files that contain Jinja template syntax:
repolish/
├── pyproject.toml.jinja # TOML with Jinja highlighting
├── config.yaml.jinja # YAML with Jinja highlighting
├── Dockerfile.jinja # Dockerfile with Jinja highlighting
└── README.md # Regular Markdown (no .jinja needed)
How It Works¶
When Repolish processes your templates, it automatically strips the .jinja
extension from filenames before copying them to your project:
pyproject.toml.jinja→pyproject.tomlconfig.yaml.jinja→config.yamlDockerfile.jinja→Dockerfile
This means you get the benefit of syntax highlighting in your editor while editing templates, but the generated files have normal extensions.
Editor Configuration¶
Many VS Code extensions recognize the .jinja extension and provide proper
syntax highlighting. For example:
- YAML files:
config.yaml.jinjagets both YAML and Jinja highlighting - TOML files:
pyproject.toml.jinjagets TOML and Jinja highlighting - HTML files:
template.html.jinjagets HTML and Jinja highlighting
Common extensions that support this pattern:
Special Case: Actual Jinja Templates¶
If you need to generate actual .jinja files (files that will themselves be
Jinja templates), use a double .jinja.jinja extension:
The rule is simple: if a template filename ends in .jinja, that extension will
be removed in the generated output.
Template Syntax¶
Templates use Jinja2 syntax to reference context variables:
# {{ cookiecutter.package_name }}
Version: {{ cookiecutter.version }}
Author: {{ cookiecutter.author }}
See the Preprocessors guide for information about advanced features like anchors, create-only blocks, and conditional content.
Jinja rendering (opt-in) and Cookiecutter deprecation¶
Historically Repolish used Cookiecutter for the final render pass. Newer
projects can opt into native Jinja2 rendering which provides stricter
validation, better error messages, and more flexible features (for example
tuple-valued file_mappings and per-file extra context).
- Enable native Jinja rendering in your
repolish.yaml:
- What changes when you opt in:
- Templates are rendered with Jinja2 using the merged provider context.
- The merged context is available both as top-level variables and under the
legacy
cookiecutternamespace to ease migration (e.g.{{ my_provider.name }}and{{ cookiecutter.my_provider.name }}both work). - File paths and file contents are Jinja-rendered (so use
{{ }}in filenames likesrc/{{ module_name }}.py.jinja). -
tuple-valuedfile_mappingsare supported (see below). -
Why migrate away from Cookiecutter
- Cookiecutter treats some data structures (notably arrays) as CLI options which complicates templates and provider context. Jinja does not have that limitation.
- Jinja provides better error reporting (we use StrictUndefined by default) which surfaces missing variables during preview/apply rather than failing silently or producing incorrect output.
-
The new renderer supports per-mapping extra context and avoids Cookiecutter CLI prompts or option-handling quirks.
-
Migration tips
- Enable
no_cookiecutter: truein a local config and runpoe preview(orrepolish preview) to validate templates. - Replace direct
cookiecutter.*references where convenient with top-level variables (both are supported during migration). - If you rely on Cookiecutter-specific behaviors, keep the old path but plan to move logic into Jinja templates or provider factories.
Best Practices¶
- Use
.jinjafor syntax highlighting: Add.jinjato files with significant Jinja templating to improve editor experience - Keep it optional: Files without Jinja syntax don't need the extension
- Consistent naming: Use the same pattern across your templates for clarity
- Test both ways: Verify your templates work with and without the extension since the generated output is identical
Conditional files (file mappings)¶
Template authors can provide multiple alternative files and conditionally choose
which one to copy based on context. This keeps filenames clean without
{% if %} clutter in paths.
How it works¶
Files in your template directory that start with _repolish. are treated as
conditional/alternative files. They are only copied to the project when
explicitly referenced in create_file_mappings() (or a file_mappings
variable) in repolish.py. They can be placed anywhere in the template
directory tree.
The file_mappings return value is a dict where:
- Keys are destination paths in the final project (must be unique)
- Values are source paths in the template, or
Noneto skip
Example¶
Template directory structure:
templates/my-template/
├── repolish.py
└── repolish/
├── README.md # Always copied
├── _repolish.poetry-pyproject.toml # Conditional
├── _repolish.setup-pyproject.toml # Conditional
└── .github/
└── workflows/
├── _repolish.github-ci.yml # Conditional (nested)
└── _repolish.gitlab-ci.yml # Conditional (nested)
repolish.py:
def create_context():
return {
"use_github_actions": True,
"use_poetry": False,
}
def create_file_mappings():
ctx = create_context()
return {
".github/workflows/ci.yml": (
".github/workflows/_repolish.github-ci.yml"
if ctx["use_github_actions"]
else ".github/workflows/_repolish.gitlab-ci.yml"
),
"pyproject.toml": (
"_repolish.poetry-pyproject.toml" if ctx["use_poetry"]
else "_repolish.setup-pyproject.toml"
),
# None means skip — don't copy this file
".pre-commit-config.yaml": None,
}
Key behaviours¶
- Conditional files (
_repolish.prefix) are only copied when explicitly listed infile_mappings. - Regular files (no prefix) are always copied normally.
- Destinations are unique: you cannot map multiple sources to the same destination within a single provider.
- None values are skipped: returning
Nonemeans "don't copy this file". - Multiple providers: file mappings from multiple providers are merged; later providers override earlier ones for the same destination.
tuple-valued mappings (available with no_cookiecutter: true) allow per-file
extra context:
def create_file_mappings():
return {
"pyproject.toml": ("_repolish.setup-pyproject.toml", {"extra_key": "value"}),
}
Create-only files¶
Files listed in create_only_files are created when they do not exist in the
project and skipped on all subsequent runs. This is useful for initial
scaffolding — source files, example tests, README stubs — that should be created
once and then owned by the developer.
How it works¶
- Present: file is skipped (user modifications are preserved)
- Absent: file is created normally from the template
- Check mode: reports
MISSINGfor absent create-only files but does not report diffs for files that already exist, even if content differs
Example¶
repolish.py:
def create_context():
return {"package_name": "awesome_tool"}
def create_create_only_files():
pkg = create_context()["package_name"]
return [
f"src/{pkg}/__init__.py",
f"src/{pkg}/py.typed",
f"src/{pkg}/main.py",
"tests/__init__.py",
"tests/test_main.py",
"README.md",
]
Alternatively, use a module-level variable:
What happens across runs¶
First run (new project):
Creates the full scaffold: src/awesome_tool/, tests/, etc., along with
pyproject.toml and CI configs.
Later runs (after the developer has written code):
- Skips
src/awesome_tool/__init__.py,tests/test_main.py, etc. — user modifications are untouched. - Updates
pyproject.toml,.github/workflows/ci.yml, and other regular template files as usual.
Key behaviours¶
- Multiple providers:
create_only_fileslists are merged additively. - Works with file_mappings: a file can be both conditional and create-only.
- Conflicts with delete_files: if a file appears in both, the delete wins.