mirror of
https://github.com/copier-org/copier.git
synced 2025-05-05 15:32:54 +00:00
build(typing): enable 'strict' mypy linting (#1527)
--------- Co-authored-by: Timothée Mazzucotelli <dev@pawamoy.fr>
This commit is contained in:
parent
bcad276ee0
commit
0c6b0b96fe
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
50
poetry.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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(
|
||||
{
|
||||
|
@ -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"""
|
||||
|
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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] = {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user