This commit is contained in:
Timothée Mazzucotelli 2020-07-23 19:57:56 +02:00 committed by Jairo Llopis
parent 45c5f0d90b
commit 3ad2c67a20
33 changed files with 1129 additions and 692 deletions

1
.gitignore vendored
View File

@ -33,6 +33,7 @@ pip-wheel-metadata
# Documentation
*/_build/*
*/site/*
/site
.mypy_cache/*
.cache
*.log

View File

@ -70,14 +70,3 @@ repos:
- id: mixed-line-ending
args: ["--fix=lf"]
- id: trailing-whitespace
# regenerate the README table of contents
- repo: https://github.com/thlorenz/doctoc
rev: v1.4.0
hooks:
- id: doctoc
args:
- --github
- --maxlevel=6
- --title=<summary>Table of contents</summary>
exclude: ^tests/

View File

@ -3,29 +3,18 @@
All notable changes to this project will be documented in this file. This project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
<details>
<!-- prettier-ignore-start -->
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
<summary>Table of contents</summary>
### Version 4.0.2 (2020-07-21)
- [Version 4.0.0 (2020-06)](#version-400-2020-06)
- [Version 3.2.0 (2020-06)](#version-320-2020-06)
- [Version 3.1.0 (2020-05)](#version-310-2020-05)
- [Version 3.0.0 (2020-03)](#version-300-2020-03)
- [Features](#features)
- [Other](#other)
- [Version 2.5 (2019-06)](#version-25-2019-06)
- [Version 2.4.2 (2019-06)](#version-242-2019-06)
- [Version 2.4 (2019-06)](#version-24-2019-06)
- [Version 2.3 (2019-04)](#version-23-2019-04)
- [Version 2.2 (2019-04)](#version-22-2019-04)
- [Version 2.1 (2019-02)](#version-21-2019-02)
- [Version 2.0 (2019-02)](#version-20-2019-02)
[All changes here](https://github.com/copier-org/copier/milestone/11?closed=1). Summary:
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
<!-- prettier-ignore-end -->
</details>
- Fix wrong templated default answers classification, which produced some questions being ignored.
### Version 4.0.1 (2020-06-23)
[All changes here](https://github.com/copier-org/copier/milestone/10?closed=1). Summary:
- Fix wrong prompt regression when updating.
- Remove redundant `dst` fixture in tests.
### Version 4.0.0 (2020-06)

View File

@ -3,25 +3,6 @@
Contributions are welcome, and they are greatly appreciated! Every little bit helps, and
credit will always be given.
<details>
<!-- prettier-ignore-start -->
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
<summary>Table of contents</summary>
- [Report Bugs](#report-bugs)
- [Fix Bugs](#fix-bugs)
- [Implement Features](#implement-features)
- [Write Documentation](#write-documentation)
- [Submit Feedback](#submit-feedback)
- [Get Started!](#get-started)
- [Pull Request Guidelines](#pull-request-guidelines)
- [Tips](#tips)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
<!-- prettier-ignore-end -->
</details>
## Report Bugs
Report bugs at <https://github.com/jpscaletti/copier/issues>.

View File

@ -23,6 +23,10 @@ clean-pyc:
find . -name '__pycache__' -exec rm -rf {} +
find . -name '.pytest_cache' -exec rm -rf {} +
.PHONY: docs
docs:
mkdocs serve
test:
pytest -x copier tests

577
README.md
View File

@ -1,4 +1,5 @@
## Think this library is awesome? Vote with a 👍 to include it in the awesome-python list: https://github.com/vinta/awesome-python/pull/1350
**Think this library is awesome? Vote with a 👍 to include it in the awesome-python
list: https://github.com/DoronCohen/awesome-python/pull/1**
# ![Copier](https://github.com/pykong/copier/raw/master/img/copier-logotype.png)
@ -19,40 +20,6 @@ A library for rendering project templates.
![Sample output](https://github.com/pykong/copier/raw/master/img/copier-output.png)
<details>
<!-- prettier-ignore-start -->
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
<summary>Table of contents</summary>
- [Installation](#installation)
- [Quick usage](#quick-usage)
- [Creating a template](#creating-a-template)
- [The `copier.yml` file](#the-copieryml-file)
- [Prompt the user for information](#prompt-the-user-for-information)
- [Advanced prompt formatting](#advanced-prompt-formatting)
- [Prompt templating](#prompt-templating)
- [Special options](#special-options)
- [Patterns syntax](#patterns-syntax)
- [Examples for pattern matching](#examples-for-pattern-matching)
- [Include other yaml files](#include-other-yaml-files)
- [The `.copier-answers.yml` file](#the-copier-answersyml-file)
- [Template helpers](#template-helpers)
- [Builtin variables/functions](#builtin-variablesfunctions)
- [Builtin filters](#builtin-filters)
- [Generating a project](#generating-a-project)
- [Updating a project](#updating-a-project)
- [Browse or tag public templates](#browse-or-tag-public-templates)
- [API](#api)
- [copier.copy()](#copiercopy)
- [Comparison with other project generators](#comparison-with-other-project-generators)
- [Cookiecutter](#cookiecutter)
- [Credits](#credits)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
<!-- prettier-ignore-end -->
</details>
## Installation
1. Install Git 2.24 or newer.
@ -85,444 +52,6 @@ copy("gl:pykong/copier.git", "path/to/destination")
copier path/to/project/template path/to/destination
```
## Creating a template
A template is a directory: usually the root folder of a git repository.
The content of the files inside the project template is copied to the destination
without changes, **unless they end with `.tmpl`** (or your chosen `templates_suffix`).
In that case, the templating engine will be used to render them.
A slightly customized Jinja2 templating is used. The main difference is those variables
are referenced with `[[ name ]]` instead of `{{ name }}` and blocks are `[% if name %]`
instead of `{% if name %}`. To read more about templating see the
[Jinja2 documentation](https://jinja.palletsprojects.com/).
If a **YAML** file named `copier.yml` is found in the root of the project
(alternatively, a YAML file named `copier.yaml`), the user will be prompted to fill in
or confirm the default values.
Since version 3.0, only Python 3.6 or later are supported. Please use the 2.5.1 version
if your project runs on a previous Python version.
### The `copier.yml` file
If a `copier.yml`, or `copier.yaml` is found in the root of the template, it will be
read and used for two purposes:
- prompting the user for information
- configuring project generation (excluding files, setting arguments defaults, etc.)
#### Prompt the user for information
For each key found, Copier will prompt the user to fill or confirm the values before
they become available to the project template. So content like this:
```yaml
name_of_the_project: My awesome project
number_of_eels: 1234
your_email: ""
```
will result in this series of questions:
<pre>
<b>name_of_the_project</b>? Format: yaml
🎤 [My awesome project]:
<b>number_of_eels</b>? Format: yaml
🎤 [1234]:
<b>your_email</b>? Format: yaml
🎤 []:
</pre>
##### Advanced prompt formatting
Apart from the simplified format, as seen above, Copier supports a more advanced format
to ask users for data. To use it, the value must be a dict.
Supported keys:
- **type**: User input must match this type. Options are: bool, float, int, json, str,
yaml.
- **help**: Additional text to help the user know what's this question for.
- **default**: Leave empty to force the user to answer. Provide a default to save him
from typing it if it's quite common. When using **choices**, the default must be the
choice _value_, not its _key_. If values are quite long, you can use
[YAML anchors](https://confluence.atlassian.com/bitbucket/yaml-anchors-960154027.html).
```yaml
love_copier:
type: bool # This makes Copier ask for y/n
help: Do you love Copier?
default: yes # Without a default, you force the user to answer
project_name:
type: str # Any value will be treated raw as a string
help: An awesome project needs an awesome name. Tell me yours.
default: paradox-specifier
rocket_launch_password:
type: str
secret: true # This value will not be logged into .copier-answers.yml
default: my top secret password
# I'll avoid default and help here, but you can use them too
age:
type: int
height:
type: float
any_json:
help: Tell me anything, but format it as a one-line JSON string
type: json
any_yaml:
help: Tell me anything, but format it as a one-line YAML string
type: yaml # This is the default type, also for short syntax questions
your_favorite_book:
# User will type 1 or 2, but your template will get the value
choices:
- The Bible
- The Hitchhiker's Guide to the Galaxy
project_license:
# User will type 1 or 2 and will see only the dict key, but you will
# get the dict value in your template
choices:
MIT: &mit_text |
Here I can write the full text of the MIT license.
This will be a long text, shortened here for example purposes.
Apache2: |
Full text of Apache2 license.
# When using choices, the default value is the value, **not** the key;
# that's why I'm using the YAML anchor declared above to avoid retyping the
# whole license
default: *mit_text
# You can still define the type, to make sure answers that come from --data
# CLI argument match the type that your template expects
type: str
close_to_work:
help: Do you live close to your work?
# This format works just like the dict one
choices:
- [at home, I work at home]
- [less than 10km, quite close]
- [more than 10km, not so close]
- [more than 100km, quite far away]
```
##### Prompt templating
Values of prompted keys can use Jinja templates.
Keep in mind that the configuration is loaded as **YAML**, so the contents must be
**valid YAML** and respect **Copier's structure**. That is why we explicitly wrap some
strings in double-quotes in the following examples.
Answers provided through interactive prompting will not be rendered with Jinja, so you
cannot use Jinja templating in your answers.
```yaml
# default
username:
type: str
organization:
type: str
email:
type: str
default: "[[ username ]]@[[ organization ]].com"
# help
copyright_holder:
type: str
help: The person or entity within [[ organization ]] that holds copyrights.
# type
target:
type: str
choices:
- humans
- machines
user_config:
type: "[% if target == 'humans' %]yaml[% else %]json[% endif %]"
# choices
title:
type: str
help: Your title within [[ organization ]]
contact:
choices:
Copyright holder: "[[ copyright_holder ]]"
CEO: Alice Bob
CTO: Carl Dave
"[[ title ]]": "[[ username ]]"
```
#### Special options
Copier will also read special configuration options from the `copier.yml` file. They all
start with an underscore.
```yaml
# Specify the minimum required version of Copier to generate a project from this template.
# The version must be follow the PEP 440 syntax.
# Upon generating or updating a project, if the installed version of Copier is less than the required one,
# the generation will be aborted and an error will be shown to the user.
_min_copier_version: "4.1.0"
# File where answers will be recorded. Defaults to `.copier-answers.yml`.
# Remember to add that file to your template if you want to support updates.
_answers_file: .my-custom-answers.yml
# Suffix that instructs which files are to be processed by Jinja as templates
_templates_suffix: .tmpl
# gitignore-style patterns files/folders that must not be copied.
# Can be overridden with the `exclude` CLI/API option.
_exclude:
- "*.bar"
- ".git"
# gitignore-style patterns files to skip, without asking, if they already exists
# in the destination folder
# Can be overridden with the `skip_if_exist` API option.
_skip_if_exists:
# Subdirectory to use as the template root when generating a project.
# If not specified, the root of the git repository is used.
# Can be overridden with the `subdirectory` CLI/API option.
_subdirectory: "project"
# Commands to execute after generating or updating a project from your template.
# They run ordered, and with the $STAGE=task variable in their environment.
# Can be overridden with the `tasks` API option.
_tasks:
# Strings get executed under system's default shell
- "git init"
- "rm [[ name_of_the_project / 'README.md' ]]"
# Arrays are executed without shell, saving you the work of escaping arguments
- [invoke, "--search-root=[[ _copier_conf.src_path ]]", after-copy]
# You are able to output the full conf to JSON, to be parsed by your script,
# but you cannot use the normal `|tojson` filter; instead, use `.json()`
- [invoke, end-process, "--full-conf=[[ _copier_conf.json() ]]"]
# Migrations are like tasks, but they are executed:
# - Evaluated using PEP 440
# - In the same order as declared here (so you could even run a migration for a higher
# version before running a migration for a lower version if the higher one is declared
# before and the update passes through both)
# - Only when new version >= declared version > old version
# - Only when updating
# - After being rendered with Jinja, with the same context as the rest of the template
# - With $VERSION_FROM, $VERSION_TO, $VERSION_CURRENT and $STAGE (before/after)
# environment variables
# - The answers file is reloaded after running migrations in the "before" stage.
_migrations:
- version: v1.0.0
before:
- rm ./old-folder
after:
# [[ _copier_conf.src_path ]] points to the path where the template was
# cloned, so it can be helpful to run migration scripts stored there.
- invoke -r [[ _copier_conf.src_path ]] -c migrations migrate $VERSION_CURRENT
# Additional paths, from where to search for templates
# Can be overridden with the `extra_paths` API option.
_extra_paths:
- ~/Projects/templates
```
##### Patterns syntax
Copier supports matching names against patterns in a gitignore style fashion. This works
for the options `exclude` and `skip`. This means you can write patterns as you would for
any `.gitignore` file. The full range of the gitignore syntax ist supported via
[pathspec](https://github.com/cpburnz/python-path-specification).
###### Examples for pattern matching
Putting the following settings in your `copier.yaml` file would exclude all files ending
with "txt" from being copied to the destination folder, except the file `a.txt`.
```yaml
_exclude:
# match all text files...
- "*.txt"
# .. but not this one:
- "!a.txt"
```
#### Include other yaml files
To reuse configurations across templates you can reference other yaml files. You just
need to state the `!include` together with the absolute or relative path to the file to
be included. Multiple files can be included per `copier.yml`. For more detailed
instructions, see [pyyaml-include](https://github.com/tanbro/pyyaml-include#usage).
```yaml
# other_place/include_me.yml
common_setting: "1"
# copier.yml
!include other_place/include_me.yml
```
### The `.copier-answers.yml` file
If the destination path exists and a `.copier-answers.yml` file is present there, it
will be used to load the last user's answers to the questions made in
[the `copier.yml` file](#the-copieryml-file).
This makes projects easier to update because when the user is asked, the default answers
will be the last ones he used.
To make sure projects based on your templates can make use of this nice feature, **add a
file called `[[ _copier_conf.answers_file ]].tmpl`** (or your chosen `templates_suffix`)
in your template's root folder, with this content:
```yml
# Changes here will be overwritten by Copier
[[_copier_answers|to_nice_yaml]]
```
If this file is called different than `[[ _copier_conf.answers_file ]].tmpl` your users
will not be able to choose a custom answers file name, and thus they will not be able to
integrate several updatable templates into one destination directory.
The builtin `_copier_answers` variable includes all data needed to smooth future updates
of this project. This includes (but is not limited to) all JSON-serializable values
declared as user questions in [the `copier.yml` file](#the-copieryml-file).
As you can see, you also have the power to customize what will be logged here. Keys that
start with an underscore (`_`) are specific to Copier. Other keys should match questions
in `copier.yml`.
If you plan to integrate several templates into one single downstream project, you can
use a different path for this file:
```yaml
# In your `copier.yml`:
_answers_file: .my-custom-answers.yml
```
### Template helpers
In addition to
[all the features Jinja supports](https://jinja.palletsprojects.com/en/2.11.x/templates/),
Copier includes:
#### Builtin variables/functions
- `now()` to get current UTC time.
- `make_secret()` to get a random string.
- `_copier_answers` includes the current answers dict, but slightly modified to make it
suitable to [autoupdate your project safely](#the-answers-file):
- It doesn't contain secret answers.
- It doesn't contain any data that is not easy to render to JSON or YAML.
- It contains special keys like `_commit` and `_src_path`, indicating how the last
template update was done.
- `_copier_conf` includes the current copier `ConfigData` object, also slightly
modified:
- It only contains JSON-serializable data.
- But you have to serialize it with `[[ _copier_conf.json() ]]` instead of
`[[ _copier_conf|tojson ]]`.
- ⚠️ It contains secret answers inside its `.data` key.
- Modifying it doesn't alter the current rendering configuration.
#### Builtin filters
- `anything|to_nice_yaml` to print as pretty-formatted YAML.
Without arguments it defaults to:
`anything|to_nice_yaml(indent=2, width=80, allow_unicode=True)`, but you can modify
those.
## Generating a project
**Warning:** Generate projects only from trusted templates as their tasks run with the
same level of access as your user.
As seen in the quick usage section, you can generate a project from a template using the
`copier` command-line tool:
```bash
copier path/to/project/template path/to/destination
```
Or within Python code:
```python
copier.copy("path/to/project/template", "path/to/destination")
```
The "template" parameter can be a local path, an URL, or a shortcut URL:
- GitHub: `gh:namespace/project`
- GitLab: `gl:namespace/project`
Use the `--data` command-line argument or the `data` parameter of the `copier.copy()`
function to pass whatever extra context you want to be available in the templates. The
arguments can be any valid Python value, even a function.
Use the `--vcs-ref` command-line argument to checkout a particular git ref before
generating the project.
All the available options are described with the `--help-all` option.
## Updating a project
The best way to update a project from its template is when all of these conditions are
true:
1. The template includes a valid `.copier-answers.yml` file.
2. The template is versioned with git (with tags).
3. The destination folder is versioned with git.
If that's your case, then just enter the destination folder, make sure `git status`
shows it clean, and run:
```bash
copier update
```
This will read all available git tags, will compare them using
[PEP 440](https://www.python.org/dev/peps/pep-0440/), and will check out the latest one
before updating. To update to the latest commit, add `--vcs-ref=HEAD`. You can use any
other git ref you want.
When updating, Copier will do its best to respect your project evolution by using the
answers you provided when copied last time. However, sometimes it's impossible for
Copier to know what to do with a diff code hunk. In those cases, you will find `*.rej`
files that contain the unresolved diffs. _You should review those manually_ before
committing.
You probably don't want `*.rej` files in your git history, but if you add them to
`.gitignore`, some important changes could pass unnoticed to you. That's why the
recommended way to deal with them is to _not_ add them to add a
[pre-commit](https://pre-commit.com/) (or equivalent) hook that forbids them, just like
this:
```yaml
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: forbidden-files
name: forbidden files
entry: found copier update rejection files; review them and remove them
language: fail
files: "\\.rej$"
```
## Browse or tag public templates
You can browse public copier templates in GitHub using
@ -531,108 +60,6 @@ inspiration!
If you want your template to appear in that list, just add the topic to it! 🏷
## API
### copier.copy()
```python
copier.copy(
src_path,
dst_path,
data=DEFAULT_DATA,
*,
exclude=DEFAULT_FILTER,
skip_if_exists=[],
tasks=[],
envops={},
extra_paths=[],
pretend=False,
force=False,
skip=False,
quiet=False,
cleanup_on_error=True,
subdirectory=None,
)
```
Uses the template in _src_path_ to generate a new project at _dst_path_.
**Arguments**:
- **src_path** (str):<br> Absolute path to the project skeleton, which can also be a
version control system URL.
- **dst_path** (str):<br> Absolute path to where to render the skeleton.
- **data** (dict):<br> Data to be passed to the templates in addition to the user data
from a `copier.yml`.
- **exclude** (list):<br> A list of names or gitignore-style patterns matching files or
folders that must not be copied.
- **skip_if_exists** (list):<br> A list of names or gitignore-style patterns matching
files or folders, that are skipped if another with the same name already exists in the
destination folder. (It only makes sense if you are copying to a folder that already
exists).
- **tasks** (list):<br> Optional lists of commands to run in order after finishing the
copy. Like in the templates files, you can use variables on the commands that will be
replaced by the real values before running the command. If one of the commands fails,
the rest of them will not run.
- **envops** (dict):<br> Extra options for the Jinja template environment. See available
options in
[Jinja's docs](https://jinja.palletsprojects.com/en/2.10.x/api/#jinja2.Environment).
Copier uses these defaults that are different from Jinja's:
```yml
# copier.yml
_envops:
block_start_string: "[%"
block_end_string: "%]"
comment_start_string: "[#"
comment_end_string: "#]"
variable_start_string: "[["
variable_end_string: "]]"
keep_trailing_newline: true
```
You can use default Jinja syntax with:
```yml
# copier.yml
_envops:
block_start_string: "{%"
block_end_string: "%}"
comment_start_string: "{#"
comment_end_string: "#}"
variable_start_string: "{{"
variable_end_string: "}}"
keep_trailing_newline: false
```
- **extra_paths** (list):<br> Additional paths, from where to search for templates. This
is intended to be used with shared parent templates, files with macros, etc. outside
the copied project skeleton.
- **pretend** (bool):<br> Run but do not make any changes.
- **force** (bool):<br> Overwrite files that already exist, without asking.
- **skip** (bool):<br> Skip files that already exist, without asking.
- **quiet** (bool):<br> Suppress the status output.
- **cleanup_on_error** (bool):<br> Remove the destination folder if the copy process or
one of the tasks fails. True by default.
- **subdirectory** (str):<br> Path to a sub-folder to use as the root of the template
when generating the project. If not specified, the root of the git repository is used.
## Comparison with other project generators
### Cookiecutter

View File

@ -1,4 +1,10 @@
#!/usr/bin/env python
"""
Command line entrypoint.
This module declares the Plumbum CLI application, its subcommand and options.
"""
import sys
from textwrap import dedent
@ -7,10 +13,12 @@ from plumbum import cli, colors
from . import __version__
from .config.objects import UserMessageError
from .main import copy
from .types import AnyByStrDict, OptStr
from .types import AnyByStrDict, List, OptStr
def handle_exceptions(method):
"""Handle keyboard interruption while running a method."""
def _wrapper(*args, **kwargs):
try:
try:
@ -25,6 +33,21 @@ def handle_exceptions(method):
class CopierApp(cli.Application):
"""The Plumbum CLI application.
Attributes:
answers_file: The switch for the answers file option.
extra_paths: The switch for the extra paths option.
exclude: The switch for the exclude option.
vcs_ref: The switch for the VCS ref option.
subdirectory: The switch for the subdirectory option.
pretend: The flag for the pretend option.
force: The flag for the force option.
skip: The flag for the skip option.
quiet: The flag for the quiet option.
only_diff: The flag for the only diff option.
"""
DESCRIPTION = "Create a new project from a template."
DESCRIPTION_MORE = colors.yellow | dedent(
"""
@ -42,7 +65,7 @@ class CopierApp(cli.Application):
CALL_MAIN_IF_NESTED_COMMAND = False
data: AnyByStrDict = {}
answers_file = cli.SwitchAttr(
answers_file: cli.SwitchAttr = cli.SwitchAttr(
["-a", "--answers-file"],
default=None,
help=(
@ -50,13 +73,13 @@ class CopierApp(cli.Application):
"to find the answers file"
),
)
extra_paths = cli.SwitchAttr(
extra_paths: cli.SwitchAttr = cli.SwitchAttr(
["-p", "--extra-paths"],
str,
list=True,
help="Additional directories to find parent templates in",
)
exclude = cli.SwitchAttr(
exclude: cli.SwitchAttr = cli.SwitchAttr(
["-x", "--exclude"],
str,
list=True,
@ -65,7 +88,7 @@ class CopierApp(cli.Application):
"that must not be copied"
),
)
vcs_ref = cli.SwitchAttr(
vcs_ref: cli.SwitchAttr = cli.SwitchAttr(
["-r", "--vcs-ref"],
str,
help=(
@ -75,7 +98,7 @@ class CopierApp(cli.Application):
"the latest version, use `--vcs-ref=HEAD`."
),
)
subdirectory = cli.SwitchAttr(
subdirectory: cli.SwitchAttr = cli.SwitchAttr(
["-b", "--subdirectory"],
str,
help=(
@ -84,14 +107,16 @@ class CopierApp(cli.Application):
),
)
pretend = cli.Flag(["-n", "--pretend"], help="Run but do not make any changes")
force = cli.Flag(
pretend: cli.Flag = cli.Flag(
["-n", "--pretend"], help="Run but do not make any changes"
)
force: cli.Flag = cli.Flag(
["-f", "--force"], help="Overwrite files that already exist, without asking"
)
skip = cli.Flag(
skip: cli.Flag = cli.Flag(
["-s", "--skip"], help="Skip files that already exist, without asking"
)
quiet = cli.Flag(["-q", "--quiet"], help="Suppress status output")
quiet: cli.Flag = cli.Flag(["-q", "--quiet"], help="Suppress status output")
@cli.switch(
["-d", "--data"],
@ -100,11 +125,25 @@ class CopierApp(cli.Application):
list=True,
help="Make VARIABLE available as VALUE when rendering the template",
)
def data_switch(self, values):
self.data.update(value.split("=", 1) for value in values)
def data_switch(self, values: List[str]) -> None:
"""
Update data with provided values.
Arguments:
values: The list of values to apply.
Each value in the list is of the following form: `NAME=VALUE`.
"""
self.data.update(value.split("=", 1) for value in values) # type: ignore
def _copy(self, src_path: OptStr = None, dst_path: str = ".", **kwargs) -> None:
"""Run Copier's internal API using CLI switches."""
"""
Run Copier's internal API using CLI switches.
Arguments:
src_path: The source path of the template to generate the project from.
dst_path: The path to generate the project to.
**kwargs: Arguments passed to [`copy`][copier.main.copy].
"""
return copy(
data=self.data,
dst_path=dst_path,
@ -151,16 +190,26 @@ class CopierApp(cli.Application):
@CopierApp.subcommand("copy")
class CopierCopySubApp(cli.Application):
"""The `copy` subcommand."""
DESCRIPTION = "Copy form a template source to a destination"
@handle_exceptions
def main(self, template_src: str, destination_path: str) -> int:
"""The code of the `copy` subcommand."""
self.parent._copy(template_src, destination_path)
return 0
@CopierApp.subcommand("update")
class CopierUpdateSubApp(cli.Application):
"""
The `update` subcommand.
Attributes:
only_diff: The flag for the only diff option.
"""
DESCRIPTION = "Update a copy from its original template"
DESCRIPTION_MORE = """
The copy must have a valid answers file which contains info
@ -172,12 +221,13 @@ class CopierUpdateSubApp(cli.Application):
generated since the last `copier` execution. To disable that, use `--no-diff`.
"""
only_diff = cli.Flag(
only_diff: cli.Flag = cli.Flag(
["-D", "--no-diff"], default=True, help="Disable smart diff detection."
)
@handle_exceptions
def main(self, destination_path: cli.ExistingDirectory = ".") -> int:
"""The code of the `update` subcommand."""
self.parent._copy(
dst_path=destination_path, only_diff=self.only_diff,
)

View File

@ -1,3 +1,5 @@
"""Functions used to generate configuration data."""
from collections import ChainMap
from typing import Tuple

View File

@ -1,3 +1,5 @@
"""Pydantic models, exceptions and default values."""
import datetime
from collections import ChainMap
from copy import deepcopy
@ -41,6 +43,8 @@ class NoSrcPathError(UserMessageError):
class EnvOps(BaseModel):
"""Jinja2 environment options."""
autoescape: StrictBool = False
block_start_string: str = "[%"
block_end_string: str = "%]"
@ -62,6 +66,8 @@ class Migrations(BaseModel):
class ConfigData(BaseModel):
"""A model holding configuration data."""
src_path: Path
subdirectory: OptStr
dst_path: Path

View File

@ -1,3 +1,5 @@
"""Functions used to load user data."""
import json
import re
from collections import ChainMap

View File

@ -1,3 +1,5 @@
"""The main functions, used to generate or update projects."""
import filecmp
import os
import shutil
@ -58,72 +60,71 @@ def copy(
subdirectory: OptStr = None,
) -> None:
"""
Uses the template in src_path to generate a new project at dst_path.
Uses the template in `src_path` to generate a new project at `dst_path`.
Arguments:
src_path:
Absolute path to the project skeleton. May be a version control system URL.
If `None`, it will be taken from `dst_path/answers_file` or fail.
- src_path (str):
Absolute path to the project skeleton. May be a version control system URL.
If `None`, it will be taken from `dst_path/answers_file` or fail.
dst_path:
Absolute path to where to render the skeleton
- dst_path (str):
Absolute path to where to render the skeleton
data:
Optional. Data to be passed to the templates in addtion to the user data
from a `copier.json`.
- data (dict):
Optional. Data to be passed to the templates in addtion to the user data
from a `copier.json`.
answers_file:
Path where to obtain the answers recorded from the last update.
The path must be relative to `dst_path`.
- answers_file (str):
Path where to obtain the answers recorded from the last update. The path
must be relative to `dst_path`.
exclude:
A list of names or gitignore-style patterns matching files or folders that
must not be copied.
- exclude (list):
A list of names or gitignore-style patterns matching files or folders that
must not be copied.
skip_if_exists:
A list of names or gitignore-style patterns matching files or folders,
that are skipped if another with the same name already exists in the
destination folder. (It only makes sense if you are copying to a folder
that already exists).
- skip_if_exists (list):
A list of names or gitignore-style patterns matching files or folders,
that are skipped if another with the same name already exists in the
destination folder. (It only makes sense if you are copying to a folder
that already exists).
tasks:
Optional lists of commands to run in order after finishing the copy.
Like in the templates files, you can use variables on the commands that
will be replaced by the real values before running the command.
If one of the commands fail, the rest of them will not run.
- tasks (list):
Optional lists of commands to run in order after finishing the copy.
Like in the templates files, you can use variables on the commands that
will be replaced by the real values before running the command.
If one of the commands fail, the rest of them will not run.
envops:
Extra options for the Jinja template environment.
- envops (dict):
Extra options for the Jinja template environment.
extra_paths:
Optional. Additional paths, outside the `src_path`, from where to search
for templates. This is intended to be used with shared parent templates,
files with macros, etc. outside the copied project skeleton.
- extra_paths (list):
Optional. Additional paths, outside the `src_path`, from where to search
for templates. This is intended to be used with shared parent templates,
files with macros, etc. outside the copied project skeleton.
pretend:
Run but do not make any changes.
- pretend (bool):
Run but do not make any changes
force:
Overwrite files that already exist, without asking.
- force (bool):
Overwrite files that already exist, without asking
skip:
Skip files that already exist, without asking.
- skip (bool):
Skip files that already exist, without asking
quiet:
Suppress the status output.
- quiet (bool):
Suppress the status output
cleanup_on_error:
Remove the destination folder if the copy process or one of the tasks fail.
- cleanup_on_error (bool):
Remove the destination folder if the copy process or one of the tasks fail.
vcs_ref:
VCS reference to checkout in the template.
- vcs_ref (str):
VCS reference to checkout in the template.
only_diff:
Try to update only the template diff.
- only_diff (bool):
Try to update only the template diff.
- subdirectory (str):
Specify a subdirectory to use when generating the project.
subdirectory:
Specify a subdirectory to use when generating the project.
"""
conf = make_config(**locals())
is_update = conf.original_src_path != conf.src_path and vcs.is_git_repo_root(
@ -151,7 +152,11 @@ def copy(
def copy_local(conf: ConfigData) -> None:
"""Use the given configuration to generate a project.
Arguments:
conf: Configuration obtained with [`make_config`][copier.config.factory.make_config].
"""
must_filter = create_path_filter(conf.exclude)
render = Renderer(conf)
@ -199,7 +204,12 @@ def copy_local(conf: ConfigData) -> None:
print("") # padding space
def update_diff(conf: ConfigData):
def update_diff(conf: ConfigData) -> None:
"""Use the given configuration to update a project.
Arguments:
conf: Configuration obtained with [`make_config`][copier.config.factory.make_config].
"""
# Ensure local repo is clean
if vcs.is_git_repo_root(conf.dst_path):
with local.cwd(conf.dst_path):
@ -273,6 +283,19 @@ def get_source_paths(
render: Renderer,
must_filter: Callable[[StrOrPath], bool],
) -> List[Tuple[Path, Path]]:
"""Get the paths to all the files to render.
Arguments:
conf: Configuration obtained with [`make_config`][copier.config.factory.make_config].
folder:
rel_folder: Relative path to the folder.
files:
render: The [template renderer][copier.tools.Renderer] instance.
must_filter: A callable telling whether to skip rendering a file.
Returns:
The list of files to render.
"""
source_paths = []
files_set = set(files)
for src_name in files:
@ -294,6 +317,14 @@ def get_source_paths(
def render_folder(rel_folder: Path, conf: ConfigData) -> None:
"""Render a complete folder.
This function renders the folder's name as well as its contents.
Arguments:
rel_folder: The relative path to the folder.
conf: Configuration obtained with [`make_config`][copier.config.factory.make_config].
"""
dst_path = conf.dst_path / rel_folder
rel_path = f"{rel_folder}{os.path.sep}"
@ -319,7 +350,15 @@ def render_file(
render: Renderer,
must_skip: CheckPathFunc,
) -> None:
"""Process or copy a file of the skeleton."""
"""Process or copy a file of the skeleton.
Arguments:
conf: Configuration obtained with [`make_config`][copier.config.factory.make_config].
rel_path: The relative path to the file.
src_path:
render: The [template renderer][copier.tools.Renderer] instance.
must_skip: A callable telling whether to skip a file.
"""
content: Optional[str] = None
if str(src_path).endswith(conf.templates_suffix):
content = render(src_path)
@ -346,12 +385,33 @@ def render_file(
def files_are_identical(src_path: Path, dst_path: Path, content: Optional[str]) -> bool:
"""Tell whether two files are identical.
Arguments:
src_path: Source file.
dst_path: Destination file.
content: File contents.
Returns:
True if the files are identical, False otherwise.
"""
if content is None:
return filecmp.cmp(str(src_path), str(dst_path), shallow=False)
return dst_path.read_text() == content
def overwrite_file(conf: ConfigData, dst_path: Path, rel_path: Path) -> bool:
"""Handle the case when there's an update conflict.
Arguments:
conf: Configuration obtained with [`make_config`][copier.config.factory.make_config].
dst_path: The destination file.
rel_path: The new file.
Returns:
True if the overwrite was forced or the user answered yes,
False if skipped by configuration or if the user answered no.
"""
printf("conflict", rel_path, style=Style.DANGER, quiet=conf.quiet)
if conf.force:
return True
@ -361,6 +421,13 @@ def overwrite_file(conf: ConfigData, dst_path: Path, rel_path: Path) -> bool:
def run_tasks(conf: ConfigData, render: Renderer, tasks: Sequence[Dict]) -> None:
"""Run the given tasks.
Arguments:
conf: Configuration obtained with [`make_config`][copier.config.factory.make_config].
render: The [template renderer][copier.tools.Renderer] instance.
tasks: The list of tasks to run.
"""
for i, task in enumerate(tasks):
task_cmd = task["task"]
use_shell = isinstance(task_cmd, str)

View File

@ -1,3 +1,5 @@
"""Some utility functions."""
import errno
import os
import shutil
@ -125,6 +127,8 @@ def get_jinja_env(
class Renderer:
"""The Jinja template renderer."""
def __init__(self, conf: ConfigData) -> None:
envops: EnvOps = conf.envops
paths = [str(conf.src_path)] + list(map(str, conf.extra_paths or []))

View File

@ -1,3 +1,5 @@
"""All complex types and annotations are declared here."""
from pathlib import Path
from typing import (
Any,

View File

@ -1,3 +1,5 @@
"""Utilities related to VCS."""
import re
import shutil
import tempfile

101
docs/api/index.md Normal file
View File

@ -0,0 +1,101 @@
## API
### copier.copy()
```python
copier.copy(
src_path,
dst_path,
data=DEFAULT_DATA,
*,
exclude=DEFAULT_FILTER,
skip_if_exists=[],
tasks=[],
envops={},
extra_paths=[],
pretend=False,
force=False,
skip=False,
quiet=False,
cleanup_on_error=True,
subdirectory=None,
)
```
Uses the template in _src_path_ to generate a new project at _dst_path_.
**Arguments**:
- **src_path** (str):<br> Absolute path to the project skeleton, which can also be a
version control system URL.
- **dst_path** (str):<br> Absolute path to where to render the skeleton.
- **data** (dict):<br> Data to be passed to the templates in addition to the user data
from a `copier.yml`.
- **exclude** (list):<br> A list of names or gitignore-style patterns matching files or
folders that must not be copied.
- **skip_if_exists** (list):<br> A list of names or gitignore-style patterns matching
files or folders, that are skipped if another with the same name already exists in the
destination folder. (It only makes sense if you are copying to a folder that already
exists).
- **tasks** (list):<br> Optional lists of commands to run in order after finishing the
copy. Like in the templates files, you can use variables on the commands that will be
replaced by the real values before running the command. If one of the commands fails,
the rest of them will not run.
- **envops** (dict):<br> Extra options for the Jinja template environment. See available
options in
[Jinja's docs](https://jinja.palletsprojects.com/en/2.10.x/api/#jinja2.Environment).
Copier uses these defaults that are different from Jinja's:
```yml
# copier.yml
_envops:
block_start_string: "[%"
block_end_string: "%]"
comment_start_string: "[#"
comment_end_string: "#]"
variable_start_string: "[["
variable_end_string: "]]"
keep_trailing_newline: true
```
You can use default Jinja syntax with:
```yml
# copier.yml
_envops:
block_start_string: "{%"
block_end_string: "%}"
comment_start_string: "{#"
comment_end_string: "#}"
variable_start_string: "{{"
variable_end_string: "}}"
keep_trailing_newline: false
```
- **extra_paths** (list):<br> Additional paths, from where to search for templates. This
is intended to be used with shared parent templates, files with macros, etc. outside
the copied project skeleton.
- **pretend** (bool):<br> Run but do not make any changes.
- **force** (bool):<br> Overwrite files that already exist, without asking.
- **skip** (bool):<br> Skip files that already exist, without asking.
- **quiet** (bool):<br> Suppress the status output.
- **cleanup_on_error** (bool):<br> Remove the destination folder if the copy process or
one of the tasks fails. True by default.
- **subdirectory** (str):<br> Path to a sub-folder to use as the root of the template
when generating the project. If not specified, the root of the git repository is used.

View File

@ -0,0 +1 @@
::: copier.cli

View File

@ -0,0 +1 @@
::: copier.config.factory

View File

@ -0,0 +1 @@
::: copier.config.objects

View File

@ -0,0 +1 @@
::: copier.config.user_data

View File

@ -0,0 +1 @@
::: copier.main

View File

@ -0,0 +1 @@
::: copier.tools

View File

@ -0,0 +1 @@
::: copier.types

View File

@ -0,0 +1 @@
::: copier.vcs

1
docs/changelog.md Symbolic link
View File

@ -0,0 +1 @@
../CHANGELOG.md

1
docs/contributing.md Symbolic link
View File

@ -0,0 +1 @@
../CONTRIBUTING.md

360
docs/creating.md Normal file
View File

@ -0,0 +1,360 @@
# Creating a template
A template is a directory: usually the root folder of a git repository.
The content of the files inside the project template is copied to the destination
without changes, **unless they end with `.tmpl`** (or your chosen `templates_suffix`).
In that case, the templating engine will be used to render them.
A slightly customized Jinja2 templating is used. The main difference is those variables
are referenced with `[[ name ]]` instead of `{{ name }}` and blocks are `[% if name %]`
instead of `{% if name %}`. To read more about templating see the
[Jinja2 documentation](https://jinja.palletsprojects.com/).
If a **YAML** file named `copier.yml` is found in the root of the project
(alternatively, a YAML file named `copier.yaml`), the user will be prompted to fill in
or confirm the default values.
Since version 3.0, only Python 3.6 or later are supported. Please use the 2.5.1 version
if your project runs on a previous Python version.
## The `copier.yml` file
If a `copier.yml`, or `copier.yaml` is found in the root of the template, it will be
read and used for two purposes:
- prompting the user for information
- configuring project generation (excluding files, setting arguments defaults, etc.)
### Prompt the user for information
For each key found, Copier will prompt the user to fill or confirm the values before
they become available to the project template. So content like this:
```yaml
name_of_the_project: My awesome project
number_of_eels: 1234
your_email: ""
```
will result in this series of questions:
<pre>
<b>name_of_the_project</b>? Format: yaml
🎤 [My awesome project]:
<b>number_of_eels</b>? Format: yaml
🎤 [1234]:
<b>your_email</b>? Format: yaml
🎤 []:
</pre>
#### Advanced prompt formatting
Apart from the simplified format, as seen above, Copier supports a more advanced format
to ask users for data. To use it, the value must be a dict.
Supported keys:
- **type**: User input must match this type. Options are: bool, float, int, json, str,
yaml.
- **help**: Additional text to help the user know what's this question for.
- **default**: Leave empty to force the user to answer. Provide a default to save him
from typing it if it's quite common. When using **choices**, the default must be the
choice _value_, not its _key_. If values are quite long, you can use
[YAML anchors](https://confluence.atlassian.com/bitbucket/yaml-anchors-960154027.html).
```yaml
love_copier:
type: bool # This makes Copier ask for y/n
help: Do you love Copier?
default: yes # Without a default, you force the user to answer
project_name:
type: str # Any value will be treated raw as a string
help: An awesome project needs an awesome name. Tell me yours.
default: paradox-specifier
rocket_launch_password:
type: str
secret: true # This value will not be logged into .copier-answers.yml
default: my top secret password
# I'll avoid default and help here, but you can use them too
age:
type: int
height:
type: float
any_json:
help: Tell me anything, but format it as a one-line JSON string
type: json
any_yaml:
help: Tell me anything, but format it as a one-line YAML string
type: yaml # This is the default type, also for short syntax questions
your_favorite_book:
# User will type 1 or 2, but your template will get the value
choices:
- The Bible
- The Hitchhiker's Guide to the Galaxy
project_license:
# User will type 1 or 2 and will see only the dict key, but you will
# get the dict value in your template
choices:
MIT: &mit_text |
Here I can write the full text of the MIT license.
This will be a long text, shortened here for example purposes.
Apache2: |
Full text of Apache2 license.
# When using choices, the default value is the value, **not** the key;
# that's why I'm using the YAML anchor declared above to avoid retyping the
# whole license
default: *mit_text
# You can still define the type, to make sure answers that come from --data
# CLI argument match the type that your template expects
type: str
close_to_work:
help: Do you live close to your work?
# This format works just like the dict one
choices:
- [at home, I work at home]
- [less than 10km, quite close]
- [more than 10km, not so close]
- [more than 100km, quite far away]
```
#### Prompt templating
Values of prompted keys can use Jinja templates.
Keep in mind that the configuration is loaded as **YAML**, so the contents must be
**valid YAML** and respect **Copier's structure**. That is why we explicitly wrap some
strings in double-quotes in the following examples.
Answers provided through interactive prompting will not be rendered with Jinja, so you
cannot use Jinja templating in your answers.
```yaml
# default
username:
type: str
organization:
type: str
email:
type: str
default: "[[ username ]]@[[ organization ]].com"
# help
copyright_holder:
type: str
help: The person or entity within [[ organization ]] that holds copyrights.
# type
target:
type: str
choices:
- humans
- machines
user_config:
type: "[% if target == 'humans' %]yaml[% else %]json[% endif %]"
# choices
title:
type: str
help: Your title within [[ organization ]]
contact:
choices:
Copyright holder: "[[ copyright_holder ]]"
CEO: Alice Bob
CTO: Carl Dave
"[[ title ]]": "[[ username ]]"
```
### Special options
Copier will also read special configuration options from the `copier.yml` file. They all
start with an underscore.
```yaml
# Specify the minimum required version of Copier to generate a project from this template.
# The version must be follow the PEP 440 syntax.
# Upon generating or updating a project, if the installed version of Copier is less than the required one,
# the generation will be aborted and an error will be shown to the user.
_min_copier_version: "4.1.0"
# File where answers will be recorded. Defaults to `.copier-answers.yml`.
# Remember to add that file to your template if you want to support updates.
_answers_file: .my-custom-answers.yml
# Suffix that instructs which files are to be processed by Jinja as templates
_templates_suffix: .tmpl
# gitignore-style patterns files/folders that must not be copied.
# Can be overridden with the `exclude` CLI/API option.
_exclude:
- "*.bar"
- ".git"
# gitignore-style patterns files to skip, without asking, if they already exists
# in the destination folder
# Can be overridden with the `skip_if_exist` API option.
_skip_if_exists:
# Subdirectory to use as the template root when generating a project.
# If not specified, the root of the git repository is used.
# Can be overridden with the `subdirectory` CLI/API option.
_subdirectory: "project"
# Commands to execute after generating or updating a project from your template.
# They run ordered, and with the $STAGE=task variable in their environment.
# Can be overridden with the `tasks` API option.
_tasks:
# Strings get executed under system's default shell
- "git init"
- "rm [[ name_of_the_project / 'README.md' ]]"
# Arrays are executed without shell, saving you the work of escaping arguments
- [invoke, "--search-root=[[ _copier_conf.src_path ]]", after-copy]
# You are able to output the full conf to JSON, to be parsed by your script,
# but you cannot use the normal `|tojson` filter; instead, use `.json()`
- [invoke, end-process, "--full-conf=[[ _copier_conf.json() ]]"]
# Migrations are like tasks, but they are executed:
# - Evaluated using PEP 440
# - In the same order as declared here (so you could even run a migration for a higher
# version before running a migration for a lower version if the higher one is declared
# before and the update passes through both)
# - Only when new version >= declared version > old version
# - Only when updating
# - After being rendered with Jinja, with the same context as the rest of the template
# - With $VERSION_FROM, $VERSION_TO, $VERSION_CURRENT and $STAGE (before/after)
# environment variables
# - The answers file is reloaded after running migrations in the "before" stage.
_migrations:
- version: v1.0.0
before:
- rm ./old-folder
after:
# [[ _copier_conf.src_path ]] points to the path where the template was
# cloned, so it can be helpful to run migration scripts stored there.
- invoke -r [[ _copier_conf.src_path ]] -c migrations migrate $VERSION_CURRENT
# Additional paths, from where to search for templates
# Can be overridden with the `extra_paths` API option.
_extra_paths:
- ~/Projects/templates
```
#### Patterns syntax
Copier supports matching names against patterns in a gitignore style fashion. This works
for the options `exclude` and `skip`. This means you can write patterns as you would for
any `.gitignore` file. The full range of the gitignore syntax ist supported via
[pathspec](https://github.com/cpburnz/python-path-specification).
##### Examples for pattern matching
Putting the following settings in your `copier.yaml` file would exclude all files ending
with "txt" from being copied to the destination folder, except the file `a.txt`.
```yaml
_exclude:
# match all text files...
- "*.txt"
# .. but not this one:
- "!a.txt"
```
### Include other yaml files
To reuse configurations across templates you can reference other yaml files. You just
need to state the `!include` together with the absolute or relative path to the file to
be included. Multiple files can be included per `copier.yml`. For more detailed
instructions, see [pyyaml-include](https://github.com/tanbro/pyyaml-include#usage).
```yaml
# other_place/include_me.yml
common_setting: "1"
# copier.yml
!include other_place/include_me.yml
```
## The `.copier-answers.yml` file
If the destination path exists and a `.copier-answers.yml` file is present there, it
will be used to load the last user's answers to the questions made in
[the `copier.yml` file](#the-copieryml-file).
This makes projects easier to update because when the user is asked, the default answers
will be the last ones he used.
To make sure projects based on your templates can make use of this nice feature, **add a
file called `[[ _copier_conf.answers_file ]].tmpl`** (or your chosen `templates_suffix`)
in your template's root folder, with this content:
```yml
# Changes here will be overwritten by Copier
[[_copier_answers|to_nice_yaml]]
```
If this file is called different than `[[ _copier_conf.answers_file ]].tmpl` your users
will not be able to choose a custom answers file name, and thus they will not be able to
integrate several updatable templates into one destination directory.
The builtin `_copier_answers` variable includes all data needed to smooth future updates
of this project. This includes (but is not limited to) all JSON-serializable values
declared as user questions in [the `copier.yml` file](#the-copieryml-file).
As you can see, you also have the power to customize what will be logged here. Keys that
start with an underscore (`_`) are specific to Copier. Other keys should match questions
in `copier.yml`.
If you plan to integrate several templates into one single downstream project, you can
use a different path for this file:
```yaml
# In your `copier.yml`:
_answers_file: .my-custom-answers.yml
```
## Template helpers
In addition to
[all the features Jinja supports](https://jinja.palletsprojects.com/en/2.11.x/templates/),
Copier includes:
### Builtin variables/functions
- `now()` to get current UTC time.
- `make_secret()` to get a random string.
- `_copier_answers` includes the current answers dict, but slightly modified to make it
suitable to [autoupdate your project safely](#the-answers-file):
- It doesn't contain secret answers.
- It doesn't contain any data that is not easy to render to JSON or YAML.
- It contains special keys like `_commit` and `_src_path`, indicating how the last
template update was done.
- `_copier_conf` includes the current copier `ConfigData` object, also slightly
modified:
- It only contains JSON-serializable data.
- But you have to serialize it with `[[ _copier_conf.json() ]]` instead of
`[[ _copier_conf|tojson ]]`.
- ⚠️ It contains secret answers inside its `.data` key.
- Modifying it doesn't alter the current rendering configuration.
### Builtin filters
- `anything|to_nice_yaml` to print as pretty-formatted YAML.
Without arguments it defaults to:
`anything|to_nice_yaml(indent=2, width=80, allow_unicode=True)`, but you can modify
those.

33
docs/css/mkdocstrings.css Normal file
View File

@ -0,0 +1,33 @@
div.doc-contents:not(.first) {
padding-left: 25px;
border-left: 4px solid rgba(230, 230, 230);
margin-bottom: 80px;
}
h5.doc-heading {
text-transform: none !important;
}
h6.hidden-toc {
margin: 0 !important;
position: relative;
top: -70px;
}
h6.hidden-toc::before {
margin-top: 0 !important;
padding-top: 0 !important;
}
h6.hidden-toc a.headerlink {
display: none;
}
td code {
word-break: normal !important;
}
td p {
margin-top: 0 !important;
margin-bottom: 0 !important;
}

31
docs/generating.md Normal file
View File

@ -0,0 +1,31 @@
# Generating a project
**Warning:** Generate projects only from trusted templates as their tasks run with the
same level of access as your user.
As seen in the quick usage section, you can generate a project from a template using the
`copier` command-line tool:
```bash
copier path/to/project/template path/to/destination
```
Or within Python code:
```python
copier.copy("path/to/project/template", "path/to/destination")
```
The "template" parameter can be a local path, an URL, or a shortcut URL:
- GitHub: `gh:namespace/project`
- GitLab: `gl:namespace/project`
Use the `--data` command-line argument or the `data` parameter of the `copier.copy()`
function to pass whatever extra context you want to be available in the templates. The
arguments can be any valid Python value, even a function.
Use the `--vcs-ref` command-line argument to checkout a particular git ref before
generating the project.
All the available options are described with the `--help-all` option.

1
docs/index.md Symbolic link
View File

@ -0,0 +1 @@
../README.md

44
docs/updating.md Normal file
View File

@ -0,0 +1,44 @@
# Updating a project
The best way to update a project from its template is when all of these conditions are
true:
1. The template includes a valid `.copier-answers.yml` file.
2. The template is versioned with git (with tags).
3. The destination folder is versioned with git.
If that's your case, then just enter the destination folder, make sure `git status`
shows it clean, and run:
```bash
copier update
```
This will read all available git tags, will compare them using
[PEP 440](https://www.python.org/dev/peps/pep-0440/), and will check out the latest one
before updating. To update to the latest commit, add `--vcs-ref=HEAD`. You can use any
other git ref you want.
When updating, Copier will do its best to respect your project evolution by using the
answers you provided when copied last time. However, sometimes it's impossible for
Copier to know what to do with a diff code hunk. In those cases, you will find `*.rej`
files that contain the unresolved diffs. _You should review those manually_ before
committing.
You probably don't want `*.rej` files in your git history, but if you add them to
`.gitignore`, some important changes could pass unnoticed to you. That's why the
recommended way to deal with them is to _not_ add them to add a
[pre-commit](https://pre-commit.com/) (or equivalent) hook that forbids them, just like
this:
```yaml
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: forbidden-files
name: forbidden files
entry: found copier update rejection files; review them and remove them
language: fail
files: "\\.rej$"
```

47
mkdocs.yml Normal file
View File

@ -0,0 +1,47 @@
site_name: copier
site_description: Library and command-line utility for rendering projects templates.
site_url: https://copier.readthedocs.io/
repo_url: https://github.com/copier-org/copier
repo_name: copier-org/copier
nav:
- Overview: "index.md"
- Creating a template: "creating.md"
- Generating a project: "generating.md"
- Updating a project: "updating.md"
- API:
- Main usage: "api/index.md"
- Reference:
- config:
- factory.py: "api/reference/config/factory.md"
- objects.py: "api/reference/config/objects.md"
- user_data.py: "api/reference/config/user_data.md"
- cli.py: "api/reference/cli.md"
- main.py: "api/reference/main.md"
- tools.py: "api/reference/tools.md"
- types.py: "api/reference/types.md"
- vcs.py: "api/reference/vcs.md"
- Contributing: "contributing.md"
- Changelog: "changelog.md"
theme:
name: "material"
extra_css:
- css/mkdocstrings.css
markdown_extensions:
- admonition
- codehilite:
guess_lang: false
- pymdownx.superfences
- pymdownx.emoji
- pymdownx.magiclink
- toc:
permalink: true
plugins:
- search
- mkdocstrings:
watch:
- copier

285
poetry.lock generated
View File

@ -55,6 +55,21 @@ optional = false
python-versions = "*"
version = "0.2.0"
[[package]]
category = "dev"
description = "Screen-scraping library"
name = "beautifulsoup4"
optional = false
python-versions = "*"
version = "4.9.1"
[package.dependencies]
soupsieve = [">1.2", "<2.0"]
[package.extras]
html5lib = ["html5lib"]
lxml = ["lxml"]
[[package]]
category = "dev"
description = "The uncompromising code formatter."
@ -225,6 +240,14 @@ version = "3.2.1"
flake8 = ">=1.5"
pycodestyle = "*"
[[package]]
category = "dev"
description = "Clean single-source support for Python 3 and 2"
name = "future"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "0.18.2"
[[package]]
category = "dev"
description = "File identification library for Python"
@ -360,6 +383,66 @@ MarkupSafe = ">=0.23"
[package.extras]
i18n = ["Babel (>=0.8)"]
[[package]]
category = "dev"
description = "Lightweight pipelining: using Python functions as pipeline jobs."
marker = "python_version > \"2.7\""
name = "joblib"
optional = false
python-versions = ">=3.6"
version = "0.16.0"
[[package]]
category = "dev"
description = "Python LiveReload is an awesome tool for web developers"
name = "livereload"
optional = false
python-versions = "*"
version = "2.6.2"
[package.dependencies]
six = "*"
[package.dependencies.tornado]
python = ">=2.8"
version = "*"
[[package]]
category = "dev"
description = "A Python implementation of Lunr.js"
name = "lunr"
optional = false
python-versions = "*"
version = "0.5.8"
[package.dependencies]
future = ">=0.16.0"
six = ">=1.11.0"
[package.dependencies.nltk]
optional = true
python = ">=2.8"
version = ">=3.2.5"
[package.extras]
languages = ["nltk (>=3.2.5,<3.5)", "nltk (>=3.2.5)"]
[[package]]
category = "dev"
description = "Python implementation of Markdown."
name = "markdown"
optional = false
python-versions = ">=3.5"
version = "3.2.2"
[package.dependencies]
[package.dependencies.importlib-metadata]
python = "<3.8"
version = "*"
[package.extras]
testing = ["coverage", "pyyaml"]
[[package]]
category = "main"
description = "Safely add untrusted strings to HTML/XML markup."
@ -376,6 +459,66 @@ optional = false
python-versions = "*"
version = "0.6.1"
[[package]]
category = "dev"
description = "Project documentation with Markdown."
name = "mkdocs"
optional = false
python-versions = ">=3.5"
version = "1.1.2"
[package.dependencies]
Jinja2 = ">=2.10.1"
Markdown = ">=3.2.1"
PyYAML = ">=3.10"
click = ">=3.3"
livereload = ">=2.5.1"
tornado = ">=5.0"
[package.dependencies.lunr]
extras = ["languages"]
version = "0.5.8"
[[package]]
category = "dev"
description = "A Material Design theme for MkDocs"
name = "mkdocs-material"
optional = false
python-versions = "*"
version = "5.4.0"
[package.dependencies]
Pygments = ">=2.4"
markdown = ">=3.2"
mkdocs = ">=1.1"
mkdocs-material-extensions = ">=1.0"
pymdown-extensions = ">=7.0"
[[package]]
category = "dev"
description = "Extension pack for Python Markdown."
name = "mkdocs-material-extensions"
optional = false
python-versions = ">=3.5"
version = "1.0"
[package.dependencies]
mkdocs-material = ">=5.0.0"
[[package]]
category = "dev"
description = "Automatic documentation from sources, for MkDocs."
name = "mkdocstrings"
optional = false
python-versions = ">=3.6,<4.0"
version = "0.12.1"
[package.dependencies]
beautifulsoup4 = ">=4.8.2,<5.0.0"
mkdocs = ">=1.1,<2.0"
pymdown-extensions = ">=6.3,<8.0"
pytkdocs = ">=0.2.0,<0.7.0"
[[package]]
category = "dev"
description = "More routines for operating on iterables, beyond itertools"
@ -408,6 +551,29 @@ optional = false
python-versions = "*"
version = "0.4.3"
[[package]]
category = "dev"
description = "Natural Language Toolkit"
marker = "python_version > \"2.7\""
name = "nltk"
optional = false
python-versions = "*"
version = "3.5"
[package.dependencies]
click = "*"
joblib = "*"
regex = "*"
tqdm = "*"
[package.extras]
all = ["requests", "numpy", "python-crfsuite", "scikit-learn", "twython", "pyparsing", "scipy", "matplotlib", "gensim"]
corenlp = ["requests"]
machine_learning = ["gensim", "numpy", "python-crfsuite", "scikit-learn", "scipy"]
plot = ["matplotlib"]
tgrep = ["pyparsing"]
twitter = ["twython"]
[[package]]
category = "dev"
description = "Node.js virtual environment builder"
@ -583,12 +749,22 @@ version = "2.2.0"
[[package]]
category = "dev"
description = "Pygments is a syntax highlighting package written in Python."
marker = "python_version >= \"3.4\""
name = "pygments"
optional = false
python-versions = ">=3.5"
version = "2.6.1"
[[package]]
category = "dev"
description = "Extension pack for Python Markdown."
name = "pymdown-extensions"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
version = "7.1"
[package.dependencies]
Markdown = ">=3.2"
[[package]]
category = "main"
description = "Python parsing module"
@ -677,6 +853,14 @@ six = "*"
[package.extras]
testing = ["filelock"]
[[package]]
category = "dev"
description = "Load Python objects documentation."
name = "pytkdocs"
optional = false
python-versions = ">=3.6,<4.0"
version = "0.6.0"
[[package]]
category = "main"
description = "YAML parser and emitter for Python"
@ -716,6 +900,14 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
version = "1.15.0"
[[package]]
category = "dev"
description = "A modern CSS selector implementation for Beautiful Soup."
name = "soupsieve"
optional = false
python-versions = "*"
version = "1.9.6"
[[package]]
category = "dev"
description = "Python Library for Tom's Obvious, Minimal Language"
@ -724,6 +916,26 @@ optional = false
python-versions = "*"
version = "0.10.1"
[[package]]
category = "dev"
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
name = "tornado"
optional = false
python-versions = ">= 3.5"
version = "6.0.4"
[[package]]
category = "dev"
description = "Fast, Extensible Progress Meter"
marker = "python_version > \"2.7\""
name = "tqdm"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*"
version = "4.48.0"
[package.extras]
dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"]
[[package]]
category = "dev"
description = "Traitlets Python config system"
@ -805,7 +1017,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"]
[metadata]
content-hash = "b3a1fc5823ae3bb22b89ef9a0034a9759afdbd94fc9de416392c4050dda95cb2"
content-hash = "7d7386c290b8b92a9a9a35c6e00862c60a4834deb36f9284be8524a556ee163a"
python-versions = "^3.6"
[metadata.files]
@ -833,6 +1045,11 @@ backcall = [
{file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
]
beautifulsoup4 = [
{file = "beautifulsoup4-4.9.1-py2-none-any.whl", hash = "sha256:e718f2342e2e099b640a34ab782407b7b676f47ee272d6739e60b8ea23829f2c"},
{file = "beautifulsoup4-4.9.1-py3-none-any.whl", hash = "sha256:a6237df3c32ccfaee4fd201c8f5f9d9df619b93121d01353a64a73ce8c6ef9a8"},
{file = "beautifulsoup4-4.9.1.tar.gz", hash = "sha256:73cc4d115b96f79c7d77c1c7f7a0a8d4c57860d1041df407dd1aae7f07a77fd7"},
]
black = [
{file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
@ -920,6 +1137,9 @@ flake8-comprehensions = [
flake8-debugger = [
{file = "flake8-debugger-3.2.1.tar.gz", hash = "sha256:712d7c1ff69ddf3f0130e94cc88c2519e720760bce45e8c330bfdcb61ab4090d"},
]
future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
]
identify = [
{file = "identify-1.4.19-py2.py3-none-any.whl", hash = "sha256:781fd3401f5d2b17b22a8b18b493a48d5d948e3330634e82742e23f9c20234ef"},
{file = "identify-1.4.19.tar.gz", hash = "sha256:249ebc7e2066d6393d27c1b1be3b70433f824a120b1d8274d362f1eb419e3b52"},
@ -951,6 +1171,21 @@ jinja2 = [
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
]
joblib = [
{file = "joblib-0.16.0-py3-none-any.whl", hash = "sha256:d348c5d4ae31496b2aa060d6d9b787864dd204f9480baaa52d18850cb43e9f49"},
{file = "joblib-0.16.0.tar.gz", hash = "sha256:8f52bf24c64b608bf0b2563e0e47d6fcf516abc8cfafe10cfd98ad66d94f92d6"},
]
livereload = [
{file = "livereload-2.6.2.tar.gz", hash = "sha256:d1eddcb5c5eb8d2ca1fa1f750e580da624c0f7fcb734aa5780dc81b7dcbd89be"},
]
lunr = [
{file = "lunr-0.5.8-py2.py3-none-any.whl", hash = "sha256:aab3f489c4d4fab4c1294a257a30fec397db56f0a50273218ccc3efdbf01d6ca"},
{file = "lunr-0.5.8.tar.gz", hash = "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"},
]
markdown = [
{file = "Markdown-3.2.2-py3-none-any.whl", hash = "sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59"},
{file = "Markdown-3.2.2.tar.gz", hash = "sha256:1fafe3f1ecabfb514a5285fca634a53c1b32a81cb0feb154264d55bf2ff22c17"},
]
markupsafe = [
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
@ -990,6 +1225,22 @@ mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
mkdocs = [
{file = "mkdocs-1.1.2-py3-none-any.whl", hash = "sha256:096f52ff52c02c7e90332d2e53da862fde5c062086e1b5356a6e392d5d60f5e9"},
{file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"},
]
mkdocs-material = [
{file = "mkdocs-material-5.4.0.tar.gz", hash = "sha256:7cd0fabc336ef93c78693134a0f9ac62900a76f905489aa935bab0b50910c47b"},
{file = "mkdocs_material-5.4.0-py2.py3-none-any.whl", hash = "sha256:bf34d5cfbb2a085187adfcb82e8fb3bfc3014860f8416c2656dc8e9666b82eb0"},
]
mkdocs-material-extensions = [
{file = "mkdocs-material-extensions-1.0.tar.gz", hash = "sha256:17d7491e189af75700310b7ec33c6c48a22060b8b445001deca040cb60471cde"},
{file = "mkdocs_material_extensions-1.0-py3-none-any.whl", hash = "sha256:09569c3694b5acc1e8334c9730e52b4bcde65fc9d613cc20e49af131ef1a9ca0"},
]
mkdocstrings = [
{file = "mkdocstrings-0.12.1-py3-none-any.whl", hash = "sha256:346a881267c4fdc581ba910038a7e5690ed7d99c34420a393da847d79b9bfab3"},
{file = "mkdocstrings-0.12.1.tar.gz", hash = "sha256:10632762d0f4f7d243912f42e370782f4a06edd49372f5729001fbbfcecffa83"},
]
more-itertools = [
{file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"},
{file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"},
@ -1014,6 +1265,9 @@ mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
nltk = [
{file = "nltk-3.5.zip", hash = "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35"},
]
nodeenv = [
{file = "nodeenv-1.4.0-py2.py3-none-any.whl", hash = "sha256:4b0b77afa3ba9b54f4b6396e60b0c83f59eaeb2d63dc3cc7a70f7f4af96c82bc"},
]
@ -1092,6 +1346,10 @@ pygments = [
{file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"},
{file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"},
]
pymdown-extensions = [
{file = "pymdown-extensions-7.1.tar.gz", hash = "sha256:5bf93d1ccd8281948cd7c559eb363e59b179b5373478e8a7195cf4b78e3c11b6"},
{file = "pymdown_extensions-7.1-py2.py3-none-any.whl", hash = "sha256:8f415b21ee86d80bb2c3676f4478b274d0a8ccb13af672a4c86b9ffd22bd005c"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
@ -1116,6 +1374,10 @@ pytest-xdist = [
{file = "pytest-xdist-1.32.0.tar.gz", hash = "sha256:1d4166dcac69adb38eeaedb88c8fada8588348258a3492ab49ba9161f2971129"},
{file = "pytest_xdist-1.32.0-py2.py3-none-any.whl", hash = "sha256:ba5ec9fde3410bd9a116ff7e4f26c92e02fa3d27975ef3ad03f330b3d4b54e91"},
]
pytkdocs = [
{file = "pytkdocs-0.6.0-py3-none-any.whl", hash = "sha256:e5675088061ab1950688e52e581d21a98f5d4945b92fe55ad26ba4a0670359ef"},
{file = "pytkdocs-0.6.0.tar.gz", hash = "sha256:4dec18d0465aa84e38629ea3b32c2215e2bc4714c611b0b64c35b786aae1f180"},
]
pyyaml = [
{file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"},
{file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"},
@ -1160,10 +1422,29 @@ six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
]
soupsieve = [
{file = "soupsieve-1.9.6-py2.py3-none-any.whl", hash = "sha256:feb1e937fa26a69e08436aad4a9037cd7e1d4c7212909502ba30701247ff8abd"},
{file = "soupsieve-1.9.6.tar.gz", hash = "sha256:7985bacc98c34923a439967c1a602dc4f1e15f923b6fcf02344184f86cc7efaa"},
]
toml = [
{file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
{file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
]
tornado = [
{file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"},
{file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"},
{file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"},
{file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"},
{file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"},
{file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"},
{file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"},
{file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"},
{file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"},
]
tqdm = [
{file = "tqdm-4.48.0-py2.py3-none-any.whl", hash = "sha256:fcb7cb5b729b60a27f300b15c1ffd4744f080fb483b88f31dc8654b082cc8ea5"},
{file = "tqdm-4.48.0.tar.gz", hash = "sha256:6baa75a88582b1db6d34ce4690da5501d2a1cb65c34664840a456b2c9f794d29"},
]
traitlets = [
{file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"},
{file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"},

View File

@ -53,6 +53,9 @@ pytest = "*"
pytest-cov = "*"
pytest-xdist = "*"
pytest-timeout = "^1.4.1"
mkdocs = "^1.1.2"
mkdocstrings = "^0.12.1"
mkdocs-material = "^5.4.0"
[tool.poetry-dynamic-versioning]
enable = true