A Production-Ready Meta-Template for Copier
I previously wrote about a collection of patterns and techniques for building maintainable Copier templates.
Most of those patterns are the same regardless of what the template is actually for:
- the usual copier settings you’re going to want to use
- the test harness
- release automation
- CI/CD setup
- complementary tools you and your users will value
You end up re-doing a lot of the same setup every time you start a new one, even though almost none of it is specific to the thing you’re templating.
So I created copier-template-copier-template to short cut this for a new Copier template, letting you just focus on creating your actual template contents (and testing the output works as you want it to).
Testing templates
Probably the highest value part of the template is the test harness. Copier does not have a well-trodden path for testing in the community, and patterns are immature, but it’s hugely important for a template’s long-term maintainability (and developer’s mental health).
It’s quite straightforward to assert that copier copy runs and produces some files,
as copier’s own test suite does,
but you also want to check that what got generated works,
as a user would interact with it
The included test suite therefore treats these as two separate questions:
- Does the template itself behave correctly?
- Does
copier copycreate what you intend given certain answers? - Has
copier’s behaviour changed for your template in a new version?
- Does
- Does the output actually work?
- Once a project has been generated, does the content it produced function?
- This might apply to all variations of the output your template could create
The first is a thin layer over copier.run_copy / copier.run_update
(similar to the copier test suite):
def test_copy_default(
template_dir: Path,
tmp_path: Path,
mock_answers_required_without_defaults: dict[str, Any],
) -> None:
worker = run_copy(
str(template_dir),
tmp_path,
vcs_ref='HEAD',
defaults=True,
data=mock_answers_required_without_defaults,
unsafe=True,
)
assert (tmp_path / worker.answers_relpath).exists()
The second generates a project via a helper
fixture
(sub_project) and then actually interacts with it.
A particularly powerful test I recommend is running the output (aka “subproject” based on copier’s test naming conventions) project’s own test suite, via its harness, if it has one
def test_sub_project_tests(sub_project: Path) -> None:
with local.cwd(sub_project):
task('dev-setup:venv')
task('test')
What else is included
Developer tasks
task / go-task / Taskfile tasks are self-documenting
automation for common local development activities
CI/CD
GitHub Actions or GitLab CI, depending on your platform, with matrix testing across Python versions.
Renovate dependency management config, to keep the project fresh.
python-semantic-release for automating versioned releases, and changelog generation.
Optional extensions
- Pydantic validation for answers
- local Jinja extensions
both off by default since they are not needed for simpler templates and
require the --trust flag, but can be incredibly valuable if the project becomes more complex.
Usage
Refer to the README of the project: https://github.com/cbrown1234/copier-template-copier-template