Description
Is your feature request related to a problem? Please describe.
I'd like to create a Copier base template (e.g. a general Python project template) and Copier child templates (e.g. a Python web app project template using FastAPI, a Python ML library template using PyTorch, etc.). The Copier child templates would mostly extend the Copier base template (e.g. add new files, add content to existing files from the Copier base template) but might also overwrite files/folders in the Copier base template (e.g. modify content in existing files from the Copier base template) or even delete files from the Copier base template (although this might be a rare case). Naively, I could create independent Copier templates, but then maintenance wouldn't scale because I'd have to sync the common parts manually. Instead, I would like to update a Copier child template with the changes of its Copier base template in the same way as I can update a generated project with the changes of its associated Copier template.
Describe the solution you'd like
I know this topic has been discussed a few times already (#79, #402, #416), but I haven't found a satisfactory solution and came up with a (to my knowledge) new approach that I'd like to suggest and discuss as it doesn't seem possible out of the box.
To clarify possibly confusing terminology upfront, I'd like to explicitly distinguish between Copier templates (i.e. project templates) and Jinja templates (i.e. content templates used in text files or file/folder names).
So far, I've seen this topic under the term "template inheritance" which (to me) sounds similar to the idea of, e.g., OverlayFS where multiple filesystem sources are merged, possibly enhanced with Jinja inheritance such that an overwritten file may actually be an extension of the base file rather than a complete replacement. I believe #79 tried to implement such an approach, but it was not merged for apparently good reason.
I've been giving this problem some thought for a while and believe that the word "inheritance" might be misleading. Instead of trying to inherit from a Copier base template, I think a Copier child template should be a (modified) copy of the Copier base template containing a reference to the Copier base template similar to how a generated project has a reference to its corresponding Copier template in .copier-answers.yml
. Then, a very similar update mechanism as for updating a generated project could be used to update a Copier child template with the changes of its associated Copier base template:
graph TD
%% nodes ----------------------------------------------------------
base_template_repo("base template repository")
base_template_current("/tmp/base_template<br>(current tag)")
base_template_latest("/tmp/base_template<br>(latest tag)")
child_template_regen("/tmp/child_template<br>(fresh, current version)")
child_template_current("current child template")
child_template_half("half migrated<br>child template")
child_template_updated("updated child template")
child_template_applied("updated child template<br>(diff applied)")
child_template_full("fully updated<br>and migrated child template")
update["update current<br>child template in-place<br>+ run tasks again"]
compare["compare to get diff"]
apply["apply diff"]
diff("diff")
%% edges ----------------------------------------------------------
base_template_repo --> |git clone| base_template_current
base_template_repo --> |git clone| base_template_latest
base_template_current --> |copy and run tasks| child_template_regen
child_template_current --> compare
child_template_current --> |apply pre-migrations| child_template_half
child_template_regen --> compare
child_template_half --> update
base_template_latest --> update
update --> child_template_updated
compare --> diff
diff --> apply
child_template_updated --> apply
apply --> child_template_applied
child_template_applied --> |apply post-migrations| child_template_full
%% style ----------------------------------------------------------
classDef blackborder stroke:#000;
class compare,update,apply blackborder;
This approach would offer significant flexibility for customizing a Copier child template because anything can be changed without requiring the developer of the Copier base template to anticipate extension points (i.e. Jinja blocks).
In contrast to generating projects from a Copier template, generating a Copier child template doesn't involve a questionnaire or Jinja templating. Currently, I think only the following settings (from copier.yml
) remain relevant:
answers_file
(But since there are no questions to ask for generating Copier child templates, this setting might be renamed or removed entirely and a hardcoded file name might be used instead.)migrations
min_copier_version
skip_if_exists
tasks
So perhaps a new file, e.g. copier-template.yml
, should be introduced where the settings for generating Copier child templates are put. The Git rev (_commit: <rev>
) of the Copier base template might be written to a file like .copier-extension.yml
(or a different file name; subject to discussion).
Let's make this idea more concrete.
The filesystem layout of a Copier base template might look like this (with _subdirectory: template
in copier.yml
):
.
├── copier.yml
├── copier-template.yml
└── template
├── {{_copier_conf.answers_file}}.jinja
├── pyproject.toml.jinja
└── ...
And the filesystem layout of a Copier child template (which itself could serve as a Copier base template again) might look like this:
.
├── .copier-extension.yml
├── copier.yml
├── copier-template.yml
└── template
├── {{_copier_conf.answers_file}}.jinja
├── Dockerfile.jinja
├── pyproject.toml.jinja
└── ...
In order to speed-up the adoption of this idea, it might be possible to make copier-template.yml
optional and assume sane defaults (i.e. no migrations, no tasks, ...) when this file is not present.
Finally, two new CLI subcommands would need to be added to the Copier CLI, one for generating a new Copier child template from a Copier base template and one for updating an existing Copier child template from its associated Copier base template:
copier template <base_template_src> <child_template_path>
copier template update
Describe alternatives you've considered
Both YAML includes of several copier.yml
files and appyling multiple templates to the same subproject don't solve this problem.
I also thought about using Git submodules to include a Copier base template repository in a Copier child template repository, but this approach doesn't work either for several reasons:
- Git submodules cannot be pinned to tags (at least not with SSH URLs).
- The approach would follow the idea of WIP - Folder inheritance #79 (if I'm not mistaken) while missing a lot. For instance, I don't see how Copier would fall back to copying and rendering all files and folders from the Copier base template (included via a Git submodule) unless the Copier child template contained files and folders with the same name, thus taking precedence over those in the Copier base template. And in any case, WIP - Folder inheritance #79 seems like the wrong approach.
Additional context
I think it would be a huge win for Copier to support Copier template hierarchies. If others agree with this idea or we refine it to a promising proposal, I'd be happy to work on a draft PR.