Skip to content

enable stricter ruff configuration #1982

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion copier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Docs: https://copier.readthedocs.io/
"""

from .main import * # noqa: F401,F403
from .main import * # noqa: F403

# This version is a placeholder autoupdated by poetry-dynamic-versioning
__version__ = "0.0.0"
6 changes: 3 additions & 3 deletions copier/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ def _handle_exceptions(method: Callable[[], None]) -> int:
try:
try:
method()
except KeyboardInterrupt:
raise UserMessageError("Execution stopped by user")
except KeyboardInterrupt as error:
raise UserMessageError("Execution stopped by user") from error
except UserMessageError as error:
print(colors.red | "\n".join(error.args), file=sys.stderr)
return 1
Expand Down Expand Up @@ -201,7 +201,7 @@ def _worker(
self,
src_path: Optional[str] = None,
dst_path: str = ".",
**kwargs: Any, # noqa: FA100
**kwargs: Any,
) -> Worker:
"""Run Copier's internal API using CLI switches.

Expand Down
5 changes: 3 additions & 2 deletions copier/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class ExtensionNotFoundError(UserMessageError):
"""Extensions listed in the configuration could not be loaded."""


class CopierAnswersInterrupt(CopierError, KeyboardInterrupt):
class CopierAnswersInterruptError(CopierError, KeyboardInterrupt):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add an alias

# Backwards compatibility
CopierAnswersInterrupt = CopierAnswersInterruptError

to retain backwards compatibility, just in case somebody relies on catching this exception.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, good catch @sisp.

"""CopierAnswersInterrupt is raised during interactive question prompts.

It typically follows a KeyboardInterrupt (i.e. ctrl-c) and provides an
Expand Down Expand Up @@ -108,7 +108,8 @@ def __init__(self, features: Sequence[str]):
s = "s" if len(features) > 1 else ""
super().__init__(
f"Template uses potentially unsafe feature{s}: {', '.join(features)}.\n"
"If you trust this template, consider adding the `--trust` option when running `copier copy/update`."
"If you trust this template, consider adding the `--trust` option when "
"running `copier copy/update`."
)


Expand Down
23 changes: 13 additions & 10 deletions copier/jinja_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ class YieldEnvironment(SandboxedEnvironment):
This is simple environment class that extends the SandboxedEnvironment
for use with the YieldExtension, mainly for avoiding type errors.

We use the SandboxedEnvironment because we want to minimize the risk of hidden malware
in the templates. Of course we still have the post-copy tasks to worry about, but at least
they are more visible to the final user.
We use the SandboxedEnvironment because we want to minimize the risk of hidden
malware in the templates. Of course we still have the post-copy tasks to worry
about, but at least they are more visible to the final user.
"""

yield_name: str | None
yield_iterable: Iterable[Any] | None

def __init__(self, *args: Any, **kwargs: Any):
def __init__(self, *args: Any, **kwargs: Any): # noqa: D107
super().__init__(*args, **kwargs)
self.extend(yield_name=None, yield_iterable=None)

Expand Down Expand Up @@ -58,9 +58,9 @@ class YieldExtension(Extension):
>>> env.yield_iterable
[1, 2, 3]
```
"""
""" # noqa: E501

tags = {"yield"}
tags = {"yield"} # noqa: RUF012

environment: YieldEnvironment

Expand Down Expand Up @@ -97,16 +97,19 @@ def _yield_support(
) -> str:
"""Support function for the yield tag.

Sets the `yield_name` and `yield_iterable` attributes in the environment then calls
the provided caller function. If an UndefinedError is raised, it returns an empty string.
Sets the `yield_name` and `yield_iterable` attributes in the environment then
calls the provided caller function. If an UndefinedError is raised, it returns
an empty string.
"""
if (
self.environment.yield_name is not None
or self.environment.yield_iterable is not None
):
raise MultipleYieldTagsError(
"Attempted to parse the yield tag twice. Only one yield tag is allowed per path name.\n"
f'A yield tag with the name: "{self.environment.yield_name}" and iterable: "{self.environment.yield_iterable}" already exists.'
"Attempted to parse the yield tag twice. Only one yield tag is allowed"
" per path name.\n"
f'A yield tag with the name: "{self.environment.yield_name}" and '
f'iterable: "{self.environment.yield_iterable}" already exists.'
)

self.environment.yield_name = yield_name
Expand Down
62 changes: 35 additions & 27 deletions copier/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from questionary import unsafe_prompt

from .errors import (
CopierAnswersInterrupt,
CopierAnswersInterruptError,
ExtensionNotFoundError,
UnsafeTemplateError,
UserMessageError,
Expand Down Expand Up @@ -201,7 +201,7 @@ class Worker:

skip_tasks:
When `True`, skip template tasks execution.
"""
""" # noqa: E501

src_path: str | None = None
dst_path: Path = Path()
Expand Down Expand Up @@ -359,7 +359,8 @@ def _execute_tasks(self, tasks: Sequence[Task]) -> None:
continue

working_directory = (
# We can't use _render_path here, as that function has special handling for files in the template
# We can't use _render_path here, as that function has special handling
# for files in the template
self.subproject.local_abspath
/ Path(self._render_string(str(task.working_directory), extra_context))
).absolute()
Expand Down Expand Up @@ -389,13 +390,13 @@ def _system_render_context(self) -> AnyByStrMutableMapping:
"os": OS,
}
)
return dict(
_copier_answers=self._answers_to_remember(),
_copier_conf=conf,
_external_data=self._external_data(),
_folder_name=self.subproject.local_abspath.name,
_copier_python=sys.executable,
)
return {
"_copier_answers": self._answers_to_remember(),
"_copier_conf": conf,
"_external_data": self._external_data(),
"_folder_name": self.subproject.local_abspath.name,
"_copier_python": sys.executable,
}

def _path_matcher(self, patterns: Iterable[str]) -> Callable[[Path], bool]:
"""Produce a function that matches against specified patterns."""
Expand Down Expand Up @@ -560,7 +561,7 @@ def _ask(self) -> None: # noqa: C901
answers={question.var_name: question.get_default()},
)[question.var_name]
except KeyboardInterrupt as err:
raise CopierAnswersInterrupt(
raise CopierAnswersInterruptError(
self.answers, question, self.template
) from err
self.answers.user[var_name] = new_answer
Expand Down Expand Up @@ -608,7 +609,7 @@ def jinja_env(self) -> YieldEnvironment:
f"Copier could not load some Jinja extensions:\n{error}\n"
"Make sure to install these extensions alongside Copier itself.\n"
"See the docs at https://copier.readthedocs.io/en/latest/configuring/#jinja_extensions"
)
) from error
# patch the `to_json` filter to support Pydantic dataclasses
env.filters["to_json"] = partial(
env.filters["to_json"], default=to_jsonable_python
Expand Down Expand Up @@ -700,7 +701,8 @@ def _render_file(
).encode()
if self.jinja_env.yield_name:
raise YieldTagInFileError(
f"File {src_relpath} contains a yield tag, but it is not allowed."
f"File {src_relpath} contains a yield tag, but it is not "
"allowed."
)
else:
new_content = src_abspath.read_bytes()
Expand Down Expand Up @@ -797,10 +799,11 @@ def _render_parts(
) -> Iterable[tuple[Path, AnyByStrDict | None]]:
"""Render a set of parts into path and context pairs.

If a yield tag is found in a part, it will recursively yield multiple path and context pairs.
If a yield tag is found in a part, it will recursively yield multiple path and
context pairs.
"""
if rendered_parts is None:
rendered_parts = tuple()
rendered_parts = ()

if not parts:
rendered_path = Path(*rendered_parts)
Expand All @@ -820,7 +823,8 @@ def _render_parts(
if not extra_context:
extra_context = {}

# If the `part` has a yield tag, `self.jinja_env` will be set with the yield name and iterable
# If the `part` has a yield tag, `self.jinja_env` will be set with the yield
# name and iterable
rendered_part = self._render_string(part, extra_context=extra_context)

yield_name = self.jinja_env.yield_name
Expand All @@ -835,7 +839,7 @@ def _render_parts(
continue

yield from self._render_parts(
parts, rendered_parts + (rendered_part,), new_context, is_template
parts, (*rendered_parts, rendered_part), new_context, is_template
)

return
Expand All @@ -847,7 +851,7 @@ def _render_parts(
rendered_part = self._adjust_rendered_part(rendered_part)

yield from self._render_parts(
parts, rendered_parts + (rendered_part,), extra_context, is_template
parts, (*rendered_parts, rendered_part), extra_context, is_template
)

def _render_path(self, relpath: Path) -> Iterable[tuple[Path, AnyByStrDict | None]]:
Expand Down Expand Up @@ -959,7 +963,7 @@ def run_copy(self) -> None:
self._render_template()
if not self.quiet:
# TODO Unify printing tools
print("") # padding space
print() # padding space
if not self.skip_tasks:
self._execute_tasks(self.template.tasks)
except Exception:
Expand All @@ -969,7 +973,7 @@ def run_copy(self) -> None:
self._print_message(self.template.message_after_copy)
if not self.quiet:
# TODO Unify printing tools
print("") # padding space
print() # padding space

def run_recopy(self) -> None:
"""Update a subproject, keeping answers but discarding evolution."""
Expand Down Expand Up @@ -1014,8 +1018,8 @@ def run_update(self) -> None:
raise UserMessageError("Cannot update: version from template not detected.")
if self.subproject.template.version > self.template.version:
raise UserMessageError(
f"You are downgrading from {self.subproject.template.version} to {self.template.version}. "
"Downgrades are not supported."
f"You are downgrading from {self.subproject.template.version} to "
f"{self.template.version}. Downgrades are not supported."
)
if not self.overwrite:
# Only git-tracked subprojects can be updated, so the user can
Expand Down Expand Up @@ -1101,7 +1105,8 @@ def _apply_update(self) -> None: # noqa: C901
)
)
)
# Clear last answers cache to load possible answers migration, if skip_answered flag is not set
# Clear last answers cache to load possible answers migration, if
# skip_answered flag is not set
if self.skip_answered is False:
self.answers = AnswersMap(system=self._system_render_context())
with suppress(AttributeError):
Expand Down Expand Up @@ -1234,7 +1239,8 @@ def _apply_update(self) -> None: # noqa: C901
Path(f"{fname}.rej").unlink()
# The 3-way merge might have resolved conflicts automatically,
# so we need to check if the file contains conflict markers
# before storing the file name for marking it as unmerged after the loop.
# before storing the file name for marking it as unmerged after
# the loop.
with Path(fname).open() as conflicts_candidate:
if any(
line.rstrip()
Expand All @@ -1244,9 +1250,11 @@ def _apply_update(self) -> None: # noqa: C901
conflicted.append(fname)
# We ran `git merge-file` outside of a regular merge operation,
# which means no merge conflict is recorded in the index.
# Only the usual stage 0 is recorded, with the hash of the current version.
# We therefore update the index with the missing stages:
# 1 = current (before updating), 2 = base (last update), 3 = other (after updating).
# Only the usual stage 0 is recorded, with the hash of the current
# version. We therefore update the index with the missing stages:
# 1 = current (before updating), 2 = base (last update), 3 = other
# (after updating).
#
# See this SO post: https://stackoverflow.com/questions/79309642/
# and Git docs: https://git-scm.com/docs/git-update-index#_using_index_info.
if conflicted:
Expand Down
2 changes: 1 addition & 1 deletion copier/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def from_file(cls, settings_path: Path | None = None) -> Settings:
if settings_path.is_file():
data = yaml.safe_load(settings_path.read_text())
return cls.model_validate(data)
elif env_path:
if env_path:
warnings.warn(
f"Settings file not found at {env_path}", MissingSettingsWarning
)
Expand Down
9 changes: 6 additions & 3 deletions copier/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,11 @@ def _raw_config(self) -> AnyByStrDict:
conf_paths = [
p
for p in self.local_abspath.glob("copier.*")
if p.is_file() and re.match(r"\.ya?ml", p.suffix, re.I)
if p.is_file() and re.match(r"\.ya?ml", p.suffix, re.IGNORECASE)
]
if len(conf_paths) > 1:
raise MultipleConfigFilesError(conf_paths)
elif len(conf_paths) == 1:
if len(conf_paths) == 1:
return load_template_config(conf_paths[0])
return {}

Expand Down Expand Up @@ -405,7 +405,10 @@ def migration_tasks(
if any(key in migration for key in ("before", "after")):
# Legacy configuration format
warn(
"This migration configuration is deprecated. Please switch to the new format.",
(
"This migration configuration is deprecated. Please switch to "
"the new format."
),
category=DeprecationWarning,
)
current = parse(migration["version"])
Expand Down
27 changes: 14 additions & 13 deletions copier/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from importlib.metadata import version
from pathlib import Path
from types import TracebackType
from typing import Any, Callable, Iterator, Literal, TextIO, cast
from typing import Any, Callable, ClassVar, Iterator, Literal, TextIO, cast

import colorama
from packaging.version import Version
Expand All @@ -27,11 +27,11 @@
class Style:
"""Common color styles."""

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]
OK: ClassVar[list[str]] = [colorama.Fore.GREEN, colorama.Style.BRIGHT]
WARNING: ClassVar[list[str]] = [colorama.Fore.YELLOW, colorama.Style.BRIGHT]
IGNORE: ClassVar[list[str]] = [colorama.Fore.CYAN]
DANGER: ClassVar[list[str]] = [colorama.Fore.RED, colorama.Style.BRIGHT]
RESET: ClassVar[list[str]] = [colorama.Fore.RESET, colorama.Style.RESET_ALL]


INDENT = " " * 2
Expand Down Expand Up @@ -77,7 +77,7 @@ def printf(
if not style:
return action + _msg

out = style + [action] + Style.RESET + [INDENT, _msg]
out = [*style, action, *Style.RESET, INDENT, _msg]
print(*out, sep="", file=file_)
return None

Expand All @@ -87,7 +87,7 @@ def printf_exception(
) -> None:
"""Print exception with common format."""
if not quiet:
print("", file=sys.stderr)
print(file=sys.stderr)
printf(action, msg=msg, style=Style.DANGER, indent=indent, file_=sys.stderr)
print(HLINE, file=sys.stderr)
print(e, file=sys.stderr)
Expand Down Expand Up @@ -131,7 +131,7 @@ def cast_to_bool(value: Any) -> bool:
lower = value.lower()
if lower in {"y", "yes", "t", "true", "on"}:
return True
elif lower in {"n", "no", "f", "false", "off", "~", "null", "none"}:
if lower in {"n", "no", "f", "false", "off", "~", "null", "none"}:
return False
# Assume nothing
return bool(value)
Expand All @@ -152,7 +152,8 @@ def force_str_end(original_str: str, end: str = "\n") -> str:
def handle_remove_readonly(
func: Callable[[str], None],
path: str,
# TODO: Change this union to simply `BaseException` when Python 3.11 support is dropped
# TODO: Change this union to simply `BaseException` when Python 3.11 support is
# dropped
exc: BaseException | tuple[type[BaseException], BaseException, TracebackType],
) -> None:
"""Handle errors when trying to remove read-only files through `shutil.rmtree`.
Expand Down Expand Up @@ -181,9 +182,9 @@ def handle_remove_readonly(
def normalize_git_path(path: str) -> str:
r"""Convert weird characters returned by Git to normal UTF-8 path strings.

A filename like âñ will be reported by Git as "\\303\\242\\303\\261" (octal notation).
Similarly, a filename like "<tab>foo\b<lf>ar" will be reported as "\tfoo\\b\nar".
This can be disabled with `git config core.quotepath off`.
A filename like âñ will be reported by Git as "\\303\\242\\303\\261" (octal
notation). Similarly, a filename like "<tab>foo\b<lf>ar" will be reported as
"\tfoo\\b\nar". This can be disabled with `git config core.quotepath off`.

Args:
path: The Git path to normalize.
Expand Down
Loading
Loading