mirror of
https://github.com/copier-org/copier.git
synced 2025-05-05 15:32:54 +00:00
Refactor (#314)
* Refactor * Fix #110. * Rewrite test_config_exclude, test_config_exclude_overridden and test_config_include. These tests were badly designed, using a monkeypatch that would never happen in the real world, and actually producing false positives. I moved them to test_exclude.py and rewritten to test the what and not the how. * Fix #214 by removing skip option. Relevant tests use the better skip_if_exists=["**"]. * Remove subdirectory flag from API/CLI. It was confusing and could lead to bad maintenance situations. Fixes #315. * Remove extra_paths and fix #321 * Remember that you cannot use _copier_conf.src_path as a path now * use dataclasses * Create errors module, simplify some tests, fix many others * Fix some tests, complete EnvOps removal * Fix #214 and some tests related to it * Reorder code * Update docs and imports * Modularize test_complex_questions * Interlink worker and questionary a bit better * Removal of Questionary class, which only had 1 meaningful method that is now merged into Worker to avoid circular dependencies. * Fix #280 in a simple way: only user answers are type-casted inside API, and CLI transforms all `--data` using YAML always. Predictable. * Use prereleases correctly. * Reorder AnswersMap to have a more significative repr. * Simpler cache for old `Question.get_choices()` method (renamed now). * fix wrong test * Fix test_subdirectory * Fix test_tasks (and remove tests/demo_tasks) * Fix path filter tests, and move it to test_exclude, where it belongs * Make test_config pass * Fix more wrongly designed tests * Use cached_property backport if needed * xfail test known to fail on mac * force posix paths on windows * Add typing_extensions for python < 3.8 * Sort dependencies in pyproject.toml * Support python 3.6 str-to-datetime conversion * Workaround https://bugs.python.org/issue43095 * xfail test_path_filter on windows * Upgrade mkdocs and other dependencies to fix https://github.com/pawamoy/mkdocstrings/issues/222 * Add missing reference docs. * Add workaround for https://github.com/pawamoy/mkdocstrings/pull/209 * Docs. * Remove validators module * Add workaround for https://github.com/pawamoy/mkdocstrings/issues/225 * Restore docs autorefs as explained in https://github.com/pawamoy/mkdocstrings/issues/226#issuecomment-775413562. * Workaround https://github.com/pawamoy/pytkdocs/issues/86
This commit is contained in:
parent
07a66f793e
commit
0441b86f0c
4
.flake8
Normal file
4
.flake8
Normal file
@ -0,0 +1,4 @@
|
||||
[flake8]
|
||||
extend-ignore = W503,E203,E501,D100,D101,D102,D103,D104,D105,D107
|
||||
max-complexity = 20
|
||||
max-line-length = 88
|
@ -28,7 +28,6 @@ repos:
|
||||
entry: poetry run flake8
|
||||
language: system
|
||||
types: [python]
|
||||
args: ["--config", "pyproject.toml"]
|
||||
require_serial: true
|
||||
- id: poetry_check
|
||||
description: Check the integrity of pyproject.toml
|
||||
|
@ -1,7 +1,10 @@
|
||||
"""Copier (previously known as "Voodoo")
|
||||
"""Copier.
|
||||
|
||||
Docs: https://copier.readthedocs.io/
|
||||
"""
|
||||
from .main import * # noqa
|
||||
from .tools import * # noqa
|
||||
|
||||
from .main import * # noqa: F401,F403
|
||||
from .main import run_auto as copy # noqa: F401; Backwards compatibility
|
||||
|
||||
# This version is a placeholder autoupdated by poetry-dynamic-versioning
|
||||
__version__ = "0.0.0"
|
||||
|
@ -20,11 +20,12 @@ from io import StringIO
|
||||
from textwrap import dedent
|
||||
from unittest.mock import patch
|
||||
|
||||
import yaml
|
||||
from plumbum import cli, colors
|
||||
|
||||
from . import __version__
|
||||
from .config.objects import UserMessageError
|
||||
from .main import copy
|
||||
from .errors import UserMessageError
|
||||
from .main import Worker
|
||||
from .types import AnyByStrDict, List, OptStr
|
||||
|
||||
|
||||
@ -50,10 +51,8 @@ class CopierApp(cli.Application):
|
||||
|
||||
Attributes:
|
||||
answers_file: Set [answers_file][] option.
|
||||
extra_paths: Set [extra_paths][] option.
|
||||
exclude: Set [exclude][] option.
|
||||
vcs_ref: Set [vcs_ref][] option.
|
||||
subdirectory: Set [subdirectory][] option.
|
||||
pretend: Set [pretend][] option.
|
||||
force: Set [force][] option.
|
||||
skip: Set [skip_if_exists][] option.
|
||||
@ -97,12 +96,6 @@ class CopierApp(cli.Application):
|
||||
"to find the answers file"
|
||||
),
|
||||
)
|
||||
extra_paths: cli.SwitchAttr = cli.SwitchAttr(
|
||||
["-p", "--extra-paths"],
|
||||
str,
|
||||
list=True,
|
||||
help="Additional directories to find parent templates in",
|
||||
)
|
||||
exclude: cli.SwitchAttr = cli.SwitchAttr(
|
||||
["-x", "--exclude"],
|
||||
str,
|
||||
@ -122,23 +115,17 @@ class CopierApp(cli.Application):
|
||||
"the latest version, use `--vcs-ref=HEAD`."
|
||||
),
|
||||
)
|
||||
subdirectory: cli.SwitchAttr = cli.SwitchAttr(
|
||||
["-b", "--subdirectory"],
|
||||
str,
|
||||
help=(
|
||||
"Subdirectory to use when generating the project. "
|
||||
"If you do not specify it, the root of the template is used."
|
||||
),
|
||||
)
|
||||
|
||||
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 = cli.Flag(
|
||||
["-s", "--skip"], help="Skip files that already exist, without asking"
|
||||
skip: cli.Flag = cli.SwitchAttr(
|
||||
["-s", "--skip"],
|
||||
str,
|
||||
list=True,
|
||||
help="Skip specified files if they exist already",
|
||||
)
|
||||
quiet: cli.Flag = cli.Flag(["-q", "--quiet"], help="Suppress status output")
|
||||
prereleases: cli.Flag = cli.Flag(
|
||||
@ -160,30 +147,30 @@ class CopierApp(cli.Application):
|
||||
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
|
||||
for arg in values:
|
||||
key, value = arg.split("=", 1)
|
||||
value = yaml.safe_load(value)
|
||||
self.data[key] = value
|
||||
|
||||
def _copy(self, src_path: OptStr = None, dst_path: str = ".", **kwargs) -> None:
|
||||
def _worker(self, src_path: OptStr = None, dst_path: str = ".", **kwargs) -> Worker:
|
||||
"""
|
||||
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].
|
||||
**kwargs: Arguments passed to [Worker][copier.main.Worker].
|
||||
"""
|
||||
return copy(
|
||||
return Worker(
|
||||
data=self.data,
|
||||
dst_path=dst_path,
|
||||
answers_file=self.answers_file,
|
||||
exclude=self.exclude,
|
||||
extra_paths=self.extra_paths,
|
||||
force=self.force,
|
||||
pretend=self.pretend,
|
||||
quiet=self.quiet,
|
||||
skip=self.skip,
|
||||
src_path=src_path,
|
||||
vcs_ref=self.vcs_ref,
|
||||
subdirectory=self.subdirectory,
|
||||
use_prereleases=self.prereleases,
|
||||
**kwargs,
|
||||
)
|
||||
@ -249,7 +236,7 @@ class CopierCopySubApp(cli.Application):
|
||||
|
||||
@handle_exceptions
|
||||
def main(self, template_src: str, destination_path: str) -> int:
|
||||
"""Call [`copy`][copier.main.copy] in copy mode.
|
||||
"""Call [run_copy][copier.main.Worker.run_copy].
|
||||
|
||||
Params:
|
||||
template_src:
|
||||
@ -260,12 +247,11 @@ class CopierCopySubApp(cli.Application):
|
||||
destination_path:
|
||||
Where to generate the new subproject. It must not exist or be empty.
|
||||
"""
|
||||
self.parent._copy(
|
||||
self.parent._worker(
|
||||
template_src,
|
||||
destination_path,
|
||||
cleanup_on_error=self.cleanup_on_error,
|
||||
only_diff=False,
|
||||
)
|
||||
).run_copy()
|
||||
return 0
|
||||
|
||||
|
||||
@ -293,7 +279,7 @@ class CopierUpdateSubApp(cli.Application):
|
||||
|
||||
@handle_exceptions
|
||||
def main(self, destination_path: cli.ExistingDirectory = ".") -> int:
|
||||
"""Call [`copy`][copier.main.copy] in update mode.
|
||||
"""Call [run_update][copier.main.Worker.run_update].
|
||||
|
||||
Parameters:
|
||||
destination_path:
|
||||
@ -303,10 +289,9 @@ class CopierUpdateSubApp(cli.Application):
|
||||
The subproject must exist. If not specified, the currently
|
||||
working directory is used.
|
||||
"""
|
||||
self.parent._copy(
|
||||
self.parent._worker(
|
||||
dst_path=destination_path,
|
||||
only_diff=True,
|
||||
)
|
||||
).run_update()
|
||||
return 0
|
||||
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
from .factory import make_config # noqa
|
@ -1,149 +0,0 @@
|
||||
"""Functions used to generate configuration data."""
|
||||
|
||||
from collections import ChainMap
|
||||
from typing import Tuple
|
||||
|
||||
from packaging import version
|
||||
from plumbum import local
|
||||
from plumbum.cmd import git
|
||||
|
||||
from .. import vcs
|
||||
from ..types import AnyByStrDict, OptAnyByStrDict, OptBool, OptStr, OptStrSeq
|
||||
from .objects import (
|
||||
DEFAULT_EXCLUDE,
|
||||
ConfigData,
|
||||
EnvOps,
|
||||
NoSrcPathError,
|
||||
UserMessageError,
|
||||
)
|
||||
from .user_data import load_answersfile_data, load_config_data, query_user_data
|
||||
|
||||
__all__ = ("make_config",)
|
||||
|
||||
|
||||
def filter_config(data: AnyByStrDict) -> Tuple[AnyByStrDict, AnyByStrDict]:
|
||||
"""Separates config and questions data."""
|
||||
conf_data: AnyByStrDict = {"secret_questions": set()}
|
||||
questions_data = {}
|
||||
for k, v in data.items():
|
||||
if k == "_secret_questions":
|
||||
conf_data["secret_questions"].update(v)
|
||||
elif k.startswith("_"):
|
||||
conf_data[k[1:]] = v
|
||||
else:
|
||||
# Transform simplified questions format into complex
|
||||
if not isinstance(v, dict):
|
||||
v = {"default": v}
|
||||
questions_data[k] = v
|
||||
if v.get("secret"):
|
||||
conf_data["secret_questions"].add(k)
|
||||
return conf_data, questions_data
|
||||
|
||||
|
||||
def verify_minimum_version(version_str: str) -> None:
|
||||
"""Raise an error if the current Copier version is less than the given version."""
|
||||
# Importing __version__ at the top of the module creates a circular import
|
||||
# ("cannot import name '__version__' from partially initialized module 'copier'"),
|
||||
# so instead we do a lazy import here
|
||||
from .. import __version__
|
||||
|
||||
# Disable check when running copier as editable installation
|
||||
if __version__ == "0.0.0":
|
||||
return
|
||||
|
||||
if version.parse(__version__) < version.parse(version_str):
|
||||
raise UserMessageError(
|
||||
f"This template requires Copier version >= {version_str}, "
|
||||
f"while your version of Copier is {__version__}."
|
||||
)
|
||||
|
||||
|
||||
def make_config(
|
||||
src_path: OptStr = None,
|
||||
dst_path: str = ".",
|
||||
*,
|
||||
answers_file: OptStr = None,
|
||||
exclude: OptStrSeq = None,
|
||||
skip_if_exists: OptStrSeq = None,
|
||||
tasks: OptStrSeq = None,
|
||||
envops: OptAnyByStrDict = None,
|
||||
extra_paths: OptStrSeq = None,
|
||||
pretend: OptBool = None,
|
||||
force: OptBool = None,
|
||||
skip: OptBool = None,
|
||||
quiet: OptBool = None,
|
||||
cleanup_on_error: OptBool = None,
|
||||
vcs_ref: OptStr = None,
|
||||
only_diff: OptBool = True,
|
||||
subdirectory: OptStr = None,
|
||||
use_prereleases: OptBool = False,
|
||||
**kwargs,
|
||||
) -> ConfigData:
|
||||
"""Provides the configuration object, merged from the different sources.
|
||||
|
||||
The order of precedence for the merger of configuration objects is:
|
||||
function_args > user_data > defaults.
|
||||
"""
|
||||
exclude = list(exclude or [])
|
||||
# These args are provided by API or CLI call
|
||||
init_args = {k: v for k, v in locals().items() if v is not None and v != []}
|
||||
# Store different answer sources
|
||||
init_args["data_from_answers_file"] = load_answersfile_data(dst_path, answers_file)
|
||||
if "_commit" in init_args["data_from_answers_file"]:
|
||||
init_args["old_commit"] = init_args["data_from_answers_file"]["_commit"]
|
||||
# Detect original source if running in update mode
|
||||
if src_path is None:
|
||||
try:
|
||||
src_path = init_args["data_from_answers_file"]["_src_path"]
|
||||
except KeyError:
|
||||
raise NoSrcPathError(
|
||||
"No copier answers file found, or it didn't include "
|
||||
"original template information (_src_path). "
|
||||
"Run `copier copy` instead."
|
||||
)
|
||||
init_args["original_src_path"] = src_path
|
||||
if src_path:
|
||||
repo = vcs.get_repo(src_path)
|
||||
if repo:
|
||||
src_path = vcs.clone(repo, vcs_ref or "HEAD")
|
||||
vcs_ref = vcs_ref or vcs.checkout_latest_tag(src_path, use_prereleases)
|
||||
with local.cwd(src_path):
|
||||
init_args["commit"] = git("describe", "--tags", "--always").strip()
|
||||
init_args["src_path"] = src_path
|
||||
# Obtain config and query data, asking the user if needed
|
||||
file_data = load_config_data(src_path, quiet=True)
|
||||
|
||||
try:
|
||||
verify_minimum_version(file_data["_min_copier_version"])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
template_config_data, questions_data = filter_config(file_data)
|
||||
init_args["exclude"] = (
|
||||
list(template_config_data.get("exclude", DEFAULT_EXCLUDE)) + exclude
|
||||
)
|
||||
init_args["data_from_template_defaults"] = {
|
||||
k: v.get("default") for k, v in questions_data.items()
|
||||
}
|
||||
init_args["envops"] = EnvOps(**template_config_data.get("envops", {}))
|
||||
data = kwargs.get("data") or {}
|
||||
init_args["data_from_init"] = ChainMap(
|
||||
query_user_data(
|
||||
{k: v for k, v in questions_data.items() if k in data},
|
||||
{},
|
||||
data,
|
||||
init_args["data_from_template_defaults"],
|
||||
False,
|
||||
init_args["envops"],
|
||||
),
|
||||
data,
|
||||
)
|
||||
init_args["data_from_asking_user"] = query_user_data(
|
||||
questions_data,
|
||||
init_args["data_from_answers_file"],
|
||||
init_args["data_from_init"],
|
||||
init_args["data_from_template_defaults"],
|
||||
not force,
|
||||
init_args["envops"],
|
||||
)
|
||||
return ConfigData(**ChainMap(init_args, template_config_data))
|
@ -1,149 +0,0 @@
|
||||
"""Pydantic models, exceptions and default values."""
|
||||
import datetime
|
||||
from collections import ChainMap
|
||||
from copy import deepcopy
|
||||
from hashlib import sha512
|
||||
from os import urandom
|
||||
from pathlib import Path
|
||||
from typing import Any, ChainMap as t_ChainMap, Sequence, Tuple, Union
|
||||
|
||||
from pydantic import BaseModel, Extra, Field, PrivateAttr, StrictBool, validator
|
||||
|
||||
from ..types import AnyByStrDict, OptStr, PathSeq, StrOrPathSeq, StrSeq
|
||||
|
||||
# Default list of files in the template to exclude from the rendered project
|
||||
DEFAULT_EXCLUDE: Tuple[str, ...] = (
|
||||
"copier.yaml",
|
||||
"copier.yml",
|
||||
"~*",
|
||||
"*.py[co]",
|
||||
"__pycache__",
|
||||
".git",
|
||||
".DS_Store",
|
||||
".svn",
|
||||
)
|
||||
|
||||
DEFAULT_DATA: AnyByStrDict = {
|
||||
"now": datetime.datetime.utcnow,
|
||||
"make_secret": lambda: sha512(urandom(48)).hexdigest(),
|
||||
}
|
||||
|
||||
DEFAULT_TEMPLATES_SUFFIX = ".tmpl"
|
||||
|
||||
|
||||
class UserMessageError(Exception):
|
||||
"""Exit the program giving a message to the user."""
|
||||
|
||||
|
||||
class NoSrcPathError(UserMessageError):
|
||||
pass
|
||||
|
||||
|
||||
class EnvOps(BaseModel):
|
||||
"""Jinja2 environment options."""
|
||||
|
||||
autoescape: StrictBool = False
|
||||
block_start_string: str = "[%"
|
||||
block_end_string: str = "%]"
|
||||
comment_start_string: str = "[#"
|
||||
comment_end_string: str = "#]"
|
||||
variable_start_string: str = "[["
|
||||
variable_end_string: str = "]]"
|
||||
keep_trailing_newline: StrictBool = True
|
||||
|
||||
class Config:
|
||||
allow_mutation = False
|
||||
extra = Extra.allow
|
||||
|
||||
|
||||
class Migrations(BaseModel):
|
||||
version: str
|
||||
before: Sequence[Union[str, StrSeq]] = ()
|
||||
after: Sequence[Union[str, StrSeq]] = ()
|
||||
|
||||
|
||||
class ConfigData(BaseModel):
|
||||
"""A model holding configuration data."""
|
||||
|
||||
src_path: Path
|
||||
subdirectory: OptStr
|
||||
dst_path: Path
|
||||
extra_paths: PathSeq = ()
|
||||
exclude: StrOrPathSeq = DEFAULT_EXCLUDE
|
||||
skip_if_exists: StrOrPathSeq = ()
|
||||
tasks: Sequence[Union[str, StrSeq]] = ()
|
||||
envops: EnvOps = EnvOps()
|
||||
templates_suffix: str = DEFAULT_TEMPLATES_SUFFIX
|
||||
original_src_path: OptStr
|
||||
commit: OptStr
|
||||
old_commit: OptStr
|
||||
cleanup_on_error: StrictBool = True
|
||||
force: StrictBool = False
|
||||
only_diff: StrictBool = True
|
||||
pretend: StrictBool = False
|
||||
quiet: StrictBool = False
|
||||
skip: StrictBool = False
|
||||
use_prereleases: StrictBool = False
|
||||
vcs_ref: OptStr
|
||||
migrations: Sequence[Migrations] = ()
|
||||
secret_questions: StrSeq = ()
|
||||
answers_file: Path = Path(".copier-answers.yml")
|
||||
data_from_init: AnyByStrDict = Field(default_factory=dict)
|
||||
data_from_asking_user: AnyByStrDict = Field(default_factory=dict)
|
||||
data_from_answers_file: AnyByStrDict = Field(default_factory=dict)
|
||||
data_from_template_defaults: AnyByStrDict = Field(default_factory=dict)
|
||||
|
||||
# Private
|
||||
_data_mutable: AnyByStrDict = PrivateAttr(default_factory=dict)
|
||||
|
||||
def __init__(self, **kwargs: AnyByStrDict):
|
||||
super().__init__(**kwargs)
|
||||
self.data_from_template_defaults.setdefault("_folder_name", self.dst_path.name)
|
||||
|
||||
@validator("skip", always=True)
|
||||
def mutually_exclusive_flags(cls, v, values): # noqa: B902
|
||||
if v and values["force"]:
|
||||
raise ValueError("Flags `force` and `skip` are mutually exclusive.")
|
||||
return v
|
||||
|
||||
# sanitizers
|
||||
@validator("src_path", "dst_path", "extra_paths", pre=True, each_item=True)
|
||||
def resolve_path(cls, v: Path) -> Path: # noqa: B902
|
||||
return Path(v).expanduser().resolve()
|
||||
|
||||
@validator("src_path", "extra_paths", pre=True, each_item=True)
|
||||
def dir_must_exist(cls, v: Path) -> Path: # noqa: B902
|
||||
if not v.exists():
|
||||
raise ValueError("Project template not found.")
|
||||
if not v.is_dir():
|
||||
raise ValueError("Project template not a folder.")
|
||||
return v
|
||||
|
||||
@validator(
|
||||
"data_from_init",
|
||||
"data_from_asking_user",
|
||||
"data_from_answers_file",
|
||||
"data_from_template_defaults",
|
||||
pre=True,
|
||||
each_item=True,
|
||||
)
|
||||
def dict_copy(cls, v: AnyByStrDict) -> AnyByStrDict:
|
||||
"""Make sure all dicts are copied."""
|
||||
return deepcopy(v)
|
||||
|
||||
@property
|
||||
def data(self) -> t_ChainMap[str, Any]:
|
||||
"""The data object comes from different sources, sorted by priority."""
|
||||
return ChainMap(
|
||||
self._data_mutable,
|
||||
self.data_from_asking_user,
|
||||
self.data_from_init,
|
||||
self.data_from_answers_file,
|
||||
self.data_from_template_defaults,
|
||||
DEFAULT_DATA,
|
||||
)
|
||||
|
||||
# configuration
|
||||
class Config:
|
||||
allow_mutation = False
|
||||
anystr_strip_whitespace = True
|
56
copier/errors.py
Normal file
56
copier/errors.py
Normal file
@ -0,0 +1,56 @@
|
||||
"""Custom exceptions used by Copier."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic.errors import _PathValueError
|
||||
|
||||
from .tools import printf_exception
|
||||
from .types import PathSeq
|
||||
|
||||
|
||||
class UserMessageError(Exception):
|
||||
"""Exit the program giving a message to the user."""
|
||||
|
||||
|
||||
class UnsupportedVersionError(UserMessageError):
|
||||
"""Copier version does not support template version."""
|
||||
|
||||
|
||||
class ConfigFileError(ValueError):
|
||||
"""Parent class defining problems with the config file."""
|
||||
|
||||
|
||||
class InvalidConfigFileError(ConfigFileError):
|
||||
"""Indicates that the config file is wrong."""
|
||||
|
||||
def __init__(self, conf_path: Path, quiet: bool):
|
||||
msg = str(conf_path)
|
||||
printf_exception(self, "INVALID CONFIG FILE", msg=msg, quiet=quiet)
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class MultipleConfigFilesError(ConfigFileError):
|
||||
"""Both copier.yml and copier.yaml found, and that's an error."""
|
||||
|
||||
def __init__(self, conf_paths: "PathSeq", quiet: bool):
|
||||
msg = str(conf_paths)
|
||||
printf_exception(self, "MULTIPLE CONFIG FILES", msg=msg, quiet=quiet)
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class InvalidTypeError(TypeError):
|
||||
"""The question type is not among the supported ones."""
|
||||
|
||||
|
||||
class PathNotAbsoluteError(_PathValueError):
|
||||
"""The path is not absolute, but it should be."""
|
||||
|
||||
code = "path.not_absolute"
|
||||
msg_template = '"{path}" is not an absolute path'
|
||||
|
||||
|
||||
class PathNotRelativeError(_PathValueError):
|
||||
"""The path is not relative, but it should be."""
|
||||
|
||||
code = "path.not_relative"
|
||||
msg_template = '"{path}" is not a relative path'
|
1122
copier/main.py
1122
copier/main.py
File diff suppressed because it is too large
Load Diff
80
copier/subproject.py
Normal file
80
copier/subproject.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""Objects to interact with subprojects.
|
||||
|
||||
A *subproject* is a project that gets rendered and/or updated with Copier.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
from plumbum.cmd import git
|
||||
from plumbum.machines import local
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from .template import Template
|
||||
from .types import AbsolutePath, AnyByStrDict, VCSTypes
|
||||
from .vcs import is_git_repo_root
|
||||
|
||||
try:
|
||||
from functools import cached_property
|
||||
except ImportError:
|
||||
from backports.cached_property import cached_property
|
||||
|
||||
|
||||
@dataclass
|
||||
class Subproject:
|
||||
"""Object that represents the subproject and its current state.
|
||||
|
||||
Attributes:
|
||||
local_abspath:
|
||||
Absolute path on local disk pointing to the subproject root folder.
|
||||
|
||||
answers_relpath:
|
||||
Relative path to [the answers file][the-copier-answersyml-file].
|
||||
"""
|
||||
|
||||
local_abspath: AbsolutePath
|
||||
answers_relpath: Path = Path(".copier-answers.yml")
|
||||
|
||||
def is_dirty(self) -> bool:
|
||||
"""Indicates if the local template root is dirty.
|
||||
|
||||
Only applicable for VCS-tracked templates.
|
||||
"""
|
||||
if self.vcs == "git":
|
||||
with local.cwd(self.local_abspath):
|
||||
return bool(git("status", "--porcelain").strip())
|
||||
return False
|
||||
|
||||
@property
|
||||
def _raw_answers(self) -> AnyByStrDict:
|
||||
"""The last answers, loaded raw as yaml."""
|
||||
try:
|
||||
return yaml.safe_load(
|
||||
(self.local_abspath / self.answers_relpath).read_text()
|
||||
)
|
||||
except OSError:
|
||||
return {}
|
||||
|
||||
@cached_property
|
||||
def last_answers(self) -> AnyByStrDict:
|
||||
"""Last answers, excluding private ones (except _src_path and _commit)."""
|
||||
return {
|
||||
key: value
|
||||
for key, value in self._raw_answers.items()
|
||||
if key in {"_src_path", "_commit"} or not key.startswith("_")
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def template(self) -> Optional[Template]:
|
||||
"""Template, as it was used the last time."""
|
||||
last_url = self.last_answers.get("_src_path")
|
||||
last_ref = self.last_answers.get("_commit")
|
||||
if last_url:
|
||||
return Template(url=last_url, ref=last_ref)
|
||||
|
||||
@cached_property
|
||||
def vcs(self) -> Optional[VCSTypes]:
|
||||
"""VCS type of the subproject."""
|
||||
if is_git_repo_root(self.local_abspath):
|
||||
return "git"
|
316
copier/template.py
Normal file
316
copier/template.py
Normal file
@ -0,0 +1,316 @@
|
||||
"""Tools related to template management."""
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import List, Mapping, Optional, Sequence, Set, Tuple
|
||||
|
||||
from packaging import version
|
||||
from packaging.version import parse
|
||||
from plumbum.cmd import git
|
||||
from plumbum.machines import local
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from .errors import UnsupportedVersionError
|
||||
from .types import AnyByStrDict, OptStr, StrSeq, VCSTypes
|
||||
from .user_data import load_config_data
|
||||
from .vcs import checkout_latest_tag, clone, get_repo
|
||||
|
||||
try:
|
||||
from functools import cached_property
|
||||
except ImportError:
|
||||
from backports.cached_property import cached_property
|
||||
|
||||
# Default list of files in the template to exclude from the rendered project
|
||||
DEFAULT_EXCLUDE: Tuple[str, ...] = (
|
||||
"copier.yaml",
|
||||
"copier.yml",
|
||||
"~*",
|
||||
"*.py[co]",
|
||||
"__pycache__",
|
||||
".git",
|
||||
".DS_Store",
|
||||
".svn",
|
||||
)
|
||||
|
||||
DEFAULT_TEMPLATES_SUFFIX = ".tmpl"
|
||||
|
||||
|
||||
def filter_config(data: AnyByStrDict) -> Tuple[AnyByStrDict, AnyByStrDict]:
|
||||
"""Separates config and questions data."""
|
||||
conf_data: AnyByStrDict = {"secret_questions": set()}
|
||||
questions_data = {}
|
||||
for k, v in data.items():
|
||||
if k == "_secret_questions":
|
||||
conf_data["secret_questions"].update(v)
|
||||
elif k.startswith("_"):
|
||||
conf_data[k[1:]] = v
|
||||
else:
|
||||
# Transform simplified questions format into complex
|
||||
if not isinstance(v, dict):
|
||||
v = {"default": v}
|
||||
questions_data[k] = v
|
||||
if v.get("secret"):
|
||||
conf_data["secret_questions"].add(k)
|
||||
return conf_data, questions_data
|
||||
|
||||
|
||||
def verify_minimum_version(version_str: str) -> None:
|
||||
"""Raise an error if the current Copier version is less than the given version."""
|
||||
# Importing __version__ at the top of the module creates a circular import
|
||||
# ("cannot import name '__version__' from partially initialized module 'copier'"),
|
||||
# so instead we do a lazy import here
|
||||
from . import __version__
|
||||
|
||||
# Disable check when running copier as editable installation
|
||||
if __version__ == "0.0.0":
|
||||
return
|
||||
|
||||
if version.parse(__version__) < version.parse(version_str):
|
||||
raise UnsupportedVersionError(
|
||||
f"This template requires Copier version >= {version_str}, "
|
||||
f"while your version of Copier is {__version__}."
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Template:
|
||||
"""Object that represents a template and its current state.
|
||||
|
||||
See [configuring a template][configuring-a-template].
|
||||
|
||||
Attributes:
|
||||
url:
|
||||
Absolute origin that points to the template.
|
||||
|
||||
It can be:
|
||||
|
||||
- A local path.
|
||||
- A Git url. Note: if something fails, prefix the URL with `git+`.
|
||||
|
||||
ref:
|
||||
The tag to checkout in the template.
|
||||
|
||||
Only used if `url` points to a VCS-tracked template.
|
||||
|
||||
If `None`, then it will checkout the latest tag, sorted by PEP440.
|
||||
Otherwise it will checkout the reference used here.
|
||||
|
||||
Usually it should be a tag, or `None`.
|
||||
|
||||
use_prereleases:
|
||||
When `True`, the template's *latest* release will consider prereleases.
|
||||
|
||||
Only used if:
|
||||
|
||||
- `url` points to a VCS-tracked template
|
||||
- `ref` is `None`.
|
||||
|
||||
Helpful if you want to test templates before doing a proper release, but you
|
||||
need some features that require a proper PEP440 version identifier.
|
||||
"""
|
||||
|
||||
url: str
|
||||
ref: OptStr = None
|
||||
use_prereleases: bool = False
|
||||
|
||||
@cached_property
|
||||
def _raw_config(self) -> AnyByStrDict:
|
||||
"""Get template configuration, raw.
|
||||
|
||||
It reads [the `copier.yml` file][the-copieryml-file].
|
||||
"""
|
||||
result = load_config_data(self.local_abspath)
|
||||
with suppress(KeyError):
|
||||
verify_minimum_version(result["_min_copier_version"])
|
||||
return result
|
||||
|
||||
@cached_property
|
||||
def answers_relpath(self) -> Path:
|
||||
"""Get the answers file relative path, as specified in the template.
|
||||
|
||||
If not specified, returns the default `.copier-answers.yml`.
|
||||
|
||||
See [answers_file][].
|
||||
"""
|
||||
result = Path(self.config_data.get("answers_file", ".copier-answers.yml"))
|
||||
assert not result.is_absolute()
|
||||
return result
|
||||
|
||||
@cached_property
|
||||
def commit(self) -> OptStr:
|
||||
"""If the template is VCS-tracked, get its commit description."""
|
||||
if self.vcs == "git":
|
||||
with local.cwd(self.local_abspath):
|
||||
return git("describe", "--tags", "--always").strip()
|
||||
|
||||
@cached_property
|
||||
def config_data(self) -> AnyByStrDict:
|
||||
"""Get config from the template.
|
||||
|
||||
It reads [the `copier.yml` file][the-copieryml-file] to get its
|
||||
[settings][available-settings].
|
||||
"""
|
||||
return filter_config(self._raw_config)[0]
|
||||
|
||||
@cached_property
|
||||
def default_answers(self) -> AnyByStrDict:
|
||||
"""Get default answers for template's questions."""
|
||||
return {key: value.get("default") for key, value in self.questions_data.items()}
|
||||
|
||||
@cached_property
|
||||
def envops(self) -> Mapping:
|
||||
"""Get the Jinja configuration specified in the template, or default values.
|
||||
|
||||
See [envops][].
|
||||
"""
|
||||
# TODO Use Jinja defaults
|
||||
result = {
|
||||
"autoescape": False,
|
||||
"block_end_string": "%]",
|
||||
"block_start_string": "[%",
|
||||
"comment_end_string": "#]",
|
||||
"comment_start_string": "[#",
|
||||
"keep_trailing_newline": True,
|
||||
"variable_end_string": "]]",
|
||||
"variable_start_string": "[[",
|
||||
}
|
||||
result.update(self.config_data.get("envops", {}))
|
||||
return result
|
||||
|
||||
@cached_property
|
||||
def exclude(self) -> Tuple[str, ...]:
|
||||
"""Get exclusions specified in the template, or default ones.
|
||||
|
||||
See [exclude][].
|
||||
"""
|
||||
return tuple(self.config_data.get("exclude", DEFAULT_EXCLUDE))
|
||||
|
||||
@cached_property
|
||||
def metadata(self) -> AnyByStrDict:
|
||||
"""Get template metadata.
|
||||
|
||||
This data, if any, should be saved in the answers file to be able to
|
||||
restore the template to this same state.
|
||||
"""
|
||||
result: AnyByStrDict = {"_src_path": self.url}
|
||||
if self.commit:
|
||||
result["_commit"] = self.commit
|
||||
return result
|
||||
|
||||
def migration_tasks(self, stage: str, from_: str, to: str) -> Sequence[Mapping]:
|
||||
"""Get migration objects that match current version spec.
|
||||
|
||||
Versions are compared using PEP 440.
|
||||
|
||||
See [migrations][].
|
||||
"""
|
||||
result: List[dict] = []
|
||||
if not from_ or not to:
|
||||
return result
|
||||
parsed_from = parse(from_)
|
||||
parsed_to = parse(to)
|
||||
extra_env = {
|
||||
"STAGE": stage,
|
||||
"VERSION_FROM": from_,
|
||||
"VERSION_TO": to,
|
||||
}
|
||||
migration: dict
|
||||
for migration in self._raw_config.get("_migrations", []):
|
||||
if parsed_to >= parse(migration["version"]) > parsed_from:
|
||||
extra_env = dict(extra_env, VERSION_CURRENT=str(migration["version"]))
|
||||
result += [
|
||||
{"task": task, "extra_env": extra_env}
|
||||
for task in migration.get(stage, [])
|
||||
]
|
||||
return result
|
||||
|
||||
@cached_property
|
||||
def questions_data(self) -> AnyByStrDict:
|
||||
"""Get questions from the template.
|
||||
|
||||
See [questions][].
|
||||
"""
|
||||
return filter_config(self._raw_config)[1]
|
||||
|
||||
@cached_property
|
||||
def secret_questions(self) -> Set[str]:
|
||||
"""Get names of secret questions from the template.
|
||||
|
||||
These questions shouldn't be saved into the answers file.
|
||||
"""
|
||||
result = set(self.config_data.get("secret_questions", {}))
|
||||
for key, value in self.questions_data.items():
|
||||
if value.get("secret"):
|
||||
result.add(key)
|
||||
return result
|
||||
|
||||
@cached_property
|
||||
def skip_if_exists(self) -> StrSeq:
|
||||
"""Get skip patterns from the template.
|
||||
|
||||
These files will never be rewritten when rendering the template.
|
||||
|
||||
See [skip_if_exists][].
|
||||
"""
|
||||
return self.config_data.get("skip_if_exists", ())
|
||||
|
||||
@cached_property
|
||||
def subdirectory(self) -> str:
|
||||
"""Get the subdirectory as specified in the template.
|
||||
|
||||
The subdirectory points to the real template code, allowing the
|
||||
templater to separate it from other template assets, such as docs,
|
||||
tests, etc.
|
||||
|
||||
See [subdirectory][].
|
||||
"""
|
||||
return self.config_data.get("subdirectory", "")
|
||||
|
||||
@cached_property
|
||||
def tasks(self) -> Sequence:
|
||||
"""Get tasks defined in the template.
|
||||
|
||||
See [tasks][].
|
||||
"""
|
||||
return self.config_data.get("tasks", [])
|
||||
|
||||
@cached_property
|
||||
def templates_suffix(self) -> str:
|
||||
"""Get the suffix defined for templates.
|
||||
|
||||
By default: `.tmpl`.
|
||||
|
||||
See [templates_suffix][].
|
||||
"""
|
||||
return self.config_data.get("templates_suffix", DEFAULT_TEMPLATES_SUFFIX)
|
||||
|
||||
@cached_property
|
||||
def local_abspath(self) -> Path:
|
||||
"""Get the absolute path to the template on disk.
|
||||
|
||||
This may clone it if `url` points to a
|
||||
VCS-tracked template.
|
||||
"""
|
||||
result = Path(self.url)
|
||||
if self.vcs == "git":
|
||||
result = Path(clone(self.url_expanded, self.ref))
|
||||
if self.ref is None:
|
||||
checkout_latest_tag(result, self.use_prereleases)
|
||||
if not result.is_dir():
|
||||
raise ValueError("Local template must be a directory.")
|
||||
return result.absolute()
|
||||
|
||||
@cached_property
|
||||
def url_expanded(self) -> str:
|
||||
"""Get usable URL.
|
||||
|
||||
`url` can be specified in shortcut
|
||||
format, which wouldn't be understood by the underlying VCS system. This
|
||||
property returns the expanded version, which should work properly.
|
||||
"""
|
||||
return get_repo(self.url) or self.url
|
||||
|
||||
@cached_property
|
||||
def vcs(self) -> Optional[VCSTypes]:
|
||||
"""Get VCS system used by the template, if any."""
|
||||
if get_repo(self.url):
|
||||
return "git"
|
136
copier/tools.py
136
copier/tools.py
@ -1,34 +1,16 @@
|
||||
"""Some utility functions."""
|
||||
|
||||
import errno
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import unicodedata
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, TextIO, Union
|
||||
from typing import Any, Optional, TextIO, Union
|
||||
|
||||
import colorama
|
||||
import pathspec
|
||||
from jinja2 import FileSystemLoader
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
from packaging import version
|
||||
from pydantic import StrictBool
|
||||
from yaml import safe_dump
|
||||
|
||||
from .config.objects import ConfigData, EnvOps
|
||||
from .types import (
|
||||
AnyByStrDict,
|
||||
CheckPathFunc,
|
||||
Filters,
|
||||
IntSeq,
|
||||
JSONSerializable,
|
||||
LoaderPaths,
|
||||
StrOrPath,
|
||||
StrOrPathSeq,
|
||||
T,
|
||||
)
|
||||
from .types import IntSeq
|
||||
|
||||
__all__ = ("Style", "printf")
|
||||
|
||||
@ -80,12 +62,6 @@ def printf_exception(
|
||||
print(HLINE, file=sys.stderr)
|
||||
|
||||
|
||||
def required(value: T, **kwargs: Any) -> T:
|
||||
if not value:
|
||||
raise ValueError()
|
||||
return value
|
||||
|
||||
|
||||
def cast_str_to_bool(value: Any) -> bool:
|
||||
"""Parse anything to bool.
|
||||
|
||||
@ -113,15 +89,6 @@ def cast_str_to_bool(value: Any) -> bool:
|
||||
return bool(value)
|
||||
|
||||
|
||||
def make_folder(folder: Path) -> None:
|
||||
if not folder.exists():
|
||||
try:
|
||||
os.makedirs(str(folder))
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
|
||||
def copy_file(src_path: Path, dst_path: Path, follow_symlinks: bool = True) -> None:
|
||||
shutil.copy2(src_path, dst_path, follow_symlinks=follow_symlinks)
|
||||
|
||||
@ -137,69 +104,6 @@ def to_nice_yaml(data: Any, **kwargs) -> str:
|
||||
return result or ""
|
||||
|
||||
|
||||
def get_jinja_env(
|
||||
envops: "EnvOps",
|
||||
filters: Optional[Filters] = None,
|
||||
paths: Optional[LoaderPaths] = None,
|
||||
**kwargs: Any,
|
||||
) -> SandboxedEnvironment:
|
||||
"""Return a pre-configured Jinja environment."""
|
||||
loader = FileSystemLoader(paths) if paths else None
|
||||
# We want to minimize the risk of hidden malware in the templates
|
||||
# so we use the SandboxedEnvironment instead of the regular one.
|
||||
# Of couse we still have the post-copy tasks to worry about, but at least
|
||||
# they are more visible to the final user.
|
||||
env = SandboxedEnvironment(loader=loader, **envops.dict(), **kwargs)
|
||||
default_filters = {"to_nice_yaml": to_nice_yaml}
|
||||
default_filters.update(filters or {})
|
||||
env.filters.update(default_filters)
|
||||
return 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 []))
|
||||
self.env = get_jinja_env(envops=envops, paths=paths)
|
||||
self.conf = conf
|
||||
answers: AnyByStrDict = {}
|
||||
# All internal values must appear first
|
||||
if conf.commit:
|
||||
answers["_commit"] = conf.commit
|
||||
if conf.original_src_path is not None:
|
||||
answers["_src_path"] = conf.original_src_path
|
||||
# Other data goes next
|
||||
answers.update(
|
||||
(k, v)
|
||||
for (k, v) in conf.data.items()
|
||||
if not k.startswith("_")
|
||||
and k not in conf.secret_questions
|
||||
and isinstance(k, JSONSerializable)
|
||||
and isinstance(v, JSONSerializable)
|
||||
)
|
||||
self.data = dict(
|
||||
conf.data,
|
||||
_copier_answers=answers,
|
||||
_copier_conf=conf.copy(deep=True, exclude={"data": {"now", "make_secret"}}),
|
||||
)
|
||||
|
||||
def __call__(self, fullpath: StrOrPath) -> str:
|
||||
relpath = Path(fullpath).relative_to(self.conf.src_path).as_posix()
|
||||
tmpl = self.env.get_template(str(relpath))
|
||||
return tmpl.render(**self.data)
|
||||
|
||||
def string(self, string: StrOrPath) -> str:
|
||||
tmpl = self.env.from_string(str(string))
|
||||
return tmpl.render(**self.data)
|
||||
|
||||
|
||||
def normalize_str(text: StrOrPath, form: str = "NFD") -> str:
|
||||
"""Normalize unicode text. Uses the NFD algorithm by default."""
|
||||
return unicodedata.normalize(form, str(text))
|
||||
|
||||
|
||||
def force_str_end(original_str: str, end: str = "\n") -> str:
|
||||
"""Make sure a `original_str` ends with `end`.
|
||||
|
||||
@ -210,39 +114,3 @@ def force_str_end(original_str: str, end: str = "\n") -> str:
|
||||
if not original_str.endswith(end):
|
||||
return original_str + end
|
||||
return original_str
|
||||
|
||||
|
||||
def create_path_filter(patterns: StrOrPathSeq) -> CheckPathFunc:
|
||||
"""Returns a function that matches a path against given patterns."""
|
||||
patterns = [normalize_str(p) for p in patterns]
|
||||
spec = pathspec.PathSpec.from_lines("gitwildmatch", patterns)
|
||||
|
||||
def match(path: StrOrPath) -> bool:
|
||||
return spec.match_file(str(path))
|
||||
|
||||
return match
|
||||
|
||||
|
||||
def get_migration_tasks(conf: "ConfigData", stage: str) -> List[Dict]:
|
||||
"""Get migration objects that match current version spec.
|
||||
|
||||
Versions are compared using PEP 440.
|
||||
"""
|
||||
result: List[Dict] = []
|
||||
if not conf.old_commit or not conf.commit:
|
||||
return result
|
||||
vfrom = version.parse(conf.old_commit)
|
||||
vto = version.parse(conf.commit)
|
||||
extra_env = {
|
||||
"STAGE": stage,
|
||||
"VERSION_FROM": conf.old_commit,
|
||||
"VERSION_TO": conf.commit,
|
||||
}
|
||||
for migration in conf.migrations:
|
||||
if vto >= version.parse(migration.version) > vfrom:
|
||||
extra_env = dict(extra_env, VERSION_CURRENT=str(migration.version))
|
||||
result += [
|
||||
{"task": task, "extra_env": extra_env}
|
||||
for task in migration.dict().get(stage, [])
|
||||
]
|
||||
return result
|
||||
|
@ -1,7 +1,8 @@
|
||||
"""All complex types and annotations are declared here."""
|
||||
"""Complex types, annotations, validators."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
@ -9,13 +10,26 @@ from typing import (
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from pydantic.validators import path_validator
|
||||
|
||||
try:
|
||||
from typing import Literal
|
||||
except ImportError:
|
||||
from typing_extensions import Literal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydantic.typing import CallableGenerator
|
||||
|
||||
|
||||
# simple types
|
||||
StrOrPath = Union[str, Path]
|
||||
AnyByStrDict = Dict[str, Any]
|
||||
AnyByStrDictOrTuple = Union[AnyByStrDict, Tuple[str, Any]]
|
||||
|
||||
# sequences
|
||||
IntSeq = Sequence[int]
|
||||
@ -38,3 +52,40 @@ T = TypeVar("T")
|
||||
JSONSerializable = (dict, list, str, int, float, bool, type(None))
|
||||
Filters = Dict[str, Callable]
|
||||
LoaderPaths = Union[str, Iterable[str]]
|
||||
VCSTypes = Literal["git"]
|
||||
|
||||
|
||||
class AllowArbitraryTypes:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
# Validators
|
||||
def path_is_absolute(value: Path) -> Path:
|
||||
if not value.is_absolute():
|
||||
from .errors import PathNotAbsoluteError
|
||||
|
||||
raise PathNotAbsoluteError(path=value)
|
||||
return value
|
||||
|
||||
|
||||
def path_is_relative(value: Path) -> Path:
|
||||
if value.is_absolute():
|
||||
from .errors import PathNotRelativeError
|
||||
|
||||
raise PathNotRelativeError(path=value)
|
||||
return value
|
||||
|
||||
|
||||
# Validated types
|
||||
class AbsolutePath(Path):
|
||||
@classmethod
|
||||
def __get_validators__(cls) -> "CallableGenerator":
|
||||
yield path_validator
|
||||
yield path_is_absolute
|
||||
|
||||
|
||||
class RelativePath(Path):
|
||||
@classmethod
|
||||
def __get_validators__(cls) -> "CallableGenerator":
|
||||
yield path_validator
|
||||
yield path_is_relative
|
||||
|
@ -1,53 +1,118 @@
|
||||
"""Functions used to load user data."""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
from collections import ChainMap
|
||||
from contextlib import suppress
|
||||
from dataclasses import field
|
||||
from hashlib import sha512
|
||||
from os import urandom
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, ChainMap as t_ChainMap, Dict, List, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
ChainMap as t_ChainMap,
|
||||
Dict,
|
||||
List,
|
||||
Union,
|
||||
)
|
||||
|
||||
import yaml
|
||||
from iteration_utilities import deepflatten
|
||||
from jinja2 import UndefinedError
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
from prompt_toolkit.lexers import PygmentsLexer
|
||||
from pydantic import BaseModel, Field, PrivateAttr, validator
|
||||
from pydantic import validator
|
||||
from pydantic.dataclasses import dataclass
|
||||
from pygments.lexers.data import JsonLexer, YamlLexer
|
||||
from questionary import unsafe_prompt
|
||||
from questionary.prompts.common import Choice
|
||||
from yamlinclude import YamlIncludeConstructor
|
||||
|
||||
from ..tools import cast_str_to_bool, force_str_end, get_jinja_env, printf_exception
|
||||
from ..types import AnyByStrDict, OptStrOrPath, PathSeq, StrOrPath
|
||||
from .objects import DEFAULT_DATA, EnvOps, UserMessageError
|
||||
from .errors import (
|
||||
InvalidConfigFileError,
|
||||
InvalidTypeError,
|
||||
MultipleConfigFilesError,
|
||||
UserMessageError,
|
||||
)
|
||||
from .tools import cast_str_to_bool, force_str_end
|
||||
from .types import AllowArbitraryTypes, AnyByStrDict, OptStr, OptStrOrPath, StrOrPath
|
||||
|
||||
__all__ = ("load_config_data", "query_user_data")
|
||||
try:
|
||||
from functools import cached_property
|
||||
except ImportError:
|
||||
from backports.cached_property import cached_property
|
||||
|
||||
|
||||
class ConfigFileError(ValueError):
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
class InvalidConfigFileError(ConfigFileError):
|
||||
def __init__(self, conf_path: Path, quiet: bool):
|
||||
msg = str(conf_path)
|
||||
printf_exception(self, "INVALID CONFIG FILE", msg=msg, quiet=quiet)
|
||||
super().__init__(msg)
|
||||
DEFAULT_DATA: AnyByStrDict = {
|
||||
"now": datetime.datetime.utcnow,
|
||||
"make_secret": lambda: sha512(urandom(48)).hexdigest(),
|
||||
}
|
||||
|
||||
|
||||
class MultipleConfigFilesError(ConfigFileError):
|
||||
def __init__(self, conf_paths: PathSeq, quiet: bool):
|
||||
msg = str(conf_paths)
|
||||
printf_exception(self, "MULTIPLE CONFIG FILES", msg=msg, quiet=quiet)
|
||||
super().__init__(msg)
|
||||
@dataclass
|
||||
class AnswersMap:
|
||||
"""Object that gathers answers from different sources.
|
||||
|
||||
Attributes:
|
||||
local:
|
||||
Local overrides to other answers.
|
||||
|
||||
user:
|
||||
Answers provided by the user, interactively.
|
||||
|
||||
init:
|
||||
Answers provided on init.
|
||||
|
||||
This will hold those answers that come from `--data` in
|
||||
CLI mode.
|
||||
|
||||
See [data][].
|
||||
|
||||
metadata:
|
||||
Data used to be able to reproduce the template.
|
||||
|
||||
It comes from [copier.template.Template.metadata][].
|
||||
|
||||
last:
|
||||
Data from [the answers file][the-copier-answersyml-file].
|
||||
|
||||
default:
|
||||
Default data from the template.
|
||||
|
||||
See [copier.template.Template.default_answers][].
|
||||
"""
|
||||
|
||||
# Private
|
||||
local: AnyByStrDict = field(default_factory=dict, init=False)
|
||||
|
||||
# Public
|
||||
user: AnyByStrDict = field(default_factory=dict)
|
||||
init: AnyByStrDict = field(default_factory=dict)
|
||||
metadata: AnyByStrDict = field(default_factory=dict)
|
||||
last: AnyByStrDict = field(default_factory=dict)
|
||||
default: AnyByStrDict = field(default_factory=dict)
|
||||
|
||||
@cached_property
|
||||
def combined(self) -> t_ChainMap[str, Any]:
|
||||
"""Answers combined from different sources, sorted by priority."""
|
||||
return ChainMap(
|
||||
self.local,
|
||||
self.user,
|
||||
self.init,
|
||||
self.metadata,
|
||||
self.last,
|
||||
self.default,
|
||||
DEFAULT_DATA,
|
||||
)
|
||||
|
||||
def old_commit(self) -> OptStr:
|
||||
return self.last.get("_commit")
|
||||
|
||||
|
||||
class InvalidTypeError(TypeError):
|
||||
pass
|
||||
|
||||
|
||||
class Question(BaseModel):
|
||||
@dataclass(config=AllowArbitraryTypes)
|
||||
class Question:
|
||||
"""One question asked to the user.
|
||||
|
||||
All attributes are init kwargs.
|
||||
@ -61,7 +126,7 @@ class Question(BaseModel):
|
||||
Default value presented to the user to make it easier to respond.
|
||||
Can be templated.
|
||||
|
||||
help_text:
|
||||
help:
|
||||
Additional text printed to the user, explaining the purpose of
|
||||
this question. Can be templated.
|
||||
|
||||
@ -74,10 +139,6 @@ class Question(BaseModel):
|
||||
Text that appears if there's nothing written in the input field,
|
||||
but disappears as soon as the user writes anything. Can be templated.
|
||||
|
||||
questionary:
|
||||
Reference to the [Questionary][] object where this [Question][] is
|
||||
attached.
|
||||
|
||||
secret:
|
||||
Indicates if the question should be removed from the answers file.
|
||||
If the question type is str, it will hide user input on the screen
|
||||
@ -97,53 +158,74 @@ class Question(BaseModel):
|
||||
boolean values.
|
||||
"""
|
||||
|
||||
choices: Union[Dict[Any, Any], List[Any]] = Field(default_factory=list)
|
||||
var_name: str
|
||||
answers: AnswersMap
|
||||
jinja_env: SandboxedEnvironment
|
||||
choices: Union[Dict[Any, Any], List[Any]] = field(default_factory=list)
|
||||
default: Any = None
|
||||
help_text: str = ""
|
||||
help: str = ""
|
||||
ask_user: bool = False
|
||||
multiline: Union[str, bool] = False
|
||||
placeholder: str = ""
|
||||
questionary: "Questionary"
|
||||
secret: bool = False
|
||||
type_name: str = ""
|
||||
var_name: str
|
||||
type: str = ""
|
||||
when: Union[str, bool] = True
|
||||
|
||||
# Private
|
||||
_cached_choices: List[Choice] = PrivateAttr(default_factory=list)
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# Transform arguments that are named like python keywords
|
||||
to_rename = (("help", "help_text"), ("type", "type_name"))
|
||||
for from_, to in to_rename:
|
||||
with suppress(KeyError):
|
||||
kwargs.setdefault(to, kwargs.pop(from_))
|
||||
# Infer type from default if missing
|
||||
super().__init__(**kwargs)
|
||||
self.questionary.questions.append(self)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Question({self.var_name})"
|
||||
|
||||
@validator("var_name")
|
||||
def _check_var_name(cls, v):
|
||||
if v in DEFAULT_DATA:
|
||||
raise ValueError("Invalid question name")
|
||||
return v
|
||||
|
||||
@validator("type_name", always=True)
|
||||
def _check_type_name(cls, v, values):
|
||||
@validator("type", always=True)
|
||||
def _check_type(cls, v, values):
|
||||
if v == "":
|
||||
default_type_name = type(values.get("default")).__name__
|
||||
v = default_type_name if default_type_name in CAST_STR_TO_NATIVE else "yaml"
|
||||
return v
|
||||
|
||||
def _generate_choices(self) -> None:
|
||||
"""Iterates choices in a format that the questionary lib likes."""
|
||||
if self._cached_choices:
|
||||
return
|
||||
def get_default(self) -> Any:
|
||||
"""Get the default value for this question, casted to its expected type."""
|
||||
cast_fn = self.get_cast_fn()
|
||||
try:
|
||||
result = self.answers.init[self.var_name]
|
||||
except KeyError:
|
||||
try:
|
||||
result = self.answers.last[self.var_name]
|
||||
except KeyError:
|
||||
result = self.render_value(self.default)
|
||||
result = cast_answer_type(result, cast_fn)
|
||||
return result
|
||||
|
||||
def get_default_rendered(self) -> Union[bool, str, Choice, None]:
|
||||
"""Get default answer rendered for the questionary lib.
|
||||
|
||||
The questionary lib expects some specific data types, and returns
|
||||
it when the user answers. Sometimes you need to compare the response
|
||||
to the rendered one, or viceversa.
|
||||
|
||||
This helper allows such usages.
|
||||
"""
|
||||
default = self.get_default()
|
||||
# If there are choices, return the one that matches the expressed default
|
||||
if self.choices:
|
||||
for choice in self._formatted_choices:
|
||||
if choice.value == default:
|
||||
return choice
|
||||
return None
|
||||
# Yes/No questions expect and return bools
|
||||
if isinstance(default, bool) and self.type == "bool":
|
||||
return default
|
||||
# Emptiness is expressed as an empty str
|
||||
if default is None:
|
||||
return ""
|
||||
# All other data has to be str
|
||||
return str(default)
|
||||
|
||||
@cached_property
|
||||
def _formatted_choices(self) -> List[Choice]:
|
||||
"""Obtain choices rendered and properly formatted."""
|
||||
result = []
|
||||
choices = self.choices
|
||||
if isinstance(self.choices, dict):
|
||||
choices = list(self.choices.items())
|
||||
@ -162,51 +244,9 @@ class Question(BaseModel):
|
||||
name = str(name)
|
||||
# The value can be templated
|
||||
value = self.render_value(value)
|
||||
self._cached_choices.append(Choice(name, value))
|
||||
|
||||
def get_default(self) -> Any:
|
||||
"""Get the default value for this question, casted to its expected type."""
|
||||
cast_fn = self.get_cast_fn()
|
||||
try:
|
||||
result = self.questionary.answers_forced[self.var_name]
|
||||
except KeyError:
|
||||
try:
|
||||
result = self.questionary.answers_last[self.var_name]
|
||||
except KeyError:
|
||||
result = self.render_value(self.default)
|
||||
result = cast_answer_type(result, cast_fn)
|
||||
result.append(Choice(name, value))
|
||||
return result
|
||||
|
||||
def get_default_rendered(self) -> Union[bool, str, Choice, None]:
|
||||
"""Get default answer rendered for the questionary lib.
|
||||
|
||||
The questionary lib expects some specific data types, and returns
|
||||
it when the user answers. Sometimes you need to compare the response
|
||||
to the rendered one, or viceversa.
|
||||
|
||||
This helper allows such usages.
|
||||
"""
|
||||
default = self.get_default()
|
||||
# If there are choices, return the one that matches the expressed default
|
||||
if self.choices:
|
||||
for choice in self.get_choices():
|
||||
if choice.value == default:
|
||||
return choice
|
||||
return None
|
||||
# Yes/No questions expect and return bools
|
||||
if isinstance(default, bool) and self.type_name == "bool":
|
||||
return default
|
||||
# Emptiness is expressed as an empty str
|
||||
if default is None:
|
||||
return ""
|
||||
# All other data has to be str
|
||||
return str(default)
|
||||
|
||||
def get_choices(self) -> List[Choice]:
|
||||
"""Obtain choices rendered and properly formatted."""
|
||||
self._generate_choices()
|
||||
return self._cached_choices
|
||||
|
||||
def filter_answer(self, answer) -> Any:
|
||||
"""Cast the answer to the desired type."""
|
||||
if answer == self.get_default_rendered():
|
||||
@ -216,10 +256,10 @@ class Question(BaseModel):
|
||||
def get_message(self) -> str:
|
||||
"""Get the message that will be printed to the user."""
|
||||
message = ""
|
||||
if self.help_text:
|
||||
rendered_help = self.render_value(self.help_text)
|
||||
if self.help:
|
||||
rendered_help = self.render_value(self.help)
|
||||
message = force_str_end(rendered_help)
|
||||
message += f"{self.var_name}? Format: {self.type_name}"
|
||||
message += f"{self.var_name}? Format: {self.type}"
|
||||
return message
|
||||
|
||||
def get_placeholder(self) -> str:
|
||||
@ -239,17 +279,17 @@ class Question(BaseModel):
|
||||
"when": self.get_when,
|
||||
}
|
||||
questionary_type = "input"
|
||||
if self.type_name == "bool":
|
||||
if self.type == "bool":
|
||||
questionary_type = "confirm"
|
||||
if self.choices:
|
||||
questionary_type = "select"
|
||||
result["choices"] = self.get_choices()
|
||||
result["choices"] = self._formatted_choices
|
||||
if questionary_type == "input":
|
||||
if self.secret:
|
||||
questionary_type = "password"
|
||||
elif self.type_name == "yaml":
|
||||
elif self.type == "yaml":
|
||||
lexer = PygmentsLexer(YamlLexer)
|
||||
elif self.type_name == "json":
|
||||
elif self.type == "json":
|
||||
lexer = PygmentsLexer(JsonLexer)
|
||||
if lexer:
|
||||
result["lexer"] = lexer
|
||||
@ -263,7 +303,7 @@ class Question(BaseModel):
|
||||
|
||||
def get_cast_fn(self) -> Callable:
|
||||
"""Obtain function to cast user answer to desired type."""
|
||||
type_name = self.render_value(self.type_name)
|
||||
type_name = self.render_value(self.type)
|
||||
if type_name not in CAST_STR_TO_NATIVE:
|
||||
raise InvalidTypeError("Invalid question type")
|
||||
return CAST_STR_TO_NATIVE.get(type_name, parse_yaml_string)
|
||||
@ -287,9 +327,9 @@ class Question(BaseModel):
|
||||
"""Get skip condition for question."""
|
||||
if (
|
||||
# Skip on --force
|
||||
not self.questionary.ask_user
|
||||
not self.ask_user
|
||||
# Skip on --data=this_question=some_answer
|
||||
or self.var_name in self.questionary.answers_forced
|
||||
or self.var_name in self.answers.init
|
||||
):
|
||||
return False
|
||||
when = self.when
|
||||
@ -303,97 +343,16 @@ class Question(BaseModel):
|
||||
If the value cannot be used as a template, it will be returned as is.
|
||||
"""
|
||||
try:
|
||||
template = self.questionary.env.from_string(value)
|
||||
template = self.jinja_env.from_string(value)
|
||||
except TypeError:
|
||||
# value was not a string
|
||||
return value
|
||||
try:
|
||||
return template.render(
|
||||
**self.questionary.get_best_answers(), **DEFAULT_DATA
|
||||
)
|
||||
return template.render(**self.answers.combined)
|
||||
except UndefinedError as error:
|
||||
raise UserMessageError(str(error)) from error
|
||||
|
||||
|
||||
class Questionary(BaseModel):
|
||||
"""An object holding all [Question][] items and user answers.
|
||||
|
||||
All attributes are also init kwargs.
|
||||
|
||||
Attributes:
|
||||
answers_default:
|
||||
Default answers as specified in the template.
|
||||
|
||||
answers_forced:
|
||||
Answers forced by the user, either by an API call like
|
||||
`data={'some_question': 'forced_answer'}` or by a CLI call like
|
||||
`--data=some_question=forced_answer`.
|
||||
|
||||
answers_last:
|
||||
Answers obtained from the `.copier-answers.yml` file.
|
||||
|
||||
answers_user:
|
||||
Dict containing user answers for the current questionary. It should
|
||||
be empty always.
|
||||
|
||||
ask_user:
|
||||
Indicates if the questionary should be asked, or just forced.
|
||||
|
||||
env:
|
||||
The Jinja environment for rendering.
|
||||
|
||||
questions:
|
||||
A list containing all [Question][] objects for this [Questionary][].
|
||||
"""
|
||||
|
||||
answers_default: AnyByStrDict = Field(default_factory=dict)
|
||||
answers_forced: AnyByStrDict = Field(default_factory=dict)
|
||||
answers_last: AnyByStrDict = Field(default_factory=dict)
|
||||
answers_user: AnyByStrDict = Field(default_factory=dict)
|
||||
ask_user: bool = True
|
||||
env: SandboxedEnvironment
|
||||
questions: List[Question] = Field(default_factory=list)
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_best_answers(self) -> t_ChainMap[str, Any]:
|
||||
"""Get dict-like object with the best answers for each question."""
|
||||
return ChainMap(
|
||||
self.answers_user,
|
||||
self.answers_last,
|
||||
self.answers_forced,
|
||||
self.answers_default,
|
||||
)
|
||||
|
||||
def get_answers(self) -> AnyByStrDict:
|
||||
"""Obtain answers for all questions.
|
||||
|
||||
It produces a TUI for querying the user if `ask_user` is true. Otherwise,
|
||||
it gets answers from other sources.
|
||||
"""
|
||||
previous_answers = self.get_best_answers()
|
||||
if self.ask_user:
|
||||
self.answers_user = unsafe_prompt(
|
||||
(question.get_questionary_structure() for question in self.questions),
|
||||
answers=previous_answers,
|
||||
)
|
||||
else:
|
||||
# Avoid prompting to not requiring a TTy when --force
|
||||
for question in self.questions:
|
||||
new_answer = question.get_default()
|
||||
previous_answer = previous_answers.get(question.var_name)
|
||||
if new_answer != previous_answer:
|
||||
self.answers_user[question.var_name] = new_answer
|
||||
return self.answers_user
|
||||
|
||||
|
||||
Question.update_forward_refs()
|
||||
|
||||
|
||||
def load_yaml_data(conf_path: Path, quiet: bool = False) -> AnyByStrDict:
|
||||
"""Load the `copier.yml` file.
|
||||
|
||||
@ -493,24 +452,3 @@ CAST_STR_TO_NATIVE: Dict[str, Callable] = {
|
||||
"str": str,
|
||||
"yaml": parse_yaml_string,
|
||||
}
|
||||
|
||||
|
||||
def query_user_data(
|
||||
questions_data: AnyByStrDict,
|
||||
last_answers_data: AnyByStrDict,
|
||||
forced_answers_data: AnyByStrDict,
|
||||
default_answers_data: AnyByStrDict,
|
||||
ask_user: bool,
|
||||
envops: EnvOps,
|
||||
) -> AnyByStrDict:
|
||||
"""Query the user for questions given in the config file."""
|
||||
questionary = Questionary(
|
||||
answers_forced=forced_answers_data,
|
||||
answers_last=last_answers_data,
|
||||
answers_default=default_answers_data,
|
||||
ask_user=ask_user,
|
||||
env=get_jinja_env(envops=envops),
|
||||
)
|
||||
for question, details in questions_data.items():
|
||||
Question(var_name=question, questionary=questionary, **details)
|
||||
return questionary.get_answers()
|
@ -10,8 +10,6 @@ from plumbum.cmd import git
|
||||
|
||||
from .types import OptBool, OptStr, StrOrPath
|
||||
|
||||
__all__ = ("get_repo", "clone")
|
||||
|
||||
GIT_PREFIX = ("git@", "git://", "git+")
|
||||
GIT_POSTFIX = (".git",)
|
||||
REPLACEMENTS = (
|
||||
@ -22,10 +20,10 @@ REPLACEMENTS = (
|
||||
)
|
||||
|
||||
|
||||
def is_git_repo_root(path: Path) -> bool:
|
||||
def is_git_repo_root(path: StrOrPath) -> bool:
|
||||
"""Indicate if a given path is a git repo root directory."""
|
||||
try:
|
||||
with local.cwd(path / ".git"):
|
||||
with local.cwd(Path(path, ".git")):
|
||||
return bool(git("rev-parse", "--is-inside-git-dir").strip() == "true")
|
||||
except OSError:
|
||||
return False
|
||||
@ -40,6 +38,19 @@ def is_git_bundle(path: Path) -> bool:
|
||||
|
||||
|
||||
def get_repo(url: str) -> OptStr:
|
||||
"""Transforms `url` into a git-parseable origin URL.
|
||||
|
||||
Args:
|
||||
url:
|
||||
Valid examples:
|
||||
|
||||
- gh:copier-org/copier
|
||||
- gl:copier-org/copier
|
||||
- git@github.com:copier-org/copier.git
|
||||
- git+https://mywebsiteisagitrepo.example.com/
|
||||
- /local/path/to/git/repo
|
||||
- /local/path/to/git/bundle/file.bundle
|
||||
"""
|
||||
for pattern, replacement in REPLACEMENTS:
|
||||
url = re.sub(pattern, replacement, url)
|
||||
url_path = Path(url)
|
||||
@ -85,10 +96,19 @@ def checkout_latest_tag(local_repo: StrOrPath, use_prereleases: OptBool = False)
|
||||
return latest_tag
|
||||
|
||||
|
||||
def clone(url: str, ref: str = "HEAD") -> str:
|
||||
def clone(url: str, ref: OptStr = None) -> str:
|
||||
"""Clone repo into some temporary destination.
|
||||
|
||||
Args:
|
||||
url:
|
||||
Git-parseable URL of the repo. As returned by
|
||||
[get_repo][copier.vcs.get_repo].
|
||||
ref:
|
||||
Reference to checkout. For Git repos, defaults to `HEAD`.
|
||||
"""
|
||||
location = tempfile.mkdtemp(prefix=f"{__name__}.clone.")
|
||||
git("clone", "--no-checkout", url, location)
|
||||
with local.cwd(location):
|
||||
git("checkout", ref)
|
||||
git("checkout", ref or "HEAD")
|
||||
git("submodule", "update", "--checkout", "--init", "--recursive", "--force")
|
||||
return location
|
||||
|
@ -290,6 +290,32 @@ is overriden, and don't ask user anything else:
|
||||
copier -fd 'user_name=Manuel Calavera' copy template destination
|
||||
```
|
||||
|
||||
### `envops`
|
||||
|
||||
- Format: `dict`
|
||||
- CLI flags: N/A
|
||||
- Default value:
|
||||
|
||||
```yaml
|
||||
{
|
||||
"autoescape": False,
|
||||
"block_end_string": "%]",
|
||||
"block_start_string": "[%",
|
||||
"comment_end_string": "#]",
|
||||
"comment_start_string": "[#",
|
||||
"keep_trailing_newline": True,
|
||||
"variable_end_string": "]]",
|
||||
"variable_start_string": "[[",
|
||||
}
|
||||
```
|
||||
|
||||
Configurations for the Jinja environment.
|
||||
|
||||
These defaults are different from upstream Jinja's.
|
||||
|
||||
See [upstream docs](https://jinja.palletsprojects.com/en/2.11.x/api/#jinja2.Environment)
|
||||
to know available options.
|
||||
|
||||
### `exclude`
|
||||
|
||||
- Format: `List[str]`
|
||||
@ -329,21 +355,6 @@ copier --exclude '*' --exclude '!file-i-want' copy template destination
|
||||
|
||||
Instead, CLI/API definitions **will extend** those from `copier.yml`.
|
||||
|
||||
### `extra_paths`
|
||||
|
||||
- Format: `List[str]`
|
||||
- CLI flags: `-p`, `--extra-paths`
|
||||
- Default value: N/A
|
||||
|
||||
Additional paths from where to search for templates.
|
||||
|
||||
Example `copier.yml`:
|
||||
|
||||
```yaml
|
||||
_extra_paths:
|
||||
- ~/Projects/templates
|
||||
```
|
||||
|
||||
### `force`
|
||||
|
||||
- Format: `bool`
|
||||
@ -484,7 +495,7 @@ _skip_if_exists: .secret_password.yml
|
||||
### `subdirectory`
|
||||
|
||||
- Format: `str`
|
||||
- CLI flags: `-b`, `--subdirectory`
|
||||
- CLI flags: N/A
|
||||
- Default value: N/A
|
||||
|
||||
Subdirectory to use as the template root when generating a project. If not specified,
|
||||
@ -496,12 +507,6 @@ Example `copier.yml`:
|
||||
_subdirectory: src
|
||||
```
|
||||
|
||||
Example CLI usage to choose a different subdirectory template:
|
||||
|
||||
```sh
|
||||
copier --subdirectory template2 -b copy template destination
|
||||
```
|
||||
|
||||
### `tasks`
|
||||
|
||||
- Format: `List[str|List[str]]`
|
||||
@ -617,7 +622,7 @@ will be the last ones he used.
|
||||
|
||||
The file **must be called exactly `[[ _copier_conf.answers_file ]].tmpl`** (or ended
|
||||
with [your chosen suffix](#templates_suffix)) in your template's root folder) to allow
|
||||
[applying multiple templates to the same subproject][applying-multiple-templates-to-the-same-subproject].
|
||||
[applying multiple templates to the same subproject](#applying-multiple-templates-to-the-same-subproject).
|
||||
|
||||
The default name will be `.copier-answers.yml`, but
|
||||
[you can define a different default path for this file](#answers_file).
|
||||
|
@ -1 +0,0 @@
|
||||
::: copier.config.factory
|
@ -1 +0,0 @@
|
||||
::: copier.config.objects
|
@ -1 +0,0 @@
|
||||
::: copier.config.user_data
|
1
docs/reference/errors.md
Normal file
1
docs/reference/errors.md
Normal file
@ -0,0 +1 @@
|
||||
::: copier.errors
|
1
docs/reference/subproject.md
Normal file
1
docs/reference/subproject.md
Normal file
@ -0,0 +1 @@
|
||||
::: copier.subproject
|
1
docs/reference/template.md
Normal file
1
docs/reference/template.md
Normal file
@ -0,0 +1 @@
|
||||
::: copier.template
|
1
docs/reference/user_data.md
Normal file
1
docs/reference/user_data.md
Normal file
@ -0,0 +1 @@
|
||||
::: copier.user_data
|
@ -11,14 +11,14 @@ nav:
|
||||
- Generating a project: "generating.md"
|
||||
- Updating a project: "updating.md"
|
||||
- Reference:
|
||||
- config:
|
||||
- factory.py: "reference/config/factory.md"
|
||||
- objects.py: "reference/config/objects.md"
|
||||
- user_data.py: "reference/config/user_data.md"
|
||||
- cli.py: "reference/cli.md"
|
||||
- errors.py: "reference/errors.md"
|
||||
- main.py: "reference/main.md"
|
||||
- subproject.py: "reference/subproject.md"
|
||||
- template.py: "reference/template.md"
|
||||
- tools.py: "reference/tools.md"
|
||||
- types.py: "reference/types.md"
|
||||
- user_data.py: "reference/user_data.md"
|
||||
- vcs.py: "reference/vcs.md"
|
||||
- Comparisons: comparisons.md
|
||||
- Contributing: "contributing.md"
|
||||
@ -41,6 +41,7 @@ markdown_extensions:
|
||||
permalink: true
|
||||
|
||||
plugins:
|
||||
- autorefs
|
||||
- search
|
||||
- mermaid2:
|
||||
arguments:
|
||||
|
3
mypy.ini
Normal file
3
mypy.ini
Normal file
@ -0,0 +1,3 @@
|
||||
[mypy]
|
||||
warn_no_return = False
|
||||
ignore_missing_imports = True
|
463
poetry.lock
generated
463
poetry.lock
generated
@ -47,6 +47,17 @@ python-versions = "*"
|
||||
[package.dependencies]
|
||||
pyflakes = ">=1.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "backports.cached-property"
|
||||
version = "1.0.1"
|
||||
description = "cached_property() - computed once per instance, cached as attribute"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
|
||||
[package.dependencies]
|
||||
typing = {version = ">=3.6", markers = "python_version < \"3.7\""}
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.9.3"
|
||||
@ -85,6 +96,14 @@ typing-extensions = ">=3.7.4"
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
||||
|
||||
[[package]]
|
||||
name = "cached-property"
|
||||
version = "1.5.2"
|
||||
description = "A decorator for caching properties in classes."
|
||||
category = "main"
|
||||
optional = true
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2020.12.5"
|
||||
@ -127,7 +146,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "5.3"
|
||||
version = "5.4"
|
||||
description = "Code coverage measurement for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -154,7 +173,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "editorconfig"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
description = "EditorConfig File Locator and Interpreter for Python"
|
||||
category = "main"
|
||||
optional = true
|
||||
@ -162,11 +181,11 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "execnet"
|
||||
version = "1.7.1"
|
||||
version = "1.8.0"
|
||||
description = "execnet: rapid multi-Python deployment"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.dependencies]
|
||||
apipkg = ">=1.4"
|
||||
@ -213,11 +232,11 @@ dev = ["coverage", "black", "hypothesis", "hypothesmith"]
|
||||
|
||||
[[package]]
|
||||
name = "flake8-comprehensions"
|
||||
version = "3.3.0"
|
||||
version = "3.3.1"
|
||||
description = "A flake8 plugin to help you write better list/set/dict comprehensions."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
flake8 = ">=3.0,<3.2.0 || >3.2.0,<4"
|
||||
@ -245,7 +264,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "1.5.10"
|
||||
version = "1.5.13"
|
||||
description = "File identification library for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -264,7 +283,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "3.3.0"
|
||||
version = "3.4.0"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -275,22 +294,23 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
|
||||
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
|
||||
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
|
||||
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-resources"
|
||||
version = "3.3.0"
|
||||
version = "5.1.0"
|
||||
description = "Read resources from Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
zipp = {version = ">=0.4", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "rst.linker", "jaraco.packaging"]
|
||||
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
|
||||
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "pytest-black (>=0.3.7)", "pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
@ -315,7 +335,7 @@ test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "2.11.2"
|
||||
version = "2.11.3"
|
||||
description = "A very fast and expressive template engine."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -329,7 +349,7 @@ i18n = ["Babel (>=0.8)"]
|
||||
|
||||
[[package]]
|
||||
name = "joblib"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
description = "Lightweight pipelining with Python functions"
|
||||
category = "main"
|
||||
optional = true
|
||||
@ -337,7 +357,7 @@ python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "jsbeautifier"
|
||||
version = "1.13.0"
|
||||
version = "1.13.5"
|
||||
description = "JavaScript unobfuscator and beautifier."
|
||||
category = "main"
|
||||
optional = true
|
||||
@ -467,20 +487,26 @@ requests = "*"
|
||||
|
||||
[[package]]
|
||||
name = "mkdocstrings"
|
||||
version = "0.13.6"
|
||||
version = "0.14.0"
|
||||
description = "Automatic documentation from sources, for MkDocs."
|
||||
category = "main"
|
||||
optional = true
|
||||
python-versions = ">=3.6,<4.0"
|
||||
python-versions = "^3.6"
|
||||
develop = false
|
||||
|
||||
[package.dependencies]
|
||||
beautifulsoup4 = ">=4.8.2,<5.0.0"
|
||||
mkdocs = ">=1.1,<2.0"
|
||||
pymdown-extensions = ">=6.3,<9.0"
|
||||
pytkdocs = ">=0.2.0,<0.10.0"
|
||||
Jinja2 = "^2.11"
|
||||
Markdown = "^3.3"
|
||||
MarkupSafe = "^1.1"
|
||||
mkdocs = "^1.1"
|
||||
pymdown-extensions = ">=6.3, <9.0"
|
||||
pytkdocs = ">=0.2.0, <0.11.0"
|
||||
|
||||
[package.extras]
|
||||
tests = ["coverage (>=5.2.1,<6.0.0)", "invoke (>=1.4.1,<2.0.0)", "mkdocs-material (>=5.5.12,<6.0.0)", "mypy (>=0.782,<0.783)", "pytest (>=6.0.1,<7.0.0)", "pytest-cov (>=2.10.1,<3.0.0)", "pytest-randomly (>=3.4.1,<4.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=2.1.0,<3.0.0)"]
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/pawamoy/mkdocstrings.git"
|
||||
reference = "master"
|
||||
resolved_reference = "bf217e66c3b459781100d53c2a048272260b355c"
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
@ -538,7 +564,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "20.8"
|
||||
version = "20.9"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -590,11 +616,19 @@ dev = ["pre-commit", "tox"]
|
||||
|
||||
[[package]]
|
||||
name = "plumbum"
|
||||
version = "1.6.9"
|
||||
version = "1.7.0"
|
||||
description = "Plumbum: shell combinators library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4,>=2.7"
|
||||
|
||||
[package.dependencies]
|
||||
pypiwin32 = {version = "*", markers = "platform_system == \"Windows\" and platform_python_implementation != \"PyPy\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["pytest", "pytest-cov", "pytest-mock", "pytest-timeout", "paramiko", "psutil"]
|
||||
docs = ["recommonmark (>=0.5.0)", "Sphinx (>=3.0.0)", "sphinx-rtd-theme (>=0.5.0)"]
|
||||
ssh = ["paramiko"]
|
||||
|
||||
[[package]]
|
||||
name = "poethepoet"
|
||||
@ -610,7 +644,7 @@ tomlkit = ">=0.6.0,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "2.9.3"
|
||||
version = "2.10.1"
|
||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -628,7 +662,7 @@ virtualenv = ">=20.0.8"
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.8"
|
||||
version = "3.0.14"
|
||||
description = "Library for building powerful interactive command lines in Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -639,7 +673,7 @@ wcwidth = "*"
|
||||
|
||||
[[package]]
|
||||
name = "ptyprocess"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
description = "Run a subprocess in a pseudo terminal"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -687,7 +721,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.7.3"
|
||||
version = "2.7.4"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -695,11 +729,11 @@ python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "pymdown-extensions"
|
||||
version = "8.0.1"
|
||||
version = "8.1.1"
|
||||
description = "Extension pack for Python Markdown."
|
||||
category = "main"
|
||||
optional = true
|
||||
python-versions = ">=3.5"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
Markdown = ">=3.2"
|
||||
@ -712,9 +746,20 @@ category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "pypiwin32"
|
||||
version = "223"
|
||||
description = ""
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
pywin32 = ">=223"
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "6.2.1"
|
||||
version = "6.2.2"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -736,14 +781,14 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "2.10.1"
|
||||
version = "2.11.1"
|
||||
description = "Pytest plugin for measuring coverage."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.dependencies]
|
||||
coverage = ">=4.4"
|
||||
coverage = ">=5.2.1"
|
||||
pytest = ">=4.6"
|
||||
|
||||
[package.extras]
|
||||
@ -780,22 +825,38 @@ testing = ["filelock"]
|
||||
|
||||
[[package]]
|
||||
name = "pytkdocs"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
description = "Load Python objects documentation."
|
||||
category = "main"
|
||||
optional = true
|
||||
python-versions = ">=3.6,<4.0"
|
||||
python-versions = "^3.6"
|
||||
develop = false
|
||||
|
||||
[package.extras]
|
||||
tests = ["coverage (>=5.2.1,<6.0.0)", "invoke (>=1.4.1,<2.0.0)", "marshmallow (>=3.5.2,<4.0.0)", "mypy (>=0.782,<0.783)", "pydantic (>=1.5.1,<2.0.0)", "pytest (>=6.0.1,<7.0.0)", "pytest-cov (>=2.10.1,<3.0.0)", "pytest-randomly (>=3.4.1,<4.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=2.1.0,<3.0.0)"]
|
||||
[package.dependencies]
|
||||
cached-property = {version = "^1.5.2", markers = "python_version < \"3.8\""}
|
||||
typing-extensions = {version = "^3.7.4.3", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/pawamoy/pytkdocs.git"
|
||||
reference = "master"
|
||||
resolved_reference = "4052eabdd45a7f4fe8c3fc8591bb23e1763a5a0f"
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "300"
|
||||
description = "Python for Window Extensions"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "5.3.1"
|
||||
version = "5.4.1"
|
||||
description = "YAML parser and emitter for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml-include"
|
||||
@ -814,7 +875,7 @@ toml = ["toml"]
|
||||
|
||||
[[package]]
|
||||
name = "questionary"
|
||||
version = "1.8.1"
|
||||
version = "1.9.0"
|
||||
description = "Python library to build pretty command line user prompts ⭐️"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -823,6 +884,9 @@ python-versions = ">=3.6,<3.10"
|
||||
[package.dependencies]
|
||||
prompt_toolkit = ">=2.0,<4.0"
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx (>=3.3,<4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2020.11.13"
|
||||
@ -891,23 +955,32 @@ python-versions = ">= 3.5"
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.54.1"
|
||||
version = "4.56.0"
|
||||
description = "Fast, Extensible Progress Meter"
|
||||
category = "main"
|
||||
optional = true
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
|
||||
|
||||
[package.extras]
|
||||
dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown", "wheel"]
|
||||
dev = ["py-make (>=0.1.0)", "twine", "wheel"]
|
||||
telegram = ["requests"]
|
||||
|
||||
[[package]]
|
||||
name = "typed-ast"
|
||||
version = "1.4.1"
|
||||
version = "1.4.2"
|
||||
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "typing"
|
||||
version = "3.7.4.3"
|
||||
description = "Type Hints for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "3.7.4.3"
|
||||
@ -918,7 +991,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.2"
|
||||
version = "1.26.3"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = true
|
||||
@ -931,7 +1004,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.2.2"
|
||||
version = "20.4.2"
|
||||
description = "Virtual Python Environment builder"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -947,7 +1020,7 @@ six = ">=1.9.0,<2"
|
||||
|
||||
[package.extras]
|
||||
docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
|
||||
testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "pytest-xdist (>=1.31.0)", "packaging (>=20.0)", "xonsh (>=0.9.16)"]
|
||||
testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
@ -970,12 +1043,12 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
|
||||
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
|
||||
|
||||
[extras]
|
||||
docs = ["mkdocstrings", "mkdocs-material", "mkdocs-mermaid2-plugin"]
|
||||
docs = ["mkdocstrings", "mkdocs-material", "mkdocs-mermaid2-plugin", "pytkdocs"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = ">=3.6.1,<3.10"
|
||||
content-hash = "9dfbeeecd75c12b862b91d1902d5d398ecd6dca08d35a49fa043621f9b551df3"
|
||||
content-hash = "43f9a0471608ea8927a222fb0f67c650dfa395888c7abc638be5ae6bb95a7fc4"
|
||||
|
||||
[metadata.files]
|
||||
apipkg = [
|
||||
@ -997,6 +1070,10 @@ attrs = [
|
||||
autoflake = [
|
||||
{file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"},
|
||||
]
|
||||
"backports.cached-property" = [
|
||||
{file = "backports.cached-property-1.0.1.tar.gz", hash = "sha256:1a5ef1e750f8bc7d0204c807aae8e0f450c655be0cf4b30407a35fd4bb27186c"},
|
||||
{file = "backports.cached_property-1.0.1-py3-none-any.whl", hash = "sha256:687b5fe14be40aadcf547cae91337a1fdb84026046a39370274e54d3fe4fb4f9"},
|
||||
]
|
||||
beautifulsoup4 = [
|
||||
{file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"},
|
||||
{file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"},
|
||||
@ -1006,6 +1083,10 @@ black = [
|
||||
{file = "black-20.8b1-py3-none-any.whl", hash = "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"},
|
||||
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
|
||||
]
|
||||
cached-property = [
|
||||
{file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"},
|
||||
{file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
|
||||
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
|
||||
@ -1027,40 +1108,55 @@ colorama = [
|
||||
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
|
||||
]
|
||||
coverage = [
|
||||
{file = "coverage-5.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270"},
|
||||
{file = "coverage-5.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4"},
|
||||
{file = "coverage-5.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9"},
|
||||
{file = "coverage-5.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729"},
|
||||
{file = "coverage-5.3-cp27-cp27m-win32.whl", hash = "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d"},
|
||||
{file = "coverage-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418"},
|
||||
{file = "coverage-5.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9"},
|
||||
{file = "coverage-5.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5"},
|
||||
{file = "coverage-5.3-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822"},
|
||||
{file = "coverage-5.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097"},
|
||||
{file = "coverage-5.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9"},
|
||||
{file = "coverage-5.3-cp35-cp35m-win32.whl", hash = "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636"},
|
||||
{file = "coverage-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f"},
|
||||
{file = "coverage-5.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237"},
|
||||
{file = "coverage-5.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54"},
|
||||
{file = "coverage-5.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7"},
|
||||
{file = "coverage-5.3-cp36-cp36m-win32.whl", hash = "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a"},
|
||||
{file = "coverage-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d"},
|
||||
{file = "coverage-5.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"},
|
||||
{file = "coverage-5.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f"},
|
||||
{file = "coverage-5.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c"},
|
||||
{file = "coverage-5.3-cp37-cp37m-win32.whl", hash = "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751"},
|
||||
{file = "coverage-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709"},
|
||||
{file = "coverage-5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516"},
|
||||
{file = "coverage-5.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f"},
|
||||
{file = "coverage-5.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259"},
|
||||
{file = "coverage-5.3-cp38-cp38-win32.whl", hash = "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82"},
|
||||
{file = "coverage-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221"},
|
||||
{file = "coverage-5.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978"},
|
||||
{file = "coverage-5.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21"},
|
||||
{file = "coverage-5.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24"},
|
||||
{file = "coverage-5.3-cp39-cp39-win32.whl", hash = "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7"},
|
||||
{file = "coverage-5.3-cp39-cp39-win_amd64.whl", hash = "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7"},
|
||||
{file = "coverage-5.3.tar.gz", hash = "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0"},
|
||||
{file = "coverage-5.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:6d9c88b787638a451f41f97446a1c9fd416e669b4d9717ae4615bd29de1ac135"},
|
||||
{file = "coverage-5.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:66a5aae8233d766a877c5ef293ec5ab9520929c2578fd2069308a98b7374ea8c"},
|
||||
{file = "coverage-5.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9754a5c265f991317de2bac0c70a746efc2b695cf4d49f5d2cddeac36544fb44"},
|
||||
{file = "coverage-5.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:fbb17c0d0822684b7d6c09915677a32319f16ff1115df5ec05bdcaaee40b35f3"},
|
||||
{file = "coverage-5.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:b7f7421841f8db443855d2854e25914a79a1ff48ae92f70d0a5c2f8907ab98c9"},
|
||||
{file = "coverage-5.4-cp27-cp27m-win32.whl", hash = "sha256:4a780807e80479f281d47ee4af2eb2df3e4ccf4723484f77da0bb49d027e40a1"},
|
||||
{file = "coverage-5.4-cp27-cp27m-win_amd64.whl", hash = "sha256:87c4b38288f71acd2106f5d94f575bc2136ea2887fdb5dfe18003c881fa6b370"},
|
||||
{file = "coverage-5.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6809ebcbf6c1049002b9ac09c127ae43929042ec1f1dbd8bb1615f7cd9f70a0"},
|
||||
{file = "coverage-5.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ba7ca81b6d60a9f7a0b4b4e175dcc38e8fef4992673d9d6e6879fd6de00dd9b8"},
|
||||
{file = "coverage-5.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:89fc12c6371bf963809abc46cced4a01ca4f99cba17be5e7d416ed7ef1245d19"},
|
||||
{file = "coverage-5.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a8eb7785bd23565b542b01fb39115a975fefb4a82f23d407503eee2c0106247"},
|
||||
{file = "coverage-5.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:7e40d3f8eb472c1509b12ac2a7e24158ec352fc8567b77ab02c0db053927e339"},
|
||||
{file = "coverage-5.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1ccae21a076d3d5f471700f6d30eb486da1626c380b23c70ae32ab823e453337"},
|
||||
{file = "coverage-5.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:755c56beeacac6a24c8e1074f89f34f4373abce8b662470d3aa719ae304931f3"},
|
||||
{file = "coverage-5.4-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:322549b880b2d746a7672bf6ff9ed3f895e9c9f108b714e7360292aa5c5d7cf4"},
|
||||
{file = "coverage-5.4-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:60a3307a84ec60578accd35d7f0c71a3a971430ed7eca6567399d2b50ef37b8c"},
|
||||
{file = "coverage-5.4-cp35-cp35m-win32.whl", hash = "sha256:1375bb8b88cb050a2d4e0da901001347a44302aeadb8ceb4b6e5aa373b8ea68f"},
|
||||
{file = "coverage-5.4-cp35-cp35m-win_amd64.whl", hash = "sha256:16baa799ec09cc0dcb43a10680573269d407c159325972dd7114ee7649e56c66"},
|
||||
{file = "coverage-5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2f2cf7a42d4b7654c9a67b9d091ec24374f7c58794858bff632a2039cb15984d"},
|
||||
{file = "coverage-5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b62046592b44263fa7570f1117d372ae3f310222af1fc1407416f037fb3af21b"},
|
||||
{file = "coverage-5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:812eaf4939ef2284d29653bcfee9665f11f013724f07258928f849a2306ea9f9"},
|
||||
{file = "coverage-5.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:859f0add98707b182b4867359e12bde806b82483fb12a9ae868a77880fc3b7af"},
|
||||
{file = "coverage-5.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:04b14e45d6a8e159c9767ae57ecb34563ad93440fc1b26516a89ceb5b33c1ad5"},
|
||||
{file = "coverage-5.4-cp36-cp36m-win32.whl", hash = "sha256:ebfa374067af240d079ef97b8064478f3bf71038b78b017eb6ec93ede1b6bcec"},
|
||||
{file = "coverage-5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:84df004223fd0550d0ea7a37882e5c889f3c6d45535c639ce9802293b39cd5c9"},
|
||||
{file = "coverage-5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1b811662ecf72eb2d08872731636aee6559cae21862c36f74703be727b45df90"},
|
||||
{file = "coverage-5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6b588b5cf51dc0fd1c9e19f622457cc74b7d26fe295432e434525f1c0fae02bc"},
|
||||
{file = "coverage-5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3fe50f1cac369b02d34ad904dfe0771acc483f82a1b54c5e93632916ba847b37"},
|
||||
{file = "coverage-5.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:32ab83016c24c5cf3db2943286b85b0a172dae08c58d0f53875235219b676409"},
|
||||
{file = "coverage-5.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:68fb816a5dd901c6aff352ce49e2a0ffadacdf9b6fae282a69e7a16a02dad5fb"},
|
||||
{file = "coverage-5.4-cp37-cp37m-win32.whl", hash = "sha256:a636160680c6e526b84f85d304e2f0bb4e94f8284dd765a1911de9a40450b10a"},
|
||||
{file = "coverage-5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:bb32ca14b4d04e172c541c69eec5f385f9a075b38fb22d765d8b0ce3af3a0c22"},
|
||||
{file = "coverage-5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4d7165a4e8f41eca6b990c12ee7f44fef3932fac48ca32cecb3a1b2223c21f"},
|
||||
{file = "coverage-5.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a565f48c4aae72d1d3d3f8e8fb7218f5609c964e9c6f68604608e5958b9c60c3"},
|
||||
{file = "coverage-5.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fff1f3a586246110f34dc762098b5afd2de88de507559e63553d7da643053786"},
|
||||
{file = "coverage-5.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:a839e25f07e428a87d17d857d9935dd743130e77ff46524abb992b962eb2076c"},
|
||||
{file = "coverage-5.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:6625e52b6f346a283c3d563d1fd8bae8956daafc64bb5bbd2b8f8a07608e3994"},
|
||||
{file = "coverage-5.4-cp38-cp38-win32.whl", hash = "sha256:5bee3970617b3d74759b2d2df2f6a327d372f9732f9ccbf03fa591b5f7581e39"},
|
||||
{file = "coverage-5.4-cp38-cp38-win_amd64.whl", hash = "sha256:03ed2a641e412e42cc35c244508cf186015c217f0e4d496bf6d7078ebe837ae7"},
|
||||
{file = "coverage-5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:14a9f1887591684fb59fdba8feef7123a0da2424b0652e1b58dd5b9a7bb1188c"},
|
||||
{file = "coverage-5.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9564ac7eb1652c3701ac691ca72934dd3009997c81266807aef924012df2f4b3"},
|
||||
{file = "coverage-5.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:0f48fc7dc82ee14aeaedb986e175a429d24129b7eada1b7e94a864e4f0644dde"},
|
||||
{file = "coverage-5.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:107d327071061fd4f4a2587d14c389a27e4e5c93c7cba5f1f59987181903902f"},
|
||||
{file = "coverage-5.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:0cdde51bfcf6b6bd862ee9be324521ec619b20590787d1655d005c3fb175005f"},
|
||||
{file = "coverage-5.4-cp39-cp39-win32.whl", hash = "sha256:c67734cff78383a1f23ceba3b3239c7deefc62ac2b05fa6a47bcd565771e5880"},
|
||||
{file = "coverage-5.4-cp39-cp39-win_amd64.whl", hash = "sha256:c669b440ce46ae3abe9b2d44a913b5fd86bb19eb14a8701e88e3918902ecd345"},
|
||||
{file = "coverage-5.4-pp36-none-any.whl", hash = "sha256:c0ff1c1b4d13e2240821ef23c1efb1f009207cb3f56e16986f713c2b0e7cd37f"},
|
||||
{file = "coverage-5.4-pp37-none-any.whl", hash = "sha256:cd601187476c6bed26a0398353212684c427e10a903aeafa6da40c63309d438b"},
|
||||
{file = "coverage-5.4.tar.gz", hash = "sha256:6d2e262e5e8da6fa56e774fb8e2643417351427604c2b177f8e8c5f75fc928ca"},
|
||||
]
|
||||
dataclasses = [
|
||||
{file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
|
||||
@ -1071,12 +1167,12 @@ distlib = [
|
||||
{file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"},
|
||||
]
|
||||
editorconfig = [
|
||||
{file = "EditorConfig-0.12.2-py2-none-any.whl", hash = "sha256:60d6f10b87d2572ac1581cb8c9f018163e2b13a9e49588f9fb6dc8c715a1744c"},
|
||||
{file = "EditorConfig-0.12.2.tar.gz", hash = "sha256:1b0ef345f9c3a673e492cfe608ed644b236139f7fceab5c6f513a71bcaf8a56c"},
|
||||
{file = "EditorConfig-0.12.3-py3-none-any.whl", hash = "sha256:6b0851425aa875b08b16789ee0eeadbd4ab59666e9ebe728e526314c4a2e52c1"},
|
||||
{file = "EditorConfig-0.12.3.tar.gz", hash = "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e"},
|
||||
]
|
||||
execnet = [
|
||||
{file = "execnet-1.7.1-py2.py3-none-any.whl", hash = "sha256:d4efd397930c46415f62f8a31388d6be4f27a91d7550eb79bc64a756e0056547"},
|
||||
{file = "execnet-1.7.1.tar.gz", hash = "sha256:cacb9df31c9680ec5f95553976c4da484d407e85e41c83cb812aa014f0eddc50"},
|
||||
{file = "execnet-1.8.0-py2.py3-none-any.whl", hash = "sha256:7a13113028b1e1cc4c6492b28098b3c6576c9dccc7973bfe47b342afadafb2ac"},
|
||||
{file = "execnet-1.8.0.tar.gz", hash = "sha256:b73c5565e517f24b62dea8a5ceac178c661c4309d3aa0c3e420856c072c411b4"},
|
||||
]
|
||||
filelock = [
|
||||
{file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
|
||||
@ -1091,8 +1187,8 @@ flake8-bugbear = [
|
||||
{file = "flake8_bugbear-20.11.1-py36.py37.py38-none-any.whl", hash = "sha256:f35b8135ece7a014bc0aee5b5d485334ac30a6da48494998cc1fabf7ec70d703"},
|
||||
]
|
||||
flake8-comprehensions = [
|
||||
{file = "flake8-comprehensions-3.3.0.tar.gz", hash = "sha256:355ef47288523cad7977cb9c1bc81b71c82b7091e425cd9fbcd7e5c19a613677"},
|
||||
{file = "flake8_comprehensions-3.3.0-py3-none-any.whl", hash = "sha256:c1dd6d8a00e9722619a5c5e0e6c5747f5cf23c089032c86eaf614c14a2e40adb"},
|
||||
{file = "flake8-comprehensions-3.3.1.tar.gz", hash = "sha256:e734bf03806bb562886d9bf635d23a65a1a995c251b67d7e007a7b608af9bd22"},
|
||||
{file = "flake8_comprehensions-3.3.1-py3-none-any.whl", hash = "sha256:6d80dfafda0d85633f88ea5bc7de949485f71f1e28db7af7719563fe5f62dcb1"},
|
||||
]
|
||||
flake8-debugger = [
|
||||
{file = "flake8-debugger-3.2.1.tar.gz", hash = "sha256:712d7c1ff69ddf3f0130e94cc88c2519e720760bce45e8c330bfdcb61ab4090d"},
|
||||
@ -1101,20 +1197,20 @@ future = [
|
||||
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
|
||||
]
|
||||
identify = [
|
||||
{file = "identify-1.5.10-py2.py3-none-any.whl", hash = "sha256:cc86e6a9a390879dcc2976cef169dd9cc48843ed70b7380f321d1b118163c60e"},
|
||||
{file = "identify-1.5.10.tar.gz", hash = "sha256:943cd299ac7f5715fcb3f684e2fc1594c1e0f22a90d15398e5888143bd4144b5"},
|
||||
{file = "identify-1.5.13-py2.py3-none-any.whl", hash = "sha256:9dfb63a2e871b807e3ba62f029813552a24b5289504f5b071dea9b041aee9fe4"},
|
||||
{file = "identify-1.5.13.tar.gz", hash = "sha256:70b638cf4743f33042bebb3b51e25261a0a10e80f978739f17e7fd4837664a66"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
|
||||
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
|
||||
]
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-3.3.0-py3-none-any.whl", hash = "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"},
|
||||
{file = "importlib_metadata-3.3.0.tar.gz", hash = "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed"},
|
||||
{file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"},
|
||||
{file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"},
|
||||
]
|
||||
importlib-resources = [
|
||||
{file = "importlib_resources-3.3.0-py2.py3-none-any.whl", hash = "sha256:a3d34a8464ce1d5d7c92b0ea4e921e696d86f2aa212e684451cb1482c8d84ed5"},
|
||||
{file = "importlib_resources-3.3.0.tar.gz", hash = "sha256:7b51f0106c8ec564b1bef3d9c588bc694ce2b92125bbb6278f4f2f5b54ec3592"},
|
||||
{file = "importlib_resources-5.1.0-py3-none-any.whl", hash = "sha256:885b8eae589179f661c909d699a546cf10d83692553e34dca1bf5eb06f7f6217"},
|
||||
{file = "importlib_resources-5.1.0.tar.gz", hash = "sha256:bfdad047bce441405a49cf8eb48ddce5e56c696e185f59147a8b79e75e9e6380"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
@ -1144,15 +1240,15 @@ iteration-utilities = [
|
||||
{file = "iteration_utilities-0.10.1.tar.gz", hash = "sha256:536e3e87c5c139c775f9d95bb771c4b366a1d58eb7a39436d4ac839b53742569"},
|
||||
]
|
||||
jinja2 = [
|
||||
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
|
||||
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
|
||||
{file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"},
|
||||
{file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"},
|
||||
]
|
||||
joblib = [
|
||||
{file = "joblib-1.0.0-py3-none-any.whl", hash = "sha256:75ead23f13484a2a414874779d69ade40d4fa1abe62b222a23cd50d4bc822f6f"},
|
||||
{file = "joblib-1.0.0.tar.gz", hash = "sha256:7ad866067ac1fdec27d51c8678ea760601b70e32ff1881d4dc8e1171f2b64b24"},
|
||||
{file = "joblib-1.0.1-py3-none-any.whl", hash = "sha256:feeb1ec69c4d45129954f1b7034954241eedfd6ba39b5e9e4b6883be3332d5e5"},
|
||||
{file = "joblib-1.0.1.tar.gz", hash = "sha256:9c17567692206d2f3fb9ecf5e991084254fe631665c450b443761c4186a613f7"},
|
||||
]
|
||||
jsbeautifier = [
|
||||
{file = "jsbeautifier-1.13.0.tar.gz", hash = "sha256:f5565fbcd95f79945e124324815e586ae0d2e43df5af82a4400390e6ea789e8b"},
|
||||
{file = "jsbeautifier-1.13.5.tar.gz", hash = "sha256:4532a6bc85ba91ffc542b55d65cd13cedc971a934f26f51ed56d4c680b3fbe66"},
|
||||
]
|
||||
livereload = [
|
||||
{file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"},
|
||||
@ -1220,10 +1316,7 @@ mkdocs-mermaid2-plugin = [
|
||||
{file = "mkdocs-mermaid2-plugin-0.5.1.tar.gz", hash = "sha256:a267b77d0e80336ca12a72851209e90a07ba86b3551fa5422f3cd2ee1886f38a"},
|
||||
{file = "mkdocs_mermaid2_plugin-0.5.1-py3-none-any.whl", hash = "sha256:03709ef450ddcbd0a07ebeee2b68d6a90e63c4611401c2d3cf8bca24e075ff3e"},
|
||||
]
|
||||
mkdocstrings = [
|
||||
{file = "mkdocstrings-0.13.6-py3-none-any.whl", hash = "sha256:79d2a16b8c86a467bdc84846dfb90552551d2d9fd35578df9f92de13fb3b4537"},
|
||||
{file = "mkdocstrings-0.13.6.tar.gz", hash = "sha256:79e5086c79f60d1ae1d4b222f658d348ebdd6302c970cc06ee8394f2839d7c4d"},
|
||||
]
|
||||
mkdocstrings = []
|
||||
mypy = [
|
||||
{file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"},
|
||||
{file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"},
|
||||
@ -1252,8 +1345,8 @@ nodeenv = [
|
||||
{file = "nodeenv-1.5.0.tar.gz", hash = "sha256:ab45090ae383b716c4ef89e690c41ff8c2b257b85b309f01f3654df3d084bd7c"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"},
|
||||
{file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"},
|
||||
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
|
||||
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
|
||||
]
|
||||
pastel = [
|
||||
{file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"},
|
||||
@ -1272,24 +1365,24 @@ pluggy = [
|
||||
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
|
||||
]
|
||||
plumbum = [
|
||||
{file = "plumbum-1.6.9-py2.py3-none-any.whl", hash = "sha256:91418dcc66b58ab9d2e3b04b3d1e0d787dc45923154fb8b4a826bd9316dba0d6"},
|
||||
{file = "plumbum-1.6.9.tar.gz", hash = "sha256:16b9e19d96c80f2e9d051ef5f04927b834a6ac0ce5d2768eb8662b5cd53e43df"},
|
||||
{file = "plumbum-1.7.0-py2.py3-none-any.whl", hash = "sha256:139bbe08ee065b522a8a07d4f7e9f8eddffd78cc218b65b11cca2b33683e6b57"},
|
||||
{file = "plumbum-1.7.0.tar.gz", hash = "sha256:317744342c755319907c773cc87c3a30adaa3a41b0d34c0ce02d9d1904922dce"},
|
||||
]
|
||||
poethepoet = [
|
||||
{file = "poethepoet-0.9.0-py3-none-any.whl", hash = "sha256:6b1df9a755c297d5b10749cd4713924055b41edfa62055770c8bd6b5da8e2c69"},
|
||||
{file = "poethepoet-0.9.0.tar.gz", hash = "sha256:ab2263fd7be81d16d38a4b4fe42a055d992d04421e61cad36498b1e4bd8ee2a6"},
|
||||
]
|
||||
pre-commit = [
|
||||
{file = "pre_commit-2.9.3-py2.py3-none-any.whl", hash = "sha256:6c86d977d00ddc8a60d68eec19f51ef212d9462937acf3ea37c7adec32284ac0"},
|
||||
{file = "pre_commit-2.9.3.tar.gz", hash = "sha256:ee784c11953e6d8badb97d19bc46b997a3a9eded849881ec587accd8608d74a4"},
|
||||
{file = "pre_commit-2.10.1-py2.py3-none-any.whl", hash = "sha256:16212d1fde2bed88159287da88ff03796863854b04dc9f838a55979325a3d20e"},
|
||||
{file = "pre_commit-2.10.1.tar.gz", hash = "sha256:399baf78f13f4de82a29b649afd74bef2c4e28eb4f021661fc7f29246e8c7a3a"},
|
||||
]
|
||||
prompt-toolkit = [
|
||||
{file = "prompt_toolkit-3.0.8-py3-none-any.whl", hash = "sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63"},
|
||||
{file = "prompt_toolkit-3.0.8.tar.gz", hash = "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c"},
|
||||
{file = "prompt_toolkit-3.0.14-py3-none-any.whl", hash = "sha256:c96b30925025a7635471dc083ffb6af0cc67482a00611bd81aeaeeeb7e5a5e12"},
|
||||
{file = "prompt_toolkit-3.0.14.tar.gz", hash = "sha256:7e966747c18ececaec785699626b771c1ba8344c8d31759a1915d6b12fad6525"},
|
||||
]
|
||||
ptyprocess = [
|
||||
{file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"},
|
||||
{file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"},
|
||||
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
|
||||
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
|
||||
]
|
||||
py = [
|
||||
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
|
||||
@ -1328,24 +1421,28 @@ pyflakes = [
|
||||
{file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
|
||||
]
|
||||
pygments = [
|
||||
{file = "Pygments-2.7.3-py3-none-any.whl", hash = "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"},
|
||||
{file = "Pygments-2.7.3.tar.gz", hash = "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716"},
|
||||
{file = "Pygments-2.7.4-py3-none-any.whl", hash = "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435"},
|
||||
{file = "Pygments-2.7.4.tar.gz", hash = "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337"},
|
||||
]
|
||||
pymdown-extensions = [
|
||||
{file = "pymdown-extensions-8.0.1.tar.gz", hash = "sha256:9ba704052d4bdc04a7cd63f7db4ef6add73bafcef22c0cf6b2e3386cf4ece51e"},
|
||||
{file = "pymdown_extensions-8.0.1-py2.py3-none-any.whl", hash = "sha256:a3689c04f4cbddacd9d569425c571ae07e2673cc4df63a26cdbf1abc15229137"},
|
||||
{file = "pymdown-extensions-8.1.1.tar.gz", hash = "sha256:632371fa3bf1b21a0e3f4063010da59b41db049f261f4c0b0872069a9b6d1735"},
|
||||
{file = "pymdown_extensions-8.1.1-py3-none-any.whl", hash = "sha256:478b2c04513fbb2db61688d5f6e9030a92fb9be14f1f383535c43f7be9dff95b"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
|
||||
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
|
||||
]
|
||||
pypiwin32 = [
|
||||
{file = "pypiwin32-223-py3-none-any.whl", hash = "sha256:67adf399debc1d5d14dffc1ab5acacb800da569754fafdc576b2a039485aa775"},
|
||||
{file = "pypiwin32-223.tar.gz", hash = "sha256:71be40c1fbd28594214ecaecb58e7aa8b708eabfa0125c8a109ebd51edbd776a"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"},
|
||||
{file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"},
|
||||
{file = "pytest-6.2.2-py3-none-any.whl", hash = "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"},
|
||||
{file = "pytest-6.2.2.tar.gz", hash = "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9"},
|
||||
]
|
||||
pytest-cov = [
|
||||
{file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"},
|
||||
{file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"},
|
||||
{file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"},
|
||||
{file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"},
|
||||
]
|
||||
pytest-forked = [
|
||||
{file = "pytest-forked-1.3.0.tar.gz", hash = "sha256:6aa9ac7e00ad1a539c41bec6d21011332de671e938c7637378ec9710204e37ca"},
|
||||
@ -1355,29 +1452,48 @@ pytest-xdist = [
|
||||
{file = "pytest-xdist-2.2.0.tar.gz", hash = "sha256:1d8edbb1a45e8e1f8e44b1260583107fc23f8bc8da6d18cb331ff61d41258ecf"},
|
||||
{file = "pytest_xdist-2.2.0-py3-none-any.whl", hash = "sha256:f127e11e84ad37cc1de1088cb2990f3c354630d428af3f71282de589c5bb779b"},
|
||||
]
|
||||
pytkdocs = [
|
||||
{file = "pytkdocs-0.9.0-py3-none-any.whl", hash = "sha256:12ed87d71b3518301c7b8c12c1a620e4b481a9d2fca1038aea665955000fad7f"},
|
||||
{file = "pytkdocs-0.9.0.tar.gz", hash = "sha256:c8c39acb63824f69c3f6f58b3aed6ae55250c35804b76fd0cba09d5c11be13da"},
|
||||
pytkdocs = []
|
||||
pywin32 = [
|
||||
{file = "pywin32-300-cp35-cp35m-win32.whl", hash = "sha256:1c204a81daed2089e55d11eefa4826c05e604d27fe2be40b6bf8db7b6a39da63"},
|
||||
{file = "pywin32-300-cp35-cp35m-win_amd64.whl", hash = "sha256:350c5644775736351b77ba68da09a39c760d75d2467ecec37bd3c36a94fbed64"},
|
||||
{file = "pywin32-300-cp36-cp36m-win32.whl", hash = "sha256:a3b4c48c852d4107e8a8ec980b76c94ce596ea66d60f7a697582ea9dce7e0db7"},
|
||||
{file = "pywin32-300-cp36-cp36m-win_amd64.whl", hash = "sha256:27a30b887afbf05a9cbb05e3ffd43104a9b71ce292f64a635389dbad0ed1cd85"},
|
||||
{file = "pywin32-300-cp37-cp37m-win32.whl", hash = "sha256:d7e8c7efc221f10d6400c19c32a031add1c4a58733298c09216f57b4fde110dc"},
|
||||
{file = "pywin32-300-cp37-cp37m-win_amd64.whl", hash = "sha256:8151e4d7a19262d6694162d6da85d99a16f8b908949797fd99c83a0bfaf5807d"},
|
||||
{file = "pywin32-300-cp38-cp38-win32.whl", hash = "sha256:fbb3b1b0fbd0b4fc2a3d1d81fe0783e30062c1abed1d17c32b7879d55858cfae"},
|
||||
{file = "pywin32-300-cp38-cp38-win_amd64.whl", hash = "sha256:60a8fa361091b2eea27f15718f8eb7f9297e8d51b54dbc4f55f3d238093d5190"},
|
||||
{file = "pywin32-300-cp39-cp39-win32.whl", hash = "sha256:638b68eea5cfc8def537e43e9554747f8dee786b090e47ead94bfdafdb0f2f50"},
|
||||
{file = "pywin32-300-cp39-cp39-win_amd64.whl", hash = "sha256:b1609ce9bd5c411b81f941b246d683d6508992093203d4eb7f278f4ed1085c3f"},
|
||||
]
|
||||
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"},
|
||||
{file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"},
|
||||
{file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"},
|
||||
{file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"},
|
||||
{file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"},
|
||||
{file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"},
|
||||
{file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"},
|
||||
{file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"},
|
||||
{file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"},
|
||||
{file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"},
|
||||
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
|
||||
{file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
|
||||
{file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
|
||||
{file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
|
||||
{file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
|
||||
{file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
|
||||
{file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
|
||||
{file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
|
||||
{file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
|
||||
{file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
|
||||
{file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
|
||||
{file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
|
||||
{file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
|
||||
{file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
|
||||
{file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
|
||||
{file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
|
||||
{file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
|
||||
{file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
|
||||
{file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
|
||||
{file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
|
||||
{file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
|
||||
]
|
||||
pyyaml-include = [
|
||||
{file = "pyyaml_include-1.2.post2-py3-none-any.whl", hash = "sha256:d74c7209f5150d841a529acf47d7a023769c9160949f366fb4e17efa8999c4be"},
|
||||
]
|
||||
questionary = [
|
||||
{file = "questionary-1.8.1-py3-none-any.whl", hash = "sha256:e03ec1a585b2ff5a1463238fe63c12d82c740834e17e7b8efae9be58393a8fc9"},
|
||||
{file = "questionary-1.8.1.tar.gz", hash = "sha256:f2999f01735db77a80d6cb119766cb15b84c468cab325168941a3e0d91207437"},
|
||||
{file = "questionary-1.9.0-py3-none-any.whl", hash = "sha256:fa50c06af4e3826d986efbc90be16e42ff367a634e6a169e42a3f9fccd90648b"},
|
||||
{file = "questionary-1.9.0.tar.gz", hash = "sha256:a050fdbb81406cddca679a6f492c6272da90cb09988963817828f697cf091c55"},
|
||||
]
|
||||
regex = [
|
||||
{file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"},
|
||||
@ -1486,31 +1602,44 @@ tornado = [
|
||||
{file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"},
|
||||
]
|
||||
tqdm = [
|
||||
{file = "tqdm-4.54.1-py2.py3-none-any.whl", hash = "sha256:d4f413aecb61c9779888c64ddf0c62910ad56dcbe857d8922bb505d4dbff0df1"},
|
||||
{file = "tqdm-4.54.1.tar.gz", hash = "sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5"},
|
||||
{file = "tqdm-4.56.0-py2.py3-none-any.whl", hash = "sha256:4621f6823bab46a9cc33d48105753ccbea671b68bab2c50a9f0be23d4065cb5a"},
|
||||
{file = "tqdm-4.56.0.tar.gz", hash = "sha256:fe3d08dd00a526850568d542ff9de9bbc2a09a791da3c334f3213d8d0bbbca65"},
|
||||
]
|
||||
typed-ast = [
|
||||
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
|
||||
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
|
||||
{file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"},
|
||||
{file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
|
||||
{file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
|
||||
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"},
|
||||
{file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"},
|
||||
{file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"},
|
||||
{file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"},
|
||||
{file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"},
|
||||
{file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"},
|
||||
]
|
||||
typing = [
|
||||
{file = "typing-3.7.4.3-py2-none-any.whl", hash = "sha256:283d868f5071ab9ad873e5e52268d611e851c870a2ba354193026f2dfb29d8b5"},
|
||||
{file = "typing-3.7.4.3.tar.gz", hash = "sha256:1187fb9c82fd670d10aa07bbb6cfcfe4bdda42d6fab8d5134f04e8c4d0b71cc9"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
|
||||
@ -1518,12 +1647,12 @@ typing-extensions = [
|
||||
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"},
|
||||
{file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"},
|
||||
{file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"},
|
||||
{file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"},
|
||||
]
|
||||
virtualenv = [
|
||||
{file = "virtualenv-20.2.2-py2.py3-none-any.whl", hash = "sha256:54b05fc737ea9c9ee9f8340f579e5da5b09fb64fd010ab5757eb90268616907c"},
|
||||
{file = "virtualenv-20.2.2.tar.gz", hash = "sha256:b7a8ec323ee02fb2312f098b6b4c9de99559b462775bc8fe3627a73706603c1b"},
|
||||
{file = "virtualenv-20.4.2-py2.py3-none-any.whl", hash = "sha256:2be72df684b74df0ea47679a7df93fd0e04e72520022c57b479d8f881485dbe3"},
|
||||
{file = "virtualenv-20.4.2.tar.gz", hash = "sha256:147b43894e51dd6bba882cf9c282447f780e2251cd35172403745fc381a0a80d"},
|
||||
]
|
||||
wcwidth = [
|
||||
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
|
||||
|
@ -26,24 +26,30 @@ copier = "copier.cli:CopierApp.run"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.6.1,<3.10"
|
||||
"backports.cached-property" = {version = "^1.0.0", python = "<3.8"}
|
||||
colorama = "^0.4.3"
|
||||
iteration_utilities = "^0.10.1"
|
||||
jinja2 = "^2.11.2"
|
||||
mkdocs-material = {version = "^5.5.2", optional = true}
|
||||
mkdocs-mermaid2-plugin = {version = "^0.5.0", optional = true}
|
||||
# HACK https://github.com/pawamoy/mkdocstrings/issues/222#issuecomment-774394285
|
||||
# TODO install from Pypi when 0.15 is released
|
||||
mkdocstrings = {git = "https://github.com/pawamoy/mkdocstrings.git", rev = "master", optional = true}
|
||||
packaging = "^20.4" # packaging is needed when installing from PyPI
|
||||
pathspec = "^0.8.0"
|
||||
plumbum = "^1.6.9"
|
||||
pydantic = "^1.7.2"
|
||||
Pygments = "^2.7.1"
|
||||
questionary = "^1.8.1"
|
||||
pyyaml = "^5.3.1"
|
||||
pyyaml-include = "^1.2"
|
||||
# packaging is needed when installing from PyPI
|
||||
packaging = "^20.4"
|
||||
mkdocstrings = {version = "^0.13.1", optional = true}
|
||||
mkdocs-material = {version = "^5.5.2", optional = true}
|
||||
mkdocs-mermaid2-plugin = {version = "^0.5.0", optional = true}
|
||||
questionary = "^1.8.1"
|
||||
typing-extensions = {version = "^3.7.4", python = "<3.8"}
|
||||
# HACK https://github.com/pawamoy/pytkdocs/issues/86
|
||||
# HACK https://github.com/pawamoy/mkdocstrings/issues/225#issuecomment-774669999
|
||||
pytkdocs = {git = "https://github.com/pawamoy/pytkdocs.git", rev = "master", optional = true}
|
||||
|
||||
[tool.poetry.extras]
|
||||
docs = ["mkdocstrings", "mkdocs-material", "mkdocs-mermaid2-plugin"]
|
||||
docs = ["mkdocstrings", "mkdocs-material", "mkdocs-mermaid2-plugin", "pytkdocs"]
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
autoflake = "^1.4"
|
||||
@ -103,10 +109,6 @@ line_length = 88
|
||||
multi_line_output = 3 # black interop
|
||||
use_parentheses = true
|
||||
|
||||
[flake8]
|
||||
max-complexity = 20
|
||||
ignore = ",W503,E203,E501,D100,D101,D102,D103,D104,D105,D107,"
|
||||
|
||||
[build-system]
|
||||
# TODO Switch back to poetry-core when it's possible again
|
||||
# HACK https://github.com/mtkennerly/poetry-dynamic-versioning/issues/35
|
||||
|
@ -1,2 +0,0 @@
|
||||
# Changes here will be overwritten by Copier
|
||||
[[ _copier_answers|to_nice_yaml ]]
|
@ -1,16 +0,0 @@
|
||||
_answers_file: .answers-file-changed-in-template.yml
|
||||
|
||||
round: 1st
|
||||
|
||||
# A str question without a default should default to None
|
||||
str_question_without_default:
|
||||
type: str
|
||||
|
||||
# password_1 and password_2 must not appear in answers file
|
||||
_secret_questions:
|
||||
- password_1
|
||||
|
||||
password_1: password one
|
||||
password_2:
|
||||
secret: yes
|
||||
default: password two
|
@ -1,3 +0,0 @@
|
||||
It's the [[round]] round.
|
||||
password_1=[[password_1]]
|
||||
password_2=[[password_2]]
|
@ -1,53 +0,0 @@
|
||||
love_me:
|
||||
help: I need to know it. Do you love me?
|
||||
type: bool
|
||||
your_name:
|
||||
help: Please tell me your name.
|
||||
type: str
|
||||
your_age:
|
||||
help: How old are you?
|
||||
type: int
|
||||
your_height:
|
||||
help: What's your height?
|
||||
type: float
|
||||
more_json_info:
|
||||
multiline: true
|
||||
type: json
|
||||
anything_else:
|
||||
multiline: true
|
||||
help: Wanna give me any more info?
|
||||
|
||||
# In choices, the user will always write the choice index (1, 2, 3...)
|
||||
choose_list:
|
||||
help: You see the value of the list items
|
||||
default: first
|
||||
choices:
|
||||
- first
|
||||
- second
|
||||
- third
|
||||
choose_tuple:
|
||||
help: You see the 1st tuple item, but I get the 2nd item as a result
|
||||
default: second
|
||||
choices:
|
||||
- [one, first]
|
||||
- [two, second]
|
||||
- [three, third]
|
||||
choose_dict:
|
||||
help: You see the dict key, but I get the dict value
|
||||
default: third
|
||||
choices:
|
||||
one: first
|
||||
two: second
|
||||
three: third
|
||||
choose_number:
|
||||
help: This must be a number
|
||||
default: null
|
||||
type: float
|
||||
choices:
|
||||
- -1.1
|
||||
- 0
|
||||
- 1
|
||||
|
||||
# Simplified format is still supported
|
||||
minutes_under_water: 10
|
||||
optional_value: null
|
@ -1,13 +0,0 @@
|
||||
|
||||
love_me: [[love_me|tojson]]
|
||||
your_name: [[your_name|tojson]]
|
||||
your_age: [[your_age|tojson]]
|
||||
your_height: [[your_height|tojson]]
|
||||
more_json_info: [[more_json_info|tojson]]
|
||||
anything_else: [[anything_else|tojson]]
|
||||
choose_list: [[choose_list|tojson]]
|
||||
choose_tuple: [[choose_tuple|tojson]]
|
||||
choose_dict: [[choose_dict|tojson]]
|
||||
choose_number: [[choose_number|tojson]]
|
||||
minutes_under_water: [[minutes_under_water|tojson]]
|
||||
optional_value: [[optional_value|tojson]]
|
@ -9,6 +9,3 @@ _skip_if_exists:
|
||||
_tasks:
|
||||
- "touch 1"
|
||||
- "touch 2"
|
||||
|
||||
_extra_paths:
|
||||
- "tests"
|
||||
|
@ -1 +0,0 @@
|
||||
[% extends "parent.txt" %]
|
@ -1 +0,0 @@
|
||||
[% extends "parent.txt" %]
|
@ -1,2 +0,0 @@
|
||||
_extra_paths:
|
||||
- "./tests/demo_extra_paths/parent"
|
@ -1 +0,0 @@
|
||||
PARENT_CONTENT
|
@ -1 +0,0 @@
|
||||
_min_copier_version: "10.5.1"
|
@ -1,3 +0,0 @@
|
||||
# Demo subdirectory
|
||||
|
||||
Generated using previous answers `_subdirectory` value.
|
@ -1 +0,0 @@
|
||||
_subdirectory: conf_project
|
@ -1,6 +0,0 @@
|
||||
# This tests two things:
|
||||
# 1. That the tasks are being executed in the destiantion folder; and
|
||||
# 2. That the tasks are being executed in order, one after another
|
||||
_tasks:
|
||||
- mkdir hello
|
||||
- cd hello && touch world
|
@ -1,17 +0,0 @@
|
||||
_envops:
|
||||
block_start_string: "<%"
|
||||
block_end_string: "%>"
|
||||
comment_start_string: "<#"
|
||||
comment_end_string: "#>"
|
||||
variable_start_string: "<<"
|
||||
variable_end_string: ">>"
|
||||
|
||||
powerlevel:
|
||||
type: int
|
||||
default: 9000
|
||||
|
||||
sentence:
|
||||
type: str
|
||||
default:
|
||||
"<% if powerlevel >= 9000 %>It's over 9000!<% else %>It's only << powerlevel >>...<%
|
||||
endif %>"
|
@ -37,6 +37,10 @@ COPIER_CMD = local.get(
|
||||
)
|
||||
COPIER_PATH = str(COPIER_CMD.executable)
|
||||
|
||||
# Helper to parse back an iso-formatted datetime; needed for py3.6, where
|
||||
# datetime.datetime.fromisoformat() doesn't exist
|
||||
ISOFORMAT = "%Y-%m-%d %H:%M:%S.%f"
|
||||
|
||||
|
||||
class Keyboard(str, Enum):
|
||||
ControlH = REVERSE_ANSI_SEQUENCES[Keys.ControlH]
|
||||
|
@ -3,20 +3,58 @@ from textwrap import dedent
|
||||
import pytest
|
||||
|
||||
import copier
|
||||
from copier.config.user_data import load_answersfile_data
|
||||
from copier.user_data import load_answersfile_data
|
||||
|
||||
from .helpers import PROJECT_TEMPLATE
|
||||
from .helpers import build_file_tree
|
||||
|
||||
SRC = f"{PROJECT_TEMPLATE}_answersfile"
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def template_path(tmp_path_factory) -> str:
|
||||
root = tmp_path_factory.mktemp("template")
|
||||
build_file_tree(
|
||||
{
|
||||
root
|
||||
/ "[[ _copier_conf.answers_file ]].tmpl": """\
|
||||
# Changes here will be overwritten by Copier
|
||||
[[ _copier_answers|to_nice_yaml ]]
|
||||
""",
|
||||
root
|
||||
/ "copier.yml": """\
|
||||
_answers_file: .answers-file-changed-in-template.yml
|
||||
|
||||
round: 1st
|
||||
|
||||
# A str question without a default should default to None
|
||||
str_question_without_default:
|
||||
type: str
|
||||
|
||||
# password_1 and password_2 must not appear in answers file
|
||||
_secret_questions:
|
||||
- password_1
|
||||
|
||||
password_1: password one
|
||||
password_2:
|
||||
secret: yes
|
||||
default: password two
|
||||
""",
|
||||
root
|
||||
/ "round.txt.tmpl": """\
|
||||
It's the [[round]] round.
|
||||
password_1=[[password_1]]
|
||||
password_2=[[password_2]]
|
||||
""",
|
||||
}
|
||||
)
|
||||
return str(root)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("answers_file", [None, ".changed-by-user.yaml"])
|
||||
def test_answersfile(tmp_path, answers_file):
|
||||
def test_answersfile(template_path, tmp_path, answers_file):
|
||||
"""Test copier behaves properly when using an answersfile."""
|
||||
round_file = tmp_path / "round.txt"
|
||||
|
||||
# Check 1st round is properly executed and remembered
|
||||
copier.copy(SRC, tmp_path, answers_file=answers_file, force=True)
|
||||
copier.copy(template_path, tmp_path, answers_file=answers_file, force=True)
|
||||
answers_file = answers_file or ".answers-file-changed-in-template.yml"
|
||||
assert (
|
||||
round_file.read_text()
|
||||
@ -35,7 +73,9 @@ def test_answersfile(tmp_path, answers_file):
|
||||
assert "password_2" not in log
|
||||
|
||||
# Check 2nd round is properly executed and remembered
|
||||
copier.copy(SRC, tmp_path, {"round": "2nd"}, answers_file=answers_file, force=True)
|
||||
copier.copy(
|
||||
template_path, tmp_path, {"round": "2nd"}, answers_file=answers_file, force=True
|
||||
)
|
||||
assert (
|
||||
round_file.read_text()
|
||||
== dedent(
|
||||
@ -53,7 +93,7 @@ def test_answersfile(tmp_path, answers_file):
|
||||
assert "password_2" not in log
|
||||
|
||||
# Check repeating 2nd is properly executed and remembered
|
||||
copier.copy(SRC, tmp_path, answers_file=answers_file, force=True)
|
||||
copier.copy(template_path, tmp_path, answers_file=answers_file, force=True)
|
||||
assert (
|
||||
round_file.read_text()
|
||||
== dedent(
|
||||
|
@ -2,18 +2,98 @@ from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
import pexpect
|
||||
import pytest
|
||||
|
||||
from copier import copy
|
||||
|
||||
from .helpers import COPIER_PATH, PROJECT_TEMPLATE, Keyboard
|
||||
|
||||
SRC = f"{PROJECT_TEMPLATE}_complex_questions"
|
||||
from .helpers import COPIER_PATH, Keyboard, build_file_tree
|
||||
|
||||
|
||||
def test_api(tmp_path):
|
||||
@pytest.fixture(scope="module")
|
||||
def template_path(tmp_path_factory) -> str:
|
||||
root = tmp_path_factory.mktemp("template")
|
||||
build_file_tree(
|
||||
{
|
||||
root
|
||||
/ "copier.yml": """\
|
||||
love_me:
|
||||
help: I need to know it. Do you love me?
|
||||
type: bool
|
||||
your_name:
|
||||
help: Please tell me your name.
|
||||
type: str
|
||||
your_age:
|
||||
help: How old are you?
|
||||
type: int
|
||||
your_height:
|
||||
help: What's your height?
|
||||
type: float
|
||||
more_json_info:
|
||||
multiline: true
|
||||
type: json
|
||||
anything_else:
|
||||
multiline: true
|
||||
help: Wanna give me any more info?
|
||||
|
||||
# In choices, the user will always write the choice index (1, 2, 3...)
|
||||
choose_list:
|
||||
help: You see the value of the list items
|
||||
default: first
|
||||
choices:
|
||||
- first
|
||||
- second
|
||||
- third
|
||||
choose_tuple:
|
||||
help: You see the 1st tuple item, but I get the 2nd item as a result
|
||||
default: second
|
||||
choices:
|
||||
- [one, first]
|
||||
- [two, second]
|
||||
- [three, third]
|
||||
choose_dict:
|
||||
help: You see the dict key, but I get the dict value
|
||||
default: third
|
||||
choices:
|
||||
one: first
|
||||
two: second
|
||||
three: third
|
||||
choose_number:
|
||||
help: This must be a number
|
||||
default: null
|
||||
type: float
|
||||
choices:
|
||||
- -1.1
|
||||
- 0
|
||||
- 1
|
||||
|
||||
# Simplified format is still supported
|
||||
minutes_under_water: 10
|
||||
optional_value: null
|
||||
""",
|
||||
root
|
||||
/ "results.txt.tmpl": """\
|
||||
love_me: [[love_me|tojson]]
|
||||
your_name: [[your_name|tojson]]
|
||||
your_age: [[your_age|tojson]]
|
||||
your_height: [[your_height|tojson]]
|
||||
more_json_info: [[more_json_info|tojson]]
|
||||
anything_else: [[anything_else|tojson]]
|
||||
choose_list: [[choose_list|tojson]]
|
||||
choose_tuple: [[choose_tuple|tojson]]
|
||||
choose_dict: [[choose_dict|tojson]]
|
||||
choose_number: [[choose_number|tojson]]
|
||||
minutes_under_water: [[minutes_under_water|tojson]]
|
||||
optional_value: [[optional_value|tojson]]
|
||||
""",
|
||||
}
|
||||
)
|
||||
return str(root)
|
||||
|
||||
|
||||
def test_api(tmp_path, template_path):
|
||||
"""Test copier correctly processes advanced questions and answers through API."""
|
||||
copy(
|
||||
SRC,
|
||||
template_path,
|
||||
tmp_path,
|
||||
{
|
||||
"love_me": False,
|
||||
@ -27,7 +107,7 @@ def test_api(tmp_path):
|
||||
)
|
||||
results_file = tmp_path / "results.txt"
|
||||
assert results_file.read_text() == dedent(
|
||||
"""
|
||||
"""\
|
||||
love_me: false
|
||||
your_name: "LeChuck"
|
||||
your_age: 220
|
||||
@ -44,13 +124,13 @@ def test_api(tmp_path):
|
||||
)
|
||||
|
||||
|
||||
def test_cli_interactive(tmp_path, spawn):
|
||||
def test_cli_interactive(tmp_path, spawn, template_path):
|
||||
"""Test copier correctly processes advanced questions and answers through CLI."""
|
||||
invalid = [
|
||||
"Invalid value",
|
||||
"please try again",
|
||||
]
|
||||
tui = spawn([COPIER_PATH, "copy", SRC, str(tmp_path)], timeout=10)
|
||||
tui = spawn([COPIER_PATH, "copy", template_path, str(tmp_path)], timeout=10)
|
||||
tui.expect_exact(["I need to know it. Do you love me?", "love_me", "Format: bool"])
|
||||
tui.send("y")
|
||||
tui.expect_exact(["Please tell me your name.", "your_name", "Format: str"])
|
||||
@ -120,13 +200,13 @@ def test_cli_interactive(tmp_path, spawn):
|
||||
tui.expect_exact(pexpect.EOF)
|
||||
results_file = tmp_path / "results.txt"
|
||||
assert results_file.read_text() == dedent(
|
||||
r"""
|
||||
"""\
|
||||
love_me: true
|
||||
your_name: "Guybrush Threpwood"
|
||||
your_age: 22
|
||||
your_height: 1.56
|
||||
more_json_info: {"objective": "be a pirate"}
|
||||
anything_else: ["Want some grog?", "I\u0027d love it"]
|
||||
anything_else: ["Want some grog?", "I\\u0027d love it"]
|
||||
choose_list: "first"
|
||||
choose_tuple: "second"
|
||||
choose_dict: "third"
|
||||
@ -137,13 +217,13 @@ def test_cli_interactive(tmp_path, spawn):
|
||||
)
|
||||
|
||||
|
||||
def test_api_str_data(tmp_path):
|
||||
def test_api_str_data(tmp_path, template_path):
|
||||
"""Test copier when all data comes as a string.
|
||||
|
||||
This happens i.e. when using the --data CLI argument.
|
||||
"""
|
||||
copy(
|
||||
SRC,
|
||||
template_path,
|
||||
tmp_path,
|
||||
data={
|
||||
"love_me": "false",
|
||||
@ -158,24 +238,26 @@ def test_api_str_data(tmp_path):
|
||||
)
|
||||
results_file = tmp_path / "results.txt"
|
||||
assert results_file.read_text() == dedent(
|
||||
r"""
|
||||
love_me: false
|
||||
"""\
|
||||
love_me: "false"
|
||||
your_name: "LeChuck"
|
||||
your_age: 220
|
||||
your_height: 1.9
|
||||
more_json_info: ["bad", "guy"]
|
||||
anything_else: {"hates": "all"}
|
||||
your_age: "220"
|
||||
your_height: "1.9"
|
||||
more_json_info: "[\\"bad\\", \\"guy\\"]"
|
||||
anything_else: "{\\u0027hates\\u0027: \\u0027all\\u0027}"
|
||||
choose_list: "first"
|
||||
choose_tuple: "second"
|
||||
choose_dict: "third"
|
||||
choose_number: 0.0
|
||||
choose_number: "0"
|
||||
minutes_under_water: 10
|
||||
optional_value: null
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_cli_interatively_with_flag_data_and_type_casts(tmp_path: Path, spawn):
|
||||
def test_cli_interatively_with_flag_data_and_type_casts(
|
||||
tmp_path: Path, spawn, template_path
|
||||
):
|
||||
"""Assert how choices work when copier is invoked with --data interactively."""
|
||||
invalid = [
|
||||
"Invalid value",
|
||||
@ -189,7 +271,7 @@ def test_cli_interatively_with_flag_data_and_type_casts(tmp_path: Path, spawn):
|
||||
"--data=choose_tuple=third",
|
||||
"--data=choose_number=1",
|
||||
"copy",
|
||||
SRC,
|
||||
template_path,
|
||||
str(tmp_path),
|
||||
],
|
||||
timeout=10,
|
||||
@ -229,17 +311,17 @@ def test_cli_interatively_with_flag_data_and_type_casts(tmp_path: Path, spawn):
|
||||
tui.expect_exact(pexpect.EOF)
|
||||
results_file = tmp_path / "results.txt"
|
||||
assert results_file.read_text() == dedent(
|
||||
r"""
|
||||
"""\
|
||||
love_me: true
|
||||
your_name: "Guybrush Threpwood"
|
||||
your_age: 22
|
||||
your_height: 1.56
|
||||
more_json_info: {"objective": "be a pirate"}
|
||||
anything_else: ["Want some grog?", "I\u0027d love it"]
|
||||
anything_else: ["Want some grog?", "I\\u0027d love it"]
|
||||
choose_list: "second"
|
||||
choose_tuple: "third"
|
||||
choose_dict: "first"
|
||||
choose_number: 1.0
|
||||
choose_number: 1
|
||||
minutes_under_water: 10
|
||||
optional_value: null
|
||||
"""
|
||||
|
@ -5,14 +5,9 @@ from plumbum import local
|
||||
from pydantic import ValidationError
|
||||
|
||||
import copier
|
||||
from copier.config.factory import make_config
|
||||
from copier.config.objects import DEFAULT_EXCLUDE, ConfigData, EnvOps
|
||||
from copier.config.user_data import (
|
||||
InvalidConfigFileError,
|
||||
MultipleConfigFilesError,
|
||||
load_config_data,
|
||||
load_yaml_data,
|
||||
)
|
||||
from copier.errors import InvalidConfigFileError, MultipleConfigFilesError
|
||||
from copier.template import DEFAULT_EXCLUDE
|
||||
from copier.user_data import load_config_data, load_yaml_data
|
||||
|
||||
from .helpers import build_file_tree
|
||||
|
||||
@ -34,7 +29,6 @@ def test_config_data_is_loaded_from_file():
|
||||
assert config["_exclude"] == ["exclude1", "exclude2"]
|
||||
assert config["_skip_if_exists"] == ["skip_if_exists1", "skip_if_exists2"]
|
||||
assert config["_tasks"] == ["touch 1", "touch 2"]
|
||||
assert config["_extra_paths"] == ["tests"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("template", ["tests/demo_yaml", "tests/demo_yml"])
|
||||
@ -145,150 +139,60 @@ def test_multiple_config_file_error(capsys):
|
||||
{"pretend": "not_a_bool"},
|
||||
{"quiet": "not_a_bool"},
|
||||
{"force": "not_a_bool"},
|
||||
{"skip": "not_a_bool"},
|
||||
{"cleanup_on_error": "not_a_bool"},
|
||||
{"force": True, "skip": True},
|
||||
{"force": True, "skip_if_exists": True},
|
||||
),
|
||||
)
|
||||
def test_flags_bad_data(data):
|
||||
with pytest.raises(ValidationError):
|
||||
ConfigData(**data)
|
||||
copier.Worker(**data)
|
||||
|
||||
|
||||
def test_flags_extra_ignored():
|
||||
def test_flags_extra_fails():
|
||||
key = "i_am_not_a_member"
|
||||
conf_data = {"src_path": "..", "dst_path": ".", key: "and_i_do_not_belong_here"}
|
||||
confs = ConfigData(**conf_data)
|
||||
assert key not in confs.dict()
|
||||
with pytest.raises(TypeError):
|
||||
copier.Worker(**conf_data)
|
||||
|
||||
|
||||
# EnvOps
|
||||
@pytest.mark.parametrize(
|
||||
"data",
|
||||
(
|
||||
{"autoescape": "not_a_bool"},
|
||||
{"block_start_string": None},
|
||||
{"block_end_string": None},
|
||||
{"variable_start_string": None},
|
||||
{"variable_end_string": None},
|
||||
{"keep_trailing_newline": "not_a_bool"},
|
||||
),
|
||||
)
|
||||
def test_envops_bad_data(data):
|
||||
with pytest.raises(ValidationError):
|
||||
EnvOps(**data)
|
||||
|
||||
|
||||
def test_envops_good_data():
|
||||
ops = EnvOps(**GOOD_ENV_OPS)
|
||||
assert ops.dict() == GOOD_ENV_OPS
|
||||
|
||||
|
||||
# ConfigData
|
||||
def test_config_data_paths_required():
|
||||
try:
|
||||
ConfigData(envops=EnvOps())
|
||||
except ValidationError as e:
|
||||
assert len(e.errors()) == 2
|
||||
for i, p in enumerate(("src_path", "dst_path")):
|
||||
err = e.errors()[i]
|
||||
assert err["loc"][0] == p
|
||||
assert err["type"] == "value_error.missing"
|
||||
else:
|
||||
raise AssertionError()
|
||||
|
||||
|
||||
def test_config_data_paths_existing(tmp_path):
|
||||
try:
|
||||
ConfigData(
|
||||
src_path="./i_do_not_exist",
|
||||
extra_paths=["./i_do_not_exist"],
|
||||
dst_path=tmp_path,
|
||||
envops=EnvOps(),
|
||||
)
|
||||
except ValidationError as e:
|
||||
assert len(e.errors()) == 2
|
||||
for i, p in enumerate(("src_path", "extra_paths")):
|
||||
err = e.errors()[i]
|
||||
assert err["loc"][0] == p
|
||||
assert err["msg"] == "Project template not found."
|
||||
else:
|
||||
raise AssertionError()
|
||||
|
||||
|
||||
def test_config_data_good_data(tmp_path):
|
||||
tmp_path = Path(tmp_path).expanduser().resolve()
|
||||
expected = {
|
||||
"src_path": tmp_path,
|
||||
"commit": None,
|
||||
"old_commit": None,
|
||||
"dst_path": tmp_path,
|
||||
"extra_paths": [tmp_path],
|
||||
"exclude": DEFAULT_EXCLUDE,
|
||||
"original_src_path": None,
|
||||
"skip_if_exists": ["skip_me"],
|
||||
"tasks": ["echo python rulez"],
|
||||
"templates_suffix": ".tmpl",
|
||||
"cleanup_on_error": True,
|
||||
"envops": EnvOps().dict(),
|
||||
"force": False,
|
||||
"only_diff": True,
|
||||
"pretend": False,
|
||||
"quiet": False,
|
||||
"skip": False,
|
||||
"vcs_ref": None,
|
||||
"migrations": (),
|
||||
"secret_questions": (),
|
||||
"subdirectory": None,
|
||||
}
|
||||
conf = ConfigData(**expected)
|
||||
conf.data["_folder_name"] = tmp_path.name
|
||||
expected["answers_file"] = Path(".copier-answers.yml")
|
||||
conf_dict = conf.dict()
|
||||
for key, value in expected.items():
|
||||
assert conf_dict[key] == value
|
||||
|
||||
|
||||
def test_make_config_bad_data(tmp_path):
|
||||
with pytest.raises(ValidationError):
|
||||
make_config("./i_do_not_exist", tmp_path)
|
||||
def test_missing_template(tmp_path):
|
||||
with pytest.raises(ValueError):
|
||||
copier.copy("./i_do_not_exist", tmp_path)
|
||||
|
||||
|
||||
def is_subdict(small, big):
|
||||
return {**big, **small} == big
|
||||
|
||||
|
||||
def test_make_config_good_data(tmp_path):
|
||||
conf = make_config("./tests/demo_data", tmp_path)
|
||||
assert conf is not None
|
||||
assert "_folder_name" in conf.data
|
||||
assert conf.data["_folder_name"] == tmp_path.name
|
||||
assert conf.exclude == ["exclude1", "exclude2"]
|
||||
assert conf.skip_if_exists == ["skip_if_exists1", "skip_if_exists2"]
|
||||
assert conf.tasks == ["touch 1", "touch 2"]
|
||||
assert conf.extra_paths == [Path("tests").resolve()]
|
||||
def test_worker_good_data(tmp_path):
|
||||
# This test is probably useless, as it tests the what and not the how
|
||||
conf = copier.Worker("./tests/demo_data", tmp_path)
|
||||
assert conf._render_context()["_folder_name"] == tmp_path.name
|
||||
assert conf.all_exclusions == ("exclude1", "exclude2")
|
||||
assert conf.template.skip_if_exists == ["skip_if_exists1", "skip_if_exists2"]
|
||||
assert conf.template.tasks == ["touch 1", "touch 2"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_input, expected",
|
||||
"test_input, expected_exclusions",
|
||||
[
|
||||
# func_args > defaults
|
||||
(
|
||||
{"src_path": ".", "exclude": ["aaa"]},
|
||||
{"exclude": list(DEFAULT_EXCLUDE) + ["aaa"]},
|
||||
tuple(DEFAULT_EXCLUDE) + ("aaa",),
|
||||
),
|
||||
# func_args > user_data
|
||||
(
|
||||
{"src_path": "tests/demo_data", "exclude": ["aaa"]},
|
||||
{"exclude": ["exclude1", "exclude2", "aaa"]},
|
||||
("exclude1", "exclude2", "aaa"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_make_config_precedence(tmp_path, test_input, expected):
|
||||
conf = make_config(dst_path=tmp_path, vcs_ref="HEAD", **test_input)
|
||||
assert is_subdict(expected, conf.dict())
|
||||
def test_worker_config_precedence(tmp_path, test_input, expected_exclusions):
|
||||
conf = copier.Worker(dst_path=tmp_path, vcs_ref="HEAD", **test_input)
|
||||
assert expected_exclusions == conf.all_exclusions
|
||||
|
||||
|
||||
def test_config_data_transclusion():
|
||||
config = load_config_data("tests/demo_transclude/demo")
|
||||
assert config["_exclude"] == ["exclude1", "exclude2"]
|
||||
config = copier.Worker("tests/demo_transclude/demo")
|
||||
assert config.all_exclusions == ("exclude1", "exclude2")
|
||||
|
@ -9,14 +9,7 @@ from plumbum.cmd import git
|
||||
|
||||
import copier
|
||||
|
||||
from .helpers import (
|
||||
DATA,
|
||||
PROJECT_TEMPLATE,
|
||||
assert_file,
|
||||
build_file_tree,
|
||||
filecmp,
|
||||
render,
|
||||
)
|
||||
from .helpers import PROJECT_TEMPLATE, assert_file, build_file_tree, filecmp, render
|
||||
|
||||
|
||||
def test_project_not_found(tmp_path):
|
||||
@ -148,7 +141,7 @@ def test_skip_if_exists(tmp_path):
|
||||
force=True,
|
||||
)
|
||||
|
||||
assert (tmp_path / "a.noeof.txt").read_text() == "OVERWRITTEN"
|
||||
assert (tmp_path / "a.noeof.txt").read_text() == "SKIPPED"
|
||||
assert (tmp_path / "b.noeof.txt").read_text() == "SKIPPED"
|
||||
assert (tmp_path / "meh" / "c.noeof.txt").read_text() == "SKIPPED"
|
||||
|
||||
@ -162,43 +155,17 @@ def test_skip_if_exists_rendered_patterns(tmp_path):
|
||||
skip_if_exists=["[[ name ]]/c.noeof.txt"],
|
||||
force=True,
|
||||
)
|
||||
assert (tmp_path / "a.noeof.txt").read_text() == "OVERWRITTEN"
|
||||
assert (tmp_path / "a.noeof.txt").read_text() == "SKIPPED"
|
||||
assert (tmp_path / "b.noeof.txt").read_text() == "OVERWRITTEN"
|
||||
assert (tmp_path / "meh" / "c.noeof.txt").read_text() == "SKIPPED"
|
||||
|
||||
|
||||
def test_config_exclude(tmp_path, monkeypatch):
|
||||
def fake_data(*_args, **_kwargs):
|
||||
return {"_exclude": ["*.txt"]}
|
||||
|
||||
monkeypatch.setattr(copier.config.factory, "load_config_data", fake_data)
|
||||
copier.copy(str(PROJECT_TEMPLATE), tmp_path, data=DATA, quiet=True)
|
||||
assert not (tmp_path / "aaaa.txt").exists()
|
||||
|
||||
|
||||
def test_config_exclude_overridden(tmp_path):
|
||||
def fake_data(*_args, **_kwargs):
|
||||
return {"_exclude": ["*.txt"]}
|
||||
|
||||
copier.copy(str(PROJECT_TEMPLATE), tmp_path, data=DATA, quiet=True, exclude=[])
|
||||
assert (tmp_path / "aaaa.txt").exists()
|
||||
|
||||
|
||||
def test_config_include(tmp_path, monkeypatch):
|
||||
def fake_data(*_args, **_kwargs):
|
||||
return {"_exclude": ["!.svn"]}
|
||||
|
||||
monkeypatch.setattr(copier.config.factory, "load_config_data", fake_data)
|
||||
copier.copy(str(PROJECT_TEMPLATE), tmp_path, data=DATA, quiet=True)
|
||||
assert (tmp_path / ".svn").exists()
|
||||
|
||||
|
||||
def test_skip_option(tmp_path):
|
||||
render(tmp_path)
|
||||
path = tmp_path / "pyproject.toml"
|
||||
content = "lorem ipsum"
|
||||
path.write_text(content)
|
||||
render(tmp_path, skip=True)
|
||||
render(tmp_path, skip_if_exists=["**"])
|
||||
assert path.read_text() == content
|
||||
|
||||
|
||||
@ -216,11 +183,3 @@ def test_pretend_option(tmp_path):
|
||||
assert not (tmp_path / "doc").exists()
|
||||
assert not (tmp_path / "config.py").exists()
|
||||
assert not (tmp_path / "pyproject.toml").exists()
|
||||
|
||||
|
||||
def test_subdirectory(tmp_path: Path):
|
||||
render(tmp_path, subdirectory="doc")
|
||||
assert not (tmp_path / "doc").exists()
|
||||
assert not (tmp_path / "config.py").exists()
|
||||
assert (tmp_path / "images").exists()
|
||||
assert (tmp_path / "manana.txt").exists()
|
||||
|
@ -10,9 +10,8 @@ from .helpers import PROJECT_TEMPLATE
|
||||
REPO_BUNDLE_PATH = Path(f"{PROJECT_TEMPLATE}_update_tasks.bundle").absolute()
|
||||
|
||||
|
||||
def test_update_tasks(tmpdir):
|
||||
def test_update_tasks(tmp_path):
|
||||
"""Test that updating a template runs tasks from the expected version."""
|
||||
tmp_path = tmpdir / "tmp_path"
|
||||
# Copy the 1st version
|
||||
copy(
|
||||
str(REPO_BUNDLE_PATH),
|
||||
|
@ -1,12 +1,17 @@
|
||||
from copier.main import copy
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
from .helpers import PROJECT_TEMPLATE
|
||||
import pytest
|
||||
|
||||
from copier.main import run_auto
|
||||
|
||||
from .helpers import PROJECT_TEMPLATE, build_file_tree
|
||||
|
||||
|
||||
def test_exclude_recursive(tmp_path):
|
||||
"""Copy is done properly when excluding recursively."""
|
||||
src = f"{PROJECT_TEMPLATE}_exclude"
|
||||
copy(src, tmp_path)
|
||||
run_auto(src, tmp_path)
|
||||
assert not (tmp_path / "bad").exists()
|
||||
assert not (tmp_path / "bad").is_dir()
|
||||
|
||||
@ -14,7 +19,75 @@ def test_exclude_recursive(tmp_path):
|
||||
def test_exclude_recursive_negate(tmp_path):
|
||||
"""Copy is done properly when copy_me.txt is the sole file copied."""
|
||||
src = f"{PROJECT_TEMPLATE}_exclude_negate"
|
||||
copy(src, tmp_path)
|
||||
run_auto(src, tmp_path)
|
||||
assert (tmp_path / "copy_me.txt").exists()
|
||||
assert (tmp_path / "copy_me.txt").is_file()
|
||||
assert not (tmp_path / "do_not_copy_me.txt").exists()
|
||||
|
||||
|
||||
def test_config_exclude(tmp_path):
|
||||
src, dst = tmp_path / "src", tmp_path / "dst"
|
||||
build_file_tree({src / "copier.yml": "_exclude: ['*.txt']", src / "aaaa.txt": ""})
|
||||
run_auto(str(src), dst, quiet=True)
|
||||
assert not (dst / "aaaa.txt").exists()
|
||||
assert (dst / "copier.yml").exists()
|
||||
|
||||
|
||||
def test_config_exclude_extended(tmp_path):
|
||||
src, dst = tmp_path / "src", tmp_path / "dst"
|
||||
build_file_tree({src / "copier.yml": "_exclude: ['*.txt']", src / "aaaa.txt": ""})
|
||||
run_auto(str(src), dst, quiet=True, exclude=["*.yml"])
|
||||
assert not (dst / "aaaa.txt").exists()
|
||||
assert not (dst / "copier.yml").exists()
|
||||
|
||||
|
||||
def test_config_include(tmp_path):
|
||||
src, dst = tmp_path / "src", tmp_path / "dst"
|
||||
build_file_tree(
|
||||
{src / "copier.yml": "_exclude: ['!.svn']", src / ".svn" / "hello": ""}
|
||||
)
|
||||
run_auto(str(src), dst, quiet=True)
|
||||
assert (dst / ".svn" / "hello").exists()
|
||||
assert (dst / "copier.yml").exists()
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
condition=platform.system() in {"Darwin", "Windows"},
|
||||
reason="OS without proper UTF-8 filesystem.",
|
||||
strict=True,
|
||||
)
|
||||
def test_path_filter(tmp_path_factory):
|
||||
src, dst = tmp_path_factory.mktemp("src"), tmp_path_factory.mktemp("dst")
|
||||
file_excluded = {
|
||||
"x.exclude": True,
|
||||
"do_not.exclude!": False,
|
||||
# dir patterns and their negations
|
||||
Path("exclude_dir", "x"): True,
|
||||
Path("exclude_dir", "please_copy_me"): False, # no mercy
|
||||
Path("not_exclude_dir", "x!"): False,
|
||||
# unicode patterns
|
||||
"mañana.txt": True,
|
||||
"mañana.txt": False,
|
||||
"manana.txt": False,
|
||||
}
|
||||
file_tree_spec = {
|
||||
src
|
||||
/ "copier.yaml": """
|
||||
_exclude:
|
||||
# simple file patterns and their negations
|
||||
- "*.exclude"
|
||||
- "!do_not.exclude"
|
||||
# dir patterns and their negations
|
||||
- "exclude_dir/"
|
||||
- "!exclude_dir/please_copy_me"
|
||||
- "!not_exclude_dir/x"
|
||||
# unicode patterns
|
||||
- "mañana.txt"
|
||||
""",
|
||||
}
|
||||
for key, value in file_excluded.items():
|
||||
file_tree_spec[src / key] = str(value)
|
||||
build_file_tree(file_tree_spec)
|
||||
run_auto(str(src), dst)
|
||||
for key, value in file_excluded.items():
|
||||
assert (dst / key).exists() != value
|
||||
|
@ -1,38 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
|
||||
import copier
|
||||
|
||||
CHILD_DIR = "./tests/demo_extra_paths/children"
|
||||
CHILD_DIR_CONFIG = "./tests/demo_extra_paths/children_config"
|
||||
PARENT_DIR = "./tests/demo_extra_paths/parent"
|
||||
|
||||
|
||||
def test_template_not_found(tmp_path):
|
||||
with pytest.raises(TemplateNotFound):
|
||||
copier.copy(CHILD_DIR, tmp_path)
|
||||
|
||||
|
||||
def test_parent_dir_not_found(tmp_path):
|
||||
with pytest.raises(ValueError):
|
||||
copier.copy(CHILD_DIR, tmp_path, extra_paths="foobar")
|
||||
|
||||
|
||||
def test_copy_with_extra_paths(tmp_path):
|
||||
copier.copy(CHILD_DIR, tmp_path, extra_paths=[PARENT_DIR])
|
||||
|
||||
gen_file = tmp_path / "child.txt"
|
||||
result = gen_file.read_text()
|
||||
expected = Path(PARENT_DIR + "/parent.txt").read_text()
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_copy_with_extra_paths_from_config(tmp_path):
|
||||
copier.copy(CHILD_DIR_CONFIG, tmp_path)
|
||||
|
||||
gen_file = tmp_path / "child.txt"
|
||||
result = gen_file.read_text()
|
||||
expected = Path(PARENT_DIR + "/parent.txt").read_text()
|
||||
assert result == expected
|
@ -10,7 +10,7 @@ from plumbum import local
|
||||
from plumbum.cmd import git
|
||||
|
||||
from copier import copy
|
||||
from copier.config.objects import UserMessageError
|
||||
from copier.errors import UserMessageError
|
||||
|
||||
from .helpers import PROJECT_TEMPLATE, build_file_tree
|
||||
|
||||
|
@ -3,31 +3,47 @@ from plumbum import local
|
||||
from plumbum.cmd import git
|
||||
|
||||
import copier
|
||||
from copier.config.factory import make_config
|
||||
from copier.config.objects import UserMessageError
|
||||
from copier.errors import UnsupportedVersionError
|
||||
|
||||
from .helpers import build_file_tree
|
||||
|
||||
|
||||
def test_version_less_than_required(monkeypatch):
|
||||
@pytest.fixture(scope="module")
|
||||
def template_path(tmp_path_factory) -> str:
|
||||
root = tmp_path_factory.mktemp("template")
|
||||
build_file_tree(
|
||||
{
|
||||
root
|
||||
/ "copier.yaml": """\
|
||||
_min_copier_version: "10.5.1"
|
||||
""",
|
||||
root / "README.md": "",
|
||||
}
|
||||
)
|
||||
return str(root)
|
||||
|
||||
|
||||
def test_version_less_than_required(template_path, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("copier.__version__", "0.0.0a0")
|
||||
with pytest.raises(UserMessageError):
|
||||
make_config("./tests/demo_minimum_version")
|
||||
with pytest.raises(UnsupportedVersionError):
|
||||
copier.copy(template_path, tmp_path)
|
||||
|
||||
|
||||
def test_version_equal_required(monkeypatch):
|
||||
def test_version_equal_required(template_path, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("copier.__version__", "10.5.1")
|
||||
# assert no error
|
||||
make_config("./tests/demo_minimum_version")
|
||||
copier.copy(template_path, tmp_path)
|
||||
|
||||
|
||||
def test_version_greater_than_required(monkeypatch):
|
||||
def test_version_greater_than_required(template_path, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("copier.__version__", "99.99.99")
|
||||
# assert no error
|
||||
make_config("./tests/demo_minimum_version")
|
||||
copier.copy(template_path, tmp_path)
|
||||
|
||||
|
||||
def test_minimum_version_update(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("copier.__version__", "99.99.99")
|
||||
copier.copy("./tests/demo_minimum_version", tmp_path)
|
||||
def test_minimum_version_update(template_path, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("copier.__version__", "11.0.0")
|
||||
copier.copy(template_path, tmp_path)
|
||||
|
||||
with local.cwd(tmp_path):
|
||||
git("init")
|
||||
@ -37,19 +53,19 @@ def test_minimum_version_update(tmp_path, monkeypatch):
|
||||
git("commit", "-m", "hello world")
|
||||
|
||||
monkeypatch.setattr("copier.__version__", "0.0.0.post0")
|
||||
with pytest.raises(UserMessageError):
|
||||
make_config("./tests/demo_minimum_version", tmp_path)
|
||||
with pytest.raises(UnsupportedVersionError):
|
||||
copier.copy(template_path, tmp_path)
|
||||
|
||||
monkeypatch.setattr("copier.__version__", "10.5.1")
|
||||
# assert no error
|
||||
make_config("./tests/demo_minimum_version", tmp_path)
|
||||
copier.copy(template_path, tmp_path)
|
||||
|
||||
monkeypatch.setattr("copier.__version__", "99.99.99")
|
||||
# assert no error
|
||||
make_config("./tests/demo_minimum_version", tmp_path)
|
||||
copier.copy(template_path, tmp_path)
|
||||
|
||||
|
||||
def test_version_0_0_0_ignored(monkeypatch):
|
||||
def test_version_0_0_0_ignored(template_path, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("copier.__version__", "0.0.0")
|
||||
# assert no error
|
||||
make_config("./tests/demo_minimum_version")
|
||||
copier.copy(template_path, tmp_path)
|
||||
|
@ -33,7 +33,7 @@ def test_output_force(capsys, tmp_path):
|
||||
def test_output_skip(capsys, tmp_path):
|
||||
render(tmp_path)
|
||||
capsys.readouterr()
|
||||
render(tmp_path, quiet=False, skip=True)
|
||||
render(tmp_path, quiet=False, skip_if_exists=["config.py"])
|
||||
_, err = capsys.readouterr()
|
||||
assert re.search(r"conflict[^\s]* config\.py", err)
|
||||
assert re.search(r"skip[^\s]* config\.py", err)
|
||||
|
@ -77,7 +77,7 @@ def test_copy_default_advertised(tmp_path_factory, spawn, name):
|
||||
git("add", ".")
|
||||
assert "_commit: v1" in Path(".copier-answers.yml").read_text()
|
||||
git("commit", "-m", "v1")
|
||||
tui = spawn([COPIER_PATH], timeout=10)
|
||||
tui = spawn([COPIER_PATH], timeout=20)
|
||||
# Check what was captured
|
||||
tui.expect_exact(["in_love?", "Format: bool", "(Y/n)"])
|
||||
tui.sendline()
|
||||
@ -302,6 +302,11 @@ def test_update_choice(tmp_path_factory, spawn, choices):
|
||||
/ "[[ _copier_conf.answers_file ]].tmpl": "[[ _copier_answers|to_nice_yaml ]]",
|
||||
}
|
||||
)
|
||||
with local.cwd(template):
|
||||
git("init")
|
||||
git("add", ".")
|
||||
git("commit", "-m one")
|
||||
git("tag", "v1")
|
||||
# Copy
|
||||
tui = spawn([COPIER_PATH, str(template), str(subproject)], timeout=10)
|
||||
tui.expect_exact(["pick_one?"])
|
||||
|
@ -1,11 +1,12 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from plumbum import local
|
||||
from plumbum.cmd import git
|
||||
|
||||
import copier
|
||||
from copier.config import make_config
|
||||
from copier.main import update_diff
|
||||
|
||||
from .helpers import build_file_tree
|
||||
|
||||
|
||||
def git_init(message="hello world"):
|
||||
@ -16,28 +17,63 @@ def git_init(message="hello world"):
|
||||
git("commit", "-m", message)
|
||||
|
||||
|
||||
def test_copy_subdirectory_api_option(tmp_path):
|
||||
@pytest.fixture(scope="module")
|
||||
def demo_template(tmp_path_factory):
|
||||
root = tmp_path_factory.mktemp("demo_subdirectory")
|
||||
build_file_tree(
|
||||
{
|
||||
root / "api_project" / "api_readme.md": "",
|
||||
root
|
||||
/ "api_project"
|
||||
/ "[[ _copier_conf.answers_file ]].tmpl": "[[ _copier_answers|to_nice_yaml ]]",
|
||||
root
|
||||
/ "conf_project"
|
||||
/ "conf_readme.md": """\
|
||||
# Demo subdirectory
|
||||
|
||||
Generated using previous answers `_subdirectory` value.
|
||||
""",
|
||||
root
|
||||
/ "conf_project"
|
||||
/ "[[ _copier_conf.answers_file ]].tmpl": "[[ _copier_answers|to_nice_yaml ]]",
|
||||
root
|
||||
/ "copier.yml": """\
|
||||
choose_subdir:
|
||||
type: str
|
||||
default: conf_project
|
||||
choices:
|
||||
- api_project
|
||||
- conf_project
|
||||
_subdirectory: "[[ choose_subdir ]]"
|
||||
""",
|
||||
}
|
||||
)
|
||||
with local.cwd(root):
|
||||
git_init()
|
||||
return str(root)
|
||||
|
||||
|
||||
def test_copy_subdirectory_api_option(demo_template, tmp_path):
|
||||
copier.copy(
|
||||
"./tests/demo_subdirectory", tmp_path, force=True, subdirectory="api_project"
|
||||
demo_template, tmp_path, force=True, data={"choose_subdir": "api_project"}
|
||||
)
|
||||
assert (tmp_path / "api_readme.md").exists()
|
||||
assert not (tmp_path / "conf_readme.md").exists()
|
||||
|
||||
|
||||
def test_copy_subdirectory_config(tmp_path):
|
||||
copier.copy("./tests/demo_subdirectory", tmp_path, force=True)
|
||||
def test_copy_subdirectory_config(demo_template, tmp_path):
|
||||
copier.copy(demo_template, tmp_path, force=True)
|
||||
assert (tmp_path / "conf_readme.md").exists()
|
||||
assert not (tmp_path / "api_readme.md").exists()
|
||||
|
||||
|
||||
def test_update_subdirectory(tmp_path):
|
||||
copier.copy("./tests/demo_subdirectory", tmp_path, force=True)
|
||||
def test_update_subdirectory(demo_template, tmp_path):
|
||||
copier.copy(demo_template, tmp_path, force=True)
|
||||
|
||||
with local.cwd(tmp_path):
|
||||
git_init()
|
||||
|
||||
conf = make_config("./tests/demo_subdirectory", str(tmp_path), force=True)
|
||||
update_diff(conf)
|
||||
copier.copy(dst_path=tmp_path, force=True)
|
||||
assert not (tmp_path / "conf_project").exists()
|
||||
assert not (tmp_path / "api_project").exists()
|
||||
assert not (tmp_path / "api_readme.md").exists()
|
||||
|
@ -1,17 +1,40 @@
|
||||
import pytest
|
||||
|
||||
import copier
|
||||
|
||||
from .helpers import DATA, render
|
||||
from .helpers import build_file_tree
|
||||
|
||||
|
||||
def test_render_tasks(tmp_path):
|
||||
tasks = ["touch [[ myvar ]]/1.txt", "touch [[ myvar ]]/2.txt"]
|
||||
render(tmp_path, tasks=tasks)
|
||||
assert (tmp_path / DATA["myvar"] / "1.txt").exists()
|
||||
assert (tmp_path / DATA["myvar"] / "2.txt").exists()
|
||||
@pytest.fixture(scope="module")
|
||||
def demo_template(tmp_path_factory):
|
||||
root = tmp_path_factory.mktemp("demo_tasks")
|
||||
build_file_tree(
|
||||
{
|
||||
root
|
||||
/ "copier.yaml": """
|
||||
other_file: bye
|
||||
|
||||
# This tests two things:
|
||||
# 1. That the tasks are being executed in the destiantion folder; and
|
||||
# 2. That the tasks are being executed in order, one after another
|
||||
_tasks:
|
||||
- mkdir hello
|
||||
- cd hello && touch world
|
||||
- touch [[ other_file ]]
|
||||
"""
|
||||
}
|
||||
)
|
||||
return str(root)
|
||||
|
||||
|
||||
def test_copy_tasks(tmp_path):
|
||||
copier.copy("tests/demo_tasks", tmp_path, quiet=True)
|
||||
def test_render_tasks(tmp_path, demo_template):
|
||||
copier.copy(demo_template, tmp_path, data={"other_file": "custom"})
|
||||
assert (tmp_path / "custom").is_file()
|
||||
|
||||
|
||||
def test_copy_tasks(tmp_path, demo_template):
|
||||
copier.copy(demo_template, tmp_path, quiet=True, force=True)
|
||||
assert (tmp_path / "hello").exists()
|
||||
assert (tmp_path / "hello").is_dir()
|
||||
assert (tmp_path / "hello" / "world").exists()
|
||||
assert (tmp_path / "bye").is_file()
|
||||
|
@ -1,14 +1,15 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pexpect
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from copier.config.factory import filter_config, make_config
|
||||
from copier.config.objects import EnvOps
|
||||
from copier.config.user_data import InvalidTypeError, query_user_data
|
||||
from copier import Worker
|
||||
from copier.errors import InvalidTypeError
|
||||
|
||||
from .helpers import COPIER_PATH, build_file_tree
|
||||
from .helpers import COPIER_PATH, ISOFORMAT, build_file_tree
|
||||
|
||||
envops = EnvOps()
|
||||
envops = {}
|
||||
main_default = "copier"
|
||||
main_question = {"main": {"default": main_default}}
|
||||
|
||||
@ -120,7 +121,7 @@ def test_templated_prompt(
|
||||
tmp_path_factory.mktemp("template"),
|
||||
tmp_path_factory.mktemp("subproject"),
|
||||
)
|
||||
questions_combined = filter_config({**main_question, **questions_data})[1]
|
||||
questions_combined = {**main_question, **questions_data}
|
||||
# There's always only 1 question; get its name
|
||||
question_name = questions_data.copy().popitem()[0]
|
||||
build_file_tree(
|
||||
@ -140,71 +141,84 @@ def test_templated_prompt(
|
||||
assert answers[question_name] == expected_value
|
||||
|
||||
|
||||
def test_templated_prompt_custom_envops(tmp_path):
|
||||
conf = make_config("./tests/demo_templated_prompt", tmp_path, force=True)
|
||||
assert conf.data["sentence"] == "It's over 9000!"
|
||||
def test_templated_prompt_custom_envops(tmp_path_factory):
|
||||
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
|
||||
build_file_tree(
|
||||
{
|
||||
src
|
||||
/ "copier.yml": """
|
||||
_envops:
|
||||
block_start_string: "<%"
|
||||
block_end_string: "%>"
|
||||
comment_start_string: "<#"
|
||||
comment_end_string: "#>"
|
||||
variable_start_string: "<<"
|
||||
variable_end_string: ">>"
|
||||
|
||||
conf = make_config(
|
||||
"./tests/demo_templated_prompt", tmp_path, data={"powerlevel": 1}, force=True
|
||||
powerlevel:
|
||||
type: int
|
||||
default: 9000
|
||||
|
||||
sentence:
|
||||
type: str
|
||||
default:
|
||||
"<% if powerlevel >= 9000 %>It's over 9000!<% else %>It's only << powerlevel >>...<%
|
||||
endif %>"
|
||||
""",
|
||||
src / "result.tmpl": "<<sentence>>",
|
||||
}
|
||||
)
|
||||
assert conf.data["sentence"] == "It's only 1..."
|
||||
worker1 = Worker(str(src), dst, force=True)
|
||||
worker1.run_copy()
|
||||
assert (dst / "result").read_text() == "It's over 9000!"
|
||||
|
||||
worker2 = Worker(str(src), dst, data={"powerlevel": 1}, force=True)
|
||||
worker2.run_copy()
|
||||
assert (dst / "result").read_text() == "It's only 1..."
|
||||
|
||||
|
||||
def test_templated_prompt_builtins():
|
||||
data = query_user_data(
|
||||
{"question": {"default": "[[ now() ]]"}}, {}, {}, {}, False, envops
|
||||
def test_templated_prompt_builtins(tmp_path_factory):
|
||||
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
|
||||
build_file_tree(
|
||||
{
|
||||
src
|
||||
/ "copier.yaml": """
|
||||
question1:
|
||||
default: "[[ now() ]]"
|
||||
question2:
|
||||
default: "[[ make_secret() ]]"
|
||||
""",
|
||||
src / "now.tmpl": "[[ question1 ]]",
|
||||
src / "make_secret.tmpl": "[[ question2 ]]",
|
||||
}
|
||||
)
|
||||
Worker(str(src), dst, force=True).run_copy()
|
||||
that_now = datetime.strptime((dst / "now").read_text(), ISOFORMAT)
|
||||
assert that_now <= datetime.now()
|
||||
assert len((dst / "make_secret").read_text()) == 128
|
||||
|
||||
data = query_user_data(
|
||||
{"question": {"default": "[[ make_secret() ]]"}},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
False,
|
||||
envops,
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"questions, raises, returns",
|
||||
(
|
||||
({"question": {"default": "[[ not_valid ]]"}}, None, ""),
|
||||
({"question": {"help": "[[ not_valid ]]"}}, None, "None"),
|
||||
({"question": {"type": "[[ not_valid ]]"}}, InvalidTypeError, "None"),
|
||||
({"question": {"choices": ["[[ not_valid ]]"]}}, None, "None"),
|
||||
),
|
||||
)
|
||||
def test_templated_prompt_invalid(tmp_path_factory, questions, raises, returns):
|
||||
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
|
||||
build_file_tree(
|
||||
{
|
||||
src / "copier.yml": yaml.safe_dump(questions),
|
||||
src / "result.tmpl": "[[question]]",
|
||||
}
|
||||
)
|
||||
assert isinstance(data["question"], str) and len(data["question"]) == 128
|
||||
|
||||
|
||||
def test_templated_prompt_invalid():
|
||||
# assert no exception in non-strict mode
|
||||
query_user_data(
|
||||
{"question": {"default": "[[ not_valid ]]"}},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
False,
|
||||
envops,
|
||||
)
|
||||
|
||||
# assert no exception in non-strict mode
|
||||
query_user_data(
|
||||
{"question": {"help": "[[ not_valid ]]"}}, {}, {}, {}, False, envops
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidTypeError):
|
||||
query_user_data(
|
||||
{"question": {"type": "[[ not_valid ]]"}},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
False,
|
||||
envops,
|
||||
)
|
||||
|
||||
# assert no exception in non-strict mode
|
||||
query_user_data(
|
||||
{"question": {"choices": ["[[ not_valid ]]"]}},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
False,
|
||||
envops,
|
||||
)
|
||||
|
||||
# TODO: uncomment this later when EnvOps supports setting the undefined behavior
|
||||
# envops.undefined = StrictUndefined
|
||||
# with pytest.raises(UserMessageError):
|
||||
# query_user_data(
|
||||
# {"question": {"default": "[[ not_valid ]]"}}, {}, False, envops
|
||||
# )
|
||||
worker = Worker(str(src), dst, force=True)
|
||||
if raises:
|
||||
with pytest.raises(raises):
|
||||
worker.run_copy()
|
||||
else:
|
||||
worker.run_copy()
|
||||
assert (dst / "result").read_text() == returns
|
||||
|
@ -1,67 +1,7 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from poethepoet.app import PoeThePoet
|
||||
|
||||
from copier import tools
|
||||
from copier.config.factory import ConfigData, EnvOps
|
||||
|
||||
from .helpers import DATA, PROJECT_TEMPLATE
|
||||
|
||||
|
||||
def test_render(tmp_path):
|
||||
envops = EnvOps().dict()
|
||||
render = tools.Renderer(
|
||||
ConfigData(
|
||||
src_path=PROJECT_TEMPLATE,
|
||||
dst_path=tmp_path,
|
||||
data_from_init=DATA,
|
||||
envops=envops,
|
||||
)
|
||||
)
|
||||
|
||||
assert render.string("/hello/[[ what ]]/") == "/hello/world/"
|
||||
assert render.string("/hello/world/") == "/hello/world/"
|
||||
|
||||
sourcepath = PROJECT_TEMPLATE / "pyproject.toml.tmpl"
|
||||
result = render(sourcepath)
|
||||
expected = Path("./tests/reference_files/pyproject.toml").read_text()
|
||||
assert result == expected
|
||||
|
||||
|
||||
TEST_PATTERNS = (
|
||||
# simple file patterns and their negations
|
||||
"*.exclude",
|
||||
"!do_not.exclude",
|
||||
# dir patterns and their negations
|
||||
"exclude_dir/",
|
||||
"!exclude_dir/please_copy_me",
|
||||
"!not_exclude_dir/x",
|
||||
# unicode patterns
|
||||
"mañana.txt",
|
||||
)
|
||||
path_filter = tools.create_path_filter(TEST_PATTERNS)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pattern,should_match",
|
||||
(
|
||||
# simple file patterns and their negations
|
||||
("x.exclude", True),
|
||||
("do_not.exclude!", False),
|
||||
# dir patterns and their negations
|
||||
("exclude_dir/x", True),
|
||||
("exclude_dir/please_copy_me", False), # no mercy
|
||||
("not_exclude_dir/x!", False),
|
||||
# unicode patterns
|
||||
("mañana.txt", True),
|
||||
("mañana.txt", False),
|
||||
("manana.txt", False),
|
||||
),
|
||||
)
|
||||
def test_create_path_filter(pattern, should_match):
|
||||
assert path_filter(pattern) == should_match
|
||||
|
||||
|
||||
def test_lint():
|
||||
"""Ensure source code formatting"""
|
||||
|
@ -6,7 +6,7 @@ import pytest
|
||||
from plumbum import local
|
||||
from plumbum.cmd import git
|
||||
|
||||
from copier import copy
|
||||
from copier import Worker, copy
|
||||
from copier.cli import CopierApp
|
||||
|
||||
from .helpers import PROJECT_TEMPLATE, build_file_tree
|
||||
@ -141,12 +141,11 @@ def test_updatediff(tmpdir):
|
||||
)
|
||||
commit("-m", "Subproject evolved")
|
||||
# Reapply template ignoring subproject evolution
|
||||
copy(
|
||||
Worker(
|
||||
data={"author_name": "Largo LaGrande", "project_name": "to steal a lot"},
|
||||
force=True,
|
||||
vcs_ref="HEAD",
|
||||
only_diff=False,
|
||||
)
|
||||
).run_copy()
|
||||
assert readme.read_text() == dedent(
|
||||
"""
|
||||
Let me introduce myself.
|
||||
|
Loading…
x
Reference in New Issue
Block a user