
Handling multiple errors in Rust iterator adapters
Approaches for handling multiple errors within iterator adapters

Better FastAPI Background Jobs
A more featureful background task runner for Async apps like FastAPI or Discord bots


Latest Updated Post
Effective Repository Templates with Copier
Copier is immensely useful as a vehicle for building and distributing developer tooling that remains maintainable at scale
The critical feature it provides beyond tools like cookiecutter is the ability to update the code you have generated from it to a newer version of the template, while respecting any changes you’ve made to it
In this post I’ll detail a number of sensible defaults, techniques, design patterns and practices that enhance the utility of copier templates
Set _min_copier_version
Unless you’re using CI to test your template with multiple versions of copier I suggest setting this to the latest version available.
With tools like uv
or pipx
for isolated installs of python CLI tools, in the vast majority of cases there is no need to support older versions and so you can nudge them to update.
This ensures your users are using the most up to date software with all the fixes to the copier cli tool
and that as the template author, you can use all features described in the docs
Set _subdirectory
As a sensible default, I suggest setting
# copier.yml
_subdirectory: template
which means your template will be the contents of the template/
directory rather than the root of the template repository
This is quite useful for separating the template contents from files for the maintenance of the template itself such as; tests, docs, CI/CD config, and developer tooling
A more complex pattern, that can be useful if you’re supporting multiple variants of template that require a fundamentally different structure, is to template this value with the answer to a question
# copier.yml
_subdirectory: '{{template}}'
template:
choices:
- Tech Stack A: template_dir_for_tech_stack_a
- Tech Stack B: template_dir_for_tech_stack_b
However, if the templates are really so different that they require their own variant like this, I would recommend that you consider if they should be entirely separate templates (i.e. in separate repos) so they can be maintained and versioned separately
If only part of your template is fundamentally different you can achieve similar results with conditional directories: e.g.
template/{% if your_variable == 'feature_A' %}feature_A_dir{% endif %}/
Hardcoded template values
Sometimes it’s useful to include variables that are used within the template as a helper for the template maintainers, but don’t need to be asked to users. You can achieve this with answers that are never asked
template_name:
type: str
default: "your-template-name"
when: false
Note: These values do not appear in the users answers file
Highlight merge conflicts
The way copier provides the ability to update code from an older version of the template is by using git to apply the differences between the old template and the new template to your code. Copier raises merge conflicts for you to resolve if you’ve modified the code differently in your instance of the files.
By including a .pre-commit-config.yaml
file with check-merge-conflict
for use with
pre-commit in the template, you can help
ensure users more easily find conflicts if they occur when they update the template
# .pre-commit-config.yaml
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-merge-conflict
args: [--assume-in-merge]
Ideally pre-commit validation should be checked in CI with a job that runs pre-commit run --all-files
in case a developer has forgotten to install it
Handling templates inside your template
If files you’re including in your template are also themselves templates and use the same template delimiters as jinja defaults ({{
}}
) used by copier,
then things can get a bit awkward.
For instance if you include task’s Taskfile.yaml
or just’s justfile
or you use jinja
in the output project, and you want to template values inside those files, you can end up with the delimiters conflicting.
Then you either have to use lots of {% raw %}{% endraw %}
blocks to escape the templating manually.
Alternatively, there are a number of settings to modify the (jinja) templating within _envops
which are helpful to change the delimiters used by copier in the template
_envops:
block_end_string: "%]"
block_start_string: "[%"
comment_end_string: "#]"
comment_start_string: "[#"
variable_end_string: "]]"
variable_start_string: "[["
You might also consider customizing the suffix of files that get templated (from the default of .jinja
) if it clashes with your desired output too
_templates_suffix: .tmpl
Whitespace control
Familiarity with the whitespace-control functionality offered by jinja templating is very helpful for maintaining functional and tidy output files
This includes the use of -
or +
to control whitespace around the jinja blocks
{%- -%}
and/or setting global defaults you can customize within _envops
_envops:
trim_blocks: true
keep_trailing_newline: true
lstrip_blocks: true
Loop helpers
Jinja templating provides you with some helpers when you are using for
loops via the loop
variable which has various helper
attributes and methods attached
For instance to construct a valid json array, you need comma’s for all the lines except the last:
[
{% for item in items %}
"{{ item }}"{% if not loop.last %},{% endif %}
{% endfor %}
]
See the jinja docs for details of other attributes and method
Validate your inputs
Empty strings are valid inputs to stings but might not be what you want, so make sure to validate your inputs where appropriate
question:
type: str
validator: >-
{% if question == "" %}
Answer must not be an empty string
{% endif %}
You can utilize the guard clause pattern to create more complex validation
question:
type: str
validator: |-
{% if question == "" %}
Answer must not be an empty string
{% endif %}
{% if len(question) > 20 %}
Answer must be less than (or equal to) 20 characters
{% endif %}
If describing the validation via jinja templating is getting unmanageable you can use jinja extensions to use python to do more complex validation. You can add jinja “filters” (a python function that receives the value piped in) that will parse the input and produce the validation sting.
question:
type: str
validator: |-
{{ question | your_validation_filter }}
This is made easier with copier through the copier_templates_extensions
extension enabling easier authoring of extensions as part of the template.
_jinja_extensions:
- copier_templates_extensions.TemplateExtensionLoader
The downside of this approach is that it requires the user to use the --trust
flag (as it can run arbitrary code)
which potentially will cause them some concern or require them to investigate what it’s doing before being happy using it.
However if your template already requires the use of the trust flag for other reasons (e.g. migrations) then this is less of a issue.
Creating files/directories with for loops
Copier recently (in version 9.5
) introduced support for looping over iterables to construct files and directories
With this change, it increases the value in asking questions that have iterable answers, to construct data structures to loop over
There are a number of different ways to support this with copier
For lists of values known statically ahead of time you can use choices
with multiselect
:
environments:
multiselect: true
choices:
- dev
- test
- staging
- production
📁 dotenv
└── 📄 {% yield env from environments %}{{ env }}{% endyield %}.env.jinja
# {% yield env from environments %}{{ env }}{% endyield %}.env.jinja
ENVIRONMENT={{ env }}
Using yaml (or json) inputs which expect nested maps allows you to construct more useful files, but can require more complex validation
# helper list
required_config:
type: yaml
default: |-
db_host: string
db_port: number
when: false
environments_config:
type: yaml
multiline: true
default: |-
dev:
db_host: localhost
db_port: '5432'
validator: |-
{% if environments_config is not mapping %}
Environment config must be a yaml map with keys as the environment name
{% endif %}
{% for env, config in environments_config.items() %}
{% if config is not mapping %}
Environment '{{ env }}' value must be a yaml map with keys: {{ required_config | join(',') }}
{% endif %}
{% for var in required_config %}
{% if var not in config %}
{{ env }} must contain {{ var }} setting
{% endif %}
{% if config[var] is not string %}
{{ var }} must be a string setting
{% endif %}
{% endfor %}
{% endfor %}
Then your file can be more advanced like this
# {% yield env from environments_config %}{{ env }}{% endyield %}.env.jinja
ENVIRONMENT={{ env }}
DB_HOST={{ environments_config[env].db_host }}
DB_PORT={{ environments_config[env].db_port }}
Modular templates
A collection of approaches supporting composition of multiple copier templates
Use Separate Directory structure
Put your files in separate directories using locations like
.gitlab-ci/{{ template_name }}/.gitlab-ci.yml
scripts/{{ template_name }}/your_templates_script.sh
{{ user_specified_name }}/your_files.py
Where needed these file can be imported/included into top level files with tools that support this
Set a unique _answers_file
name
_answers_file: .copier.unique-name-for-template.yaml
Answering questions from existing yaml files
If you construct sets of related modular copier templates, then you might be able to reuse answers from another copier answers file, or construct a special interface file in a parent template for use in a child template to reduce the number of questions users need to answer.
The most basic way to do this is with
copier --data-file answers-in-file.yaml
For more advanced cases _external_data
can be used with jinja templating e.g. to convert names
# path/to/file/relative/to/copier/target/directory.yaml
different_name: value
_external_data:
existing_file: "path/to/file/relative/to/copier/target/directory.yaml"
question:
default: "{{ _external_data.existing_file.different_name }}"