build(typing): enable 'strict' mypy linting (#1527)

---------

Co-authored-by: Timothée Mazzucotelli <dev@pawamoy.fr>
This commit is contained in:
danieleades 2024-03-23 11:58:20 +00:00 committed by GitHub
parent bcad276ee0
commit 0c6b0b96fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 215 additions and 113 deletions

View File

@ -52,7 +52,7 @@ import sys
from os import PathLike
from pathlib import Path
from textwrap import dedent
from typing import Callable
from typing import Any, Callable, Iterable, Optional
import yaml
from plumbum import cli, colors
@ -60,7 +60,7 @@ from plumbum import cli, colors
from .errors import UnsafeTemplateError, UserMessageError
from .main import Worker
from .tools import copier_version
from .types import AnyByStrDict, OptStr, StrSeq
from .types import AnyByStrDict
def _handle_exceptions(method: Callable[[], None]) -> int:
@ -80,7 +80,7 @@ def _handle_exceptions(method: Callable[[], None]) -> int:
return 0
class CopierApp(cli.Application):
class CopierApp(cli.Application): # type: ignore[misc]
"""The Copier CLI application."""
DESCRIPTION = "Create a new project from a template."
@ -105,10 +105,10 @@ class CopierApp(cli.Application):
CALL_MAIN_IF_NESTED_COMMAND = False
class _Subcommand(cli.Application):
class _Subcommand(cli.Application): # type: ignore[misc]
"""Base class for Copier subcommands."""
def __init__(self, executable: PathLike) -> None:
def __init__(self, executable: "PathLike[str]") -> None:
self.data: AnyByStrDict = {}
super().__init__(executable)
@ -158,14 +158,14 @@ class _Subcommand(cli.Application):
),
)
@cli.switch(
@cli.switch( # type: ignore[misc]
["-d", "--data"],
str,
"VARIABLE=VALUE",
list=True,
help="Make VARIABLE available as VALUE when rendering the template",
)
def data_switch(self, values: StrSeq) -> None:
def data_switch(self, values: Iterable[str]) -> None:
"""Update [data][] with provided values.
Arguments:
@ -176,7 +176,7 @@ class _Subcommand(cli.Application):
key, value = arg.split("=", 1)
self.data[key] = value
@cli.switch(
@cli.switch( # type: ignore[misc]
["--data-file"],
cli.ExistingFile,
help="Load data from a YAML file",
@ -195,7 +195,9 @@ class _Subcommand(cli.Application):
}
self.data.update(updates_without_cli_overrides)
def _worker(self, src_path: OptStr = None, dst_path: str = ".", **kwargs) -> Worker:
def _worker(
self, src_path: Optional[str] = None, dst_path: str = ".", **kwargs: Any # noqa: FA100
) -> Worker:
"""Run Copier's internal API using CLI switches.
Arguments:

View File

@ -13,7 +13,17 @@ from itertools import chain
from pathlib import Path
from shutil import rmtree
from tempfile import TemporaryDirectory
from typing import Callable, Iterable, Literal, Mapping, Sequence, get_args
from types import TracebackType
from typing import (
Any,
Callable,
Iterable,
Literal,
Mapping,
Sequence,
get_args,
overload,
)
from unicodedata import normalize
from jinja2.loaders import FileSystemLoader
@ -36,15 +46,7 @@ from .errors import (
from .subproject import Subproject
from .template import Task, Template
from .tools import OS, Style, normalize_git_path, printf, readlink
from .types import (
MISSING,
AnyByStrDict,
JSONSerializable,
OptStr,
RelativePath,
StrOrPath,
StrSeq,
)
from .types import MISSING, AnyByStrDict, JSONSerializable, RelativePath, StrOrPath
from .user_data import DEFAULT_DATA, AnswersMap, Question
from .vcs import get_git
@ -162,11 +164,11 @@ class Worker:
src_path: str | None = None
dst_path: Path = Path(".")
answers_file: RelativePath | None = None
vcs_ref: OptStr = None
vcs_ref: str | None = None
data: AnyByStrDict = field(default_factory=dict)
exclude: StrSeq = ()
exclude: Sequence[str] = ()
use_prereleases: bool = False
skip_if_exists: StrSeq = ()
skip_if_exists: Sequence[str] = ()
cleanup_on_error: bool = True
defaults: bool = False
user_defaults: AnyByStrDict = field(default_factory=dict)
@ -179,13 +181,26 @@ class Worker:
skip_answered: bool = False
answers: AnswersMap = field(default_factory=AnswersMap, init=False)
_cleanup_hooks: list[Callable] = field(default_factory=list, init=False)
_cleanup_hooks: list[Callable[[], None]] = field(default_factory=list, init=False)
def __enter__(self):
def __enter__(self) -> Worker:
"""Allow using worker as a context manager."""
return self
def __exit__(self, type, value, traceback):
@overload
def __exit__(self, type: None, value: None, traceback: None) -> None: ...
@overload
def __exit__(
self, type: type[BaseException], value: BaseException, traceback: TracebackType
) -> None: ...
def __exit__(
self,
type: type[BaseException] | None,
value: BaseException | None,
traceback: TracebackType | None,
) -> None:
"""Clean up garbage files after worker usage ends."""
if value is not None:
# exception was raised from code inside context manager:
@ -196,7 +211,7 @@ class Worker:
# otherwise clean up and let any exception bubble up
self._cleanup()
def _cleanup(self):
def _cleanup(self) -> None:
"""Execute all stored cleanup methods."""
for method in self._cleanup_hooks:
method()
@ -226,7 +241,7 @@ class Worker:
if message and not self.quiet:
print(self._render_string(message), file=sys.stderr)
def _answers_to_remember(self) -> Mapping:
def _answers_to_remember(self) -> Mapping[str, Any]:
"""Get only answers that will be remembered in the copier answers file."""
# All internal values must appear first
answers: AnyByStrDict = {}
@ -273,7 +288,7 @@ class Worker:
with local.cwd(self.subproject.local_abspath), local.env(**task.extra_env):
subprocess.run(task_cmd, shell=use_shell, check=True, env=local.env)
def _render_context(self) -> Mapping:
def _render_context(self) -> Mapping[str, Any]:
"""Produce render context for Jinja."""
# Backwards compatibility
# FIXME Remove it?
@ -305,7 +320,7 @@ class Worker:
spec = PathSpec.from_lines("gitwildmatch", normalized_patterns)
return spec.match_file
def _solve_render_conflict(self, dst_relpath: Path):
def _solve_render_conflict(self, dst_relpath: Path) -> bool:
"""Properly solve render conflicts.
It can ask the user if running in interactive mode.
@ -468,7 +483,7 @@ class Worker:
return Path(template.render(**self.answers.combined))
@cached_property
def all_exclusions(self) -> StrSeq:
def all_exclusions(self) -> Sequence[str]:
"""Combine default, template and user-chosen exclusions."""
return self.template.exclude + tuple(self.exclude)
@ -766,7 +781,7 @@ class Worker:
f"from `{self.subproject.answers_relpath}`."
)
with replace(self, src_path=self.subproject.template.url) as new_worker:
return new_worker.run_copy()
new_worker.run_copy()
def run_update(self) -> None:
"""Update a subproject that was already generated.
@ -818,7 +833,7 @@ class Worker:
self._apply_update()
self._print_message(self.template.message_after_update)
def _apply_update(self): # noqa: C901
def _apply_update(self) -> None: # noqa: C901
git = get_git()
subproject_top = Path(
git(
@ -840,8 +855,8 @@ class Worker:
data=self.subproject.last_answers,
defaults=True,
quiet=True,
src_path=self.subproject.template.url,
vcs_ref=self.subproject.template.commit,
src_path=self.subproject.template.url, # type: ignore[union-attr]
vcs_ref=self.subproject.template.commit, # type: ignore[union-attr]
) as old_worker:
old_worker.run_copy()
# Extract diff between temporary destination and real destination
@ -863,7 +878,7 @@ class Worker:
diff = diff_cmd("--inter-hunk-context=0")
# Run pre-migration tasks
self._execute_tasks(
self.template.migration_tasks("before", self.subproject.template)
self.template.migration_tasks("before", self.subproject.template) # type: ignore[arg-type]
)
# Clear last answers cache to load possible answers migration, if skip_answered flag is not set
if self.skip_answered is False:
@ -885,10 +900,10 @@ class Worker:
with replace(
self,
dst_path=new_copy / subproject_subdir,
data=self.answers.combined,
data=self.answers.combined, # type: ignore[arg-type]
defaults=True,
quiet=True,
src_path=self.subproject.template.url,
src_path=self.subproject.template.url, # type: ignore[union-attr]
) as new_worker:
new_worker.run_copy()
compared = dircmp(old_copy, new_copy)
@ -968,10 +983,10 @@ class Worker:
# Run post-migration tasks
self._execute_tasks(
self.template.migration_tasks("after", self.subproject.template)
self.template.migration_tasks("after", self.subproject.template) # type: ignore[arg-type]
)
def _git_initialize_repo(self):
def _git_initialize_repo(self) -> None:
"""Initialize a git repository in the current directory."""
git = get_git()
git("init", retcode=None)
@ -1004,7 +1019,7 @@ def run_copy(
src_path: str,
dst_path: StrOrPath = ".",
data: AnyByStrDict | None = None,
**kwargs,
**kwargs: Any,
) -> Worker:
"""Copy a template to a destination, from zero.
@ -1020,7 +1035,7 @@ def run_copy(
def run_recopy(
dst_path: StrOrPath = ".", data: AnyByStrDict | None = None, **kwargs
dst_path: StrOrPath = ".", data: AnyByStrDict | None = None, **kwargs: Any
) -> Worker:
"""Update a subproject from its template, discarding subproject evolution.
@ -1038,7 +1053,7 @@ def run_recopy(
def run_update(
dst_path: StrOrPath = ".",
data: AnyByStrDict | None = None,
**kwargs,
**kwargs: Any,
) -> Worker:
"""Update a subproject, from its template.
@ -1053,7 +1068,7 @@ def run_update(
return worker
def _remove_old_files(prefix: Path, cmp: dircmp, rm_common: bool = False) -> None:
def _remove_old_files(prefix: Path, cmp: dircmp[str], rm_common: bool = False) -> None:
"""Remove files and directories only found in "old" template.
This is an internal helper method used to process a comparison of 2

View File

@ -33,7 +33,7 @@ class Subproject:
local_abspath: AbsolutePath
answers_relpath: Path = Path(".copier-answers.yml")
_cleanup_hooks: list[Callable] = field(default_factory=list, init=False)
_cleanup_hooks: list[Callable[[], None]] = field(default_factory=list, init=False)
def is_dirty(self) -> bool:
"""Indicate if the local template root is dirty.
@ -45,7 +45,7 @@ class Subproject:
return bool(get_git()("status", "--porcelain").strip())
return False
def _cleanup(self):
def _cleanup(self) -> None:
"""Remove temporary files and folders created by the subproject."""
for method in self._cleanup_hooks:
method()
@ -78,9 +78,11 @@ class Subproject:
result = Template(url=last_url, ref=last_ref)
self._cleanup_hooks.append(result._cleanup)
return result
return None
@cached_property
def vcs(self) -> VCSTypes | None:
"""VCS type of the subproject."""
if is_in_git_repo(self.local_abspath):
return "git"
return None

View File

@ -28,7 +28,7 @@ from .errors import (
UnsupportedVersionError,
)
from .tools import copier_version, handle_remove_readonly
from .types import AnyByStrDict, Env, OptStr, StrSeq, Union, VCSTypes
from .types import AnyByStrDict, Env, VCSTypes
from .vcs import checkout_latest_tag, clone, get_git, get_repo
# Default list of files in the template to exclude from the rendered project
@ -157,7 +157,7 @@ class Task:
Additional environment variables to set while executing the command.
"""
cmd: Union[str, Sequence[str]]
cmd: str | Sequence[str]
extra_env: Env = field(default_factory=dict)
@ -199,7 +199,7 @@ class Template:
"""
url: str
ref: OptStr = None
ref: str | None = None
use_prereleases: bool = False
def _cleanup(self) -> None:
@ -264,17 +264,19 @@ class Template:
return result
@cached_property
def commit(self) -> OptStr:
def commit(self) -> str | None:
"""If the template is VCS-tracked, get its commit description."""
if self.vcs == "git":
with local.cwd(self.local_abspath):
return get_git()("describe", "--tags", "--always").strip()
return None
@cached_property
def commit_hash(self) -> OptStr:
def commit_hash(self) -> str | None:
"""If the template is VCS-tracked, get its commit full hash."""
if self.vcs == "git":
return get_git()("-C", self.local_abspath, "rev-parse", "HEAD").strip()
return None
@cached_property
def config_data(self) -> AnyByStrDict:
@ -289,7 +291,7 @@ class Template:
return result
@cached_property
def envops(self) -> Mapping:
def envops(self) -> Mapping[str, Any]:
"""Get the Jinja configuration specified in the template, or default values.
See [envops][].
@ -378,7 +380,7 @@ class Template:
"VERSION_PEP440_FROM": str(from_template.version),
"VERSION_PEP440_TO": str(self.version),
}
migration: dict
migration: dict[str, Any]
for migration in self._raw_config.get("_migrations", []):
current = parse(migration["version"])
if self.version >= current > from_template.version:
@ -429,7 +431,7 @@ class Template:
return result
@cached_property
def skip_if_exists(self) -> StrSeq:
def skip_if_exists(self) -> Sequence[str]:
"""Get skip patterns from the template.
These files will never be rewritten when rendering the template.
@ -543,3 +545,4 @@ class Template:
"""Get VCS system used by the template, if any."""
if get_repo(self.url):
return "git"
return None

View File

@ -19,19 +19,17 @@ import colorama
from packaging.version import Version
from pydantic import StrictBool
from .types import IntSeq
colorama.just_fix_windows_console()
class Style:
"""Common color styles."""
OK: IntSeq = [colorama.Fore.GREEN, colorama.Style.BRIGHT]
WARNING: IntSeq = [colorama.Fore.YELLOW, colorama.Style.BRIGHT]
IGNORE: IntSeq = [colorama.Fore.CYAN]
DANGER: IntSeq = [colorama.Fore.RED, colorama.Style.BRIGHT]
RESET: IntSeq = [colorama.Fore.RESET, colorama.Style.RESET_ALL]
OK = [colorama.Fore.GREEN, colorama.Style.BRIGHT]
WARNING = [colorama.Fore.YELLOW, colorama.Style.BRIGHT]
IGNORE = [colorama.Fore.CYAN]
DANGER = [colorama.Fore.RED, colorama.Style.BRIGHT]
RESET = [colorama.Fore.RESET, colorama.Style.RESET_ALL]
INDENT = " " * 2
@ -64,22 +62,22 @@ def copier_version() -> Version:
def printf(
action: str,
msg: Any = "",
style: IntSeq | None = None,
style: list[str] | None = None,
indent: int = 10,
quiet: bool | StrictBool = False,
file_: TextIO = sys.stdout,
) -> str | None:
"""Print string with common format."""
if quiet:
return None # HACK: Satisfy MyPy
return None
_msg = str(msg)
action = action.rjust(indent, " ")
if not style:
return action + _msg
out = style + [action] + Style.RESET + [INDENT, _msg] # type: ignore[operator]
out = style + [action] + Style.RESET + [INDENT, _msg]
print(*out, sep="", file=file_)
return None # HACK: Satisfy MyPy
return None
def printf_exception(
@ -150,7 +148,7 @@ def force_str_end(original_str: str, end: str = "\n") -> str:
def handle_remove_readonly(
func: Callable,
func: Callable[[str], None],
path: str,
# TODO: Change this union to simply `BaseException` when Python 3.11 support is dropped
exc: BaseException | tuple[type[BaseException], BaseException, TracebackType],
@ -189,7 +187,7 @@ def readlink(link: Path) -> Path:
_re_octal = re.compile(r"\\([0-9]{3})\\([0-9]{3})")
def _re_octal_replace(match: re.Match) -> str:
def _re_octal_replace(match: re.Match[str]) -> str:
return bytes([int(match.group(1), 8), int(match.group(2), 8)]).decode("utf8")

View File

@ -27,13 +27,11 @@ AnyByStrDict = Dict[str, Any]
# sequences
IntSeq = Sequence[int]
StrSeq = Sequence[str]
PathSeq = Sequence[Path]
# optional types
OptBool = Optional[bool]
OptStrOrPath = Optional[StrOrPath]
OptStr = Optional[str]
# miscellaneous
T = TypeVar("T")

View File

@ -24,7 +24,7 @@ from questionary.prompts.common import Choice
from .errors import InvalidTypeError, UserMessageError
from .tools import cast_to_bool, cast_to_str, force_str_end
from .types import MISSING, AnyByStrDict, MissingType, OptStr, OptStrOrPath, StrOrPath
from .types import MISSING, AnyByStrDict, MissingType, OptStrOrPath, StrOrPath
# TODO Remove these two functions as well as DEFAULT_DATA in a future release
@ -108,7 +108,7 @@ class AnswersMap:
)
)
def old_commit(self) -> OptStr:
def old_commit(self) -> str | None:
"""Commit when the project was updated from this template the last time."""
return self.last.get("_commit")
@ -190,14 +190,14 @@ class Question:
@field_validator("var_name")
@classmethod
def _check_var_name(cls, v: str):
def _check_var_name(cls, v: str) -> str:
if v in DEFAULT_DATA:
raise ValueError("Invalid question name")
return v
@field_validator("type")
@classmethod
def _check_type(cls, v: str, info: ValidationInfo):
def _check_type(cls, v: str, info: ValidationInfo) -> str:
if v == "":
default_type_name = type(info.data.get("default")).__name__
v = default_type_name if default_type_name in CAST_STR_TO_NATIVE else "yaml"
@ -205,7 +205,9 @@ class Question:
@field_validator("secret")
@classmethod
def _check_secret_question_default_value(cls, v: bool, info: ValidationInfo):
def _check_secret_question_default_value(
cls, v: bool, info: ValidationInfo
) -> bool:
if v and info.data["default"] is MISSING:
raise ValueError("Secret question requires a default value")
return v
@ -491,7 +493,7 @@ def load_answersfile_data(
return {}
CAST_STR_TO_NATIVE: Mapping[str, Callable] = {
CAST_STR_TO_NATIVE: Mapping[str, Callable[[str], Any]] = {
"bool": cast_to_bool,
"float": float,
"int": int,

View File

@ -1,4 +1,6 @@
"""Utilities related to VCS."""
from __future__ import annotations
import os
import re
import sys
@ -13,7 +15,7 @@ from plumbum import TF, ProcessExecutionError, colors, local
from plumbum.machines import LocalCommand
from .errors import DirtyLocalWarning, ShallowCloneWarning
from .types import OptBool, OptStr, OptStrOrPath, StrOrPath
from .types import OptBool, OptStrOrPath, StrOrPath
def get_git(context_dir: OptStrOrPath = None) -> LocalCommand:
@ -80,7 +82,7 @@ def is_git_bundle(path: Path) -> bool:
return bool(get_git()["bundle", "verify", path] & TF)
def get_repo(url: str) -> OptStr:
def get_repo(url: str) -> str | None:
"""Transform `url` into a git-parseable origin URL.
Args:
@ -146,7 +148,7 @@ def checkout_latest_tag(local_repo: StrOrPath, use_prereleases: OptBool = False)
return latest_tag
def clone(url: str, ref: OptStr = None) -> str:
def clone(url: str, ref: str | None = None) -> str:
"""Clone repo into some temporary destination.
Includes dirty changes for local templates by copying into a temp

View File

@ -9,7 +9,7 @@ _logger = logging.getLogger(__name__)
HERE = Path(__file__).parent
def clean():
def clean() -> None:
"""Clean build, test or other process artifacts from the project workspace."""
build_artefacts = (
"build/",
@ -35,14 +35,14 @@ def clean():
matching_path.unlink()
def dev_setup():
def dev_setup() -> None:
"""Set up a development environment."""
with local.cwd(HERE):
local["direnv"]("allow")
local["poetry"]("install")
def lint():
def lint() -> None:
"""Lint and format the project."""
args = [
"--extra-experimental-features",

50
poetry.lock generated
View File

@ -1489,6 +1489,28 @@ files = [
{file = "types_backports-0.1.3-py2.py3-none-any.whl", hash = "sha256:dafcd61848081503e738a7768872d1dd6c018401b4d2a1cfb608ea87ec9864b9"},
]
[[package]]
name = "types-colorama"
version = "0.4.15.20240311"
description = "Typing stubs for colorama"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-colorama-0.4.15.20240311.tar.gz", hash = "sha256:a28e7f98d17d2b14fb9565d32388e419f4108f557a7d939a66319969b2b99c7a"},
{file = "types_colorama-0.4.15.20240311-py3-none-any.whl", hash = "sha256:6391de60ddc0db3f147e31ecb230006a6823e81e380862ffca1e4695c13a0b8e"},
]
[[package]]
name = "types-docutils"
version = "0.20.0.20240317"
description = "Typing stubs for docutils"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-docutils-0.20.0.20240317.tar.gz", hash = "sha256:23657aab0de58634d111914b677b1855867f16cd9a9ea110254e23b48653e1a8"},
{file = "types_docutils-0.20.0.20240317-py3-none-any.whl", hash = "sha256:4f11b3986b74f39169313ab528ffac101c45fca9c36c4cd22dbeec6143c99b7f"},
]
[[package]]
name = "types-psutil"
version = "5.9.5.20240316"
@ -1500,6 +1522,21 @@ files = [
{file = "types_psutil-5.9.5.20240316-py3-none-any.whl", hash = "sha256:2fdd64ea6e97befa546938f486732624f9255fde198b55e6f00fda236f059f64"},
]
[[package]]
name = "types-pygments"
version = "2.17.0.20240310"
description = "Typing stubs for Pygments"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-Pygments-2.17.0.20240310.tar.gz", hash = "sha256:b1d97e905ce36343c7283b0319182ae6d4f967188f361f45502a18ae43e03e1f"},
{file = "types_Pygments-2.17.0.20240310-py3-none-any.whl", hash = "sha256:b101ca9448aaff52af6966506f1fdd73b1e60a79b8a79a8bace3366cbf1f7ed9"},
]
[package.dependencies]
types-docutils = "*"
types-setuptools = "*"
[[package]]
name = "types-pyyaml"
version = "6.0.12.20240311"
@ -1511,6 +1548,17 @@ files = [
{file = "types_PyYAML-6.0.12.20240311-py3-none-any.whl", hash = "sha256:b845b06a1c7e54b8e5b4c683043de0d9caf205e7434b3edc678ff2411979b8f6"},
]
[[package]]
name = "types-setuptools"
version = "69.2.0.20240317"
description = "Typing stubs for setuptools"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-setuptools-69.2.0.20240317.tar.gz", hash = "sha256:b607c4c48842ef3ee49dc0c7fe9c1bad75700b071e1018bb4d7e3ac492d47048"},
{file = "types_setuptools-69.2.0.20240317-py3-none-any.whl", hash = "sha256:cf91ff7c87ab7bf0625c3f0d4d90427c9da68561f3b0feab77977aaf0bbf7531"},
]
[[package]]
name = "typing-extensions"
version = "4.10.0"
@ -1627,4 +1675,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata]
lock-version = "2.0"
python-versions = ">=3.8"
content-hash = "9ddfce4f522dfba028be8e3eafee5d34651d1f81ba23af0b6c6d7643bf59cb5e"
content-hash = "265a53c7c9ecff7e83f12d4ebd4732a5f7f5f23a6775aa30c146a526d018a87d"

View File

@ -57,8 +57,10 @@ pytest-cov = ">=3.0.0"
pytest-gitconfig = ">=0.6.0"
pytest-xdist = ">=2.5.0"
types-backports = ">=0.1.3"
types-pyyaml = ">=6.0.4"
types-colorama = ">=0.4"
types-psutil = "*"
types-pygments = ">=2.17"
types-pyyaml = ">=6.0.4"
[tool.poetry.group.docs]
optional = true
@ -135,9 +137,32 @@ known-first-party = ["copier"]
convention = "google"
[tool.mypy]
ignore_missing_imports = true
strict = true
plugins = ["pydantic.mypy"]
warn_no_return = false
[[tool.mypy.overrides]]
module = [
"coverage.tracer",
"funcy",
"pexpect.*",
"plumbum.*",
"poethepoet.app",
]
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = [
"copier.subproject",
"copier.template",
"copier.tools",
"copier.user_data",
"copier.vcs",
]
warn_return_any = false
[[tool.mypy.overrides]]
module = ["tests.test_cli", "tests.test_prompt"]
disable_error_code = ["no-untyped-def"]
[tool.pytest.ini_options]
addopts = "-n auto -ra"

View File

@ -8,7 +8,7 @@ import pytest
from coverage.tracer import CTracer
from pexpect.popen_spawn import PopenSpawn
from plumbum import local
from pytest_gitconfig import GitConfig
from pytest_gitconfig.plugin import GitConfig
from .helpers import Spawn

View File

@ -8,7 +8,7 @@ import textwrap
from enum import Enum
from hashlib import sha1
from pathlib import Path
from typing import Mapping, Protocol
from typing import Any, Mapping, Protocol
from pexpect.popen_spawn import PopenSpawn
from plumbum import local
@ -17,7 +17,7 @@ from prompt_toolkit.input.ansi_escape_sequences import REVERSE_ANSI_SEQUENCES
from prompt_toolkit.keys import Keys
import copier
from copier.types import OptStr, StrOrPath
from copier.types import StrOrPath
PROJECT_TEMPLATE = Path(__file__).parent / "demo"
@ -61,8 +61,7 @@ SUFFIX_TMPL = ".tmpl"
class Spawn(Protocol):
def __call__(self, cmd: tuple[str, ...], *, timeout: int | None) -> PopenSpawn:
...
def __call__(self, cmd: tuple[str, ...], *, timeout: int | None) -> PopenSpawn: ...
class Keyboard(str, Enum):
@ -85,7 +84,7 @@ class Keyboard(str, Enum):
Backspace = ControlH
def render(tmp_path: Path, **kwargs) -> None:
def render(tmp_path: Path, **kwargs: Any) -> None:
kwargs.setdefault("quiet", True)
copier.run_copy(str(PROJECT_TEMPLATE), tmp_path, data=DATA, **kwargs)
@ -96,7 +95,9 @@ def assert_file(tmp_path: Path, *path: str) -> None:
assert filecmp.cmp(p1, p2)
def build_file_tree(spec: Mapping[StrOrPath, str | bytes | Path], dedent: bool = True):
def build_file_tree(
spec: Mapping[StrOrPath, str | bytes | Path], dedent: bool = True
) -> None:
"""Builds a file tree based on the received spec.
Params:
@ -122,7 +123,7 @@ def build_file_tree(spec: Mapping[StrOrPath, str | bytes | Path], dedent: bool =
def expect_prompt(
tui: PopenSpawn, name: str, expected_type: str, help: OptStr = None
tui: PopenSpawn, name: str, expected_type: str, help: str | None = None
) -> None:
"""Check that we get a prompt in the standard form"""
if help:
@ -135,7 +136,7 @@ def expect_prompt(
def git_save(
dst: StrOrPath = ".", message: str = "Test commit", tag: str | None = None
):
) -> None:
"""Save the current repo state in git.
Args:
@ -151,7 +152,7 @@ def git_save(
git("tag", tag)
def git_init(message="hello world") -> None:
def git_init(message: str = "hello world") -> None:
"""Initialize a Git repository with a first commit.
Args:

View File

@ -1,10 +1,11 @@
from __future__ import annotations
from pathlib import Path
from textwrap import dedent
import pytest
import copier
from copier.types import OptStr
from copier.user_data import load_answersfile_data
from .helpers import BRACKET_ENVOPS_JSON, SUFFIX_TMPL, build_file_tree
@ -52,7 +53,9 @@ def template_path(tmp_path_factory: pytest.TempPathFactory) -> str:
@pytest.mark.parametrize("answers_file", [None, ".changed-by-user.yaml"])
def test_answersfile(template_path: str, tmp_path: Path, answers_file: OptStr) -> None:
def test_answersfile(
template_path: str, tmp_path: Path, answers_file: str | None
) -> None:
"""Test copier behaves properly when using an answersfile."""
round_file = tmp_path / "round.txt"

View File

@ -170,7 +170,7 @@ def test_data_file_parsed_by_question_type(
def test_data_cli_takes_precedence_over_data_file(
tmp_path_factory: pytest.TempPathFactory,
):
) -> None:
src, dst, local = map(tmp_path_factory.mktemp, ("src", "dst", "local"))
build_file_tree(
{

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import json
from collections import deque
from pathlib import Path
@ -11,7 +13,6 @@ from plumbum import local
from plumbum.cmd import git
from copier import run_copy, run_update
from copier.types import OptStr
from .helpers import (
BRACKET_ENVOPS,
@ -122,7 +123,7 @@ def check_invalid(
name: str,
format: str,
invalid_value: str,
help: OptStr = None,
help: str | None = None,
err: str = "Invalid input",
) -> None:
"""Check that invalid input is reported correctly"""

View File

@ -29,7 +29,7 @@ GOOD_ENV_OPS = {
}
def git_init(message="hello world") -> None:
def git_init(message: str = "hello world") -> None:
git("init")
git("config", "user.name", "Copier Test")
git("config", "user.email", "test@copier")
@ -126,7 +126,7 @@ def test_invalid_config_data(
) -> None:
template = Template(conf_path)
with pytest.raises(InvalidConfigFileError):
template.config_data # noqa: B018
template.config_data # noqa: B018
if check_err:
_, err = capsys.readouterr()
assert check_err(err)
@ -261,7 +261,7 @@ def test_config_data_empty() -> None:
def test_multiple_config_file_error() -> None:
template = Template("tests/demo_multi_config")
with pytest.raises(MultipleConfigFilesError):
template.config_data # noqa: B018
template.config_data # noqa: B018
# ConfigData

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import filecmp
import platform
import stat
import sys
@ -24,7 +25,6 @@ from .helpers import (
PROJECT_TEMPLATE,
assert_file,
build_file_tree,
filecmp,
git_save,
render,
)
@ -87,7 +87,7 @@ def test_tag_autodetect_v_prefix_optional(
tmp_path_factory: pytest.TempPathFactory,
tag2: str,
use_prereleases: bool,
expected_version,
expected_version: str,
prefix: str,
) -> None:
"""Tags with and without `v` prefix should work the same."""

View File

@ -4,7 +4,7 @@ from shutil import copy2, copytree
import pytest
from plumbum import local
from plumbum.cmd import git
from pytest_gitconfig import GitConfig
from pytest_gitconfig.plugin import GitConfig
import copier
from copier.errors import DirtyLocalWarning

View File

@ -3,7 +3,8 @@ from pathlib import Path
from typing import Any
import pytest
from jinja2.ext import Environment, Extension
from jinja2 import Environment
from jinja2.ext import Extension
import copier

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import sys
from pathlib import Path
from typing import Any, Dict, List, Mapping, Protocol, Union
@ -24,10 +25,10 @@ from .helpers import (
git_save,
)
try:
from typing import TypeAlias # type: ignore[attr-defined]
except ImportError:
if sys.version_info < (3, 10):
from typing_extensions import TypeAlias
else:
from typing import TypeAlias
MARIO_TREE: Mapping[StrOrPath, str | bytes] = {

View File

@ -11,7 +11,7 @@ from pexpect.popen_spawn import PopenSpawn
from copier import Worker
from copier.errors import InvalidTypeError
from copier.types import AnyByStrDict, OptStr
from copier.types import AnyByStrDict
from .helpers import (
BRACKET_ENVOPS,
@ -32,7 +32,7 @@ main_question = {
class Prompt:
def __init__(self, name: str, format: str, help: OptStr = None) -> None:
def __init__(self, name: str, format: str, help: str | None = None) -> None:
self.name = name
self.format = format
self.help = help

View File

@ -44,7 +44,7 @@ def test_api(
template_path: str,
tmp_path_factory: pytest.TempPathFactory,
monkeypatch: pytest.MonkeyPatch,
):
) -> None:
tmp, dst = map(tmp_path_factory.mktemp, ["tmp", "dst"])
_git = git["-C", dst]
# Mock tmp dir to assert it ends up clean
@ -73,7 +73,7 @@ def test_cli(
template_path: str,
tmp_path_factory: pytest.TempPathFactory,
monkeypatch: pytest.MonkeyPatch,
):
) -> None:
tmp, dst = map(tmp_path_factory.mktemp, ["tmp", "dst"])
_git = git["-C", dst]
# Mock tmp dir to assert it ends up clean