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 copy create what you intend given certain answers?
    • Has copier’s behaviour changed for your template in a new version?
  • 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

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