Usage

The copie fixture will allow you to copy or copy a template and run tests against it. It will also clean up the generated project after the tests have been run.

For these examples, let’s assume the current folder is a git repository containing a copier template. It should include a copier.yml file and a template folder containing jinja templates.

Tip

If needed you can also switch to the copie_session fixture to get the same functionalities but session scoped.

Note

The name of the template folder can be anything as long as it matches the _subdirectory key in the copier.yml file.

demo_template/
├── .git/
├── template
│   ├── {{ _copier_conf.answers_file }}.jinja  # required only to test update()
│   └── README.rst.jinja
├── tests/
│   └── test_template.py
└── copier.yaml

the copier.yaml file has the following content:

repo_name:
   type: str
   default: foobar
short_description:
   type: str
   default: Test Project
_subdirectory: template

The answers file contain the following:

# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
{{ _copier_answers|to_nice_yaml -}}

And the readme template is:

{{ repo_name }}
===============

{{ short_description }}

default project

Use the following code in your test file to generate the project with all the default values:

def test_template(copie):
    result = copie.copy()

    assert result.exit_code == 0
    assert result.exception is None
    assert result.project_dir.is_dir()
    with open(result.project_dir / "README.rst") as f:
       assert f.readline() == "foobar\n"

It will generate a new repository based on your template, eg:

foobar/
└── .copier-answers.yml
└── README.rst

the Return object can then be used to access the process outputs:

  • result.project_dir

  • result.exception

  • result.exit_code

  • result.answers

To test the generation for a particular git tag or commit use the vcs_ref argument, when calling copie.copy():

def test_template(copie):
    result = copie.copy(vcs_ref="v1")  # tests template generation from v1

Naturally, if not specified, vcs_ref defaults to HEAD.

To test for an update, you should first generate a copy based on a historical commit or tag from the template, initialize a git repository with those contents (required by Copier itself), and then test the current changes on the top of the desired reference:

import plumbum

def test_template(copie):
    result = copie.copy(vcs_ref="v1")
    assert result.exit_code == 0
    with open(result.project_dir / "README.rst") as f:
       assert f.readline() == "foobar\n"

    with plumbum.local.cwd(result.project_dir):
        git = copie.git()
        git("init")
        git("add", ".")
        git("commit", "-m", "Initial commit")

    updated_result = copie.update(result)  # updates to "HEAD" by default
    assert updated_result.exception is None
    with open(result.project_dir / "README.rst") as f:
       assert f.readline() == "foobar\nlatest modifications\n"

You may use this mechanism to test migrations from/to any tagged versions of your current template, for as long as you can assign a proper vcs_ref to it. To test an update to a specific vcs_ref, use the form copie.update(vcs_ref="v2") instead of the default "HEAD" tag.

The temp folder will be cleaned up after the test is run.

Custom answers

Use the extra_answers parameter to pass custom answers to the copier.yaml questions. The parameter is a dictionary with the question name as key and the answer as value.

def test_template_with_extra_answers(copie):
    result = copie.copy(extra_answers={"repo_name": "helloworld"})

    assert result.exit_code == 0
    assert result.exception is None
    assert result.project_dir.is_dir()
    with open(result.project_dir / "README.rst") as f:
       assert f.readline() == "helloworld\n"

Custom template

By default copy() looks for a copier template in the current directory. This can be overridden on the command line by passing a --template parameter to pytest:

pytest --template TEMPLATE

You can also customize the template directory from a test by passing in the optional template parameter:

@pytest.fixture
def custom_template(tmp_path) -> Path:
    # Create custom copier template directory
    (template := tmp_path / "copier-template").mkdir()
    questions = {"custom_name": {"type": "str", "default": "my_default_name"}}
    # Create custom subdirectory
    (repo_dir := template / "custom_template").mkdir()
    questions.update({"_subdirectory": "custom_template"})
    # Write the data to copier.yaml file
    (template /"copier.yaml").write_text(yaml.dump(questions, sort_keys=False))
    # Create custom template text files
    (repo_dir / "README.rst.jinja").write_text("{{custom_name}}\n")

    return template


def test_copie_custom_project(copie, custom_template):

    result = copie.copy(
      template_dir=custom_template, extra_answers={"custom_name": "tutu"}
   )

    assert result.project_dir.is_dir()
    with open(result.project_dir / "README.rst") as f:
       assert f.readline() == "tutu\n"

Important

The template parameter will override any --template parameter passed on the command line.

Subprojects

Copier allows you to create :subprojects:`https://copier.readthedocs.io/en/stable/configuring/#applying-multiple-templates-to-the-same-subproject`, which are projects that are copied into the main project directory, and may consume answers from previously copied projects.

Consider that you have two templates to run on the same project that will be run in the following order:

  • You use one framework that has a public template to generate a project. It’s available at path/to/parent_template.

  • You are developing a second template (with a copier.yml template in the CWD, path/to/child_template`) that will generate a subproject in the main project directory, and will consume answers from the first template.

def test_parent_child(copie):
   parent_template = Path("path/to/parent_template")
   child_template  = Path("path/to/child_template")

   parent_result = copie.copy(template_dir=parent_template)

   child_copie   = copie(
      parent_result=parent_result,
      child_tpl=child_template
   )
   child_result  = child_copie.copy()

Keep output

By default copie fixture removes copied projects at the end of the test. However, you can pass the keep-copied-projects flag if you’d like to keep them in the temp directory.

Note

It won’t clutter as pytest only keeps the three newest temporary directories

pytest --keep-copied-projects