Skip to content

Commit 448c53e

Browse files
authored
Merge branch 'master' into ruff-more-lints
2 parents a59e4ba + 0fb2d06 commit 448c53e

23 files changed

+218
-114
lines changed

copier/cli.py

+11-9
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@
5252
from os import PathLike
5353
from pathlib import Path
5454
from textwrap import dedent
55-
from typing import Callable
55+
from typing import Any, Callable, Iterable, Optional
5656

5757
import yaml
5858
from plumbum import cli, colors
5959

6060
from .errors import UnsafeTemplateError, UserMessageError
6161
from .main import Worker
6262
from .tools import copier_version
63-
from .types import AnyByStrDict, OptStr, StrSeq
63+
from .types import AnyByStrDict
6464

6565

6666
def _handle_exceptions(method: Callable[[], None]) -> int:
@@ -80,7 +80,7 @@ def _handle_exceptions(method: Callable[[], None]) -> int:
8080
return 0
8181

8282

83-
class CopierApp(cli.Application):
83+
class CopierApp(cli.Application): # type: ignore[misc]
8484
"""The Copier CLI application."""
8585

8686
DESCRIPTION = "Create a new project from a template."
@@ -105,10 +105,10 @@ class CopierApp(cli.Application):
105105
CALL_MAIN_IF_NESTED_COMMAND = False
106106

107107

108-
class _Subcommand(cli.Application):
108+
class _Subcommand(cli.Application): # type: ignore[misc]
109109
"""Base class for Copier subcommands."""
110110

111-
def __init__(self, executable: PathLike) -> None:
111+
def __init__(self, executable: "PathLike[str]") -> None:
112112
self.data: AnyByStrDict = {}
113113
super().__init__(executable)
114114

@@ -158,14 +158,14 @@ def __init__(self, executable: PathLike) -> None:
158158
),
159159
)
160160

161-
@cli.switch(
161+
@cli.switch( # type: ignore[misc]
162162
["-d", "--data"],
163163
str,
164164
"VARIABLE=VALUE",
165165
list=True,
166166
help="Make VARIABLE available as VALUE when rendering the template",
167167
)
168-
def data_switch(self, values: StrSeq) -> None:
168+
def data_switch(self, values: Iterable[str]) -> None:
169169
"""Update [data][] with provided values.
170170
171171
Arguments:
@@ -176,7 +176,7 @@ def data_switch(self, values: StrSeq) -> None:
176176
key, value = arg.split("=", 1)
177177
self.data[key] = value
178178

179-
@cli.switch(
179+
@cli.switch( # type: ignore[misc]
180180
["--data-file"],
181181
cli.ExistingFile,
182182
help="Load data from a YAML file",
@@ -195,7 +195,9 @@ def data_file_switch(self, path: cli.ExistingFile) -> None:
195195
}
196196
self.data.update(updates_without_cli_overrides)
197197

198-
def _worker(self, src_path: OptStr = None, dst_path: str = ".", **kwargs) -> Worker:
198+
def _worker(
199+
self, src_path: Optional[str] = None, dst_path: str = ".", **kwargs: Any # noqa: FA100
200+
) -> Worker:
199201
"""Run Copier's internal API using CLI switches.
200202
201203
Arguments:

copier/main.py

+52-34
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,17 @@
1313
from pathlib import Path
1414
from shutil import rmtree
1515
from tempfile import TemporaryDirectory
16-
from typing import Callable, Iterable, Literal, Mapping, Sequence, get_args
16+
from types import TracebackType
17+
from typing import (
18+
Any,
19+
Callable,
20+
Iterable,
21+
Literal,
22+
Mapping,
23+
Sequence,
24+
get_args,
25+
overload,
26+
)
1727
from unicodedata import normalize
1828

1929
from jinja2.loaders import FileSystemLoader
@@ -36,15 +46,7 @@
3646
from .subproject import Subproject
3747
from .template import Task, Template
3848
from .tools import OS, Style, normalize_git_path, printf, readlink
39-
from .types import (
40-
MISSING,
41-
AnyByStrDict,
42-
JSONSerializable,
43-
OptStr,
44-
RelativePath,
45-
StrOrPath,
46-
StrSeq,
47-
)
49+
from .types import MISSING, AnyByStrDict, JSONSerializable, RelativePath, StrOrPath
4850
from .user_data import DEFAULT_DATA, AnswersMap, Question
4951
from .vcs import get_git
5052

@@ -157,16 +159,19 @@ class Worker:
157159
When `True`, allow usage of unsafe templates.
158160
159161
See [unsafe][]
162+
163+
skip_answered:
164+
When `True`, skip questions that have already been answered.
160165
"""
161166

162167
src_path: str | None = None
163168
dst_path: Path = Path(".")
164169
answers_file: RelativePath | None = None
165-
vcs_ref: OptStr = None
170+
vcs_ref: str | None = None
166171
data: AnyByStrDict = field(default_factory=dict)
167-
exclude: StrSeq = ()
172+
exclude: Sequence[str] = ()
168173
use_prereleases: bool = False
169-
skip_if_exists: StrSeq = ()
174+
skip_if_exists: Sequence[str] = ()
170175
cleanup_on_error: bool = True
171176
defaults: bool = False
172177
user_defaults: AnyByStrDict = field(default_factory=dict)
@@ -179,13 +184,26 @@ class Worker:
179184
skip_answered: bool = False
180185

181186
answers: AnswersMap = field(default_factory=AnswersMap, init=False)
182-
_cleanup_hooks: list[Callable] = field(default_factory=list, init=False)
187+
_cleanup_hooks: list[Callable[[], None]] = field(default_factory=list, init=False)
183188

184-
def __enter__(self):
189+
def __enter__(self) -> Worker:
185190
"""Allow using worker as a context manager."""
186191
return self
187192

188-
def __exit__(self, type, value, traceback):
193+
@overload
194+
def __exit__(self, type: None, value: None, traceback: None) -> None: ...
195+
196+
@overload
197+
def __exit__(
198+
self, type: type[BaseException], value: BaseException, traceback: TracebackType
199+
) -> None: ...
200+
201+
def __exit__(
202+
self,
203+
type: type[BaseException] | None,
204+
value: BaseException | None,
205+
traceback: TracebackType | None,
206+
) -> None:
189207
"""Clean up garbage files after worker usage ends."""
190208
if value is not None:
191209
# exception was raised from code inside context manager:
@@ -196,7 +214,7 @@ def __exit__(self, type, value, traceback):
196214
# otherwise clean up and let any exception bubble up
197215
self._cleanup()
198216

199-
def _cleanup(self):
217+
def _cleanup(self) -> None:
200218
"""Execute all stored cleanup methods."""
201219
for method in self._cleanup_hooks:
202220
method()
@@ -226,7 +244,7 @@ def _print_message(self, message: str) -> None:
226244
if message and not self.quiet:
227245
print(self._render_string(message), file=sys.stderr)
228246

229-
def _answers_to_remember(self) -> Mapping:
247+
def _answers_to_remember(self) -> Mapping[str, Any]:
230248
"""Get only answers that will be remembered in the copier answers file."""
231249
# All internal values must appear first
232250
answers: AnyByStrDict = {}
@@ -273,7 +291,7 @@ def _execute_tasks(self, tasks: Sequence[Task]) -> None:
273291
with local.cwd(self.subproject.local_abspath), local.env(**task.extra_env):
274292
subprocess.run(task_cmd, shell=use_shell, check=True, env=local.env)
275293

276-
def _render_context(self) -> Mapping:
294+
def _render_context(self) -> Mapping[str, Any]:
277295
"""Produce render context for Jinja."""
278296
# Backwards compatibility
279297
# FIXME Remove it?
@@ -305,7 +323,7 @@ def _path_matcher(self, patterns: Iterable[str]) -> Callable[[Path], bool]:
305323
spec = PathSpec.from_lines("gitwildmatch", normalized_patterns)
306324
return spec.match_file
307325

308-
def _solve_render_conflict(self, dst_relpath: Path):
326+
def _solve_render_conflict(self, dst_relpath: Path) -> bool:
309327
"""Properly solve render conflicts.
310328
311329
It can ask the user if running in interactive mode.
@@ -468,7 +486,7 @@ def answers_relpath(self) -> Path:
468486
return Path(template.render(**self.answers.combined))
469487

470488
@cached_property
471-
def all_exclusions(self) -> StrSeq:
489+
def all_exclusions(self) -> Sequence[str]:
472490
"""Combine default, template and user-chosen exclusions."""
473491
return self.template.exclude + tuple(self.exclude)
474492

@@ -766,7 +784,7 @@ def run_recopy(self) -> None:
766784
f"from `{self.subproject.answers_relpath}`."
767785
)
768786
with replace(self, src_path=self.subproject.template.url) as new_worker:
769-
return new_worker.run_copy()
787+
new_worker.run_copy()
770788

771789
def run_update(self) -> None:
772790
"""Update a subproject that was already generated.
@@ -818,7 +836,7 @@ def run_update(self) -> None:
818836
self._apply_update()
819837
self._print_message(self.template.message_after_update)
820838

821-
def _apply_update(self):
839+
def _apply_update(self) -> None: # noqa: C901
822840
git = get_git()
823841
subproject_top = Path(
824842
git(
@@ -840,8 +858,8 @@ def _apply_update(self):
840858
data=self.subproject.last_answers,
841859
defaults=True,
842860
quiet=True,
843-
src_path=self.subproject.template.url,
844-
vcs_ref=self.subproject.template.commit,
861+
src_path=self.subproject.template.url, # type: ignore[union-attr]
862+
vcs_ref=self.subproject.template.commit, # type: ignore[union-attr]
845863
) as old_worker:
846864
old_worker.run_copy()
847865
# Extract diff between temporary destination and real destination
@@ -863,7 +881,7 @@ def _apply_update(self):
863881
diff = diff_cmd("--inter-hunk-context=0")
864882
# Run pre-migration tasks
865883
self._execute_tasks(
866-
self.template.migration_tasks("before", self.subproject.template)
884+
self.template.migration_tasks("before", self.subproject.template) # type: ignore[arg-type]
867885
)
868886
# Clear last answers cache to load possible answers migration, if skip_answered flag is not set
869887
if self.skip_answered is False:
@@ -885,10 +903,10 @@ def _apply_update(self):
885903
with replace(
886904
self,
887905
dst_path=new_copy / subproject_subdir,
888-
data=self.answers.combined,
906+
data=self.answers.combined, # type: ignore[arg-type]
889907
defaults=True,
890908
quiet=True,
891-
src_path=self.subproject.template.url,
909+
src_path=self.subproject.template.url, # type: ignore[union-attr]
892910
) as new_worker:
893911
new_worker.run_copy()
894912
compared = dircmp(old_copy, new_copy)
@@ -968,10 +986,10 @@ def _apply_update(self):
968986

969987
# Run post-migration tasks
970988
self._execute_tasks(
971-
self.template.migration_tasks("after", self.subproject.template)
989+
self.template.migration_tasks("after", self.subproject.template) # type: ignore[arg-type]
972990
)
973991

974-
def _git_initialize_repo(self):
992+
def _git_initialize_repo(self) -> None:
975993
"""Initialize a git repository in the current directory."""
976994
git = get_git()
977995
git("init", retcode=None)
@@ -1004,7 +1022,7 @@ def run_copy(
10041022
src_path: str,
10051023
dst_path: StrOrPath = ".",
10061024
data: AnyByStrDict | None = None,
1007-
**kwargs,
1025+
**kwargs: Any,
10081026
) -> Worker:
10091027
"""Copy a template to a destination, from zero.
10101028
@@ -1020,7 +1038,7 @@ def run_copy(
10201038

10211039

10221040
def run_recopy(
1023-
dst_path: StrOrPath = ".", data: AnyByStrDict | None = None, **kwargs
1041+
dst_path: StrOrPath = ".", data: AnyByStrDict | None = None, **kwargs: Any
10241042
) -> Worker:
10251043
"""Update a subproject from its template, discarding subproject evolution.
10261044
@@ -1038,7 +1056,7 @@ def run_recopy(
10381056
def run_update(
10391057
dst_path: StrOrPath = ".",
10401058
data: AnyByStrDict | None = None,
1041-
**kwargs,
1059+
**kwargs: Any,
10421060
) -> Worker:
10431061
"""Update a subproject, from its template.
10441062
@@ -1053,7 +1071,7 @@ def run_update(
10531071
return worker
10541072

10551073

1056-
def _remove_old_files(prefix: Path, cmp: dircmp, rm_common: bool = False) -> None:
1074+
def _remove_old_files(prefix: Path, cmp: dircmp[str], rm_common: bool = False) -> None:
10571075
"""Remove files and directories only found in "old" template.
10581076
10591077
This is an internal helper method used to process a comparison of 2

copier/subproject.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class Subproject:
3333
local_abspath: AbsolutePath
3434
answers_relpath: Path = Path(".copier-answers.yml")
3535

36-
_cleanup_hooks: list[Callable] = field(default_factory=list, init=False)
36+
_cleanup_hooks: list[Callable[[], None]] = field(default_factory=list, init=False)
3737

3838
def is_dirty(self) -> bool:
3939
"""Indicate if the local template root is dirty.
@@ -45,7 +45,7 @@ def is_dirty(self) -> bool:
4545
return bool(get_git()("status", "--porcelain").strip())
4646
return False
4747

48-
def _cleanup(self):
48+
def _cleanup(self) -> None:
4949
"""Remove temporary files and folders created by the subproject."""
5050
for method in self._cleanup_hooks:
5151
method()
@@ -78,9 +78,11 @@ def template(self) -> Template | None:
7878
result = Template(url=last_url, ref=last_ref)
7979
self._cleanup_hooks.append(result._cleanup)
8080
return result
81+
return None
8182

8283
@cached_property
8384
def vcs(self) -> VCSTypes | None:
8485
"""VCS type of the subproject."""
8586
if is_in_git_repo(self.local_abspath):
8687
return "git"
88+
return None

0 commit comments

Comments
 (0)