diff --git a/.github/action_helper.py b/.github/action_helper.py index e6ab5d95..d2a6fce2 100644 --- a/.github/action_helper.py +++ b/.github/action_helper.py @@ -22,11 +22,14 @@ def filter_version(version: str) -> str: version_parts = version_.split(".") if len(version_parts) < 2: - raise ValueError(f"invalid version: {version}") + msg = f"invalid version: {version}" + raise ValueError(msg) if not version_parts[0].isdigit(): - raise ValueError(f"invalid major python version: {version}") + msg = f"invalid major python version: {version}" + raise ValueError(msg) if not version_parts[1].isdigit(): - raise ValueError(f"invalid minor python version: {version}") + msg = f"invalid minor python version: {version}" + raise ValueError(msg) return ".".join(version_parts[:2]) @@ -36,20 +39,22 @@ def setup_action(input_: str) -> None: pypy_versions = [version for version in versions if version.startswith("pypy")] pypy_versions_filtered = [filter_version(version) for version in pypy_versions] if len(pypy_versions) != len(set(pypy_versions_filtered)): - raise ValueError( + msg = ( "multiple versions specified for the same 'major.minor' PyPy interpreter:" f" {pypy_versions}" ) + raise ValueError(msg) cpython_versions = [version for version in versions if version not in pypy_versions] cpython_versions_filtered = [ filter_version(version) for version in cpython_versions ] if len(cpython_versions) != len(set(cpython_versions_filtered)): - raise ValueError( + msg = ( "multiple versions specified for the same 'major.minor' CPython" f" interpreter: {cpython_versions}" ) + raise ValueError(msg) # cpython shall be installed last because # other interpreters also define pythonX.Y symlinks. @@ -70,5 +75,6 @@ def setup_action(input_: str) -> None: if __name__ == "__main__": if len(sys.argv) != 2: - raise AssertionError(f"invalid arguments: {sys.argv}") + msg = f"invalid arguments: {sys.argv}" + raise AssertionError(msg) setup_action(sys.argv[1]) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce78361a..a7dda492 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.3 + rev: v0.8.0 hooks: - id: ruff args: ["--fix", "--show-fixes"] diff --git a/nox/__init__.py b/nox/__init__.py index 8121e62a..5338044d 100644 --- a/nox/__init__.py +++ b/nox/__init__.py @@ -17,7 +17,7 @@ from nox import project from nox._cli import main from nox._options import noxfile_options as options -from nox._parametrize import Param as param +from nox._parametrize import Param as param # noqa: N813 from nox._parametrize import parametrize_decorator as parametrize from nox.registry import session_decorator as session from nox.sessions import Session @@ -26,11 +26,11 @@ __all__ = [ "Session", + "main", "needs_version", "options", "param", "parametrize", "project", "session", - "main", ] diff --git a/nox/_decorators.py b/nox/_decorators.py index 62db1452..6de7315c 100644 --- a/nox/_decorators.py +++ b/nox/_decorators.py @@ -18,12 +18,12 @@ import functools import inspect import types -from collections.abc import Iterable, Mapping, Sequence from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast -from . import _typing - if TYPE_CHECKING: + from collections.abc import Iterable, Mapping, Sequence + + from . import _typing from ._parametrize import Param T = TypeVar("T", bound=Callable[..., Any]) @@ -32,12 +32,12 @@ class FunctionDecorator: """This is a function decorator.""" - def __new__( + def __new__( # noqa: PYI034 cls: Any, func: Callable[..., Any], *args: Any, **kwargs: Any ) -> FunctionDecorator: - obj = super().__new__(cls) - functools.update_wrapper(obj, func) - return cast(FunctionDecorator, obj) + self = super().__new__(cls) + functools.update_wrapper(self, func) + return cast(FunctionDecorator, self) def _copy_func(src: T, name: str | None = None) -> T: diff --git a/nox/_option_set.py b/nox/_option_set.py index 573da600..10597b9c 100644 --- a/nox/_option_set.py +++ b/nox/_option_set.py @@ -23,13 +23,15 @@ import functools from argparse import ArgumentError as ArgumentError # noqa: PLC0414 from argparse import ArgumentParser, Namespace -from collections.abc import Callable, Iterable -from typing import Any, Literal +from typing import TYPE_CHECKING, Any, Literal import argcomplete import attrs import attrs.validators as av +if TYPE_CHECKING: + from collections.abc import Callable, Iterable + av_opt_str = av.optional(av.instance_of(str)) av_opt_list_str = av.optional( av.deep_iterable( @@ -198,6 +200,7 @@ def make_flag_pair( name: str, enable_flags: tuple[str, str] | tuple[str], disable_flags: tuple[str, str] | tuple[str], + *, default: bool | Callable[[], bool] = False, **kwargs: Any, ) -> tuple[Option, Option]: @@ -277,9 +280,8 @@ def parser(self) -> ArgumentParser: # Every option must have a group (except for hidden options) if option.group is None: - raise ValueError( - f"Option {option.name} must either have a group or be hidden." - ) + msg = f"Option {option.name} must either have a group or be hidden." + raise ValueError(msg) argument = groups[option.group.name].add_argument( *option.flags, help=option.help, default=option.default, **option.kwargs @@ -331,7 +333,8 @@ def namespace(self, **kwargs: Any) -> argparse.Namespace: # used in tests. for key, value in kwargs.items(): if key not in args: - raise KeyError(f"{key} is not an option.") + msg = f"{key} is not an option." + raise KeyError(msg) args[key] = value return argparse.Namespace(**args) diff --git a/nox/_options.py b/nox/_options.py index 77c5b7f7..a98a9bf4 100644 --- a/nox/_options.py +++ b/nox/_options.py @@ -19,16 +19,19 @@ import itertools import os import sys -from collections.abc import Iterable -from typing import Any, Callable, Literal, Sequence +from typing import TYPE_CHECKING, Any, Callable, Literal, Sequence import argcomplete from nox import _option_set -from nox._option_set import NoxOptions from nox.tasks import discover_manifest, filter_manifest, load_nox_module from nox.virtualenv import ALL_VENVS +if TYPE_CHECKING: + from collections.abc import Iterable + + from nox._option_set import NoxOptions + ReuseVenvType = Literal["no", "yes", "never", "always"] """All of Nox's configuration options.""" @@ -129,9 +132,8 @@ def _force_venv_backend_merge_func( command_args.force_venv_backend is not None and command_args.force_venv_backend != "none" ): - raise ValueError( - "You can not use `--no-venv` with a non-none `--force-venv-backend`" - ) + msg = "You can not use `--no-venv` with a non-none `--force-venv-backend`" + raise ValueError(msg) return "none" return command_args.force_venv_backend or noxfile_args.force_venv_backend # type: ignore[return-value] @@ -191,7 +193,7 @@ def _default_list() -> list[str] | None: return _default_list -def _color_finalizer(value: bool, args: argparse.Namespace) -> bool: +def _color_finalizer(_value: bool, args: argparse.Namespace) -> bool: # noqa: FBT001 """Figures out the correct value for "color" based on the two color flags. Args: @@ -224,7 +226,7 @@ def _force_pythons_finalizer( return value -def _R_finalizer(value: bool, args: argparse.Namespace) -> bool: +def _R_finalizer(value: bool, args: argparse.Namespace) -> bool: # noqa: FBT001 """Propagate -R to --reuse-existing-virtualenvs and --no-install and --reuse-venv=yes.""" if value: args.reuse_venv = "yes" @@ -233,7 +235,8 @@ def _R_finalizer(value: bool, args: argparse.Namespace) -> bool: def _reuse_existing_virtualenvs_finalizer( - value: bool, args: argparse.Namespace + value: bool, # noqa: FBT001 + args: argparse.Namespace, ) -> bool: """Propagate --reuse-existing-virtualenvs to --reuse-venv=yes.""" if value: @@ -242,7 +245,7 @@ def _reuse_existing_virtualenvs_finalizer( def _posargs_finalizer( - value: Sequence[Any], args: argparse.Namespace + value: Sequence[Any], _args: argparse.Namespace ) -> Sequence[Any] | list[Any]: """Removes the leading "--"s in the posargs array (if any) and asserts that remaining arguments came after a "--". @@ -268,7 +271,9 @@ def _posargs_finalizer( def _python_completer( - prefix: str, parsed_args: argparse.Namespace, **kwargs: Any + prefix: str, # noqa: ARG001 + parsed_args: argparse.Namespace, + **kwargs: Any, ) -> Iterable[str]: module = load_nox_module(parsed_args) manifest = discover_manifest(module, parsed_args) @@ -282,7 +287,9 @@ def _python_completer( def _session_completer( - prefix: str, parsed_args: argparse.Namespace, **kwargs: Any + prefix: str, # noqa: ARG001 + parsed_args: argparse.Namespace, + **kwargs: Any, ) -> Iterable[str]: parsed_args.list_sessions = True module = load_nox_module(parsed_args) @@ -296,7 +303,9 @@ def _session_completer( def _tag_completer( - prefix: str, parsed_args: argparse.Namespace, **kwargs: Any + prefix: str, # noqa: ARG001 + parsed_args: argparse.Namespace, + **kwargs: Any, ) -> Iterable[str]: module = load_nox_module(parsed_args) manifest = discover_manifest(module, parsed_args) diff --git a/nox/_parametrize.py b/nox/_parametrize.py index 281607fc..15f802e1 100644 --- a/nox/_parametrize.py +++ b/nox/_parametrize.py @@ -16,8 +16,10 @@ import functools import itertools -from collections.abc import Callable, Sequence -from typing import Any, Iterable, Union +from typing import TYPE_CHECKING, Any, Iterable, Union + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence class Param: @@ -73,9 +75,9 @@ def copy(self) -> Param: def update(self, other: Param) -> None: self.id = ", ".join([str(self), str(other)]) - self.args = self.args + other.args - self.arg_names = self.arg_names + other.arg_names - self.tags = self.tags + other.tags + self.args += other.args + self.arg_names += other.arg_names + self.tags += other.tags def __eq__(self, other: object) -> bool: if isinstance(other, self.__class__): diff --git a/nox/_resolver.py b/nox/_resolver.py index c5c2a8e5..e95af676 100644 --- a/nox/_resolver.py +++ b/nox/_resolver.py @@ -28,6 +28,7 @@ class CycleError(ValueError): def lazy_stable_topo_sort( dependencies: Mapping[Node, Iterable[Node]], root: Node, + *, drop_root: bool = True, ) -> Iterator[Node]: """Returns the "lazy, stable" topological sort of a dependency graph. @@ -118,7 +119,7 @@ def lazy_stable_topo_sort( ~nox._resolver.CycleError: If a dependency cycle is encountered. """ - visited = {node: False for node in dependencies} + visited = dict.fromkeys(dependencies, False) def prepended_by_dependencies( node: Node, @@ -191,7 +192,8 @@ def extend_walk( # Dependency cycle found. walk_list = list(walk) cycle = walk_list[walk_list.index(node) :] + [node] - raise CycleError("Nodes are in a dependency cycle", tuple(cycle)) + msg = "Nodes are in a dependency cycle" + raise CycleError(msg, tuple(cycle)) walk[node] = None return walk diff --git a/nox/_version.py b/nox/_version.py index 3d1026d3..f233c09b 100644 --- a/nox/_version.py +++ b/nox/_version.py @@ -61,7 +61,7 @@ def _parse_needs_version(source: str, filename: str = "") -> str | None def _read_needs_version(filename: str) -> str | None: """Read ``nox.needs_version`` from the user's Noxfile.""" - with open(filename) as io: + with open(filename, encoding="utf-8") as io: source = io.read() return _parse_needs_version(source, filename=filename) @@ -81,9 +81,8 @@ def _check_nox_version_satisfies(needs_version: str) -> None: raise InvalidVersionSpecifier(message) from error if not specifiers.contains(version, prereleases=True): - raise VersionCheckFailed( - f"The Noxfile requires Nox {specifiers}, you have {version}" - ) + msg = f"The Noxfile requires Nox {specifiers}, you have {version}" + raise VersionCheckFailed(msg) def check_nox_version(filename: str) -> None: diff --git a/nox/command.py b/nox/command.py index 11333479..976e7f9c 100644 --- a/nox/command.py +++ b/nox/command.py @@ -20,14 +20,13 @@ import subprocess import sys from collections.abc import Iterable, Mapping, Sequence -from typing import Literal, overload +from typing import TYPE_CHECKING, Literal, overload from nox.logger import logger from nox.popen import DEFAULT_INTERRUPT_TIMEOUT, DEFAULT_TERMINATE_TIMEOUT, popen -TYPE_CHECKING = False - if TYPE_CHECKING: + from collections.abc import Iterable, Mapping, Sequence from typing import IO ExternalType = Literal["error", True, False] @@ -53,7 +52,8 @@ def which(program: str | os.PathLike[str], paths: Sequence[str] | None) -> str: return os.fspath(full_path) logger.error(f"Program {program} not found.") - raise CommandFailed(f"Program {program} not found") + msg = f"Program {program} not found" + raise CommandFailed(msg) def _clean_env(env: Mapping[str, str | None] | None = None) -> dict[str, str] | None: @@ -162,7 +162,8 @@ def run( f" at {cmd_path}. Pass external=True into run() to explicitly allow" " this." ) - raise CommandFailed("External program disallowed.") + msg = "External program disallowed." + raise CommandFailed(msg) if external is False: logger.warning( f"Warning: {cmd} is not installed into the virtualenv, it is" @@ -192,13 +193,14 @@ def run( if silent: sys.stderr.write(output) - raise CommandFailed(f"Returned code {return_code}") + msg = f"Returned code {return_code}" + raise CommandFailed(msg) if output: logger.output(output) - return output if silent else True - except KeyboardInterrupt: logger.error("Interrupted...") raise + + return output if silent else True diff --git a/nox/logger.py b/nox/logger.py index e3ecf650..6dd2360e 100644 --- a/nox/logger.py +++ b/nox/logger.py @@ -23,7 +23,7 @@ OUTPUT = logging.DEBUG - 1 -def _get_format(colorlog: bool, add_timestamp: bool) -> str: +def _get_format(*, colorlog: bool, add_timestamp: bool) -> str: if colorlog: if add_timestamp: return "%(cyan)s%(name)s > [%(asctime)s] %(log_color)s%(message)s" @@ -36,7 +36,7 @@ def _get_format(colorlog: bool, add_timestamp: bool) -> str: class NoxFormatter(logging.Formatter): - def __init__(self, add_timestamp: bool = False) -> None: + def __init__(self, *, add_timestamp: bool = False) -> None: super().__init__(fmt=_get_format(colorlog=False, add_timestamp=add_timestamp)) self._simple_fmt = logging.Formatter("%(message)s") @@ -49,6 +49,7 @@ def format(self, record: Any) -> str: class NoxColoredFormatter(ColoredFormatter): def __init__( self, + *, datefmt: Any = None, style: Any = None, log_colors: Any = None, @@ -91,7 +92,7 @@ def output(self, msg: str, *args: Any, **kwargs: Any) -> None: logger = cast(LoggerWithSuccessAndOutput, logging.getLogger("nox")) -def _get_formatter(color: bool, add_timestamp: bool) -> logging.Formatter: +def _get_formatter(*, color: bool, add_timestamp: bool) -> logging.Formatter: if color: return NoxColoredFormatter( reset=True, @@ -111,7 +112,7 @@ def _get_formatter(color: bool, add_timestamp: bool) -> logging.Formatter: def setup_logging( - color: bool, verbose: bool = False, add_timestamp: bool = False + *, color: bool, verbose: bool = False, add_timestamp: bool = False ) -> None: # pragma: no cover """Setup logging. @@ -126,7 +127,7 @@ def setup_logging( root_logger.setLevel(logging.DEBUG) handler = logging.StreamHandler() - handler.setFormatter(_get_formatter(color, add_timestamp)) + handler.setFormatter(_get_formatter(color=color, add_timestamp=add_timestamp)) root_logger.addHandler(handler) # Silence noisy loggers diff --git a/nox/manifest.py b/nox/manifest.py index 1bb6e009..c3ca9fd5 100644 --- a/nox/manifest.py +++ b/nox/manifest.py @@ -14,18 +14,20 @@ from __future__ import annotations -import argparse import ast import itertools import operator from collections import OrderedDict -from collections.abc import Iterable, Iterator, Sequence -from typing import Any, Mapping, cast +from typing import TYPE_CHECKING, Any, Mapping, cast from nox._decorators import Call, Func from nox._resolver import CycleError, lazy_stable_topo_sort from nox.sessions import Session, SessionRunner +if TYPE_CHECKING: + import argparse + from collections.abc import Iterable, Iterator, Sequence + WARN_PYTHONS_IGNORED = "python_ignored" @@ -156,12 +158,12 @@ def filter_by_name(self, specified_sessions: Iterable[str]) -> None: """ # Filter the sessions remaining in the queue based on # whether they are individually specified. - queue = [] - for session_name in specified_sessions: - for session in self._queue: - if _normalized_session_match(session_name, session): - queue.append(session) - self._queue = queue + self._queue = [ + session + for session_name in specified_sessions + for session in self._queue + if _normalized_session_match(session_name, session) + ] # If a session was requested and was not found, complain loudly. all_sessions = set( @@ -181,7 +183,8 @@ def filter_by_name(self, specified_sessions: Iterable[str]) -> None: if _normalize_arg(session_name) not in all_sessions ] if missing_sessions: - raise KeyError(f"Sessions not found: {', '.join(missing_sessions)}") + msg = f"Sessions not found: {', '.join(missing_sessions)}" + raise KeyError(msg) def filter_by_default(self) -> None: """Filter sessions in the queue based on the default flag.""" @@ -241,7 +244,7 @@ def add_dependencies(self) -> None: session.signatures[0] for session in parametrized_sessions ] parent_session = SessionRunner( - parent_name, [], parent_func, self._config, self, False + parent_name, [], parent_func, self._config, self, multi=False ) parent_sessions.add(parent_session) sessions_by_id[parent_name] = parent_session @@ -272,7 +275,7 @@ def add_dependencies(self) -> None: ] def make_session( - self, name: str, func: Func, multi: bool = False + self, name: str, func: Func, *, multi: bool = False ) -> list[SessionRunner]: """Create a session object from the session function. @@ -338,7 +341,9 @@ def make_session( if func.python: long_names.append(f"{name}-{func.python}") - return [SessionRunner(name, long_names, func, self._config, self, multi)] + return [ + SessionRunner(name, long_names, func, self._config, self, multi=multi) + ] # Since this function is parametrized, we need to add a distinct # session for each permutation. @@ -354,21 +359,23 @@ def make_session( long_names.append(f"{name}-{func.python}") sessions.append( - SessionRunner(name, long_names, call, self._config, self, multi) + SessionRunner(name, long_names, call, self._config, self, multi=multi) ) # Edge case: If the parameters made it such that there were no valid # calls, add an empty, do-nothing session. if not calls: sessions.append( - SessionRunner(name, [], _null_session_func, self._config, self, multi) + SessionRunner( + name, [], _null_session_func, self._config, self, multi=multi + ) ) # Return the list of sessions. return sessions def next(self) -> SessionRunner: - return self.__next__() + return next(self) def notify( self, session: str | SessionRunner, posargs: Iterable[str] | None = None @@ -407,7 +414,8 @@ def notify( return True # The session was not found in the list of sessions. - raise ValueError(f"Session {session} not found.") + msg = f"Session {session} not found." + raise ValueError(msg) class KeywordLocals(Mapping[str, bool]): @@ -434,8 +442,9 @@ def __len__(self) -> int: def keyword_match(expression: str, keywords: Iterable[str]) -> Any: """See if an expression matches the given set of keywords.""" + # TODO: see if we can use ast.literal_eval here. locals = KeywordLocals(set(keywords)) - return eval(expression, {}, locals) + return eval(expression, {}, locals) # noqa: S307 def _null_session_func_(session: Session) -> None: diff --git a/nox/popen.py b/nox/popen.py index 5f32a9e9..b6f7bc6d 100644 --- a/nox/popen.py +++ b/nox/popen.py @@ -18,8 +18,10 @@ import locale import subprocess import sys -from collections.abc import Mapping, Sequence -from typing import IO +from typing import IO, TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence DEFAULT_INTERRUPT_TIMEOUT = 0.3 DEFAULT_TERMINATE_TIMEOUT = 0.2 @@ -55,7 +57,7 @@ def decode_output(output: bytes) -> str: return output.decode("utf-8") except UnicodeDecodeError: second_encoding = locale.getpreferredencoding() - if second_encoding.casefold() in ("utf8", "utf-8"): + if second_encoding.casefold() in {"utf8", "utf-8"}: raise return output.decode(second_encoding) @@ -63,6 +65,7 @@ def decode_output(output: bytes) -> str: def popen( args: Sequence[str], + *, env: Mapping[str, str] | None = None, silent: bool = False, stdout: int | IO[str] | None = None, @@ -71,10 +74,11 @@ def popen( terminate_timeout: float | None = DEFAULT_TERMINATE_TIMEOUT, ) -> tuple[int, str]: if silent and stdout is not None: - raise ValueError( + msg = ( "Can not specify silent and stdout; passing a custom stdout always silences" " the commands output in Nox's log." ) + raise ValueError(msg) if silent: stdout = subprocess.PIPE @@ -82,11 +86,11 @@ def popen( proc = subprocess.Popen(args, env=env, stdout=stdout, stderr=stderr) try: - out, err = proc.communicate() + out, _err = proc.communicate() sys.stdout.flush() except KeyboardInterrupt: - out, err = shutdown_process(proc, interrupt_timeout, terminate_timeout) + out, _err = shutdown_process(proc, interrupt_timeout, terminate_timeout) if proc.returncode != 0: raise diff --git a/nox/project.py b/nox/project.py index 61c63152..d808c9a9 100644 --- a/nox/project.py +++ b/nox/project.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os import re import sys from pathlib import Path @@ -11,6 +10,7 @@ from dependency_groups import resolve if TYPE_CHECKING: + import os from typing import Any if sys.version_info < (3, 11): @@ -19,7 +19,7 @@ import tomllib -__all__ = ["load_toml", "python_versions", "dependency_groups"] +__all__ = ["dependency_groups", "load_toml", "python_versions"] def __dir__() -> list[str]: @@ -71,9 +71,11 @@ def _load_script_block(filepath: Path) -> dict[str, Any]: matches = list(filter(lambda m: m.group("type") == name, REGEX.finditer(script))) if not matches: - raise ValueError(f"No {name} block found in {filepath}") + msg = f"No {name} block found in {filepath}" + raise ValueError(msg) if len(matches) > 1: - raise ValueError(f"Multiple {name} blocks found in {filepath}") + msg = f"Multiple {name} blocks found in {filepath}" + raise ValueError(msg) content = "".join( line[2:] if line.startswith("# ") else line[1:] @@ -113,18 +115,21 @@ def python_versions( ] if from_classifiers: return from_classifiers - raise ValueError('No Python version classifiers found in "project.classifiers"') + msg = 'No Python version classifiers found in "project.classifiers"' + raise ValueError(msg) requires_python_str = pyproject.get("project", {}).get("requires-python", "") if not requires_python_str: - raise ValueError('No "project.requires-python" value set') + msg = 'No "project.requires-python" value set' + raise ValueError(msg) for spec in packaging.specifiers.SpecifierSet(requires_python_str): if spec.operator in {">", ">=", "~="}: min_minor_version = int(spec.version.split(".")[1]) break else: - raise ValueError('No minimum version found in "project.requires-python"') + msg = 'No minimum version found in "project.requires-python"' + raise ValueError(msg) max_minor_version = int(max_version.split(".")[1]) diff --git a/nox/registry.py b/nox/registry.py index 1697121c..8669b541 100644 --- a/nox/registry.py +++ b/nox/registry.py @@ -17,11 +17,14 @@ import collections import copy import functools -from collections.abc import Sequence -from typing import Any, Callable, overload +from typing import TYPE_CHECKING, Any, Callable, overload from ._decorators import Func -from ._typing import Python + +if TYPE_CHECKING: + from collections.abc import Sequence + + from ._typing import Python RawFunc = Callable[..., Any] @@ -29,12 +32,13 @@ @overload -def session_decorator(__func: RawFunc | Func) -> Func: ... +def session_decorator(func: RawFunc | Func, /) -> Func: ... @overload def session_decorator( - __func: None = ..., + func: None = ..., + /, python: Python | None = ..., py: Python | None = ..., reuse_venv: bool | None = ..., @@ -50,6 +54,7 @@ def session_decorator( def session_decorator( func: Callable[..., Any] | Func | None = None, + /, python: Python | None = None, py: Python | None = None, reuse_venv: bool | None = None, @@ -84,10 +89,11 @@ def session_decorator( ) if py is not None and python is not None: - raise ValueError( + msg = ( "The py argument to nox.session is an alias for the python " "argument, please only specify one." ) + raise ValueError(msg) if python is None: python = py diff --git a/nox/sessions.py b/nox/sessions.py index f7819c51..8851c5a5 100644 --- a/nox/sessions.py +++ b/nox/sessions.py @@ -14,7 +14,6 @@ from __future__ import annotations -import argparse import contextlib import enum import hashlib @@ -25,15 +24,6 @@ import subprocess import sys import unicodedata -from collections.abc import ( - Callable, - Generator, - Iterable, - Iterator, - Mapping, - Sequence, -) -from types import TracebackType from typing import ( TYPE_CHECKING, Any, @@ -42,7 +32,6 @@ import nox.command import nox.virtualenv -from nox._decorators import Func from nox.logger import logger from nox.popen import DEFAULT_INTERRUPT_TIMEOUT, DEFAULT_TERMINATE_TIMEOUT from nox.virtualenv import ( @@ -54,8 +43,19 @@ ) if TYPE_CHECKING: + import argparse + from collections.abc import ( + Callable, + Generator, + Iterable, + Iterator, + Mapping, + Sequence, + ) + from types import TracebackType from typing import IO + from nox._decorators import Func from nox.command import ExternalType from nox.manifest import Manifest @@ -88,7 +88,7 @@ def _normalize_path(envdir: str, path: str | bytes) -> str: full_path = os.path.join(envdir, path) if len(full_path) > 100 - len("bin/pythonX.Y"): if len(envdir) < 100 - 9: - path = hashlib.sha1(path.encode("ascii")).hexdigest()[:8] + path = hashlib.sha1(path.encode("ascii")).hexdigest()[:8] # noqa: S324 full_path = os.path.join(envdir, path) logger.warning("The virtualenv name was hashed to avoid being too long.") else: @@ -108,9 +108,8 @@ def _dblquote_pkg_install_args(args: Iterable[str]) -> tuple[str, ...]: def _dblquote_pkg_install_arg(pkg_req_str: str) -> str: # sanity check: we need an even number of double-quotes if pkg_req_str.count('"') % 2 != 0: - raise ValueError( - f"ill-formatted argument with odd number of quotes: {pkg_req_str}" - ) + msg = f"ill-formatted argument with odd number of quotes: {pkg_req_str}" + raise ValueError(msg) if "<" in pkg_req_str or ">" in pkg_req_str: if pkg_req_str[0] == pkg_req_str[-1] == '"': @@ -118,7 +117,8 @@ def _dblquote_pkg_install_arg(pkg_req_str: str) -> str: return pkg_req_str # need to double-quote string if '"' in pkg_req_str: - raise ValueError(f"Cannot escape requirement string: {pkg_req_str}") + msg = f"Cannot escape requirement string: {pkg_req_str}" + raise ValueError(msg) return f'"{pkg_req_str}"' # no dangerous char: no need to double-quote string return pkg_req_str @@ -147,7 +147,7 @@ def __init__(self, dir: str | os.PathLike[str]) -> None: self._prev_working_dir = os.getcwd() os.chdir(dir) - def __enter__(self) -> _WorkingDirContext: + def __enter__(self) -> _WorkingDirContext: # noqa: PYI034 return self def __exit__( @@ -201,7 +201,8 @@ def virtualenv(self) -> ProcessEnv: """The virtualenv that all commands are run in.""" venv = self._runner.venv if venv is None: - raise ValueError("A virtualenv has not been created for this session") + msg = "A virtualenv has not been created for this session" + raise ValueError(msg) return venv @property @@ -227,7 +228,8 @@ def bin(self) -> str: """The first bin directory for the virtualenv.""" paths = self.bin_paths if paths is None: - raise ValueError("The environment does not have a bin directory.") + msg = "The environment does not have a bin directory." + raise ValueError(msg) return paths[0] def create_tmp(self) -> str: @@ -278,6 +280,7 @@ def install_and_run_script( silent=silent, success_codes=success_codes, external=None, + log=log, stdout=stdout, stderr=stderr, interrupt_timeout=interrupt_timeout, @@ -439,7 +442,8 @@ def run( :type stderr: file or file descriptor """ if not args: - raise ValueError("At least one argument required to run().") + msg = "At least one argument required to run()." + raise ValueError(msg) if len(args) == 1 and isinstance(args[0], (list, tuple)): msg = "First argument to `session.run` is a list. Did you mean to use `session.run(*args)`?" @@ -529,7 +533,8 @@ def run_install( return None if not args: - raise ValueError("At least one argument required to run_install().") + msg = "At least one argument required to run_install()" + raise ValueError(msg) return self._run( *args, @@ -696,13 +701,15 @@ def conda_install( if isinstance(venv, CondaEnv): prefix_args = ("--prefix", venv.location) elif not isinstance(venv, PassthroughEnv): - raise ValueError( + msg = ( "A session without a conda environment can not install dependencies" " from conda." ) + raise TypeError(msg) if not args: - raise ValueError("At least one argument required to install().") + msg = "At least one argument required to install()." + raise ValueError(msg) if self._runner.global_config.no_install and ( isinstance(venv, PassthroughEnv) or venv._reused @@ -756,7 +763,7 @@ def install( silent: bool | None = None, success_codes: Iterable[int] | None = None, log: bool = True, - external: ExternalType | None = None, + external: ExternalType | None = None, # noqa: ARG002 stdout: int | IO[str] | None = None, stderr: int | IO[str] | None = subprocess.STDOUT, interrupt_timeout: float | None = DEFAULT_INTERRUPT_TIMEOUT, @@ -803,20 +810,21 @@ def install( if not isinstance( venv, (CondaEnv, VirtualEnv, PassthroughEnv) ): # pragma: no cover - raise ValueError( - "A session without a virtualenv can not install dependencies." - ) + msg = "A session without a virtualenv can not install dependencies." + raise TypeError(msg) if isinstance(venv, PassthroughEnv): if self._runner.global_config.no_install: return - raise ValueError( + msg = ( f"Session {self.name} does not have a virtual environment, so use of" " session.install() is no longer allowed since it would modify the" " global Python environment. If you're really sure that is what you" ' want to do, use session.run("pip", "install", ...) instead.' ) + raise ValueError(msg) if not args: - raise ValueError("At least one argument required to install().") + msg = "At least one argument required to install()." + raise ValueError(msg) if self._runner.global_config.no_install and venv._reused: return @@ -909,6 +917,7 @@ def __init__( func: Func, global_config: argparse.Namespace, manifest: Manifest, + *, multi: bool = False, ) -> None: self.name = name @@ -985,7 +994,8 @@ def get_direct_dependencies( else: yield from map(sessions_by_id.__getitem__, self.func.requires) except KeyError as exc: - raise KeyError(f"Session not found: {exc.args[0]}") from exc + msg = f"Session not found: {exc.args[0]}" + raise KeyError(msg) from exc def _create_venv(self) -> None: reuse_existing = self.reuse_existing_venv() @@ -1135,9 +1145,6 @@ def __init__( def __bool__(self) -> bool: return self.status.value > 0 - def __nonzero__(self) -> bool: - return self.__bool__() - @property def imperfect(self) -> str: """Return the English imperfect tense for the status. diff --git a/nox/tasks.py b/nox/tasks.py index 62bee9b5..99dd3eef 100644 --- a/nox/tasks.py +++ b/nox/tasks.py @@ -19,9 +19,7 @@ import json import os import sys -import types -from argparse import Namespace -from typing import Sequence, TypeVar +from typing import TYPE_CHECKING, Sequence, TypeVar from colorlog.escape_codes import parse_colors @@ -33,6 +31,10 @@ from nox.manifest import WARN_PYTHONS_IGNORED, Manifest from nox.sessions import Result +if TYPE_CHECKING: + import types + from argparse import Namespace + def _load_and_exec_nox_module(global_config: Namespace) -> types.ModuleType: """ @@ -281,7 +283,7 @@ def _produce_listing(manifest: Manifest, global_config: Namespace) -> None: ) -def _produce_json_listing(manifest: Manifest, global_config: Namespace) -> None: +def _produce_json_listing(manifest: Manifest, global_config: Namespace) -> None: # noqa: ARG001 report = [] for session, selected in manifest.list_all_sessions(): if selected: @@ -370,7 +372,8 @@ def run_manifest(manifest: Manifest, global_config: Namespace) -> list[Result]: def print_summary( - results: Sequence_Results_T, global_config: Namespace + results: Sequence_Results_T, + global_config: Namespace, # noqa: ARG001 ) -> Sequence_Results_T: """Print a summary of the results. @@ -416,7 +419,7 @@ def create_report( return results # Write the JSON report. - with open(global_config.report, "w") as report_file: + with open(global_config.report, "w", encoding="utf-8") as report_file: json.dump( { "result": int(all(results)), @@ -430,7 +433,7 @@ def create_report( return results -def final_reduce(results: list[Result], global_config: Namespace) -> int: +def final_reduce(results: list[Result], global_config: Namespace) -> int: # noqa: ARG001 """Reduce the results to a final exit code. Args: diff --git a/nox/tox_to_nox.py b/nox/tox_to_nox.py index 34eeaf9e..a8e183c8 100644 --- a/nox/tox_to_nox.py +++ b/nox/tox_to_nox.py @@ -70,7 +70,7 @@ def fixname(envname: str) -> str: def write_output_to_file(output: str, filename: str) -> None: """Write output to a file.""" - with open(filename, "w") as outfile: + with open(filename, "w", encoding="utf-8") as outfile: outfile.write(output) @@ -81,7 +81,7 @@ def main() -> None: args = parser.parse_args() if TOX4: - output = check_output(["tox", "config"], text=True) + output = check_output(["tox", "config"], text=True) # noqa: S607 original_config = ConfigParser() original_config.read_string(output) config: dict[str, dict[str, Any]] = {} @@ -95,11 +95,11 @@ def main() -> None: set_env = {} for var in section.get("set_env", "").strip().splitlines(): k, v = var.split("=") - if k not in ( + if k not in { "PYTHONHASHSEED", "PIP_DISABLE_PIP_VERSION_CHECK", "PYTHONIOENCODING", - ): + }: set_env[k] = v config[name]["set_env"] = set_env diff --git a/nox/virtualenv.py b/nox/virtualenv.py index 6091e171..2584b90d 100644 --- a/nox/virtualenv.py +++ b/nox/virtualenv.py @@ -24,18 +24,21 @@ import shutil import subprocess import sys -from collections.abc import Callable, Mapping, Sequence from pathlib import Path from socket import gethostbyname -from typing import Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar from packaging import version import nox import nox.command -from nox._typing import Python from nox.logger import logger +if TYPE_CHECKING: + from collections.abc import Callable, Mapping, Sequence + + from nox._typing import Python + # Problematic environment variables that are stripped from all commands inside # of a virtualenv. See https://github.com/theacodes/nox/issues/44 _BLACKLISTED_ENV_VARS = frozenset( @@ -151,7 +154,8 @@ def bin(self) -> str: """The first bin directory for the virtualenv.""" paths = self.bin_paths if paths is None: - raise ValueError("The environment does not have a bin directory.") + msg = "The environment does not have a bin directory." + raise ValueError(msg) return paths[0] @abc.abstractmethod @@ -288,9 +292,9 @@ def __init__( self, location: str, interpreter: str | None = None, + *, reuse_existing: bool = False, venv_params: Sequence[str] = (), - *, conda_cmd: str = "conda", ): self.location_name = location @@ -386,9 +390,9 @@ def is_offline() -> bool: try: # DNS resolution to detect situation (1) or (2). host = gethostbyname("repo.anaconda.com") - return host is None except BaseException: # pragma: no cover return True + return host is None @property def venv_backend(self) -> str: @@ -422,8 +426,8 @@ def __init__( self, location: str, interpreter: str | None = None, - reuse_existing: bool = False, *, + reuse_existing: bool = False, venv_backend: str = "virtualenv", venv_params: Sequence[str] = (), ): @@ -435,7 +439,7 @@ def __init__( self._venv_backend = venv_backend self.venv_params = venv_params or [] if venv_backend not in {"virtualenv", "venv", "uv"}: - msg = f"venv_backend {venv_backend} not recognized" + msg = f"venv_backend {venv_backend!r} not recognized" raise ValueError(msg) super().__init__(env={"VIRTUAL_ENV": self.location, "CONDA_PREFIX": None}) @@ -454,7 +458,7 @@ def _clean_location(self) -> bool: def _read_pyvenv_cfg(self) -> dict[str, str] | None: """Read a pyvenv.cfg file into dict, returns None if missing.""" path = os.path.join(self.location, "pyvenv.cfg") - with contextlib.suppress(FileNotFoundError), open(path) as fp: + with contextlib.suppress(FileNotFoundError), open(path, encoding="utf-8") as fp: parts = (x.partition("=") for x in fp if "=" in x) return {k.strip(): v.strip() for k, _, v in parts} return None diff --git a/nox/workflow.py b/nox/workflow.py index 3b653257..1f313761 100644 --- a/nox/workflow.py +++ b/nox/workflow.py @@ -14,9 +14,11 @@ from __future__ import annotations -import argparse -from collections.abc import Callable, Iterable -from typing import Any +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + import argparse + from collections.abc import Callable, Iterable def execute( @@ -56,8 +58,8 @@ def execute( # and return it. if isinstance(return_value, int): return return_value - - # All tasks completed, presumably without error. - return 0 except KeyboardInterrupt: return 130 # http://tldp.org/LDP/abs/html/exitcodes.html + + # All tasks completed, presumably without error. + return 0 diff --git a/pyproject.toml b/pyproject.toml index db1b664e..f0911e8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,28 +85,50 @@ metadata.allow-ambiguous-features = true # disable normalization (tox-to-nox) fo [tool.ruff] lint.extend-select = [ + # "ANN", # flake8-annotations + "ARG", # flake8-unused-arguments "B", # flake8-bugbear "C4", # flake8-comprehensions + "EM", # flake8-errmsg "EXE", # flake8-executable + "FBT", # flake8-boolean-trap + "FLY", # flynt "FURB", # refurb "I", # isort "ICN", # flake8-import-conventions "ISC", # flake8-implicit-str-concat + "N", # flake8-naming + "PERF", # perflint "PGH", # pygrep-hooks "PIE", # flake8-pie "PL", # pylint - "RET", # flake8-return - "RUF", # Ruff-specific - "SIM", # flake8-simplify - "UP", # pyupgrade - "YTT", # flake8-2020 + "PT", # flake8-pytest-style + # "PTH", # flake8-use-pathlib + "PYI", # flake8-pyi + "RET", # flake8-return + "RUF", # Ruff-specific + "S", # eval -> literal_eval + "SIM", # flake8-simplify + "TCH", # flake8-type-checking + "TRY", # tryceratops + "UP", # pyupgrade + "YTT", # flake8-2020 ] lint.ignore = [ "ISC001", # Conflicts with formatter + "N802", # Function name should be lowercase + "N818", # Error suffix for errors "PLR09", # Too many X "PLR2004", # Magic value used in comparison + "S101", # Use of assert detected + "S603", # subprocess call - check for execution of untrusted input ] +lint.per-file-ignores."tests/*.py" = [ + "FBT001", # Boolean args okay for fixtures +] +lint.per-file-ignores."tests/resources/**.py" = [ "ARG001", "TRY002" ] lint.typing-modules = [ "nox._typing" ] +lint.flake8-unused-arguments.ignore-variadic-names = true [tool.pyproject-fmt] max_supported_python = "3.13" @@ -122,10 +144,10 @@ pythonpath = [ ".github/" ] [tool.coverage] run.branch = true -run.omit = [ "nox/_typing.py" ] run.relative_files = true run.source_pkgs = [ "nox" ] report.exclude_also = [ "def __dir__()", "if TYPE_CHECKING:", "@overload" ] +report.omit = [ "nox/_typing.py" ] [tool.mypy] python_version = "3.8" diff --git a/tests/conftest.py b/tests/conftest.py index 3f566c6f..4bb2f3aa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -43,7 +43,7 @@ def generate_noxfile(**option_mapping: str | bool) -> str: path = Path(RESOURCES) / "noxfile_options.py" text = path.read_text(encoding="utf8") if option_mapping: - for opt, _val in option_mapping.items(): + for opt in option_mapping: # "uncomment" options with values provided text = re.sub(rf"(# )?nox.options.{opt}", f"nox.options.{opt}", text) text = Template(text).safe_substitute(**option_mapping) diff --git a/tests/resources/noxfile_requires.py b/tests/resources/noxfile_requires.py index a6acfadc..f1e94b15 100644 --- a/tests/resources/noxfile_requires.py +++ b/tests/resources/noxfile_requires.py @@ -100,7 +100,8 @@ def q(session): @nox.session def r(session): print(session.name) - raise Exception("Fail!") + msg = "Fail!" + raise Exception(msg) @nox.session(requires=["r"]) diff --git a/tests/test__option_set.py b/tests/test__option_set.py index fea2ff8a..3eb5b15e 100644 --- a/tests/test__option_set.py +++ b/tests/test__option_set.py @@ -82,7 +82,10 @@ def test_parser_groupless_option(self) -> None: _option_set.Option("oh_no_i_have_no_group", group=None, default="meep") ) - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match="Option oh_no_i_have_no_group must either have a group or be hidden", + ): optionset.parser() def test_session_completer(self) -> None: diff --git a/tests/test__parametrize.py b/tests/test__parametrize.py index 4d1fda54..7c73d132 100644 --- a/tests/test__parametrize.py +++ b/tests/test__parametrize.py @@ -23,7 +23,7 @@ @pytest.mark.parametrize( - "param, other, expected", + ("param", "other", "expected"), [ (_parametrize.Param(1, 2), _parametrize.Param(1, 2), True), (_parametrize.Param(1, 2, id="a"), _parametrize.Param(1, 2, id="a"), True), diff --git a/tests/test__version.py b/tests/test__version.py index b6db4dbc..772dd9af 100644 --- a/tests/test__version.py +++ b/tests/test__version.py @@ -14,9 +14,8 @@ from __future__ import annotations -from collections.abc import Callable -from pathlib import Path from textwrap import dedent +from typing import TYPE_CHECKING import pytest @@ -29,6 +28,10 @@ get_nox_version, ) +if TYPE_CHECKING: + from collections.abc import Callable + from pathlib import Path + @pytest.fixture def temp_noxfile(tmp_path: Path) -> Callable[[str], str]: @@ -48,12 +51,12 @@ def test_needs_version_default() -> None: def test_get_nox_version() -> None: """It returns something that looks like a Nox version.""" result = get_nox_version() - year, month, day, *_ = (int(part) for part in result.split(".")) + year, _month, _day, *_ = (int(part) for part in result.split(".")) assert year >= 2020 @pytest.mark.parametrize( - "text,expected", + ("text", "expected"), [ ("", None), ( diff --git a/tests/test_command.py b/tests/test_command.py index e798e934..62c783c1 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -25,15 +25,17 @@ import time from pathlib import Path from textwrap import dedent -from typing import Any +from typing import TYPE_CHECKING, Any from unittest import mock import pytest -from _pytest.compat import LEGACY_PATH import nox.command import nox.popen +if TYPE_CHECKING: + from _pytest.compat import LEGACY_PATH + PYTHON = sys.executable skip_on_windows_primary_console_session = pytest.mark.skipif( @@ -46,7 +48,7 @@ ) -def test_run_defaults(capsys: pytest.CaptureFixture[str]) -> None: +def test_run_defaults() -> None: result = nox.command.run([PYTHON, "-c", "print(123)"]) assert result is True @@ -62,7 +64,7 @@ def test_run_silent(capsys: pytest.CaptureFixture[str]) -> None: @pytest.mark.skipif(shutil.which("git") is None, reason="Needs git") -def test_run_not_in_path(capsys: pytest.CaptureFixture[str]) -> None: +def test_run_not_in_path() -> None: # Paths falls back on the environment PATH if the command is not found. result = nox.command.run(["git", "--version"], paths=["."]) assert result is True @@ -271,9 +273,9 @@ def test_fail_with_silent(capsys: pytest.CaptureFixture[str]) -> None: ], silent=True, ) - out, err = capsys.readouterr() - assert "out" in err - assert "err" in err + _out, err = capsys.readouterr() + assert "out" in err + assert "err" in err @pytest.fixture @@ -282,7 +284,7 @@ def marker(tmp_path: Path) -> Path: return tmp_path / "marker" -def enable_ctrl_c(enabled: bool) -> None: +def enable_ctrl_c(*, enabled: bool) -> None: """Enable keyboard interrupts (CTRL-C) on Windows.""" kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) # type: ignore[attr-defined] @@ -294,7 +296,7 @@ def interrupt_process(proc: subprocess.Popen[Any]) -> None: """Send SIGINT or CTRL_C_EVENT to the process.""" if platform.system() == "Windows": # Disable Ctrl-C so we don't terminate ourselves. - enable_ctrl_c(False) + enable_ctrl_c(enabled=False) # Send the keyboard interrupt to all processes attached to the current # console session. @@ -310,7 +312,7 @@ def command_with_keyboard_interrupt( """Monkeypatch Popen.communicate to raise KeyboardInterrupt.""" if platform.system() == "Windows": # Enable Ctrl-C because the child inherits the setting from us. - enable_ctrl_c(True) + enable_ctrl_c(enabled=True) communicate = subprocess.Popen.communicate @@ -390,8 +392,8 @@ def run_pytest_in_new_console_session(test: str) -> None: """, ], ) +@pytest.mark.usefixtures("command_with_keyboard_interrupt") def test_interrupt_raises( - command_with_keyboard_interrupt: None, program: str, marker: Any, ) -> None: @@ -407,7 +409,7 @@ def test_interrupt_raises_on_windows() -> None: @skip_on_windows_primary_console_session -def test_interrupt_handled(command_with_keyboard_interrupt: None, marker: Any) -> None: +def test_interrupt_handled(command_with_keyboard_interrupt: None, marker: Any) -> None: # noqa: ARG001 """It does not raise if the child handles the keyboard interrupt.""" program = """ import signal @@ -429,7 +431,7 @@ def test_interrupt_handled_on_windows() -> None: def test_custom_stdout(capsys: pytest.CaptureFixture[str], tmpdir: LEGACY_PATH) -> None: - with open(str(tmpdir / "out.txt"), "w+t") as stdout: + with open(str(tmpdir / "out.txt"), "w+") as stdout: nox.command.run( [ PYTHON, @@ -451,10 +453,8 @@ def test_custom_stdout(capsys: pytest.CaptureFixture[str], tmpdir: LEGACY_PATH) assert "err" in tempfile_contents -def test_custom_stdout_silent_flag( - capsys: pytest.CaptureFixture[str], tmpdir: LEGACY_PATH -) -> None: - with open(str(tmpdir / "out.txt"), "w+t") as stdout: # noqa: SIM117 +def test_custom_stdout_silent_flag(tmpdir: LEGACY_PATH) -> None: + with open(str(tmpdir / "out.txt"), "w+") as stdout: # noqa: SIM117 with pytest.raises(ValueError, match="silent"): nox.command.run([PYTHON, "-c", 'print("hi")'], stdout=stdout, silent=True) @@ -462,7 +462,7 @@ def test_custom_stdout_silent_flag( def test_custom_stdout_failed_command( capsys: pytest.CaptureFixture[str], tmpdir: LEGACY_PATH ) -> None: - with open(str(tmpdir / "out.txt"), "w+t") as stdout: + with open(str(tmpdir / "out.txt"), "w+") as stdout: with pytest.raises(nox.command.CommandFailed): nox.command.run( [ @@ -486,7 +486,7 @@ def test_custom_stdout_failed_command( def test_custom_stderr(capsys: pytest.CaptureFixture[str], tmpdir: LEGACY_PATH) -> None: - with open(str(tmpdir / "err.txt"), "w+t") as stderr: + with open(str(tmpdir / "err.txt"), "w+") as stderr: nox.command.run( [ PYTHON, @@ -511,7 +511,7 @@ def test_custom_stderr(capsys: pytest.CaptureFixture[str], tmpdir: LEGACY_PATH) def test_custom_stderr_failed_command( capsys: pytest.CaptureFixture[str], tmpdir: LEGACY_PATH ) -> None: - with open(str(tmpdir / "out.txt"), "w+t") as stderr: + with open(str(tmpdir / "out.txt"), "w+") as stderr: with pytest.raises(nox.command.CommandFailed): nox.command.run( [ diff --git a/tests/test_logger.py b/tests/test_logger.py index 5b774840..a322675e 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -36,12 +36,12 @@ def test_output() -> None: def test_formatter(caplog: pytest.LogCaptureFixture) -> None: caplog.clear() - logger.setup_logging(True, verbose=True) + logger.setup_logging(color=True, verbose=True) with caplog.at_level(logging.DEBUG): logger.logger.info("bar") logger.logger.output("foo") - logs = [rec for rec in caplog.records if rec.levelname in ("INFO", "OUTPUT")] + logs = [rec for rec in caplog.records if rec.levelname in {"INFO", "OUTPUT"}] assert len(logs) == 1 assert not hasattr(logs[0], "asctime") @@ -50,7 +50,7 @@ def test_formatter(caplog: pytest.LogCaptureFixture) -> None: logger.logger.info("bar") logger.logger.output("foo") - logs = [rec for rec in caplog.records if rec.levelname in ("INFO", "OUTPUT")] + logs = [rec for rec in caplog.records if rec.levelname in {"INFO", "OUTPUT"}] assert len(logs) == 2 logs = [rec for rec in caplog.records if rec.levelname == "OUTPUT"] @@ -75,7 +75,7 @@ def test_no_color_timestamp(caplog: pytest.LogCaptureFixture, color: bool) -> No logger.logger.info("bar") logger.logger.output("foo") - logs = [rec for rec in caplog.records if rec.levelname in ("INFO", "OUTPUT")] + logs = [rec for rec in caplog.records if rec.levelname in {"INFO", "OUTPUT"}] assert len(logs) == 1 assert hasattr(logs[0], "asctime") diff --git a/tests/test_main.py b/tests/test_main.py index 6a17fea0..67cfdbbd 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -18,10 +18,9 @@ import os import subprocess import sys -from collections.abc import Callable from importlib import metadata from pathlib import Path -from typing import Any, Literal +from typing import TYPE_CHECKING, Any, Literal from unittest import mock import pytest @@ -31,6 +30,9 @@ import nox.registry import nox.sessions +if TYPE_CHECKING: + from collections.abc import Callable + RESOURCES = os.path.join(os.path.dirname(__file__), "resources") VERSION = metadata.version("nox") @@ -221,7 +223,7 @@ def test_main_explicit_sessions_with_spaces_in_names( @pytest.mark.parametrize( - "var,option,env,values", + ("var", "option", "env", "values"), [ ("NOXSESSION", "sessions", "foo", ["foo"]), ("NOXSESSION", "sessions", "foo,bar", ["foo", "bar"]), @@ -270,7 +272,7 @@ def test_main_list_option_from_nox_env_var( @pytest.mark.parametrize( - "options,env,expected", + ("options", "env", "expected"), [ (["--default-venv-backend", "conda"], "", "conda"), ([], "mamba", "mamba"), @@ -569,15 +571,11 @@ def test_main_requires_bad_python_parametrization( run_nox: Callable[..., tuple[Any, Any, Any]], ) -> None: noxfile = os.path.join(RESOURCES, "noxfile_requires.py") - with pytest.raises( - ValueError, - match="Cannot parametrize requires", - ): - returncode, _, _ = run_nox(f"--noxfile={noxfile}", "--session=q") - assert returncode != 0 + with pytest.raises(ValueError, match="Cannot parametrize requires"): + run_nox(f"--noxfile={noxfile}", "--session=q") -@pytest.mark.parametrize("session", ("s", "t")) +@pytest.mark.parametrize("session", ["s", "t"]) def test_main_requires_chain_fail( run_nox: Callable[..., tuple[Any, Any, Any]], session: str ) -> None: @@ -587,13 +585,13 @@ def test_main_requires_chain_fail( assert "Prerequisite session r was not successful" in stderr -@pytest.mark.parametrize("session", ("w", "u")) +@pytest.mark.parametrize("session", ["w", "u"]) def test_main_requries_modern_param( run_nox: Callable[..., tuple[Any, Any, Any]], session: str, ) -> None: noxfile = os.path.join(RESOURCES, "noxfile_requires.py") - returncode, _, stderr = run_nox(f"--noxfile={noxfile}", f"--session={session}") + returncode, _, _stderr = run_nox(f"--noxfile={noxfile}", f"--session={session}") assert returncode == 0 @@ -849,11 +847,9 @@ def test_main_reuse_existing_virtualenvs_no_install( with mock.patch.object(sys, "exit"): nox.main() config = execute.call_args[1]["global_config"] - assert ( - config.reuse_existing_virtualenvs - and config.no_install - and config.reuse_venv == "yes" - ) + assert config.reuse_existing_virtualenvs + assert config.no_install + assert config.reuse_venv == "yes" @pytest.mark.parametrize( @@ -914,10 +910,10 @@ def test_main_noxfile_options_with_ci_override( "never", ], ) +@pytest.mark.usefixtures("generate_noxfile_options") def test_main_reuse_venv_cli_flags( monkeypatch: pytest.MonkeyPatch, - generate_noxfile_options: Callable[..., str], - reuse_venv: Literal["yes"] | Literal["no"] | Literal["always"] | Literal["never"], + reuse_venv: Literal["yes", "no", "always", "never"], ) -> None: monkeypatch.setattr(sys, "argv", ["nox", "--reuse-venv", reuse_venv]) with mock.patch("nox.workflow.execute", return_value=0) as execute: diff --git a/tests/test_manifest.py b/tests/test_manifest.py index e79a14a3..32d3a7fe 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -17,7 +17,7 @@ import collections import typing from collections.abc import Sequence -from typing import Any +from typing import TYPE_CHECKING, Any from unittest import mock import pytest @@ -33,6 +33,9 @@ _null_session_func, ) +if TYPE_CHECKING: + from collections.abc import Sequence + def create_mock_sessions() -> collections.OrderedDict[str, mock.Mock]: sessions = collections.OrderedDict() @@ -148,7 +151,7 @@ def test_iteration() -> None: # Continuing past the end raises StopIteration. with pytest.raises(StopIteration): - manifest.__next__() + next(manifest) def test_len() -> None: @@ -205,7 +208,7 @@ def test_filter_by_keyword() -> None: @pytest.mark.parametrize( - "tags,session_count", + ("tags", "session_count"), [ (["baz", "qux"], 2), (["baz"], 2), @@ -257,7 +260,7 @@ def session_func() -> None: @pytest.mark.parametrize( - "python,extra_pythons,expected", + ("python", "extra_pythons", "expected"), [ (None, [], [None]), (None, ["3.8"], [None]), @@ -295,7 +298,7 @@ def session_func() -> None: @pytest.mark.parametrize( - "python,force_pythons,expected", + ("python", "force_pythons", "expected"), [ (None, [], [None]), (None, ["3.8"], ["3.8"]), @@ -448,7 +451,7 @@ def test_notify_with_posargs() -> None: cfg = create_mock_config() manifest = Manifest({}, cfg) - session = manifest.make_session("my_session", Func(lambda session: None))[0] + session = manifest.make_session("my_session", Func(lambda _: None))[0] manifest.add_session(session) # delete my_session from the queue @@ -461,7 +464,7 @@ def test_notify_with_posargs() -> None: def test_notify_error() -> None: manifest = Manifest({}, create_mock_config()) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Session does_not_exist not found"): manifest.notify("does_not_exist") diff --git a/tests/test_registry.py b/tests/test_registry.py index 40e14ecd..07779aec 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -14,16 +14,18 @@ from __future__ import annotations -from collections.abc import Generator -from typing import Literal +from typing import TYPE_CHECKING, Literal import pytest import nox from nox import registry +if TYPE_CHECKING: + from collections.abc import Generator -@pytest.fixture + +@pytest.fixture(autouse=True) def cleanup_registry() -> Generator[None, None, None]: """Ensure that the session registry is completely empty before and after each test. @@ -35,7 +37,7 @@ def cleanup_registry() -> Generator[None, None, None]: registry._REGISTRY.clear() -def test_session_decorator(cleanup_registry: None) -> None: +def test_session_decorator() -> None: # Establish that the use of the session decorator will cause the # function to be found in the registry. @registry.session_decorator @@ -48,7 +50,7 @@ def unit_tests(session: nox.Session) -> None: assert unit_tests.python is None -def test_session_decorator_single_python(cleanup_registry: None) -> None: +def test_session_decorator_single_python() -> None: @registry.session_decorator(python="3.6") def unit_tests(session: nox.Session) -> None: pass @@ -56,7 +58,7 @@ def unit_tests(session: nox.Session) -> None: assert unit_tests.python == "3.6" -def test_session_decorator_list_of_pythons(cleanup_registry: None) -> None: +def test_session_decorator_list_of_pythons() -> None: @registry.session_decorator(python=["3.5", "3.6"]) def unit_tests(session: nox.Session) -> None: pass @@ -64,7 +66,7 @@ def unit_tests(session: nox.Session) -> None: assert unit_tests.python == ["3.5", "3.6"] -def test_session_decorator_tags(cleanup_registry: None) -> None: +def test_session_decorator_tags() -> None: @registry.session_decorator(tags=["tag-1", "tag-2"]) def unit_tests(session: nox.Session) -> None: pass @@ -72,7 +74,7 @@ def unit_tests(session: nox.Session) -> None: assert unit_tests.tags == ["tag-1", "tag-2"] -def test_session_decorator_py_alias(cleanup_registry: None) -> None: +def test_session_decorator_py_alias() -> None: @registry.session_decorator(py=["3.5", "3.6"]) def unit_tests(session: nox.Session) -> None: pass @@ -80,7 +82,7 @@ def unit_tests(session: nox.Session) -> None: assert unit_tests.python == ["3.5", "3.6"] -def test_session_decorator_py_alias_error(cleanup_registry: None) -> None: +def test_session_decorator_py_alias_error() -> None: with pytest.raises(ValueError, match="argument"): @registry.session_decorator(python=["3.5", "3.6"], py="2.7") @@ -88,7 +90,7 @@ def unit_tests(session: nox.Session) -> None: pass -def test_session_decorator_reuse(cleanup_registry: None) -> None: +def test_session_decorator_reuse() -> None: @registry.session_decorator(reuse_venv=True) def unit_tests(session: nox.Session) -> None: pass @@ -98,8 +100,7 @@ def unit_tests(session: nox.Session) -> None: @pytest.mark.parametrize("name", ["unit-tests", "unit tests", "the unit tests"]) def test_session_decorator_name( - cleanup_registry: None, - name: Literal["unit-tests"] | Literal["unit tests"] | Literal["the unit tests"], + name: Literal["unit-tests", "unit tests", "the unit tests"], ) -> None: @registry.session_decorator(name=name) def unit_tests(session: nox.Session) -> None: @@ -112,7 +113,7 @@ def unit_tests(session: nox.Session) -> None: assert unit_tests.python is None -def test_get(cleanup_registry: None) -> None: +def test_get() -> None: # Establish that the get method returns a copy of the registry. empty = registry.get() assert empty == registry._REGISTRY diff --git a/tests/test_sessions.py b/tests/test_sessions.py index cd9457da..1ed23528 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -29,7 +29,6 @@ from unittest import mock import pytest -from _pytest.compat import LEGACY_PATH import nox._decorators import nox.command @@ -41,6 +40,9 @@ from nox import _options from nox.logger import logger +if typing.TYPE_CHECKING: + from _pytest.compat import LEGACY_PATH + HAS_CONDA = shutil.which("conda") is not None has_conda = pytest.mark.skipif(not HAS_CONDA, reason="Missing conda command.") @@ -190,7 +192,7 @@ def test_virtualenv_as_none(self) -> None: assert session.venv_backend == "none" def test_interactive(self) -> None: - session, runner = self.make_session_and_runner() + session, _runner = self.make_session_and_runner() with mock.patch("nox.sessions.sys.stdin.isatty") as m_isatty: m_isatty.return_value = True @@ -271,7 +273,8 @@ def test_run_with_func_error(self) -> None: session, _ = self.make_session_and_runner() def raise_value_error() -> NoReturn: - raise ValueError("meep") + msg = "meep" + raise ValueError(msg) with pytest.raises(nox.command.CommandFailed): assert session.run(raise_value_error) # type: ignore[arg-type] @@ -430,12 +433,11 @@ def test_run_external_with_error_on_external_run_condaenv(self) -> None: def test_run_install_bad_args(self) -> None: session, _ = self.make_session_and_runner() - with pytest.raises(ValueError) as exc_info: + with pytest.raises( + ValueError, match="At least one argument required to run_install" + ): session.run_install() - exc_args = exc_info.value.args - assert exc_args == ("At least one argument required to run_install().",) - def test_run_no_install_passthrough(self) -> None: session, runner = self.make_session_and_runner() runner.venv = nox.virtualenv.PassthroughEnv() @@ -445,9 +447,9 @@ def test_run_no_install_passthrough(self) -> None: session.conda_install("numpy") def test_run_no_conda_install(self) -> None: - session, runner = self.make_session_and_runner() + session, _runner = self.make_session_and_runner() - with pytest.raises(ValueError, match="A session without a conda"): + with pytest.raises(TypeError, match="A session without a conda"): session.conda_install("numpy") def test_run_install_success(self) -> None: @@ -455,7 +457,7 @@ def test_run_install_success(self) -> None: assert session.run_install(operator.add, 1300, 37) == 1337 # type: ignore[arg-type] - def test_run_install_install_only(self, caplog: pytest.LogCaptureFixture) -> None: + def test_run_install_install_only(self) -> None: session, runner = self.make_session_and_runner() runner.global_config.install_only = True @@ -479,8 +481,8 @@ def test_run_shutdown_process_timeouts( self, interrupt_timeout_setting: Literal["default"] | int | None, terminate_timeout_setting: Literal["default"] | int | None, - interrupt_timeout_expected: float | int | None, - terminate_timeout_expected: float | int | None, + interrupt_timeout_expected: float | None, + terminate_timeout_expected: float | None, ) -> None: session, runner = self.make_session_and_runner() @@ -567,7 +569,7 @@ def test_conda_install_not_a_condaenv(self) -> None: runner.venv = None - with pytest.raises(ValueError, match="conda environment"): + with pytest.raises(TypeError, match="conda environment"): session.conda_install() @pytest.mark.parametrize( @@ -712,7 +714,7 @@ def test_install_not_a_virtualenv(self) -> None: runner.venv = None - with pytest.raises(ValueError, match="virtualenv"): + with pytest.raises(TypeError, match="virtualenv"): session.install() def test_install(self) -> None: @@ -815,7 +817,7 @@ def test_notify(self) -> None: runner.manifest.notify.assert_called_with("other", ["--an-arg"]) # type: ignore[attr-defined] def test_posargs_are_not_shared_between_sessions( - self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + self, monkeypatch: pytest.MonkeyPatch ) -> None: registry: dict[str, nox._decorators.Func] = {} monkeypatch.setattr("nox.registry._REGISTRY", registry) @@ -827,7 +829,8 @@ def test(session: nox.Session) -> None: @nox.session(venv_backend="none") def lint(session: nox.Session) -> None: if "-x" in session.posargs: - raise RuntimeError("invalid option: -x") + msg = "invalid option: -x" + raise RuntimeError(msg) config = _options.options.namespace(posargs=[], envdir=".nox") manifest = nox.manifest.Manifest(registry, config) @@ -948,7 +951,7 @@ class SessionNoSlots(nox.sessions.Session): session = SessionNoSlots(runner=runner) monkeypatch.setattr(nox.virtualenv, "UV", "/some/uv") - monkeypatch.setattr(shutil, "which", lambda x, path=None: None) + monkeypatch.setattr(shutil, "which", lambda x, path=None: None) # noqa: ARG005 with mock.patch.object(nox.command, "run", autospec=True) as run: session.install("requests", "urllib3", silent=False) @@ -996,7 +999,9 @@ def test___dict__(self) -> None: def test_first_arg_list(self) -> None: session, _ = self.make_session_and_runner() - with pytest.raises(ValueError): + with pytest.raises( + ValueError, match="First argument to `session.run` is a list. Did you mean" + ): session.run(["ls", "-al"]) # type: ignore[arg-type] @@ -1088,7 +1093,7 @@ def test__create_venv(self, create: mock.Mock) -> None: assert runner.venv.reuse_existing is False @pytest.mark.parametrize( - "create_method,venv_backend,expected_backend", + ("create_method", "venv_backend", "expected_backend"), [ ("nox.virtualenv.VirtualEnv.create", None, nox.virtualenv.VirtualEnv), ( @@ -1109,8 +1114,7 @@ def test__create_venv_options( self, create_method: str, venv_backend: None | str, - expected_backend: type[nox.virtualenv.VirtualEnv] - | type[nox.virtualenv.CondaEnv], + expected_backend: type[nox.virtualenv.VirtualEnv | nox.virtualenv.CondaEnv], ) -> None: runner = self.make_runner() runner.func.python = "coolpython" @@ -1171,7 +1175,10 @@ def test_invalid_fallback_venv( ) with mock.patch( "nox.virtualenv.VirtualEnv.create", autospec=True - ), pytest.raises(ValueError): + ), pytest.raises( + ValueError, + match="No backends present|Only optional backends|Expected venv_backend", + ): runner._create_venv() @pytest.mark.parametrize( @@ -1307,7 +1314,7 @@ def test_execute_error_missing_interpreter(self) -> None: def test_execute_failed(self) -> None: runner = self.make_runner_with_mock_venv() - def func(session: nox.Session) -> None: + def func(session: nox.Session) -> None: # noqa: ARG001 raise nox.command.CommandFailed() func.requires = [] # type: ignore[attr-defined] @@ -1320,7 +1327,7 @@ def func(session: nox.Session) -> None: def test_execute_interrupted(self) -> None: runner = self.make_runner_with_mock_venv() - def func(session: nox.Session) -> None: + def func(session: nox.Session) -> None: # noqa: ARG001 raise KeyboardInterrupt() func.requires = [] # type: ignore[attr-defined] @@ -1332,8 +1339,9 @@ def func(session: nox.Session) -> None: def test_execute_exception(self) -> None: runner = self.make_runner_with_mock_venv() - def func(session: nox.Session) -> None: - raise ValueError("meep") + def func(session: nox.Session) -> None: # noqa: ARG001 + msg = "meep" + raise ValueError(msg) func.requires = [] # type: ignore[attr-defined] runner.func = func # type: ignore[assignment] @@ -1375,8 +1383,6 @@ def test__bool_true(self) -> None: session=typing.cast(nox.sessions.SessionRunner, object()), status=status ) assert bool(result) - assert result.__bool__() - assert result.__nonzero__() def test__bool_false(self) -> None: for status in (nox.sessions.Status.FAILED, nox.sessions.Status.ABORTED): @@ -1384,8 +1390,6 @@ def test__bool_false(self) -> None: session=typing.cast(nox.sessions.SessionRunner, object()), status=status ) assert not bool(result) - assert not result.__bool__() - assert not result.__nonzero__() def test__imperfect(self) -> None: result = nox.sessions.Result( diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 67457fde..28f8325e 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -21,8 +21,6 @@ import os import platform import typing -from collections.abc import Callable, Generator -from pathlib import Path from textwrap import dedent from types import ModuleType from unittest import mock @@ -34,6 +32,10 @@ from nox import _options, sessions, tasks from nox.manifest import WARN_PYTHONS_IGNORED, Manifest +if typing.TYPE_CHECKING: + from collections.abc import Callable, Generator + from pathlib import Path + RESOURCES = os.path.join(os.path.dirname(__file__), "resources") @@ -124,7 +126,7 @@ def test_load_nox_module_os_error(caplog: pytest.LogCaptureFixture) -> None: assert f"Failed to load Noxfile {noxfile}" in caplog.text -@pytest.fixture +@pytest.fixture(autouse=True) def reset_needs_version() -> Generator[None, None, None]: """Do not leak ``nox.needs_version`` between tests.""" try: @@ -138,9 +140,7 @@ def reset_global_nox_options() -> None: nox.options = _options.options.noxfile_namespace() -def test_load_nox_module_needs_version_static( - reset_needs_version: None, tmp_path: Path -) -> None: +def test_load_nox_module_needs_version_static(tmp_path: Path) -> None: text = dedent( """ import nox @@ -153,9 +153,7 @@ def test_load_nox_module_needs_version_static( assert tasks.load_nox_module(config) == 2 -def test_load_nox_module_needs_version_dynamic( - reset_needs_version: None, tmp_path: Path -) -> None: +def test_load_nox_module_needs_version_dynamic(tmp_path: Path) -> None: text = dedent( """ import nox @@ -283,7 +281,7 @@ def test_filter_manifest_keywords_syntax_error() -> None: @pytest.mark.parametrize( - "tags,session_count", + ("tags", "session_count"), [ (None, 8), (["foo"], 7), @@ -297,11 +295,7 @@ def test_filter_manifest_keywords_syntax_error() -> None: ) def test_filter_manifest_tags( tags: None | builtins.list[builtins.str], - session_count: builtins.int - | builtins.int - | builtins.int - | builtins.int - | builtins.int, + session_count: builtins.int, ) -> None: @nox.session(tags=["foo"]) def qux() -> None: @@ -370,9 +364,8 @@ def quux() -> None: assert "Tag selection caused no sessions to be selected." in caplog.text -def test_merge_sessions_and_tags( - reset_global_nox_options: None, generate_noxfile_options: Callable[..., str] -) -> None: +@pytest.mark.usefixtures("reset_global_nox_options") +def test_merge_sessions_and_tags(generate_noxfile_options: Callable[..., str]) -> None: @nox.session(tags=["foobar"]) def test() -> None: pass @@ -441,7 +434,7 @@ def test_honor_list_request_noop() -> None: @pytest.mark.parametrize( - "description, module_docstring", + ("description", "module_docstring"), [ (None, None), (None, "hello docstring"), @@ -595,9 +588,7 @@ def test_empty_session_list_in_noxfile( assert "No sessions selected." in capsys.readouterr().out -def test_empty_session_None_in_noxfile( - capsys: pytest.CaptureFixture[builtins.str], -) -> None: +def test_empty_session_None_in_noxfile() -> None: config = _options.options.namespace(noxfile="noxfile.py", sessions=None, posargs=[]) manifest = Manifest({"session": session_func}, config) return_value = tasks.filter_manifest(manifest, global_config=config) @@ -763,13 +754,13 @@ def test_create_report() -> None: mock.ANY, indent=2, ) - open_.assert_called_once_with("/path/to/report", "w") + open_.assert_called_once_with("/path/to/report", "w", encoding="utf-8") def test_final_reduce() -> None: config = argparse.Namespace() - true = typing.cast(sessions.Result, True) - false = typing.cast(sessions.Result, False) + true = typing.cast(sessions.Result, True) # noqa: FBT003 + false = typing.cast(sessions.Result, False) # noqa: FBT003 assert tasks.final_reduce([true, true], config) == 0 assert tasks.final_reduce([true, false], config) == 1 assert tasks.final_reduce([], config) == 0 diff --git a/tests/test_tox_to_nox.py b/tests/test_tox_to_nox.py index c5110b2b..048b5d57 100644 --- a/tests/test_tox_to_nox.py +++ b/tests/test_tox_to_nox.py @@ -16,11 +16,14 @@ import sys import textwrap -from collections.abc import Callable -from typing import Any +from typing import TYPE_CHECKING, Any import pytest -from _pytest.compat import LEGACY_PATH + +if TYPE_CHECKING: + from collections.abc import Callable + + from _pytest.compat import LEGACY_PATH tox_to_nox = pytest.importorskip("nox.tox_to_nox") diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index 7c8e0204..04282fda 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -21,20 +21,24 @@ import subprocess import sys import types -from collections.abc import Callable from importlib import metadata from pathlib import Path from textwrap import dedent -from typing import Any, NamedTuple, NoReturn +from typing import TYPE_CHECKING, Any, NamedTuple, NoReturn from unittest import mock import pytest -from _pytest.compat import LEGACY_PATH from packaging import version import nox.command import nox.virtualenv -from nox.virtualenv import CondaEnv, ProcessEnv, VirtualEnv + +if TYPE_CHECKING: + from collections.abc import Callable + + from _pytest.compat import LEGACY_PATH + + from nox.virtualenv import CondaEnv, ProcessEnv, VirtualEnv IS_WINDOWS = nox.virtualenv._SYSTEM == "Windows" HAS_CONDA = shutil.which("conda") is not None @@ -107,7 +111,7 @@ def patcher( Use the global ``RAISE_ERROR`` to have ``sysexec`` fail. """ - def special_which(name: str, path: Any = None) -> str | None: + def special_which(name: str, path: Any = None) -> str | None: # noqa: ARG001 if sysfind_result is None: return None if name.lower() in only_find: @@ -116,7 +120,7 @@ def special_which(name: str, path: Any = None) -> str | None: monkeypatch.setattr(shutil, "which", special_which) - def special_run(cmd: Any, *args: Any, **kwargs: Any) -> TextProcessResult: + def special_run(cmd: Any, *args: Any, **kwargs: Any) -> TextProcessResult: # noqa: ARG001 return TextProcessResult(sysexec_result) monkeypatch.setattr(subprocess, "run", special_run) @@ -149,7 +153,7 @@ def test_invalid_venv_create( ..., tuple[nox.virtualenv.VirtualEnv | nox.virtualenv.ProcessEnv, LEGACY_PATH] ], ) -> None: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="venv_backend 'invalid' not recognized"): make_one(venv_backend="invalid") @@ -234,18 +238,18 @@ def test_condaenv_create_interpreter( def test_conda_env_create_verbose( make_conda: Callable[..., tuple[CondaEnv, Any]], ) -> None: - venv, dir_ = make_conda() + venv, _dir = make_conda() with mock.patch("nox.virtualenv.nox.command.run") as mock_run: venv.create() - args, kwargs = mock_run.call_args + _args, kwargs = mock_run.call_args assert kwargs["log"] is False nox.options.verbose = True with mock.patch("nox.virtualenv.nox.command.run") as mock_run: venv.create() - args, kwargs = mock_run.call_args + _args, kwargs = mock_run.call_args assert kwargs["log"] @@ -264,7 +268,7 @@ def test_condaenv_bin_windows(make_conda: Callable[..., tuple[CondaEnv, Any]]) - @has_conda def test_condaenv_(make_conda: Callable[..., tuple[CondaEnv, Any]]) -> None: - venv, dir_ = make_conda() + venv, _dir = make_conda() assert not venv.is_offline() @@ -462,7 +466,7 @@ def test_create( def test_create_reuse_environment( make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Any]], ) -> None: - venv, location = make_one(reuse_existing=True) + venv, _location = make_one(reuse_existing=True) venv.create() reused = not venv.create() @@ -522,10 +526,10 @@ def test_not_stale_virtualenv_environment( # Making the reuse requirement more strict monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1") - venv, location = make_one(reuse_existing=True, venv_backend="virtualenv") + venv, _location = make_one(reuse_existing=True, venv_backend="virtualenv") venv.create() - venv, location = make_one(reuse_existing=True, venv_backend="virtualenv") + venv, _location = make_one(reuse_existing=True, venv_backend="virtualenv") reused = not venv.create() assert reused @@ -535,10 +539,10 @@ def test_not_stale_virtualenv_environment( def test_stale_virtualenv_to_conda_environment( make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Any]], ) -> None: - venv, location = make_one(reuse_existing=True, venv_backend="virtualenv") + venv, _location = make_one(reuse_existing=True, venv_backend="virtualenv") venv.create() - venv, location = make_one(reuse_existing=True, venv_backend="conda") + venv, _location = make_one(reuse_existing=True, venv_backend="conda") reused = not venv.create() # The environment is not reused because it is now conda style @@ -696,7 +700,7 @@ def test_create_reuse_uv_environment( @pytest.mark.parametrize( - ["which_result", "find_uv_bin_result", "found", "path"], + ("which_result", "find_uv_bin_result", "found", "path"), [ ("/usr/bin/uv", UV_IN_PIPX_VENV, True, UV_IN_PIPX_VENV), ("/usr/bin/uv", None, True, "uv"), @@ -726,7 +730,7 @@ def find_uv_bin() -> str: @pytest.mark.parametrize( - ["return_code", "stdout", "expected_result"], + ("return_code", "stdout", "expected_result"), [ (0, '{"version": "0.2.3", "commit_info": null}', "0.2.3"), (1, None, "0.0"), @@ -759,7 +763,7 @@ def mock_exception(*args: object, **kwargs: object) -> NoReturn: @pytest.mark.parametrize( - ["requested_python", "expected_result"], + ("requested_python", "expected_result"), [ ("3.11", True), ("pypy3.8", True), @@ -849,7 +853,7 @@ def test_inner_functions_reusing_venv( def test_create_reuse_python2_environment( make_one: Callable[..., tuple[VirtualEnv, Any]], ) -> None: - venv, location = make_one(reuse_existing=True, interpreter="2.7") + venv, _location = make_one(reuse_existing=True, interpreter="2.7") try: venv.create() @@ -864,7 +868,7 @@ def test_create_reuse_python2_environment( def test_create_venv_backend( make_one: Callable[..., tuple[VirtualEnv, Any]], ) -> None: - venv, dir_ = make_one(venv_backend="venv") + venv, _dir = make_one(venv_backend="venv") venv.create() @@ -888,7 +892,7 @@ def test__resolved_interpreter_none( @pytest.mark.parametrize( - ["input_", "expected"], + ("input_", "expected"), [ ("3", "python3"), ("3.6", "python3.6"), @@ -967,7 +971,7 @@ def test__resolved_interpreter_windows_full_path( @pytest.mark.parametrize( - ["input_", "expected"], + ("input_", "expected"), [ ("3.7", r"c:\python37-x64\python.exe"), ("python3.6", r"c:\python36-x64\python.exe"), @@ -1021,7 +1025,7 @@ def test__resolved_interpreter_windows_pyexe_fails( # Trick the nox.virtualenv._SYSTEM into thinking that it cannot find python3.6 # (it likely will on Unix). Also, when the nox.virtualenv._SYSTEM looks for the # py launcher, give it a dummy that fails. - def special_run(cmd: str, *args: str, **kwargs: object) -> TextProcessResult: + def special_run(cmd: str, *args: str, **kwargs: object) -> TextProcessResult: # noqa: ARG001 return TextProcessResult("", 1) run.side_effect = special_run @@ -1105,7 +1109,7 @@ def test__resolved_interpreter_not_found( @mock.patch("nox.virtualenv._SYSTEM", new="Windows") -@mock.patch("nox.virtualenv.locate_via_py", new=lambda _: None) # type: ignore[misc] +@mock.patch("nox.virtualenv.locate_via_py", new=lambda _: None) # type: ignore[misc] # noqa: PT008 def test__resolved_interpreter_nonstandard( make_one: Callable[..., tuple[VirtualEnv, Any]], ) -> None: