diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a940ca8..ce78361a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,8 @@ repos: rev: v1.13.0 hooks: - id: mypy - files: ^nox/ + files: ^(nox/|tests/) + exclude: ^tests/resources/ args: [] additional_dependencies: - argcomplete diff --git a/nox/command.py b/nox/command.py index ce8f0271..11333479 100644 --- a/nox/command.py +++ b/nox/command.py @@ -20,7 +20,7 @@ import subprocess import sys from collections.abc import Iterable, Mapping, Sequence -from typing import Literal +from typing import Literal, overload from nox.logger import logger from nox.popen import DEFAULT_INTERRUPT_TIMEOUT, DEFAULT_TERMINATE_TIMEOUT, popen @@ -73,6 +73,57 @@ def _shlex_join(args: Sequence[str | os.PathLike[str]]) -> str: return " ".join(shlex.quote(os.fspath(arg)) for arg in args) +@overload +def run( + args: Sequence[str | os.PathLike[str]], + *, + env: Mapping[str, str | None] | None = ..., + silent: Literal[True], + paths: Sequence[str] | None = ..., + success_codes: Iterable[int] | None = ..., + log: bool = ..., + external: ExternalType = ..., + stdout: int | IO[str] | None = ..., + stderr: int | IO[str] | None = ..., + interrupt_timeout: float | None = ..., + terminate_timeout: float | None = ..., +) -> str: ... + + +@overload +def run( + args: Sequence[str | os.PathLike[str]], + *, + env: Mapping[str, str | None] | None = ..., + silent: Literal[False] = ..., + paths: Sequence[str] | None = ..., + success_codes: Iterable[int] | None = ..., + log: bool = ..., + external: ExternalType = ..., + stdout: int | IO[str] | None = ..., + stderr: int | IO[str] | None = ..., + interrupt_timeout: float | None = ..., + terminate_timeout: float | None = ..., +) -> bool: ... + + +@overload +def run( + args: Sequence[str | os.PathLike[str]], + *, + env: Mapping[str, str | None] | None = ..., + silent: bool, + paths: Sequence[str] | None = ..., + success_codes: Iterable[int] | None = ..., + log: bool = ..., + external: ExternalType = ..., + stdout: int | IO[str] | None = ..., + stderr: int | IO[str] | None = ..., + interrupt_timeout: float | None = ..., + terminate_timeout: float | None = ..., +) -> str | bool: ... + + def run( args: Sequence[str | os.PathLike[str]], *, diff --git a/nox/manifest.py b/nox/manifest.py index 0fb8ea37..1bb6e009 100644 --- a/nox/manifest.py +++ b/nox/manifest.py @@ -419,8 +419,8 @@ class KeywordLocals(Mapping[str, bool]): returns False. """ - def __init__(self, keywords: set[str]) -> None: - self._keywords = keywords + def __init__(self, keywords: Iterable[str]) -> None: + self._keywords = frozenset(keywords) def __getitem__(self, variable_name: str) -> bool: return any(variable_name in keyword for keyword in self._keywords) diff --git a/nox/registry.py b/nox/registry.py index db3db9fb..1697121c 100644 --- a/nox/registry.py +++ b/nox/registry.py @@ -18,18 +18,18 @@ import copy import functools from collections.abc import Sequence -from typing import Any, Callable, TypeVar, overload +from typing import Any, Callable, overload from ._decorators import Func from ._typing import Python -F = TypeVar("F", bound=Callable[..., Any]) +RawFunc = Callable[..., Any] _REGISTRY: collections.OrderedDict[str, Func] = collections.OrderedDict() @overload -def session_decorator(__func: F) -> F: ... +def session_decorator(__func: RawFunc | Func) -> Func: ... @overload @@ -45,11 +45,11 @@ def session_decorator( *, default: bool = ..., requires: Sequence[str] | None = ..., -) -> Callable[[F], F]: ... +) -> Callable[[RawFunc | Func], Func]: ... def session_decorator( - func: F | None = None, + func: Callable[..., Any] | Func | None = None, python: Python | None = None, py: Python | None = None, reuse_venv: bool | None = None, @@ -60,7 +60,7 @@ def session_decorator( *, default: bool = True, requires: Sequence[str] | None = None, -) -> F | Callable[[F], F]: +) -> Func | Callable[[RawFunc | Func], Func]: """Designate the decorated function as a session.""" # If `func` is provided, then this is the decorator call with the function # being sent as part of the Python syntax (`@nox.session`). diff --git a/nox/virtualenv.py b/nox/virtualenv.py index 8b9525c6..6091e171 100644 --- a/nox/virtualenv.py +++ b/nox/virtualenv.py @@ -121,9 +121,11 @@ class ProcessEnv(abc.ABC): allowed_globals: ClassVar[tuple[Any, ...]] = () def __init__( - self, bin_paths: None = None, env: Mapping[str, str | None] | None = None + self, + bin_paths: Sequence[str] | None = None, + env: Mapping[str, str | None] | None = None, ) -> None: - self._bin_paths = bin_paths + self._bin_paths = None if bin_paths is None else list(bin_paths) self._reused = False # Filter envs now so `.env` is dict[str, str] (easier to use) diff --git a/pyproject.toml b/pyproject.toml index bbacf79f..db1b664e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,7 +112,7 @@ lint.typing-modules = [ "nox._typing" ] max_supported_python = "3.13" [tool.pytest.ini_options] -minversion = "6.0" +minversion = "7.0" addopts = [ "-ra", "--strict-markers", "--strict-config" ] xfail_strict = true filterwarnings = [ "error" ] @@ -128,7 +128,6 @@ run.source_pkgs = [ "nox" ] report.exclude_also = [ "def __dir__()", "if TYPE_CHECKING:", "@overload" ] [tool.mypy] -files = [ "nox/**/*.py", "noxfile.py" ] python_version = "3.8" strict = true warn_unreachable = true diff --git a/tests/test__option_set.py b/tests/test__option_set.py index 73f7affb..fea2ff8a 100644 --- a/tests/test__option_set.py +++ b/tests/test__option_set.py @@ -28,7 +28,7 @@ class TestOptionSet: - def test_namespace(self): + def test_namespace(self) -> None: optionset = _option_set.OptionSet() optionset.add_groups(_option_set.OptionGroup("group_a")) optionset.add_options( @@ -43,7 +43,7 @@ def test_namespace(self): assert not hasattr(namespace, "non_existent_option") assert namespace.option_a == "meep" - def test_namespace_values(self): + def test_namespace_values(self) -> None: optionset = _option_set.OptionSet() optionset.add_groups(_option_set.OptionGroup("group_a")) optionset.add_options( @@ -56,13 +56,13 @@ def test_namespace_values(self): assert namespace.option_a == "moop" - def test_namespace_non_existent_options_with_values(self): + def test_namespace_non_existent_options_with_values(self) -> None: optionset = _option_set.OptionSet() with pytest.raises(KeyError): optionset.namespace(non_existent_option="meep") - def test_parser_hidden_option(self): + def test_parser_hidden_option(self) -> None: optionset = _option_set.OptionSet() optionset.add_options( _option_set.Option( @@ -76,7 +76,7 @@ def test_parser_hidden_option(self): assert namespace.oh_boy_i_am_hidden == "meep" - def test_parser_groupless_option(self): + def test_parser_groupless_option(self) -> None: optionset = _option_set.OptionSet() optionset.add_options( _option_set.Option("oh_no_i_have_no_group", group=None, default="meep") @@ -85,46 +85,46 @@ def test_parser_groupless_option(self): with pytest.raises(ValueError): optionset.parser() - def test_session_completer(self): + def test_session_completer(self) -> None: parsed_args = _options.options.namespace( posargs=[], noxfile=str(RESOURCES.joinpath("noxfile_multiple_sessions.py")), ) actual_sessions_from_file = _options._session_completer( - prefix=None, parsed_args=parsed_args + prefix="", parsed_args=parsed_args ) expected_sessions = ["testytest", "lintylint", "typeytype"] assert expected_sessions == list(actual_sessions_from_file) - def test_session_completer_invalid_sessions(self): + def test_session_completer_invalid_sessions(self) -> None: parsed_args = _options.options.namespace( sessions=("baz",), keywords=(), posargs=[] ) all_nox_sessions = _options._session_completer( - prefix=None, parsed_args=parsed_args + prefix="", parsed_args=parsed_args ) - assert len(all_nox_sessions) == 0 + assert len(list(all_nox_sessions)) == 0 - def test_python_completer(self): + def test_python_completer(self) -> None: parsed_args = _options.options.namespace( posargs=[], noxfile=str(RESOURCES.joinpath("noxfile_pythons.py")), ) actual_pythons_from_file = _options._python_completer( - prefix=None, parsed_args=parsed_args + prefix="", parsed_args=parsed_args ) expected_pythons = {"3.6", "3.12"} assert expected_pythons == set(actual_pythons_from_file) - def test_tag_completer(self): + def test_tag_completer(self) -> None: parsed_args = _options.options.namespace( posargs=[], noxfile=str(RESOURCES.joinpath("noxfile_tags.py")), ) actual_tags_from_file = _options._tag_completer( - prefix=None, parsed_args=parsed_args + prefix="", parsed_args=parsed_args ) expected_tags = {f"tag{n}" for n in range(1, 8)} diff --git a/tests/test__parametrize.py b/tests/test__parametrize.py index d5d0fa40..4d1fda54 100644 --- a/tests/test__parametrize.py +++ b/tests/test__parametrize.py @@ -18,6 +18,7 @@ import pytest +import nox from nox import _decorators, _parametrize, parametrize, session @@ -30,94 +31,99 @@ (_parametrize.Param(1, 2, arg_names=("a", "b")), {"a": 1, "b": 2}, True), ], ) -def test_param_eq(param, other, expected): +def test_param_eq( + param: _parametrize.Param, + other: _parametrize.Param | dict[str, int], + expected: bool, +) -> None: assert (param == other) is expected -def test_param_eq_fail(): +def test_param_eq_fail() -> None: assert _parametrize.Param() != "a" -def test_parametrize_decorator_one(): - def f(): +def test_parametrize_decorator_one() -> None: + def f() -> None: pass - _parametrize.parametrize_decorator("abc", 1)(f) + # A raw int is supported, but not part of the typed API. + _parametrize.parametrize_decorator("abc", 1)(f) # type: ignore[arg-type] - assert f.parametrize == [_parametrize.Param(1, arg_names=("abc",))] + assert f.parametrize == [_parametrize.Param(1, arg_names=("abc",))] # type: ignore[attr-defined] -def test_parametrize_decorator_one_param(): - def f(): +def test_parametrize_decorator_one_param() -> None: + def f() -> None: pass _parametrize.parametrize_decorator("abc", _parametrize.Param(1))(f) - assert f.parametrize == [_parametrize.Param(1, arg_names=("abc",))] + assert f.parametrize == [_parametrize.Param(1, arg_names=("abc",))] # type: ignore[attr-defined] -def test_parametrize_decorator_one_with_args(): - def f(): +def test_parametrize_decorator_one_with_args() -> None: + def f() -> None: pass _parametrize.parametrize_decorator("abc", [1, 2, 3])(f) - assert f.parametrize == [{"abc": 1}, {"abc": 2}, {"abc": 3}] + assert f.parametrize == [{"abc": 1}, {"abc": 2}, {"abc": 3}] # type: ignore[attr-defined] -def test_parametrize_decorator_param(): - def f(): +def test_parametrize_decorator_param() -> None: + def f() -> None: pass _parametrize.parametrize_decorator(["abc", "def"], _parametrize.Param(1))(f) - assert f.parametrize == [_parametrize.Param(1, arg_names=("abc", "def"))] + assert f.parametrize == [_parametrize.Param(1, arg_names=("abc", "def"))] # type: ignore[attr-defined] -def test_parametrize_decorator_id_list(): - def f(): +def test_parametrize_decorator_id_list() -> None: + def f() -> None: pass _parametrize.parametrize_decorator("abc", [1, 2, 3], ids=["a", "b", "c"])(f) arg_names = ("abc",) - assert f.parametrize == [ + assert f.parametrize == [ # type: ignore[attr-defined] _parametrize.Param(1, arg_names=arg_names, id="a"), _parametrize.Param(2, arg_names=arg_names, id="b"), _parametrize.Param(3, arg_names=arg_names, id="c"), ] -def test_parametrize_decorator_multiple_args_as_list(): - def f(): +def test_parametrize_decorator_multiple_args_as_list() -> None: + def f() -> None: pass _parametrize.parametrize_decorator(["abc", "def"], [("a", 1), ("b", 2), ("c", 3)])( f ) - assert f.parametrize == [ + assert f.parametrize == [ # type: ignore[attr-defined] {"abc": "a", "def": 1}, {"abc": "b", "def": 2}, {"abc": "c", "def": 3}, ] -def test_parametrize_decorator_multiple_args_as_string(): - def f(): +def test_parametrize_decorator_multiple_args_as_string() -> None: + def f() -> None: pass _parametrize.parametrize_decorator("abc, def", [("a", 1), ("b", 2), ("c", 3)])(f) - assert f.parametrize == [ + assert f.parametrize == [ # type: ignore[attr-defined] {"abc": "a", "def": 1}, {"abc": "b", "def": 2}, {"abc": "c", "def": 3}, ] -def test_parametrize_decorator_mixed_params(): - def f(): +def test_parametrize_decorator_mixed_params() -> None: + def f() -> None: pass _parametrize.parametrize_decorator( @@ -125,21 +131,21 @@ def f(): )(f) arg_names = ("abc", "def") - assert f.parametrize == [ + assert f.parametrize == [ # type: ignore[attr-defined] _parametrize.Param(1, 2, arg_names=arg_names), _parametrize.Param(3, 4, arg_names=arg_names, id="b"), _parametrize.Param(5, 6, arg_names=arg_names), ] -def test_parametrize_decorator_stack(): - def f(): +def test_parametrize_decorator_stack() -> None: + def f() -> None: pass _parametrize.parametrize_decorator("abc", [1, 2, 3])(f) _parametrize.parametrize_decorator("def", ["a", "b"])(f) - assert f.parametrize == [ + assert f.parametrize == [ # type: ignore[attr-defined] {"abc": 1, "def": "a"}, {"abc": 2, "def": "a"}, {"abc": 3, "def": "a"}, @@ -149,14 +155,14 @@ def f(): ] -def test_parametrize_decorator_multiple_and_stack(): - def f(): +def test_parametrize_decorator_multiple_and_stack() -> None: + def f() -> None: pass _parametrize.parametrize_decorator("abc, def", [(1, "a"), (2, "b")])(f) _parametrize.parametrize_decorator("foo", ["bar", "baz"])(f) - assert f.parametrize == [ + assert f.parametrize == [ # type: ignore[attr-defined] {"abc": 1, "def": "a", "foo": "bar"}, {"abc": 2, "def": "b", "foo": "bar"}, {"abc": 1, "def": "a", "foo": "baz"}, @@ -164,7 +170,7 @@ def f(): ] -def test_generate_calls_simple(): +def test_generate_calls_simple() -> None: f = mock.Mock(should_warn={}, tags=[]) f.__name__ = "f" f.requires = None @@ -193,11 +199,11 @@ def test_generate_calls_simple(): # Make sure wrapping was done correctly. for call in calls: - assert call.some_prop == 42 - assert call.__name__ == "f" + assert call.some_prop == 42 # type: ignore[attr-defined] + assert call.__name__ == "f" # type: ignore[attr-defined] -def test_generate_calls_multiple_args(): +def test_generate_calls_multiple_args() -> None: f = mock.Mock(should_warn=None, tags=[]) f.__name__ = "f" f.requires = None @@ -224,7 +230,7 @@ def test_generate_calls_multiple_args(): f.assert_called_with(abc=3, foo="c") -def test_generate_calls_ids(): +def test_generate_calls_ids() -> None: f = mock.Mock(should_warn={}, tags=[]) f.__name__ = "f" f.requires = None @@ -247,7 +253,7 @@ def test_generate_calls_ids(): f.assert_called_with(foo=2) -def test_generate_calls_tags(): +def test_generate_calls_tags() -> None: f = mock.Mock(should_warn={}, tags=[], requires=[]) f.__name__ = "f" @@ -266,7 +272,7 @@ def test_generate_calls_tags(): assert calls[2].tags == ["tag4", "tag5"] -def test_generate_calls_merge_tags(): +def test_generate_calls_merge_tags() -> None: f = mock.Mock(should_warn={}, tags=["tag1", "tag2"], requires=[]) f.__name__ = "f" @@ -285,12 +291,12 @@ def test_generate_calls_merge_tags(): assert calls[2].tags == ["tag1", "tag2", "tag4", "tag5"] -def test_generate_calls_session_python(): +def test_generate_calls_session_python() -> None: called_with = [] @session @parametrize("python,dependency", [("3.8", "0.9"), ("3.9", "0.9"), ("3.9", "1.0")]) - def f(session, dependency): + def f(session: nox.Session, dependency: str) -> None: called_with.append((session, dependency)) calls = _decorators.Call.generate_calls(f, f.parametrize) @@ -313,17 +319,17 @@ def f(session, dependency): assert len(called_with) == 3 - assert called_with[0] == (session_, "0.9") - assert called_with[1] == (session_, "0.9") - assert called_with[2] == (session_, "1.0") + assert called_with[0] == (session_, "0.9") # type: ignore[comparison-overlap] + assert called_with[1] == (session_, "0.9") # type: ignore[comparison-overlap] + assert called_with[2] == (session_, "1.0") # type: ignore[comparison-overlap] -def test_generate_calls_python_compatibility(): +def test_generate_calls_python_compatibility() -> None: called_with = [] @session @parametrize("python,dependency", [("3.8", "0.9"), ("3.9", "0.9"), ("3.9", "1.0")]) - def f(session, python, dependency): + def f(session: nox.Session, python: str, dependency: str) -> None: called_with.append((session, python, dependency)) calls = _decorators.Call.generate_calls(f, f.parametrize) @@ -346,6 +352,6 @@ def f(session, python, dependency): assert len(called_with) == 3 - assert called_with[0] == (session_, "3.8", "0.9") - assert called_with[1] == (session_, "3.9", "0.9") - assert called_with[2] == (session_, "3.9", "1.0") + assert called_with[0] == (session_, "3.8", "0.9") # type: ignore[comparison-overlap] + assert called_with[1] == (session_, "3.9", "0.9") # type: ignore[comparison-overlap] + assert called_with[2] == (session_, "3.9", "1.0") # type: ignore[comparison-overlap] diff --git a/tests/test__resolver.py b/tests/test__resolver.py index 33eaf9d9..e0df0dc6 100644 --- a/tests/test__resolver.py +++ b/tests/test__resolver.py @@ -125,7 +125,9 @@ ), ], ) -def test_lazy_stable_topo_sort(dependencies, expected): +def test_lazy_stable_topo_sort( + dependencies: dict[str, tuple[str, ...]], expected: tuple[str, ...] +) -> None: actual = tuple(lazy_stable_topo_sort(dependencies, "0")) actual_with_root = tuple(lazy_stable_topo_sort(dependencies, "0", drop_root=False)) assert actual == actual_with_root[:-1] == expected @@ -156,7 +158,9 @@ def test_lazy_stable_topo_sort(dependencies, expected): ), ], ) -def test_lazy_stable_topo_sort_CycleError(dependencies, expected_cycle): +def test_lazy_stable_topo_sort_CycleError( + dependencies: dict[str, tuple[str, ...]], expected_cycle: tuple[str, ...] +) -> None: with pytest.raises(CycleError) as exc_info: tuple(lazy_stable_topo_sort(dependencies, "0")) # While the exact cycle reported is not unique and is an implementation detail, this diff --git a/tests/test__version.py b/tests/test__version.py index a2c7520a..b6db4dbc 100644 --- a/tests/test__version.py +++ b/tests/test__version.py @@ -14,6 +14,7 @@ from __future__ import annotations +from collections.abc import Callable from pathlib import Path from textwrap import dedent @@ -30,7 +31,7 @@ @pytest.fixture -def temp_noxfile(tmp_path: Path): +def temp_noxfile(tmp_path: Path) -> Callable[[str], str]: def make_temp_noxfile(content: str) -> str: path = tmp_path / "noxfile.py" path.write_text(content) @@ -100,7 +101,9 @@ def test_parse_needs_version(text: str, expected: str | None) -> None: @pytest.mark.parametrize("specifiers", ["", ">=2020.12.31", ">=2020.12.31,<9999.99.99"]) -def test_check_nox_version_succeeds(temp_noxfile, specifiers: str) -> None: +def test_check_nox_version_succeeds( + temp_noxfile: Callable[[str], str], specifiers: str +) -> None: """It does not raise if the version specifiers are satisfied.""" text = dedent( f""" @@ -112,7 +115,9 @@ def test_check_nox_version_succeeds(temp_noxfile, specifiers: str) -> None: @pytest.mark.parametrize("specifiers", [">=9999.99.99"]) -def test_check_nox_version_fails(temp_noxfile, specifiers: str) -> None: +def test_check_nox_version_fails( + temp_noxfile: Callable[[str], str], specifiers: str +) -> None: """It raises an exception if the version specifiers are not satisfied.""" text = dedent( f""" @@ -125,7 +130,9 @@ def test_check_nox_version_fails(temp_noxfile, specifiers: str) -> None: @pytest.mark.parametrize("specifiers", ["invalid", "2020.12.31"]) -def test_check_nox_version_invalid(temp_noxfile, specifiers: str) -> None: +def test_check_nox_version_invalid( + temp_noxfile: Callable[[str], str], specifiers: str +) -> None: """It raises an exception if the version specifiers cannot be parsed.""" text = dedent( f""" diff --git a/tests/test_action_helper.py b/tests/test_action_helper.py index 4886430c..9995e9fe 100644 --- a/tests/test_action_helper.py +++ b/tests/test_action_helper.py @@ -19,21 +19,21 @@ @pytest.mark.parametrize("version", VALID_VERSIONS.keys()) -def test_filter_version(version): +def test_filter_version(version: str) -> None: assert filter_version(version) == VALID_VERSIONS[version] -def test_filter_version_invalid(): +def test_filter_version_invalid() -> None: with pytest.raises(ValueError, match=r"invalid version: 3"): filter_version("3") -def test_filter_version_invalid_major(): +def test_filter_version_invalid_major() -> None: with pytest.raises(ValueError, match=r"invalid major python version: x.0"): filter_version("x.0") -def test_filter_version_invalid_minor(): +def test_filter_version_invalid_minor() -> None: with pytest.raises(ValueError, match=r"invalid minor python version: 3.x"): filter_version("3.x") @@ -63,7 +63,7 @@ def test_filter_version_invalid_minor(): @pytest.mark.parametrize("version_list", VALID_VERSION_LISTS.keys()) -def test_setup_action(capsys, version_list): +def test_setup_action(capsys: pytest.CaptureFixture[str], version_list: str) -> None: setup_action(version_list) captured = capsys.readouterr() lines = captured.out.splitlines() @@ -71,7 +71,7 @@ def test_setup_action(capsys, version_list): assert lines == [f"interpreters={json.dumps(ordered_versions)}"] -def test_setup_action_multiple_pypy(): +def test_setup_action_multiple_pypy() -> None: with pytest.raises( ValueError, match=( @@ -81,7 +81,7 @@ def test_setup_action_multiple_pypy(): setup_action("pypy3.9, pypy-3.9-v7.3.9") -def test_setup_action_multiple_cpython(): +def test_setup_action_multiple_cpython() -> None: with pytest.raises( ValueError, match=( diff --git a/tests/test_command.py b/tests/test_command.py index ae398e96..e798e934 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -25,9 +25,11 @@ import time from pathlib import Path from textwrap import dedent +from typing import Any from unittest import mock import pytest +from _pytest.compat import LEGACY_PATH import nox.command import nox.popen @@ -44,13 +46,13 @@ ) -def test_run_defaults(capsys): +def test_run_defaults(capsys: pytest.CaptureFixture[str]) -> None: result = nox.command.run([PYTHON, "-c", "print(123)"]) assert result is True -def test_run_silent(capsys): +def test_run_silent(capsys: pytest.CaptureFixture[str]) -> None: result = nox.command.run([PYTHON, "-c", "print(123)"], silent=True) out, _ = capsys.readouterr() @@ -60,13 +62,15 @@ def test_run_silent(capsys): @pytest.mark.skipif(shutil.which("git") is None, reason="Needs git") -def test_run_not_in_path(capsys): +def test_run_not_in_path(capsys: pytest.CaptureFixture[str]) -> 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 -def test_run_verbosity(capsys, caplog): +def test_run_verbosity( + capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture +) -> None: caplog.clear() with caplog.at_level(logging.DEBUG): result = nox.command.run([PYTHON, "-c", "print(123)"], silent=True) @@ -93,7 +97,9 @@ def test_run_verbosity(capsys, caplog): assert logs[0].message.strip() == "123" -def test_run_verbosity_failed_command(capsys, caplog): +def test_run_verbosity_failed_command( + capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture +) -> None: caplog.clear() with caplog.at_level(logging.DEBUG): with pytest.raises(nox.command.CommandFailed): @@ -125,7 +131,7 @@ def test_run_verbosity_failed_command(capsys, caplog): platform.system() == "Windows", reason="See https://github.com/python/cpython/issues/85815", ) -def test_run_non_str(): +def test_run_non_str() -> None: result = nox.command.run( [Path(PYTHON), "-c", "import sys; print(sys.argv)", Path(PYTHON)], silent=True, @@ -134,7 +140,7 @@ def test_run_non_str(): assert PYTHON in result -def test_run_env_unicode(): +def test_run_env_unicode() -> None: result = nox.command.run( [PYTHON, "-c", 'import os; print(os.environ["SIGIL"])'], silent=True, @@ -144,7 +150,7 @@ def test_run_env_unicode(): assert "123" in result -def test_run_env_remove(monkeypatch): +def test_run_env_remove(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("EMPTY", "notempty") nox.command.run( [PYTHON, "-c", 'import os; assert "EMPTY" in os.environ'], @@ -158,7 +164,7 @@ def test_run_env_remove(monkeypatch): @mock.patch("sys.platform", "win32") -def test_run_env_systemroot(): +def test_run_env_systemroot() -> None: systemroot = os.environ.setdefault("SYSTEMROOT", "sigil") result = nox.command.run( @@ -168,12 +174,12 @@ def test_run_env_systemroot(): assert systemroot in result -def test_run_not_found(): +def test_run_not_found() -> None: with pytest.raises(nox.command.CommandFailed): nox.command.run(["nonexistentcmd"]) -def test_run_path_nonexistent(): +def test_run_path_nonexistent() -> None: result = nox.command.run( [PYTHON, "-c", "import sys; print(sys.executable)"], silent=True, @@ -183,7 +189,7 @@ def test_run_path_nonexistent(): assert "/non/existent" not in result -def test_run_path_existent(tmp_path: Path): +def test_run_path_existent(tmp_path: Path) -> None: executable_name = ( "testexc.exe" if "windows" in platform.platform().lower() else "testexc" ) @@ -206,7 +212,9 @@ def test_run_path_existent(tmp_path: Path): ) -def test_run_external_warns(tmpdir, caplog): +def test_run_external_warns( + tmpdir: LEGACY_PATH, caplog: pytest.LogCaptureFixture +) -> None: caplog.set_level(logging.WARNING) nox.command.run([PYTHON, "--version"], silent=True, paths=[tmpdir.strpath]) @@ -214,7 +222,9 @@ def test_run_external_warns(tmpdir, caplog): assert "external=True" in caplog.text -def test_run_external_silences(tmpdir, caplog): +def test_run_external_silences( + tmpdir: LEGACY_PATH, caplog: pytest.LogCaptureFixture +) -> None: caplog.set_level(logging.WARNING) nox.command.run( @@ -224,7 +234,9 @@ def test_run_external_silences(tmpdir, caplog): assert "external=True" not in caplog.text -def test_run_external_raises(tmpdir, caplog): +def test_run_external_raises( + tmpdir: LEGACY_PATH, caplog: pytest.LogCaptureFixture +) -> None: caplog.set_level(logging.ERROR) with pytest.raises(nox.command.CommandFailed): @@ -235,7 +247,7 @@ def test_run_external_raises(tmpdir, caplog): assert "external=True" in caplog.text -def test_exit_codes(): +def test_exit_codes() -> None: assert nox.command.run([PYTHON, "-c", "import sys; sys.exit(0)"]) with pytest.raises(nox.command.CommandFailed): @@ -246,7 +258,7 @@ def test_exit_codes(): ) -def test_fail_with_silent(capsys): +def test_fail_with_silent(capsys: pytest.CaptureFixture[str]) -> None: with pytest.raises(nox.command.CommandFailed): nox.command.run( [ @@ -265,20 +277,20 @@ def test_fail_with_silent(capsys): @pytest.fixture -def marker(tmp_path): +def marker(tmp_path: Path) -> Path: """A marker file for process communication.""" return tmp_path / "marker" -def enable_ctrl_c(enabled): +def enable_ctrl_c(enabled: bool) -> None: """Enable keyboard interrupts (CTRL-C) on Windows.""" - kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) + kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) # type: ignore[attr-defined] if not kernel32.SetConsoleCtrlHandler(None, not enabled): - raise ctypes.WinError(ctypes.get_last_error()) + raise ctypes.WinError(ctypes.get_last_error()) # type: ignore[attr-defined] -def interrupt_process(proc): +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. @@ -286,13 +298,15 @@ def interrupt_process(proc): # Send the keyboard interrupt to all processes attached to the current # console session. - os.kill(0, signal.CTRL_C_EVENT) + os.kill(0, signal.CTRL_C_EVENT) # type: ignore[attr-defined] else: proc.send_signal(signal.SIGINT) @pytest.fixture -def command_with_keyboard_interrupt(monkeypatch, marker): +def command_with_keyboard_interrupt( + monkeypatch: pytest.MonkeyPatch, marker: Any +) -> None: """Monkeypatch Popen.communicate to raise KeyboardInterrupt.""" if platform.system() == "Windows": # Enable Ctrl-C because the child inherits the setting from us. @@ -300,12 +314,14 @@ def command_with_keyboard_interrupt(monkeypatch, marker): communicate = subprocess.Popen.communicate - def wrapper(proc, *args, **kwargs): + def wrapper( + proc: subprocess.Popen[Any], *args: Any, **kwargs: Any + ) -> tuple[Any, Any]: # Raise the interrupt only on the first call, so Nox has a chance to # shut down the child process subsequently. - if wrapper.firstcall: - wrapper.firstcall = False + if wrapper.firstcall: # type: ignore[attr-defined] + wrapper.firstcall = False # type: ignore[attr-defined] # Give the child time to install its signal handlers. while not marker.exists(): @@ -319,12 +335,12 @@ def wrapper(proc, *args, **kwargs): return communicate(proc, *args, **kwargs) - wrapper.firstcall = True + wrapper.firstcall = True # type: ignore[attr-defined] monkeypatch.setattr("subprocess.Popen.communicate", wrapper) -def format_program(program, marker): +def format_program(program: str, marker: Any) -> str: """Preprocess the Python program run by the child process.""" main = f""" import time @@ -336,10 +352,10 @@ def format_program(program, marker): return dedent(program).format(MAIN=dedent(main)) -def run_pytest_in_new_console_session(test): +def run_pytest_in_new_console_session(test: str) -> None: """Run the given test in a separate console session.""" env = dict(os.environ, SECONDARY_CONSOLE_SESSION="") - creationflags = subprocess.CREATE_NO_WINDOW + creationflags = subprocess.CREATE_NO_WINDOW # type: ignore[attr-defined] subprocess.run( [sys.executable, "-m", "pytest", f"{__file__}::{test}"], @@ -374,20 +390,24 @@ def run_pytest_in_new_console_session(test): """, ], ) -def test_interrupt_raises(command_with_keyboard_interrupt, program, marker): +def test_interrupt_raises( + command_with_keyboard_interrupt: None, + program: str, + marker: Any, +) -> None: """It kills the process and reraises the keyboard interrupt.""" with pytest.raises(KeyboardInterrupt): nox.command.run([PYTHON, "-c", format_program(program, marker)]) @only_on_windows -def test_interrupt_raises_on_windows(): +def test_interrupt_raises_on_windows() -> None: """It kills the process and reraises the keyboard interrupt.""" run_pytest_in_new_console_session("test_interrupt_raises") @skip_on_windows_primary_console_session -def test_interrupt_handled(command_with_keyboard_interrupt, marker): +def test_interrupt_handled(command_with_keyboard_interrupt: None, marker: Any) -> None: """It does not raise if the child handles the keyboard interrupt.""" program = """ import signal @@ -403,13 +423,13 @@ def exithandler(sig, frame): @only_on_windows -def test_interrupt_handled_on_windows(): +def test_interrupt_handled_on_windows() -> None: """It does not raise if the child handles the keyboard interrupt.""" run_pytest_in_new_console_session("test_interrupt_handled") -def test_custom_stdout(capsys, tmpdir): - with open(str(tmpdir / "out.txt"), "w+b") as stdout: +def test_custom_stdout(capsys: pytest.CaptureFixture[str], tmpdir: LEGACY_PATH) -> None: + with open(str(tmpdir / "out.txt"), "w+t") as stdout: nox.command.run( [ PYTHON, @@ -426,19 +446,23 @@ def test_custom_stdout(capsys, tmpdir): assert "out" not in err assert "err" not in err stdout.seek(0) - tempfile_contents = stdout.read().decode("utf-8") + tempfile_contents = stdout.read() assert "out" in tempfile_contents assert "err" in tempfile_contents -def test_custom_stdout_silent_flag(capsys, tmpdir): - with open(str(tmpdir / "out.txt"), "w+b") as stdout: # noqa: SIM117 +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 with pytest.raises(ValueError, match="silent"): nox.command.run([PYTHON, "-c", 'print("hi")'], stdout=stdout, silent=True) -def test_custom_stdout_failed_command(capsys, tmpdir): - with open(str(tmpdir / "out.txt"), "w+b") as stdout: +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 pytest.raises(nox.command.CommandFailed): nox.command.run( [ @@ -456,13 +480,13 @@ def test_custom_stdout_failed_command(capsys, tmpdir): assert "out" not in err assert "err" not in err stdout.seek(0) - tempfile_contents = stdout.read().decode("utf-8") + tempfile_contents = stdout.read() assert "out" in tempfile_contents assert "err" in tempfile_contents -def test_custom_stderr(capsys, tmpdir): - with open(str(tmpdir / "err.txt"), "w+b") as stderr: +def test_custom_stderr(capsys: pytest.CaptureFixture[str], tmpdir: LEGACY_PATH) -> None: + with open(str(tmpdir / "err.txt"), "w+t") as stderr: nox.command.run( [ PYTHON, @@ -479,13 +503,15 @@ def test_custom_stderr(capsys, tmpdir): assert "out" not in out assert "err" not in out stderr.seek(0) - tempfile_contents = stderr.read().decode("utf-8") + tempfile_contents = stderr.read() assert "out" not in tempfile_contents assert "err" in tempfile_contents -def test_custom_stderr_failed_command(capsys, tmpdir): - with open(str(tmpdir / "out.txt"), "w+b") as stderr: +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 pytest.raises(nox.command.CommandFailed): nox.command.run( [ @@ -503,7 +529,7 @@ def test_custom_stderr_failed_command(capsys, tmpdir): assert "out" not in out assert "err" not in out stderr.seek(0) - tempfile_contents = stderr.read().decode("utf-8") + tempfile_contents = stderr.read() assert "out" not in tempfile_contents assert "err" in tempfile_contents @@ -521,7 +547,7 @@ def test_output_decoding_non_ascii() -> None: def test_output_decoding_utf8_only_fail(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(nox.popen.locale, "getpreferredencoding", lambda: "utf8") + monkeypatch.setattr(nox.popen.locale, "getpreferredencoding", lambda: "utf8") # type: ignore[attr-defined] with pytest.raises(UnicodeDecodeError) as exc: nox.popen.decode_output(b"\x95") @@ -532,7 +558,7 @@ def test_output_decoding_utf8_only_fail(monkeypatch: pytest.MonkeyPatch) -> None def test_output_decoding_utf8_fail_cp1252_success( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr(nox.popen.locale, "getpreferredencoding", lambda: "cp1252") + monkeypatch.setattr(nox.popen.locale, "getpreferredencoding", lambda: "cp1252") # type: ignore[attr-defined] result = nox.popen.decode_output(b"\x95") @@ -540,7 +566,7 @@ def test_output_decoding_utf8_fail_cp1252_success( def test_output_decoding_both_fail(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(nox.popen.locale, "getpreferredencoding", lambda: "ascii") + monkeypatch.setattr(nox.popen.locale, "getpreferredencoding", lambda: "ascii") # type: ignore[attr-defined] with pytest.raises(UnicodeDecodeError) as exc: nox.popen.decode_output(b"\x95") diff --git a/tests/test_logger.py b/tests/test_logger.py index 990b760d..5b774840 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -22,19 +22,19 @@ from nox import logger -def test_success(): +def test_success() -> None: with mock.patch.object(logger.LoggerWithSuccessAndOutput, "_log") as _log: logger.LoggerWithSuccessAndOutput("foo").success("bar") _log.assert_called_once_with(logger.SUCCESS, "bar", ()) -def test_output(): +def test_output() -> None: with mock.patch.object(logger.LoggerWithSuccessAndOutput, "_log") as _log: logger.LoggerWithSuccessAndOutput("foo").output("bar") _log.assert_called_once_with(logger.OUTPUT, "bar", ()) -def test_formatter(caplog): +def test_formatter(caplog: pytest.LogCaptureFixture) -> None: caplog.clear() logger.setup_logging(True, verbose=True) with caplog.at_level(logging.DEBUG): @@ -68,7 +68,7 @@ def test_formatter(caplog): pytest.param(False, id="no-color"), ], ) -def test_no_color_timestamp(caplog, color): +def test_no_color_timestamp(caplog: pytest.LogCaptureFixture, color: bool) -> None: logger.setup_logging(color=color, add_timestamp=True) caplog.clear() with caplog.at_level(logging.DEBUG): diff --git a/tests/test_main.py b/tests/test_main.py index 72bbe755..6a17fea0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -18,8 +18,10 @@ 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 unittest import mock import pytest @@ -39,7 +41,7 @@ os.environ.pop("NOXSESSION", None) -def test_main_no_args(monkeypatch): +def test_main_no_args(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "argv", [sys.executable]) with mock.patch("nox.workflow.execute") as execute: execute.return_value = 0 @@ -61,7 +63,7 @@ def test_main_no_args(monkeypatch): assert config.posargs == [] -def test_main_long_form_args(): +def test_main_long_form_args() -> None: sys.argv = [ sys.executable, "--noxfile", @@ -102,7 +104,9 @@ def test_main_long_form_args(): assert config.posargs == [] -def test_main_no_venv(monkeypatch, capsys): +def test_main_no_venv( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: # Check that --no-venv overrides force_venv_backend monkeypatch.setattr( sys, @@ -129,7 +133,7 @@ def test_main_no_venv(monkeypatch, capsys): sys_exit.assert_called_once_with(0) -def test_main_no_venv_error(): +def test_main_no_venv_error() -> None: # Check that --no-venv can not be set together with a non-none --force-venv-backend sys.argv = [ sys.executable, @@ -143,7 +147,7 @@ def test_main_no_venv_error(): nox.main() -def test_main_short_form_args(monkeypatch): +def test_main_short_form_args(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( sys, "argv", @@ -180,7 +184,7 @@ def test_main_short_form_args(monkeypatch): assert config.reuse_venv == "yes" -def test_main_explicit_sessions(monkeypatch): +def test_main_explicit_sessions(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "argv", [sys.executable, "-e", "1", "2"]) with mock.patch("nox.workflow.execute") as execute: execute.return_value = 0 @@ -196,7 +200,9 @@ def test_main_explicit_sessions(monkeypatch): assert config.sessions == ["1", "2"] -def test_main_explicit_sessions_with_spaces_in_names(monkeypatch): +def test_main_explicit_sessions_with_spaces_in_names( + monkeypatch: pytest.MonkeyPatch, +) -> None: monkeypatch.setattr( sys, "argv", [sys.executable, "-e", "unit tests", "the unit tests"] ) @@ -237,7 +243,13 @@ def test_main_explicit_sessions_with_spaces_in_names(monkeypatch): "multiple_force_pythons", ], ) -def test_main_list_option_from_nox_env_var(monkeypatch, var, option, env, values): +def test_main_list_option_from_nox_env_var( + monkeypatch: pytest.MonkeyPatch, + var: str, + option: str, + env: str, + values: list[str], +) -> None: monkeypatch.setenv(var, env) monkeypatch.setattr(sys, "argv", [sys.executable]) @@ -266,7 +278,12 @@ def test_main_list_option_from_nox_env_var(monkeypatch, var, option, env, values ], ids=["option", "env", "option_over_env"], ) -def test_default_venv_backend_option(monkeypatch, options, env, expected): +def test_default_venv_backend_option( + monkeypatch: pytest.MonkeyPatch, + options: list[str], + env: str, + expected: str, +) -> None: monkeypatch.setenv("NOX_DEFAULT_VENV_BACKEND", env) monkeypatch.setattr(sys, "argv", [sys.executable, *options]) with mock.patch("nox.workflow.execute") as execute: @@ -283,7 +300,9 @@ def test_default_venv_backend_option(monkeypatch, options, env, expected): assert config.default_venv_backend == expected -def test_main_positional_args(capsys, monkeypatch): +def test_main_positional_args( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: fake_exit = mock.Mock(side_effect=ValueError("asdf!")) monkeypatch.setattr(sys, "argv", [sys.executable, "1", "2", "3"]) @@ -306,7 +325,7 @@ def test_main_positional_args(capsys, monkeypatch): fake_exit.assert_called_once_with(2) -def test_main_positional_with_double_hyphen(monkeypatch): +def test_main_positional_with_double_hyphen(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "argv", [sys.executable, "--", "1", "2", "3"]) with mock.patch("nox.workflow.execute") as execute: execute.return_value = 0 @@ -322,7 +341,9 @@ def test_main_positional_with_double_hyphen(monkeypatch): assert config.posargs == ["1", "2", "3"] -def test_main_positional_flag_like_with_double_hyphen(monkeypatch): +def test_main_positional_flag_like_with_double_hyphen( + monkeypatch: pytest.MonkeyPatch, +) -> None: monkeypatch.setattr( sys, "argv", [sys.executable, "--", "1", "2", "3", "-f", "--baz"] ) @@ -340,7 +361,9 @@ def test_main_positional_flag_like_with_double_hyphen(monkeypatch): assert config.posargs == ["1", "2", "3", "-f", "--baz"] -def test_main_version(capsys, monkeypatch): +def test_main_version( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.setattr(sys, "argv", [sys.executable, "--version"]) with contextlib.ExitStack() as stack: @@ -353,7 +376,9 @@ def test_main_version(capsys, monkeypatch): execute.assert_not_called() -def test_main_help(capsys, monkeypatch): +def test_main_help( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.setattr(sys, "argv", [sys.executable, "--help"]) with contextlib.ExitStack() as stack: @@ -366,7 +391,7 @@ def test_main_help(capsys, monkeypatch): execute.assert_not_called() -def test_main_failure(monkeypatch): +def test_main_failure(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "argv", [sys.executable]) with mock.patch("nox.workflow.execute") as execute: execute.return_value = 1 @@ -375,7 +400,9 @@ def test_main_failure(monkeypatch): exit.assert_called_once_with(1) -def test_main_nested_config(capsys, monkeypatch): +def test_main_nested_config( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.setattr( sys, "argv", @@ -396,7 +423,9 @@ def test_main_nested_config(capsys, monkeypatch): sys_exit.assert_called_once_with(0) -def test_main_session_with_names(capsys, monkeypatch): +def test_main_session_with_names( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.setattr( sys, "argv", @@ -418,8 +447,10 @@ def test_main_session_with_names(capsys, monkeypatch): @pytest.fixture -def run_nox(capsys, monkeypatch): - def _run_nox(*args): +def run_nox( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> Callable[..., tuple[int, str, str]]: + def _run_nox(*args: str) -> tuple[int, str, str]: monkeypatch.setattr(sys, "argv", ["nox", *args]) with mock.patch("sys.exit") as sys_exit: @@ -461,7 +492,9 @@ def _run_nox(*args): ), ], ) -def test_main_with_normalized_session_names(run_nox, normalized_name, session): +def test_main_with_normalized_session_names( + run_nox: Callable[..., tuple[int, str, str]], normalized_name: str, session: str +) -> None: noxfile = os.path.join(RESOURCES, "noxfile_normalization.py") returncode, _, stderr = run_nox(f"--noxfile={noxfile}", f"--session={session}") assert returncode == 0 @@ -482,7 +515,10 @@ def test_main_with_normalized_session_names(run_nox, normalized_name, session): "test(arg=datetime.datetime(1980, 1, 1))", ], ) -def test_main_with_bad_session_names(run_nox, session): +def test_main_with_bad_session_names( + run_nox: Callable[..., tuple[Any, Any, Any]], + session: str, +) -> None: noxfile = os.path.join(RESOURCES, "noxfile_normalization.py") returncode, _, stderr = run_nox(f"--noxfile={noxfile}", f"--session={session}") assert returncode != 0 @@ -499,14 +535,18 @@ def test_main_with_bad_session_names(run_nox, session): (("w",), ("u(django='1.9')", "u(django='2.0')", "w")), ], ) -def test_main_requires(run_nox, sessions, expected_order): +def test_main_requires( + run_nox: Callable[..., tuple[Any, Any, Any]], + sessions: tuple[str, ...], + expected_order: tuple[str, ...], +) -> None: noxfile = os.path.join(RESOURCES, "noxfile_requires.py") returncode, stdout, _ = run_nox(f"--noxfile={noxfile}", "--sessions", *sessions) assert returncode == 0 assert tuple(stdout.rstrip("\n").split("\n")) == expected_order -def test_main_requires_cycle(run_nox): +def test_main_requires_cycle(run_nox: Callable[..., tuple[Any, Any, Any]]) -> None: noxfile = os.path.join(RESOURCES, "noxfile_requires.py") returncode, _, stderr = run_nox(f"--noxfile={noxfile}", "--session=i") assert returncode != 0 @@ -516,14 +556,18 @@ def test_main_requires_cycle(run_nox): assert "Sessions are in a dependency cycle: i -> j -> i" in stderr -def test_main_requires_missing_session(run_nox): +def test_main_requires_missing_session( + run_nox: Callable[..., tuple[Any, Any, Any]], +) -> None: noxfile = os.path.join(RESOURCES, "noxfile_requires.py") returncode, _, stderr = run_nox(f"--noxfile={noxfile}", "--session=o") assert returncode != 0 assert "Session not found: does_not_exist" in stderr -def test_main_requires_bad_python_parametrization(run_nox): +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, @@ -534,7 +578,9 @@ def test_main_requires_bad_python_parametrization(run_nox): @pytest.mark.parametrize("session", ("s", "t")) -def test_main_requires_chain_fail(run_nox, session): +def test_main_requires_chain_fail( + 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}") assert returncode != 0 @@ -542,13 +588,18 @@ def test_main_requires_chain_fail(run_nox, session): @pytest.mark.parametrize("session", ("w", "u")) -def test_main_requries_modern_param(run_nox, session): +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}") assert returncode == 0 -def test_main_noxfile_options(monkeypatch, generate_noxfile_options): +def test_main_noxfile_options( + monkeypatch: pytest.MonkeyPatch, generate_noxfile_options: Callable[..., str] +) -> None: noxfile_path = generate_noxfile_options(reuse_existing_virtualenvs=True) monkeypatch.setattr( sys, @@ -577,7 +628,9 @@ def test_main_noxfile_options(monkeypatch, generate_noxfile_options): assert config.reuse_venv == "yes" -def test_main_noxfile_options_disabled_by_flag(monkeypatch, generate_noxfile_options): +def test_main_noxfile_options_disabled_by_flag( + monkeypatch: pytest.MonkeyPatch, generate_noxfile_options: Callable[..., str] +) -> None: noxfile_path = generate_noxfile_options(reuse_existing_virtualenvs=True) monkeypatch.setattr( sys, @@ -607,7 +660,9 @@ def test_main_noxfile_options_disabled_by_flag(monkeypatch, generate_noxfile_opt assert config.reuse_venv == "no" -def test_main_noxfile_options_sessions(monkeypatch, generate_noxfile_options): +def test_main_noxfile_options_sessions( + monkeypatch: pytest.MonkeyPatch, generate_noxfile_options: Callable[..., str] +) -> None: noxfile_path = generate_noxfile_options(reuse_existing_virtualenvs=True) monkeypatch.setattr( sys, @@ -629,7 +684,7 @@ def test_main_noxfile_options_sessions(monkeypatch, generate_noxfile_options): @pytest.fixture -def generate_noxfile_options_pythons(tmp_path): +def generate_noxfile_options_pythons(tmp_path: Path) -> Callable[[str, str, str], str]: """Generate noxfile.py with test and launch_rocket sessions. The sessions are defined for both the default and alternate Python versions. @@ -637,7 +692,9 @@ def generate_noxfile_options_pythons(tmp_path): goes into ``nox.options.sessions`` and ``nox.options.pythons``, respectively. """ - def generate_noxfile(default_session, default_python, alternate_python): + def generate_noxfile( + default_session: str, default_python: str, alternate_python: str + ) -> str: path = Path(RESOURCES) / "noxfile_options_pythons.py" text = path.read_text() text = text.format( @@ -657,8 +714,10 @@ def generate_noxfile(default_session, default_python, alternate_python): def test_main_noxfile_options_with_pythons_override( - capsys, monkeypatch, generate_noxfile_options_pythons -): + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, + generate_noxfile_options_pythons: Callable[..., str], +) -> None: noxfile = generate_noxfile_options_pythons( default_session="test", default_python=python_next_version, @@ -684,8 +743,10 @@ def test_main_noxfile_options_with_pythons_override( def test_main_noxfile_options_with_sessions_override( - capsys, monkeypatch, generate_noxfile_options_pythons -): + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, + generate_noxfile_options_pythons: Callable[..., str], +) -> None: noxfile = generate_noxfile_options_pythons( default_session="test", default_python=python_current_version, @@ -711,7 +772,9 @@ def test_main_noxfile_options_with_sessions_override( @pytest.mark.parametrize(("isatty_value", "expected"), [(True, True), (False, False)]) -def test_main_color_from_isatty(monkeypatch, isatty_value, expected): +def test_main_color_from_isatty( + monkeypatch: pytest.MonkeyPatch, isatty_value: bool, expected: bool +) -> None: monkeypatch.delenv("FORCE_COLOR", raising=False) monkeypatch.setattr(sys, "argv", [sys.executable]) with mock.patch("nox.workflow.execute") as execute: @@ -736,7 +799,9 @@ def test_main_color_from_isatty(monkeypatch, isatty_value, expected): ("--no-color", False), ], ) -def test_main_color_options(monkeypatch, color_opt, expected): +def test_main_color_options( + monkeypatch: pytest.MonkeyPatch, color_opt: str, expected: bool +) -> None: monkeypatch.delenv("FORCE_COLOR", raising=False) monkeypatch.setattr(sys, "argv", [sys.executable, color_opt]) with mock.patch("nox.workflow.execute") as execute: @@ -750,7 +815,9 @@ def test_main_color_options(monkeypatch, color_opt, expected): assert config.color == expected -def test_main_color_conflict(capsys, monkeypatch): +def test_main_color_conflict( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: monkeypatch.setattr(sys, "argv", [sys.executable, "--forcecolor", "--nocolor"]) with mock.patch("nox.workflow.execute") as execute: execute.return_value = 1 @@ -765,7 +832,7 @@ def test_main_color_conflict(capsys, monkeypatch): assert "color" in err -def test_main_force_python(monkeypatch): +def test_main_force_python(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "argv", ["nox", "--force-python=3.11"]) with mock.patch("nox.workflow.execute", return_value=0) as execute: with mock.patch.object(sys, "exit"): @@ -774,7 +841,9 @@ def test_main_force_python(monkeypatch): assert config.pythons == config.extra_pythons == ["3.11"] -def test_main_reuse_existing_virtualenvs_no_install(monkeypatch): +def test_main_reuse_existing_virtualenvs_no_install( + monkeypatch: pytest.MonkeyPatch, +) -> None: monkeypatch.setattr(sys, "argv", ["nox", "-R"]) with mock.patch("nox.workflow.execute", return_value=0) as execute: with mock.patch.object(sys, "exit"): @@ -799,12 +868,12 @@ def test_main_reuse_existing_virtualenvs_no_install(monkeypatch): ], ) def test_main_noxfile_options_with_ci_override( - monkeypatch, - generate_noxfile_options, - should_set_ci_env_var, - noxfile_option_value, - expected_final_value, -): + monkeypatch: pytest.MonkeyPatch, + generate_noxfile_options: Callable[..., str], + should_set_ci_env_var: bool, + noxfile_option_value: bool | None, + expected_final_value: bool, +) -> None: monkeypatch.delenv("CI", raising=False) # make sure we have a clean environment if should_set_ci_env_var: monkeypatch.setenv("CI", "True") @@ -845,7 +914,11 @@ def test_main_noxfile_options_with_ci_override( "never", ], ) -def test_main_reuse_venv_cli_flags(monkeypatch, generate_noxfile_options, reuse_venv): +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"], +) -> None: monkeypatch.setattr(sys, "argv", ["nox", "--reuse-venv", reuse_venv]) with mock.patch("nox.workflow.execute", return_value=0) as execute: with mock.patch.object(sys, "exit"): @@ -883,12 +956,12 @@ def test_main_reuse_venv_cli_flags(monkeypatch, generate_noxfile_options, reuse_ ], ) def test_main_noxfile_options_reuse_venv_compat_check( - monkeypatch, - generate_noxfile_options, - reuse_venv, - reuse_existing_virtualenvs, - expected, -): + monkeypatch: pytest.MonkeyPatch, + generate_noxfile_options: Callable[..., str], + reuse_venv: str, + reuse_existing_virtualenvs: str | bool | None, + expected: str, +) -> None: cmd_args = ["nox", "-l"] # CLI Compat Check if isinstance(reuse_existing_virtualenvs, str): @@ -921,33 +994,33 @@ def test_main_noxfile_options_reuse_venv_compat_check( assert config.reuse_venv == expected -def test_noxfile_options_cant_be_set(): +def test_noxfile_options_cant_be_set() -> None: with pytest.raises(AttributeError, match="reuse_venvs"): - nox.options.reuse_venvs = True + nox.options.reuse_venvs = True # type: ignore[attr-defined] -def test_noxfile_options_cant_be_set_long(): +def test_noxfile_options_cant_be_set_long() -> None: with pytest.raises(AttributeError, match="i_am_clearly_not_an_option"): - nox.options.i_am_clearly_not_an_option = True + nox.options.i_am_clearly_not_an_option = True # type: ignore[attr-defined] -def test_symlink_orig(monkeypatch): +def test_symlink_orig(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.chdir(Path(RESOURCES) / "orig_dir") subprocess.run([sys.executable, "-m", "nox", "-s", "orig"], check=True) -def test_symlink_orig_not(monkeypatch): +def test_symlink_orig_not(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.chdir(Path(RESOURCES) / "orig_dir") res = subprocess.run([sys.executable, "-m", "nox", "-s", "sym"], check=False) assert res.returncode == 1 -def test_symlink_sym(monkeypatch): +def test_symlink_sym(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.chdir(Path(RESOURCES) / "sym_dir") subprocess.run([sys.executable, "-m", "nox", "-s", "sym"], check=True) -def test_symlink_sym_not(monkeypatch): +def test_symlink_sym_not(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.chdir(Path(RESOURCES) / "sym_dir") res = subprocess.run([sys.executable, "-m", "nox", "-s", "orig"], check=False) assert res.returncode == 1 diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 5452288b..e79a14a3 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -15,7 +15,9 @@ from __future__ import annotations import collections +import typing from collections.abc import Sequence +from typing import Any from unittest import mock import pytest @@ -32,7 +34,7 @@ ) -def create_mock_sessions(): +def create_mock_sessions() -> collections.OrderedDict[str, mock.Mock]: sessions = collections.OrderedDict() sessions["foo"] = mock.Mock(spec=(), python=None, venv_backend=None, tags=["baz"]) sessions["bar"] = mock.Mock( @@ -44,7 +46,7 @@ def create_mock_sessions(): return sessions -def create_mock_config(): +def create_mock_config() -> Any: cfg = mock.sentinel.MOCKED_CONFIG cfg.force_venv_backend = None cfg.default_venv_backend = None @@ -54,7 +56,7 @@ def create_mock_config(): return cfg -def test__normalize_arg(): +def test__normalize_arg() -> None: assert _normalize_arg('test(foo="bar")') == _normalize_arg('test(foo="bar")') # In the case of SyntaxError it should fallback to string @@ -64,13 +66,13 @@ def test__normalize_arg(): ) -def test__normalized_session_match(): +def test__normalized_session_match() -> None: session_mock = mock.MagicMock() session_mock.signatures = ['test(foo="bar")'] assert _normalized_session_match("test(foo='bar')", session_mock) -def test_init(): +def test_init() -> None: sessions = create_mock_sessions() manifest = Manifest(sessions, create_mock_config()) @@ -80,7 +82,7 @@ def test_init(): assert manifest["bar"].func is sessions["bar"] -def test_contains(): +def test_contains() -> None: sessions = create_mock_sessions() manifest = Manifest(sessions, create_mock_config()) @@ -100,7 +102,7 @@ def test_contains(): assert manifest["foo"] in manifest -def test_getitem(): +def test_getitem() -> None: sessions = create_mock_sessions() manifest = Manifest(sessions, create_mock_config()) @@ -119,7 +121,7 @@ def test_getitem(): assert manifest["bar"].func is sessions["bar"] -def test_iteration(): +def test_iteration() -> None: sessions = create_mock_sessions() manifest = Manifest(sessions, create_mock_config()) @@ -149,7 +151,7 @@ def test_iteration(): manifest.__next__() -def test_len(): +def test_len() -> None: sessions = create_mock_sessions() manifest = Manifest(sessions, create_mock_config()) assert len(manifest) == 2 @@ -157,7 +159,7 @@ def test_len(): assert len(manifest) == 2 -def test_filter_by_name(): +def test_filter_by_name() -> None: sessions = create_mock_sessions() manifest = Manifest(sessions, create_mock_config()) manifest.filter_by_name(("foo",)) @@ -165,21 +167,21 @@ def test_filter_by_name(): assert "bar" not in manifest -def test_filter_by_name_maintains_order(): +def test_filter_by_name_maintains_order() -> None: sessions = create_mock_sessions() manifest = Manifest(sessions, create_mock_config()) manifest.filter_by_name(("bar", "foo")) assert [session.name for session in manifest] == ["bar", "foo"] -def test_filter_by_name_not_found(): +def test_filter_by_name_not_found() -> None: sessions = create_mock_sessions() manifest = Manifest(sessions, create_mock_config()) with pytest.raises(KeyError): manifest.filter_by_name(("baz",)) -def test_filter_by_python_interpreter(): +def test_filter_by_python_interpreter() -> None: sessions = create_mock_sessions() manifest = Manifest(sessions, create_mock_config()) manifest["foo"].func.python = "3.8" @@ -189,7 +191,7 @@ def test_filter_by_python_interpreter(): assert "bar" not in manifest -def test_filter_by_keyword(): +def test_filter_by_keyword() -> None: sessions = create_mock_sessions() manifest = Manifest(sessions, create_mock_config()) assert len(manifest) == 2 @@ -212,7 +214,7 @@ def test_filter_by_keyword(): (["baz", "missing"], 2), ], ) -def test_filter_by_tags(tags: Sequence[str], session_count: int): +def test_filter_by_tags(tags: Sequence[str], session_count: int) -> None: sessions = create_mock_sessions() manifest = Manifest(sessions, create_mock_config()) assert len(manifest) == 2 @@ -220,7 +222,7 @@ def test_filter_by_tags(tags: Sequence[str], session_count: int): assert len(manifest) == session_count -def test_list_all_sessions_with_filter(): +def test_list_all_sessions_with_filter() -> None: sessions = create_mock_sessions() manifest = Manifest(sessions, create_mock_config()) assert len(manifest) == 2 @@ -233,7 +235,7 @@ def test_list_all_sessions_with_filter(): assert all_sessions[1][1] is False -def test_add_session_plain(): +def test_add_session_plain() -> None: manifest = Manifest({}, create_mock_config()) session_func = mock.Mock(spec=(), python=None, venv_backend=None) for session in manifest.make_session("my_session", session_func): @@ -241,10 +243,10 @@ def test_add_session_plain(): assert len(manifest) == 1 -def test_add_session_multiple_pythons(): +def test_add_session_multiple_pythons() -> None: manifest = Manifest({}, create_mock_config()) - def session_func(): + def session_func() -> None: pass func = Func(session_func, python=["3.5", "3.6"]) @@ -272,13 +274,17 @@ def session_func(): (["3.5", "3.9"], ["3.5", "3.9"], ["3.5", "3.9"]), ], ) -def test_extra_pythons(python, extra_pythons, expected): +def test_extra_pythons( + python: list[str] | str | bool | None, + extra_pythons: list[str], + expected: list[None] | list[bool] | list[str], +) -> None: cfg = create_mock_config() cfg.extra_pythons = extra_pythons manifest = Manifest({}, cfg) - def session_func(): + def session_func() -> None: pass func = Func(session_func, python=python) @@ -306,14 +312,18 @@ def session_func(): (["3.5", "3.9"], ["3.5", "3.9"], ["3.5", "3.9"]), ], ) -def test_force_pythons(python, force_pythons, expected): +def test_force_pythons( + python: list[str] | str | bool | None, + force_pythons: list[str], + expected: list[None] | list[bool] | list[str], +) -> None: cfg = create_mock_config() cfg.force_pythons = force_pythons cfg.extra_pythons = force_pythons manifest = Manifest({}, cfg) - def session_func(): + def session_func() -> None: pass func = Func(session_func, python=python) @@ -323,12 +333,12 @@ def session_func(): assert expected == [session.func.python for session in manifest._all_sessions] -def test_add_session_parametrized(): +def test_add_session_parametrized() -> None: manifest = Manifest({}, create_mock_config()) # Define a session with parameters. @nox.parametrize("param", ("a", "b", "c")) - def my_session(session, param): + def my_session(session: nox.Session, param: str) -> None: pass func = Func(my_session, python=None) @@ -339,12 +349,12 @@ def my_session(session, param): assert len(manifest) == 3 -def test_add_session_parametrized_multiple_pythons(): +def test_add_session_parametrized_multiple_pythons() -> None: manifest = Manifest({}, create_mock_config()) # Define a session with parameters. @nox.parametrize("param", ("a", "b")) - def my_session(session, param): + def my_session(session: nox.Session, param: str) -> None: pass func = Func(my_session, python=["2.7", "3.6"]) @@ -355,12 +365,12 @@ def my_session(session, param): assert len(manifest) == 4 -def test_add_session_parametrized_noop(): +def test_add_session_parametrized_noop() -> None: manifest = Manifest({}, create_mock_config()) # Define a session without any parameters. @nox.parametrize("param", ()) - def my_session(session, param): + def my_session(session: nox.Session, param: object) -> None: pass my_session.python = None @@ -376,19 +386,23 @@ def my_session(session, param): assert session.func == _null_session_func -def test_notify(): +def test_notify() -> None: manifest = Manifest({}, create_mock_config()) # Define a session. - def my_session(session): + def my_session_raw(session: nox.Session) -> None: pass + my_session = typing.cast(Func, my_session_raw) + my_session.python = None my_session.venv_backend = None - def notified(session): + def notified_raw(session: nox.Session) -> None: pass + notified = typing.cast(Func, notified_raw) + notified.python = None notified.venv_backend = None @@ -408,13 +422,15 @@ def notified(session): assert len(manifest) == 2 -def test_notify_noop(): +def test_notify_noop() -> None: manifest = Manifest({}, create_mock_config()) # Define a session and add it to the manifest. - def my_session(session): + def my_session_raw(session: nox.Session) -> None: pass + my_session = typing.cast(Func, my_session_raw) + my_session.python = None my_session.venv_backend = None @@ -428,7 +444,7 @@ def my_session(session): assert len(manifest) == 1 -def test_notify_with_posargs(): +def test_notify_with_posargs() -> None: cfg = create_mock_config() manifest = Manifest({}, cfg) @@ -443,13 +459,13 @@ def test_notify_with_posargs(): assert session.posargs == ["--an-arg"] -def test_notify_error(): +def test_notify_error() -> None: manifest = Manifest({}, create_mock_config()) with pytest.raises(ValueError): manifest.notify("does_not_exist") -def test_add_session_idempotent(): +def test_add_session_idempotent() -> None: manifest = Manifest({}, create_mock_config()) session_func = mock.Mock(spec=(), python=None, venv_backend=None) for session in manifest.make_session("my_session", session_func): @@ -458,30 +474,32 @@ def test_add_session_idempotent(): assert len(manifest) == 1 -def test_null_session_function(): +def test_null_session_function() -> None: session = mock.Mock(spec=("skip",)) _null_session_func(session) assert session.skip.called -def test_keyword_locals_length(): +def test_keyword_locals_length() -> None: kw = KeywordLocals({"foo", "bar"}) assert len(kw) == 2 -def test_keyword_locals_iter(): - values = ["foo", "bar"] +def test_keyword_locals_iter() -> None: + values = ["bar", "foo"] kw = KeywordLocals(values) - assert list(kw) == values + assert sorted(kw) == values -def test_no_venv_backend_but_some_pythons(): +def test_no_venv_backend_but_some_pythons() -> None: manifest = Manifest({}, create_mock_config()) # Define a session and add it to the manifest. - def my_session(session): + def my_session_raw(session: nox.Session) -> None: pass + my_session = typing.cast(Func, my_session_raw) + # the session sets "no venv backend" but declares some pythons my_session.python = ["3.7", "3.8"] my_session.venv_backend = "none" diff --git a/tests/test_project.py b/tests/test_project.py index 3854902a..f9130886 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -3,7 +3,7 @@ from nox.project import dependency_groups, python_versions -def test_classifiers(): +def test_classifiers() -> None: pyproject = { "project": { "classifiers": [ @@ -21,19 +21,19 @@ def test_classifiers(): assert python_versions(pyproject) == ["3.7", "3.9", "3.12"] -def test_no_classifiers(): +def test_no_classifiers() -> None: pyproject = {"project": {"requires-python": ">=3.9"}} with pytest.raises(ValueError, match="No Python version classifiers"): python_versions(pyproject) -def test_no_requires_python(): +def test_no_requires_python() -> None: pyproject = {"project": {"classifiers": ["Programming Language :: Python :: 3.12"]}} with pytest.raises(ValueError, match='No "project.requires-python" value set'): python_versions(pyproject, max_version="3.13") -def test_python_range(): +def test_python_range() -> None: pyproject = { "project": { "classifiers": [ @@ -52,20 +52,20 @@ def test_python_range(): assert python_versions(pyproject, max_version="3.11") == ["3.10", "3.11"] -def test_python_range_gt(): +def test_python_range_gt() -> None: pyproject = {"project": {"requires-python": ">3.2.1,<3.3"}} assert python_versions(pyproject, max_version="3.4") == ["3.2", "3.3", "3.4"] -def test_python_range_no_min(): +def test_python_range_no_min() -> None: pyproject = {"project": {"requires-python": "==3.3.1"}} with pytest.raises(ValueError, match="No minimum version found"): python_versions(pyproject, max_version="3.5") -def test_dependency_groups(): +def test_dependency_groups() -> None: example = { "dependency-groups": { "test": ["pytest", "coverage"], diff --git a/tests/test_registry.py b/tests/test_registry.py index 304b969c..40e14ecd 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -14,13 +14,17 @@ from __future__ import annotations +from collections.abc import Generator +from typing import Literal + import pytest +import nox from nox import registry @pytest.fixture -def cleanup_registry(): +def cleanup_registry() -> Generator[None, None, None]: """Ensure that the session registry is completely empty before and after each test. """ @@ -31,11 +35,11 @@ def cleanup_registry(): registry._REGISTRY.clear() -def test_session_decorator(cleanup_registry): +def test_session_decorator(cleanup_registry: None) -> None: # Establish that the use of the session decorator will cause the # function to be found in the registry. @registry.session_decorator - def unit_tests(session): + def unit_tests(session: nox.Session) -> None: pass answer = registry.get() @@ -44,58 +48,61 @@ def unit_tests(session): assert unit_tests.python is None -def test_session_decorator_single_python(cleanup_registry): +def test_session_decorator_single_python(cleanup_registry: None) -> None: @registry.session_decorator(python="3.6") - def unit_tests(session): + def unit_tests(session: nox.Session) -> None: pass assert unit_tests.python == "3.6" -def test_session_decorator_list_of_pythons(cleanup_registry): +def test_session_decorator_list_of_pythons(cleanup_registry: None) -> None: @registry.session_decorator(python=["3.5", "3.6"]) - def unit_tests(session): + def unit_tests(session: nox.Session) -> None: pass assert unit_tests.python == ["3.5", "3.6"] -def test_session_decorator_tags(cleanup_registry): +def test_session_decorator_tags(cleanup_registry: None) -> None: @registry.session_decorator(tags=["tag-1", "tag-2"]) - def unit_tests(session): + def unit_tests(session: nox.Session) -> None: pass assert unit_tests.tags == ["tag-1", "tag-2"] -def test_session_decorator_py_alias(cleanup_registry): +def test_session_decorator_py_alias(cleanup_registry: None) -> None: @registry.session_decorator(py=["3.5", "3.6"]) - def unit_tests(session): + def unit_tests(session: nox.Session) -> None: pass assert unit_tests.python == ["3.5", "3.6"] -def test_session_decorator_py_alias_error(cleanup_registry): +def test_session_decorator_py_alias_error(cleanup_registry: None) -> None: with pytest.raises(ValueError, match="argument"): @registry.session_decorator(python=["3.5", "3.6"], py="2.7") - def unit_tests(session): + def unit_tests(session: nox.Session) -> None: pass -def test_session_decorator_reuse(cleanup_registry): +def test_session_decorator_reuse(cleanup_registry: None) -> None: @registry.session_decorator(reuse_venv=True) - def unit_tests(session): + def unit_tests(session: nox.Session) -> None: pass assert unit_tests.reuse_venv is True @pytest.mark.parametrize("name", ["unit-tests", "unit tests", "the unit tests"]) -def test_session_decorator_name(cleanup_registry, name): +def test_session_decorator_name( + cleanup_registry: None, + name: Literal["unit-tests"] | Literal["unit tests"] | Literal["the unit tests"], +) -> None: @registry.session_decorator(name=name) - def unit_tests(session): + def unit_tests(session: nox.Session) -> None: pass answer = registry.get() @@ -105,7 +112,7 @@ def unit_tests(session): assert unit_tests.python is None -def test_get(cleanup_registry): +def test_get(cleanup_registry: None) -> None: # Establish that the get method returns a copy of the registry. empty = registry.get() assert empty == registry._REGISTRY @@ -113,11 +120,11 @@ def test_get(cleanup_registry): assert len(empty) == 0 @registry.session_decorator - def unit_tests(session): + def unit_tests(session: nox.Session) -> None: pass @registry.session_decorator - def system_tests(session): + def system_tests(session: nox.Session) -> None: pass full = registry.get() diff --git a/tests/test_sessions.py b/tests/test_sessions.py index ce0b5684..cd9457da 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -23,11 +23,15 @@ import subprocess import sys import tempfile +import typing from pathlib import Path +from typing import Any, Literal, NoReturn from unittest import mock import pytest +from _pytest.compat import LEGACY_PATH +import nox._decorators import nox.command import nox.manifest import nox.popen @@ -43,7 +47,7 @@ DIR = Path(__file__).parent.resolve() -def run_with_defaults(**kwargs): +def run_with_defaults(**kwargs: Any) -> dict[str, Any]: return { "env": None, "silent": False, @@ -59,7 +63,7 @@ def run_with_defaults(**kwargs): } -def _run_with_defaults(**kwargs): +def _run_with_defaults(**kwargs: Any) -> dict[str, Any]: return { "env": None, "include_outer_env": True, @@ -75,7 +79,7 @@ def _run_with_defaults(**kwargs): } -def test__normalize_path(): +def test__normalize_path() -> None: envdir = "envdir" normalize = nox.sessions._normalize_path assert normalize(envdir, "hello") == os.path.join("envdir", "hello") @@ -89,21 +93,23 @@ def test__normalize_path(): ) -def test__normalize_path_hash(): +def test__normalize_path_hash() -> None: envdir = "d" * (100 - len("bin/pythonX.Y") - 10) norm_path = nox.sessions._normalize_path(envdir, "a-really-long-virtualenv-path") assert "a-really-long-virtualenv-path" not in norm_path assert len(norm_path) < 100 -def test__normalize_path_give_up(): +def test__normalize_path_give_up() -> None: envdir = "d" * 100 norm_path = nox.sessions._normalize_path(envdir, "any-path") assert "any-path" in norm_path class TestSession: - def make_session_and_runner(self): + def make_session_and_runner( + self, + ) -> tuple[nox.sessions.Session, nox.sessions.SessionRunner]: func = mock.Mock(spec=["python"], python="3.7") runner = nox.sessions.SessionRunner( name="test", @@ -118,12 +124,13 @@ def make_session_and_runner(self): manifest=mock.create_autospec(nox.manifest.Manifest), ) runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv) + assert runner.venv runner.venv.env = {} - runner.venv.bin_paths = ["/no/bin/for/you"] - runner.venv.venv_backend = "venv" + runner.venv.bin_paths = ["/no/bin/for/you"] # type: ignore[misc] + runner.venv.venv_backend = "venv" # type: ignore[misc] return nox.sessions.Session(runner=runner), runner - def test_create_tmp(self): + def test_create_tmp(self) -> None: session, runner = self.make_session_and_runner() with tempfile.TemporaryDirectory() as root: runner.global_config.envdir = root @@ -131,25 +138,28 @@ def test_create_tmp(self): assert session.env["TMPDIR"] == os.path.abspath(tmpdir) assert tmpdir.startswith(root) - def test_create_tmp_twice(self): + def test_create_tmp_twice(self) -> None: session, runner = self.make_session_and_runner() with tempfile.TemporaryDirectory() as root: runner.global_config.envdir = root - runner.venv.bin = bin + assert runner.venv + runner.venv.bin = bin # type: ignore[misc, assignment] session.create_tmp() tmpdir = session.create_tmp() assert session.env["TMPDIR"] == os.path.abspath(tmpdir) assert tmpdir.startswith(root) - def test_properties(self): + def test_properties(self) -> None: session, runner = self.make_session_and_runner() with tempfile.TemporaryDirectory() as root: runner.global_config.envdir = root assert session.name is runner.friendly_name + assert runner.venv assert session.env is runner.venv.env assert session.posargs == runner.global_config.posargs assert session.virtualenv is runner.venv + assert runner.venv.bin_paths assert session.bin_paths is runner.venv.bin_paths assert session.bin is runner.venv.bin_paths[0] assert session.python is runner.func.python @@ -158,17 +168,18 @@ def test_properties(self): ".cache" ) - def test_no_bin_paths(self): + def test_no_bin_paths(self) -> None: session, runner = self.make_session_and_runner() - runner.venv.bin_paths = None + assert runner.venv + runner.venv.bin_paths = None # type: ignore[misc] with pytest.raises( ValueError, match=r"^The environment does not have a bin directory\.$" ): session.bin # noqa: B018 assert session.bin_paths is None - def test_virtualenv_as_none(self): + def test_virtualenv_as_none(self) -> None: session, runner = self.make_session_and_runner() runner.venv = None @@ -178,7 +189,7 @@ def test_virtualenv_as_none(self): assert session.venv_backend == "none" - def test_interactive(self): + def test_interactive(self) -> None: session, runner = self.make_session_and_runner() with mock.patch("nox.sessions.sys.stdin.isatty") as m_isatty: @@ -190,7 +201,7 @@ def test_interactive(self): assert session.interactive is False - def test_explicit_non_interactive(self): + def test_explicit_non_interactive(self) -> None: session, runner = self.make_session_and_runner() with mock.patch("nox.sessions.sys.stdin.isatty") as m_isatty: @@ -199,7 +210,7 @@ def test_explicit_non_interactive(self): assert session.interactive is False - def test_chdir(self, tmpdir): + def test_chdir(self, tmpdir: LEGACY_PATH) -> None: cdto = str(tmpdir.join("cdbby").ensure(dir=True)) current_cwd = os.getcwd() @@ -210,7 +221,7 @@ def test_chdir(self, tmpdir): assert os.getcwd() == cdto os.chdir(current_cwd) - def test_chdir_ctx(self, tmpdir): + def test_chdir_ctx(self, tmpdir: LEGACY_PATH) -> None: cdto = str(tmpdir.join("cdbby").ensure(dir=True)) current_cwd = os.getcwd() @@ -223,7 +234,7 @@ def test_chdir_ctx(self, tmpdir): os.chdir(current_cwd) - def test_invoked_from(self, tmpdir): + def test_invoked_from(self, tmpdir: LEGACY_PATH) -> None: cdto = str(tmpdir.join("cdbby").ensure(dir=True)) current_cwd = os.getcwd() @@ -234,7 +245,7 @@ def test_invoked_from(self, tmpdir): assert session.invoked_from == current_cwd os.chdir(current_cwd) - def test_chdir_pathlib(self, tmpdir): + def test_chdir_pathlib(self, tmpdir: LEGACY_PATH) -> None: cdto = str(tmpdir.join("cdbby").ensure(dir=True)) current_cwd = os.getcwd() @@ -245,27 +256,27 @@ def test_chdir_pathlib(self, tmpdir): assert os.getcwd() == cdto os.chdir(current_cwd) - def test_run_bad_args(self): + def test_run_bad_args(self) -> None: session, _ = self.make_session_and_runner() with pytest.raises(ValueError, match="arg"): session.run() - def test_run_with_func(self): + def test_run_with_func(self) -> None: session, _ = self.make_session_and_runner() - assert session.run(operator.add, 1, 2) == 3 + assert session.run(operator.add, 1, 2) == 3 # type: ignore[arg-type] - def test_run_with_func_error(self): + def test_run_with_func_error(self) -> None: session, _ = self.make_session_and_runner() - def raise_value_error(): + def raise_value_error() -> NoReturn: raise ValueError("meep") with pytest.raises(nox.command.CommandFailed): - assert session.run(raise_value_error) + assert session.run(raise_value_error) # type: ignore[arg-type] - def test_run_install_only(self, caplog): + def test_run_install_only(self, caplog: pytest.LogCaptureFixture) -> None: caplog.set_level(logging.INFO) session, runner = self.make_session_and_runner() runner.global_config.install_only = True @@ -277,7 +288,7 @@ def test_run_install_only(self, caplog): assert "install-only" in caplog.text - def test_run_install_only_should_install(self): + def test_run_install_only_should_install(self) -> None: session, runner = self.make_session_and_runner() runner.global_config.install_only = True @@ -290,18 +301,18 @@ def test_run_install_only_should_install(self): **run_with_defaults(paths=mock.ANY, silent=True, env={}, external="error"), ) - def test_run_success(self): + def test_run_success(self) -> None: session, _ = self.make_session_and_runner() result = session.run(sys.executable, "-c", "print(123)") assert result - def test_run_error(self): + def test_run_error(self) -> None: session, _ = self.make_session_and_runner() with pytest.raises(nox.command.CommandFailed): session.run(sys.executable, "-c", "import sys; sys.exit(1)") - def test_run_install_script(self): + def test_run_install_script(self) -> None: session, _ = self.make_session_and_runner() with mock.patch.object(nox.command, "run") as run: @@ -311,8 +322,9 @@ def test_run_install_script(self): assert "rich" in run.call_args_list[0][0][0] assert DIR / "resources/pep721example1.py" in run.call_args_list[1][0][0] - def test_run_overly_env(self): + def test_run_overly_env(self) -> None: session, runner = self.make_session_and_runner() + assert runner.venv runner.venv.env["A"] = "1" runner.venv.env["B"] = "2" runner.venv.env["C"] = "4" @@ -323,10 +335,12 @@ def test_run_overly_env(self): env={"B": "3", "C": None}, silent=True, ) + assert result assert result.strip() == "1 3 5" - def test_by_default_all_invocation_env_vars_are_passed(self): + def test_by_default_all_invocation_env_vars_are_passed(self) -> None: session, runner = self.make_session_and_runner() + assert runner.venv runner.venv.env["I_SHOULD_BE_INCLUDED"] = "happy" runner.venv.env["I_SHOULD_BE_INCLUDED_TOO"] = "happier" runner.venv.env["EVERYONE_SHOULD_BE_INCLUDED_TOO"] = "happiest" @@ -336,12 +350,14 @@ def test_by_default_all_invocation_env_vars_are_passed(self): "import os; print(os.environ)", silent=True, ) + assert result assert "happy" in result assert "happier" in result assert "happiest" in result - def test_no_included_invocation_env_vars_are_passed(self): + def test_no_included_invocation_env_vars_are_passed(self) -> None: session, runner = self.make_session_and_runner() + assert runner.venv runner.venv.env["I_SHOULD_NOT_BE_INCLUDED"] = "sad" runner.venv.env["AND_NEITHER_SHOULD_I"] = "unhappy" result = session.run( @@ -352,11 +368,12 @@ def test_no_included_invocation_env_vars_are_passed(self): include_outer_env=False, silent=True, ) + assert result assert "sad" not in result assert "unhappy" not in result assert "happy" in result - def test_run_external_not_a_virtualenv(self): + def test_run_external_not_a_virtualenv(self) -> None: # Non-virtualenv sessions should always allow external programs. session, runner = self.make_session_and_runner() @@ -370,14 +387,15 @@ def test_run_external_not_a_virtualenv(self): **run_with_defaults(external=True, env=mock.ANY), ) - def test_run_external_condaenv(self): + def test_run_external_condaenv(self) -> None: # condaenv sessions should always allow conda. session, runner = self.make_session_and_runner() runner.venv = mock.create_autospec(nox.virtualenv.CondaEnv) - runner.venv.allowed_globals = ("conda",) + assert runner.venv + runner.venv.allowed_globals = ("conda",) # type: ignore[misc] runner.venv.env = {} - runner.venv.bin_paths = ["/path/to/env/bin"] - runner.venv.create.return_value = True + runner.venv.bin_paths = ["/path/to/env/bin"] # type: ignore[misc] + runner.venv.create.return_value = True # type: ignore[attr-defined] with mock.patch("nox.command.run", autospec=True) as run: session.run("conda", "--version") @@ -389,7 +407,7 @@ def test_run_external_condaenv(self): ), ) - def test_run_external_with_error_on_external_run(self): + def test_run_external_with_error_on_external_run(self) -> None: session, runner = self.make_session_and_runner() runner.global_config.error_on_external_run = True @@ -397,18 +415,19 @@ def test_run_external_with_error_on_external_run(self): with pytest.raises(nox.command.CommandFailed, match="External"): session.run(sys.executable, "--version") - def test_run_external_with_error_on_external_run_condaenv(self): + def test_run_external_with_error_on_external_run_condaenv(self) -> None: session, runner = self.make_session_and_runner() runner.venv = mock.create_autospec(nox.virtualenv.CondaEnv) + assert runner.venv runner.venv.env = {} - runner.venv.bin_paths = ["/path/to/env/bin"] + runner.venv.bin_paths = ["/path/to/env/bin"] # type: ignore[misc] runner.global_config.error_on_external_run = True with pytest.raises(nox.command.CommandFailed, match="External"): session.run(sys.executable, "--version") - def test_run_install_bad_args(self): + def test_run_install_bad_args(self) -> None: session, _ = self.make_session_and_runner() with pytest.raises(ValueError) as exc_info: @@ -417,7 +436,7 @@ def test_run_install_bad_args(self): exc_args = exc_info.value.args assert exc_args == ("At least one argument required to run_install().",) - def test_run_no_install_passthrough(self): + def test_run_no_install_passthrough(self) -> None: session, runner = self.make_session_and_runner() runner.venv = nox.virtualenv.PassthroughEnv() runner.global_config.no_install = True @@ -425,22 +444,22 @@ def test_run_no_install_passthrough(self): session.install("numpy") session.conda_install("numpy") - def test_run_no_conda_install(self): + def test_run_no_conda_install(self) -> None: session, runner = self.make_session_and_runner() with pytest.raises(ValueError, match="A session without a conda"): session.conda_install("numpy") - def test_run_install_success(self): + def test_run_install_success(self) -> None: session, _ = self.make_session_and_runner() - assert session.run_install(operator.add, 1300, 37) == 1337 + assert session.run_install(operator.add, 1300, 37) == 1337 # type: ignore[arg-type] - def test_run_install_install_only(self, caplog): + def test_run_install_install_only(self, caplog: pytest.LogCaptureFixture) -> None: session, runner = self.make_session_and_runner() runner.global_config.install_only = True - assert session.run_install(operator.add, 23, 19) == 42 + assert session.run_install(operator.add, 23, 19) == 42 # type: ignore[arg-type] @pytest.mark.parametrize( ( @@ -458,11 +477,11 @@ def test_run_install_install_only(self, caplog): ) def test_run_shutdown_process_timeouts( self, - interrupt_timeout_setting, - terminate_timeout_setting, - interrupt_timeout_expected, - terminate_timeout_expected, - ): + 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, + ) -> None: session, runner = self.make_session_and_runner() runner.venv = nox.virtualenv.PassthroughEnv() @@ -477,14 +496,14 @@ def test_run_shutdown_process_timeouts( ): shutdown_process.return_value = ("", "") - timeout_kwargs = {} + timeout_kwargs: dict[str, None | float] = {} if interrupt_timeout_setting != "default": timeout_kwargs["interrupt_timeout"] = interrupt_timeout_setting if terminate_timeout_setting != "default": timeout_kwargs["terminate_timeout"] = terminate_timeout_setting with pytest.raises(KeyboardInterrupt): - session.run(sys.executable, "--version", **timeout_kwargs) + session.run(sys.executable, "--version", **timeout_kwargs) # type: ignore[arg-type] shutdown_process.assert_called_once_with( proc=mock.ANY, @@ -503,10 +522,11 @@ def test_run_shutdown_process_timeouts( ) @pytest.mark.parametrize("run_install_func", ["run_always", "run_install"]) def test_run_install_no_install( - self, no_install, reused, run_called, run_install_func - ): + self, no_install: bool, reused: bool, run_called: bool, run_install_func: str + ) -> None: session, runner = self.make_session_and_runner() runner.global_config.no_install = no_install + assert runner.venv runner.venv._reused = reused with mock.patch.object(nox.command, "run") as run: @@ -515,31 +535,34 @@ def test_run_install_no_install( assert run.called is run_called - def test_conda_install_bad_args(self): + def test_conda_install_bad_args(self) -> None: session, runner = self.make_session_and_runner() runner.venv = mock.create_autospec(nox.virtualenv.CondaEnv) + assert runner.venv runner.venv.location = "dummy" with pytest.raises(ValueError, match="arg"): session.conda_install() - def test_conda_install_bad_args_odd_nb_double_quotes(self): + def test_conda_install_bad_args_odd_nb_double_quotes(self) -> None: session, runner = self.make_session_and_runner() runner.venv = mock.create_autospec(nox.virtualenv.CondaEnv) + assert runner.venv runner.venv.location = "./not/a/location" with pytest.raises(ValueError, match="odd number of quotes"): session.conda_install('a"a') - def test_conda_install_bad_args_cannot_escape(self): + def test_conda_install_bad_args_cannot_escape(self) -> None: session, runner = self.make_session_and_runner() runner.venv = mock.create_autospec(nox.virtualenv.CondaEnv) + assert runner.venv runner.venv.location = "./not/a/location" with pytest.raises(ValueError, match="Cannot escape"): session.conda_install('a"o" None: session, runner = self.make_session_and_runner() runner.venv = None @@ -557,7 +580,9 @@ def test_conda_install_not_a_condaenv(self): ["", "conda-forge", ["conda-forge", "bioconda"]], ids=["default", "conda-forge", "bioconda"], ) - def test_conda_install(self, auto_offline, offline, conda, channel): + def test_conda_install( + self, auto_offline: bool, offline: bool, conda: str, channel: str | list[str] + ) -> None: runner = nox.sessions.SessionRunner( name="test", signatures=["test"], @@ -566,10 +591,11 @@ def test_conda_install(self, auto_offline, offline, conda, channel): manifest=mock.create_autospec(nox.manifest.Manifest), ) runner.venv = mock.create_autospec(nox.virtualenv.CondaEnv) + assert runner.venv runner.venv.location = "/path/to/conda/env" runner.venv.env = {} - runner.venv.is_offline = lambda: offline - runner.venv.conda_cmd = conda + runner.venv.is_offline = lambda: offline # type: ignore[attr-defined] + runner.venv.conda_cmd = conda # type: ignore[attr-defined] class SessionNoSlots(nox.sessions.Session): pass @@ -606,14 +632,17 @@ class SessionNoSlots(nox.sessions.Session): (False, False, True), ], ) - def test_conda_venv_reused_with_no_install(self, no_install, reused, run_called): + def test_conda_venv_reused_with_no_install( + self, no_install: bool, reused: bool, run_called: bool + ) -> None: session, runner = self.make_session_and_runner() runner.venv = mock.create_autospec(nox.virtualenv.CondaEnv) + assert runner.venv runner.venv.location = "/path/to/conda/env" runner.venv.env = {} - runner.venv.is_offline = lambda: True - runner.venv.conda_cmd = "conda" + runner.venv.is_offline = lambda: True # type: ignore[attr-defined] + runner.venv.conda_cmd = "conda" # type: ignore[attr-defined] runner.global_config.no_install = no_install runner.venv._reused = reused @@ -628,7 +657,7 @@ def test_conda_venv_reused_with_no_install(self, no_install, reused, run_called) ["no", "yes", "already_dbl_quoted"], ids="version_constraint={}".format, ) - def test_conda_install_non_default_kwargs(self, version_constraint): + def test_conda_install_non_default_kwargs(self, version_constraint: str) -> None: runner = nox.sessions.SessionRunner( name="test", signatures=["test"], @@ -637,10 +666,11 @@ def test_conda_install_non_default_kwargs(self, version_constraint): manifest=mock.create_autospec(nox.manifest.Manifest), ) runner.venv = mock.create_autospec(nox.virtualenv.CondaEnv) + assert runner.venv runner.venv.location = "/path/to/conda/env" runner.venv.env = {} - runner.venv.is_offline = lambda: False - runner.venv.conda_cmd = "conda" + runner.venv.is_offline = lambda: False # type: ignore[attr-defined] + runner.venv.conda_cmd = "conda" # type: ignore[attr-defined] class SessionNoSlots(nox.sessions.Session): pass @@ -671,13 +701,13 @@ class SessionNoSlots(nox.sessions.Session): **_run_with_defaults(silent=False, external="error"), ) - def test_install_bad_args_no_arg(self): + def test_install_bad_args_no_arg(self) -> None: session, _ = self.make_session_and_runner() with pytest.raises(ValueError, match="arg"): session.install() - def test_install_not_a_virtualenv(self): + def test_install_not_a_virtualenv(self) -> None: session, runner = self.make_session_and_runner() runner.venv = None @@ -685,7 +715,7 @@ def test_install_not_a_virtualenv(self): with pytest.raises(ValueError, match="virtualenv"): session.install() - def test_install(self): + def test_install(self) -> None: runner = nox.sessions.SessionRunner( name="test", signatures=["test"], @@ -694,8 +724,9 @@ def test_install(self): manifest=mock.create_autospec(nox.manifest.Manifest), ) runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv) + assert runner.venv runner.venv.env = {} - runner.venv.venv_backend = "venv" + runner.venv.venv_backend = "venv" # type: ignore[misc] class SessionNoSlots(nox.sessions.Session): pass @@ -716,7 +747,7 @@ class SessionNoSlots(nox.sessions.Session): **_run_with_defaults(silent=True, external="error"), ) - def test_install_non_default_kwargs(self): + def test_install_non_default_kwargs(self) -> None: runner = nox.sessions.SessionRunner( name="test", signatures=["test"], @@ -725,8 +756,9 @@ def test_install_non_default_kwargs(self): manifest=mock.create_autospec(nox.manifest.Manifest), ) runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv) + assert runner.venv runner.venv.env = {} - runner.venv.venv_backend = "venv" + runner.venv.venv_backend = "venv" # type: ignore[misc] class SessionNoSlots(nox.sessions.Session): pass @@ -745,7 +777,7 @@ class SessionNoSlots(nox.sessions.Session): **_run_with_defaults(silent=False, external="error"), ) - def test_install_no_venv_failure(self): + def test_install_no_venv_failure(self) -> None: runner = nox.sessions.SessionRunner( name="test", signatures=["test"], @@ -754,6 +786,7 @@ def test_install_no_venv_failure(self): manifest=mock.create_autospec(nox.manifest.Manifest), ) runner.venv = mock.create_autospec(nox.virtualenv.PassthroughEnv) + assert runner.venv runner.venv.env = {} class SessionNoSlots(nox.sessions.Session): @@ -770,27 +803,29 @@ class SessionNoSlots(nox.sessions.Session): ): session.install("requests", "urllib3") - def test_notify(self): + def test_notify(self) -> None: session, runner = self.make_session_and_runner() session.notify("other") - runner.manifest.notify.assert_called_once_with("other", None) + runner.manifest.notify.assert_called_once_with("other", None) # type: ignore[attr-defined] session.notify("other", posargs=["--an-arg"]) - runner.manifest.notify.assert_called_with("other", ["--an-arg"]) + runner.manifest.notify.assert_called_with("other", ["--an-arg"]) # type: ignore[attr-defined] - def test_posargs_are_not_shared_between_sessions(self, monkeypatch, tmp_path): - registry = {} + def test_posargs_are_not_shared_between_sessions( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + registry: dict[str, nox._decorators.Func] = {} monkeypatch.setattr("nox.registry._REGISTRY", registry) @nox.session(venv_backend="none") - def test(session): + def test(session: nox.Session) -> None: session.posargs.extend(["-x"]) @nox.session(venv_backend="none") - def lint(session): + def lint(session: nox.Session) -> None: if "-x" in session.posargs: raise RuntimeError("invalid option: -x") @@ -800,7 +835,7 @@ def lint(session): assert manifest["test"].execute() assert manifest["lint"].execute() - def test_log(self, caplog): + def test_log(self, caplog: pytest.LogCaptureFixture) -> None: caplog.set_level(logging.INFO) session, _ = self.make_session_and_runner() @@ -808,7 +843,7 @@ def test_log(self, caplog): assert "meep" in caplog.text - def test_warn(self, caplog): + def test_warn(self, caplog: pytest.LogCaptureFixture) -> None: caplog.set_level(logging.WARNING) session, _ = self.make_session_and_runner() @@ -816,7 +851,7 @@ def test_warn(self, caplog): assert "meep" in caplog.text - def test_debug(self, caplog): + def test_debug(self, caplog: pytest.LogCaptureFixture) -> None: caplog.set_level(logging.DEBUG) session, _ = self.make_session_and_runner() @@ -824,20 +859,20 @@ def test_debug(self, caplog): assert "meep" in caplog.text - def test_error(self, caplog): + def test_error(self, caplog: pytest.LogCaptureFixture) -> None: caplog.set_level(logging.ERROR) session, _ = self.make_session_and_runner() with pytest.raises(nox.sessions._SessionQuit, match="meep"): session.error("meep") - def test_error_no_log(self): + def test_error_no_log(self) -> None: session, _ = self.make_session_and_runner() with pytest.raises(nox.sessions._SessionQuit): session.error() - def test_skip_no_log(self): + def test_skip_no_log(self) -> None: session, _ = self.make_session_and_runner() with pytest.raises(nox.sessions._SessionSkip): @@ -852,9 +887,12 @@ def test_skip_no_log(self): (False, False, True), ], ) - def test_session_venv_reused_with_no_install(self, no_install, reused, run_called): + def test_session_venv_reused_with_no_install( + self, no_install: bool, reused: bool, run_called: bool + ) -> None: session, runner = self.make_session_and_runner() runner.global_config.no_install = no_install + assert runner.venv runner.venv._reused = reused with mock.patch.object(nox.command, "run") as run: @@ -862,7 +900,7 @@ def test_session_venv_reused_with_no_install(self, no_install, reused, run_calle assert run.called is run_called - def test_install_uv(self): + def test_install_uv(self) -> None: runner = nox.sessions.SessionRunner( name="test", signatures=["test"], @@ -871,8 +909,9 @@ def test_install_uv(self): manifest=mock.create_autospec(nox.manifest.Manifest), ) runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv) + assert runner.venv runner.venv.env = {} - runner.venv.venv_backend = "uv" + runner.venv.venv_backend = "uv" # type: ignore[misc] class SessionNoSlots(nox.sessions.Session): pass @@ -890,7 +929,7 @@ class SessionNoSlots(nox.sessions.Session): **_run_with_defaults(silent=False, external="error"), ) - def test_install_uv_command(self, monkeypatch): + def test_install_uv_command(self, monkeypatch: pytest.MonkeyPatch) -> None: runner = nox.sessions.SessionRunner( name="test", signatures=["test"], @@ -899,8 +938,9 @@ def test_install_uv_command(self, monkeypatch): manifest=mock.create_autospec(nox.manifest.Manifest), ) runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv) + assert runner.venv runner.venv.env = {} - runner.venv.venv_backend = "uv" + runner.venv.venv_backend = "uv" # type: ignore[misc] class SessionNoSlots(nox.sessions.Session): pass @@ -941,27 +981,27 @@ class SessionNoSlots(nox.sessions.Session): "urllib3", ) - def test___slots__(self): + def test___slots__(self) -> None: session, _ = self.make_session_and_runner() with pytest.raises(AttributeError): - session.foo = "bar" + session.foo = "bar" # type: ignore[attr-defined] with pytest.raises(AttributeError): - session.quux # noqa: B018 + session.quux # type: ignore[attr-defined] # noqa: B018 - def test___dict__(self): + def test___dict__(self) -> None: session, _ = self.make_session_and_runner() expected = {name: getattr(session, name) for name in session.__slots__} assert session.__dict__ == expected - def test_first_arg_list(self): + def test_first_arg_list(self) -> None: session, _ = self.make_session_and_runner() with pytest.raises(ValueError): - session.run(["ls", "-al"]) + session.run(["ls", "-al"]) # type: ignore[arg-type] class TestSessionRunner: - def make_runner(self): + def make_runner(self) -> nox.sessions.SessionRunner: func = mock.Mock() func.python = None func.venv_backend = None @@ -981,7 +1021,7 @@ def make_runner(self): manifest=mock.create_autospec(nox.manifest.Manifest), ) - def test_properties(self): + def test_properties(self) -> None: runner = self.make_runner() assert runner.name == "test" @@ -992,23 +1032,23 @@ def test_properties(self): assert runner.global_config.posargs == [] assert isinstance(runner.manifest, nox.manifest.Manifest) - def test_str_and_friendly_name(self): + def test_str_and_friendly_name(self) -> None: runner = self.make_runner() runner.signatures = ["test(1, 2)", "test(3, 4)"] assert str(runner) == "Session(name=test, signatures=test(1, 2), test(3, 4))" assert runner.friendly_name == "test(1, 2)" - def test_description_property_one_line(self): - def foo(): + def test_description_property_one_line(self) -> None: + def foo() -> None: """Just one line""" runner = self.make_runner() - runner.func = foo + runner.func = foo # type: ignore[assignment] assert runner.description == "Just one line" - def test_description_property_multi_line(self): - def foo(): + def test_description_property_multi_line(self) -> None: + def foo() -> None: """ Multiline @@ -1016,18 +1056,18 @@ def foo(): """ runner = self.make_runner() - runner.func = foo + runner.func = foo # type: ignore[assignment] assert runner.description == "Multiline" - def test_description_property_no_doc(self): - def foo(): + def test_description_property_no_doc(self) -> None: + def foo() -> None: pass runner = self.make_runner() - runner.func = foo + runner.func = foo # type: ignore[assignment] assert runner.description is None - def test__create_venv_process_env(self): + def test__create_venv_process_env(self) -> None: runner = self.make_runner() runner.func.python = False @@ -1036,7 +1076,7 @@ def test__create_venv_process_env(self): assert isinstance(runner.venv, nox.virtualenv.ProcessEnv) @mock.patch("nox.virtualenv.VirtualEnv.create", autospec=True) - def test__create_venv(self, create): + def test__create_venv(self, create: mock.Mock) -> None: runner = self.make_runner() runner._create_venv() @@ -1065,7 +1105,13 @@ def test__create_venv(self, create): ), ], ) - def test__create_venv_options(self, create_method, venv_backend, expected_backend): + def test__create_venv_options( + self, + create_method: str, + venv_backend: None | str, + expected_backend: type[nox.virtualenv.VirtualEnv] + | type[nox.virtualenv.CondaEnv], + ) -> None: runner = self.make_runner() runner.func.python = "coolpython" runner.func.reuse_venv = True @@ -1076,10 +1122,10 @@ def test__create_venv_options(self, create_method, venv_backend, expected_backen create.assert_called_once_with(runner.venv) assert isinstance(runner.venv, expected_backend) - assert runner.venv.interpreter == "coolpython" - assert runner.venv.reuse_existing is True + assert runner.venv.interpreter == "coolpython" # type: ignore[union-attr] + assert runner.venv.reuse_existing is True # type: ignore[union-attr] - def test__create_venv_unexpected_venv_backend(self): + def test__create_venv_unexpected_venv_backend(self) -> None: runner = self.make_runner() runner.func.venv_backend = "somenewenvtool" with pytest.raises(ValueError, match="venv_backend"): @@ -1089,7 +1135,9 @@ def test__create_venv_unexpected_venv_backend(self): "venv_backend", ["uv|virtualenv", "conda|virtualenv", "mamba|conda|venv"], ) - def test_fallback_venv(self, venv_backend, monkeypatch): + def test_fallback_venv( + self, venv_backend: str, monkeypatch: pytest.MonkeyPatch + ) -> None: runner = self.make_runner() runner.func.venv_backend = venv_backend monkeypatch.setattr( @@ -1099,6 +1147,7 @@ def test_fallback_venv(self, venv_backend, monkeypatch): ) with mock.patch("nox.virtualenv.VirtualEnv.create", autospec=True): runner._create_venv() + assert runner.venv assert runner.venv.venv_backend == venv_backend.split("|")[-1] @pytest.mark.parametrize( @@ -1110,7 +1159,9 @@ def test_fallback_venv(self, venv_backend, monkeypatch): "conda|mamba", ], ) - def test_invalid_fallback_venv(self, venv_backend, monkeypatch): + def test_invalid_fallback_venv( + self, venv_backend: str, monkeypatch: pytest.MonkeyPatch + ) -> None: runner = self.make_runner() runner.func.venv_backend = venv_backend monkeypatch.setattr( @@ -1140,27 +1191,30 @@ def test_invalid_fallback_venv(self, venv_backend, monkeypatch): ("never", True, False), ], ) - def test__reuse_venv_outcome(self, reuse_venv, reuse_venv_func, should_reuse): + def test__reuse_venv_outcome( + self, reuse_venv: str, reuse_venv_func: bool | None, should_reuse: bool + ) -> None: runner = self.make_runner() runner.func.reuse_venv = reuse_venv_func runner.global_config.reuse_venv = reuse_venv assert runner.reuse_existing_venv() == should_reuse - def test__reuse_venv_invalid(self): + def test__reuse_venv_invalid(self) -> None: runner = self.make_runner() runner.global_config.reuse_venv = True msg = "nox.options.reuse_venv must be set to 'always', 'never', 'no', or 'yes', got True!" with pytest.raises(AttributeError, match=re.escape(msg)): runner.reuse_existing_venv() - def make_runner_with_mock_venv(self): + def make_runner_with_mock_venv(self) -> nox.sessions.SessionRunner: runner = self.make_runner() - runner._create_venv = mock.Mock() + runner._create_venv = mock.Mock() # type: ignore[method-assign] runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv) + assert runner.venv runner.venv.env = {} return runner - def test_execute_noop_success(self, caplog): + def test_execute_noop_success(self, caplog: pytest.LogCaptureFixture) -> None: caplog.set_level(logging.DEBUG) runner = self.make_runner_with_mock_venv() @@ -1168,122 +1222,130 @@ def test_execute_noop_success(self, caplog): result = runner.execute() assert result - runner.func.assert_called_once_with(mock.ANY) + runner.func.assert_called_once_with(mock.ANY) # type: ignore[attr-defined] assert "Running session test(1, 2)" in caplog.text - def test_execute_quit(self): + def test_execute_quit(self) -> None: runner = self.make_runner_with_mock_venv() - def func(session): + def func(session: nox.Session) -> None: session.error("meep") - func.requires = [] - runner.func = func + func.requires = [] # type: ignore[attr-defined] + runner.func = func # type: ignore[assignment] result = runner.execute() assert result.status == nox.sessions.Status.ABORTED - def test_execute_skip(self): + def test_execute_skip(self) -> None: runner = self.make_runner_with_mock_venv() - def func(session): + def func(session: nox.Session) -> None: session.skip("meep") - func.requires = [] - runner.func = func + func.requires = [] # type: ignore[attr-defined] + runner.func = func # type: ignore[assignment] result = runner.execute() assert result.status == nox.sessions.Status.SKIPPED - def test_execute_with_manifest_null_session_func(self): + def test_execute_with_manifest_null_session_func(self) -> None: runner = self.make_runner() runner.func = nox.manifest._null_session_func result = runner.execute() assert result.status == nox.sessions.Status.SKIPPED + assert result.reason assert "no parameters" in result.reason - def test_execute_skip_missing_interpreter(self, caplog, monkeypatch): + def test_execute_skip_missing_interpreter( + self, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch + ) -> None: # Important to have this first here as the runner will pick it up # to set default for --error-on-missing-interpreters monkeypatch.delenv("CI", raising=False) runner = self.make_runner_with_mock_venv() - runner._create_venv.side_effect = nox.virtualenv.InterpreterNotFound("meep") + runner._create_venv.side_effect = nox.virtualenv.InterpreterNotFound("meep") # type: ignore[attr-defined] result = runner.execute() assert result.status == nox.sessions.Status.SKIPPED + assert result.reason assert "meep" in result.reason assert ( "Missing interpreters will error by default on CI systems." in caplog.text ) - def test_execute_missing_interpreter_on_CI(self, monkeypatch): + def test_execute_missing_interpreter_on_CI( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: monkeypatch.setenv("CI", "True") runner = self.make_runner_with_mock_venv() - runner._create_venv.side_effect = nox.virtualenv.InterpreterNotFound("meep") + runner._create_venv.side_effect = nox.virtualenv.InterpreterNotFound("meep") # type: ignore[attr-defined] result = runner.execute() assert result.status == nox.sessions.Status.FAILED + assert result.reason assert "meep" in result.reason - def test_execute_error_missing_interpreter(self): + def test_execute_error_missing_interpreter(self) -> None: runner = self.make_runner_with_mock_venv() runner.global_config.error_on_missing_interpreters = True - runner._create_venv.side_effect = nox.virtualenv.InterpreterNotFound("meep") + runner._create_venv.side_effect = nox.virtualenv.InterpreterNotFound("meep") # type: ignore[attr-defined] result = runner.execute() assert result.status == nox.sessions.Status.FAILED + assert result.reason assert "meep" in result.reason - def test_execute_failed(self): + def test_execute_failed(self) -> None: runner = self.make_runner_with_mock_venv() - def func(session): + def func(session: nox.Session) -> None: raise nox.command.CommandFailed() - func.requires = [] - runner.func = func + func.requires = [] # type: ignore[attr-defined] + runner.func = func # type: ignore[assignment] result = runner.execute() assert result.status == nox.sessions.Status.FAILED - def test_execute_interrupted(self): + def test_execute_interrupted(self) -> None: runner = self.make_runner_with_mock_venv() - def func(session): + def func(session: nox.Session) -> None: raise KeyboardInterrupt() - func.requires = [] - runner.func = func + func.requires = [] # type: ignore[attr-defined] + runner.func = func # type: ignore[assignment] with pytest.raises(KeyboardInterrupt): runner.execute() - def test_execute_exception(self): + def test_execute_exception(self) -> None: runner = self.make_runner_with_mock_venv() - def func(session): + def func(session: nox.Session) -> None: raise ValueError("meep") - func.requires = [] - runner.func = func + func.requires = [] # type: ignore[attr-defined] + runner.func = func # type: ignore[assignment] result = runner.execute() assert result.status == nox.sessions.Status.FAILED - def test_execute_check_env(self): + def test_execute_check_env(self) -> None: runner = self.make_runner_with_mock_venv() - def func(session): + def func(session: nox.Session) -> None: session.run( sys.executable, "-c", @@ -1291,8 +1353,8 @@ def func(session): f' os.environ["NOX_CURRENT_SESSION"] == {session.name!r} else 0)', ) - func.requires = [] - runner.func = func + func.requires = [] # type: ignore[attr-defined] + runner.func = func # type: ignore[assignment] result = runner.execute() @@ -1300,59 +1362,83 @@ def func(session): class TestResult: - def test_init(self): + def test_init(self) -> None: result = nox.sessions.Result( session=mock.sentinel.SESSION, status=mock.sentinel.STATUS ) assert result.session == mock.sentinel.SESSION assert result.status == mock.sentinel.STATUS - def test__bool_true(self): + def test__bool_true(self) -> None: for status in (nox.sessions.Status.SUCCESS, nox.sessions.Status.SKIPPED): - result = nox.sessions.Result(session=object(), status=status) + result = nox.sessions.Result( + session=typing.cast(nox.sessions.SessionRunner, object()), status=status + ) assert bool(result) assert result.__bool__() assert result.__nonzero__() - def test__bool_false(self): + def test__bool_false(self) -> None: for status in (nox.sessions.Status.FAILED, nox.sessions.Status.ABORTED): - result = nox.sessions.Result(session=object(), status=status) + result = nox.sessions.Result( + 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): - result = nox.sessions.Result(object(), nox.sessions.Status.SUCCESS) + def test__imperfect(self) -> None: + result = nox.sessions.Result( + typing.cast(nox.sessions.SessionRunner, object()), + nox.sessions.Status.SUCCESS, + ) assert result.imperfect == "was successful" - result = nox.sessions.Result(object(), nox.sessions.Status.FAILED) + result = nox.sessions.Result( + typing.cast(nox.sessions.SessionRunner, object()), + nox.sessions.Status.FAILED, + ) assert result.imperfect == "failed" result = nox.sessions.Result( - object(), nox.sessions.Status.FAILED, reason="meep" + typing.cast(nox.sessions.SessionRunner, object()), + nox.sessions.Status.FAILED, + reason="meep", ) assert result.imperfect == "failed: meep" - def test__log_success(self): - result = nox.sessions.Result(object(), nox.sessions.Status.SUCCESS) + def test__log_success(self) -> None: + result = nox.sessions.Result( + typing.cast(nox.sessions.SessionRunner, object()), + nox.sessions.Status.SUCCESS, + ) with mock.patch.object(logger, "success") as success: result.log("foo") success.assert_called_once_with("foo") - def test__log_warning(self): - result = nox.sessions.Result(object(), nox.sessions.Status.SKIPPED) + def test__log_warning(self) -> None: + result = nox.sessions.Result( + typing.cast(nox.sessions.SessionRunner, object()), + nox.sessions.Status.SKIPPED, + ) with mock.patch.object(logger, "warning") as warning: result.log("foo") warning.assert_called_once_with("foo") - def test__log_error(self): - result = nox.sessions.Result(object(), nox.sessions.Status.FAILED) + def test__log_error(self) -> None: + result = nox.sessions.Result( + typing.cast(nox.sessions.SessionRunner, object()), + nox.sessions.Status.FAILED, + ) with mock.patch.object(logger, "error") as error: result.log("foo") error.assert_called_once_with("foo") - def test__serialize(self): + def test__serialize(self) -> None: result = nox.sessions.Result( - session=argparse.Namespace( - signatures=["siggy"], name="namey", func=mock.Mock() + session=typing.cast( + nox.sessions.SessionRunner, + argparse.Namespace( + signatures=["siggy"], name="namey", func=mock.Mock() + ), ), status=nox.sessions.Status.SUCCESS, ) diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 67b8e9be..67457fde 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -20,22 +20,30 @@ import json 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 import pytest import nox +import nox._decorators from nox import _options, sessions, tasks from nox.manifest import WARN_PYTHONS_IGNORED, Manifest RESOURCES = os.path.join(os.path.dirname(__file__), "resources") -def session_func(): +def session_func_raw() -> None: pass +session_func = typing.cast(nox._decorators.Func, session_func_raw) + + session_func.python = None session_func.venv_backend = None session_func.should_warn = {} @@ -44,32 +52,43 @@ def session_func(): session_func.requires = [] -def session_func_with_python(): +def session_func_with_python_raw() -> None: pass +session_func_with_python = typing.cast( + nox._decorators.Func, session_func_with_python_raw +) + + session_func_with_python.python = "3.8" session_func_with_python.venv_backend = None session_func_with_python.default = True session_func_with_python.requires = [] -def session_func_venv_pythons_warning(): +def session_func_venv_pythons_warning_raw() -> None: pass +session_func_venv_pythons_warning = typing.cast( + nox._decorators.Func, session_func_venv_pythons_warning_raw +) + + session_func_venv_pythons_warning.python = ["3.7"] session_func_venv_pythons_warning.venv_backend = "none" session_func_venv_pythons_warning.should_warn = {WARN_PYTHONS_IGNORED: ["3.7"]} -def test_load_nox_module(): +def test_load_nox_module() -> None: config = _options.options.namespace(noxfile=os.path.join(RESOURCES, "noxfile.py")) noxfile_module = tasks.load_nox_module(config) + assert not isinstance(noxfile_module, int) assert noxfile_module.SIGIL == "123" -def test_load_nox_module_expandvars(): +def test_load_nox_module_expandvars() -> None: # Assert that variables are expanded when looking up the path to the Noxfile # This is particular importand in Windows when one needs to use variables like # %TEMP% to point to the noxfile.py @@ -79,11 +98,14 @@ def test_load_nox_module_expandvars(): else: config = _options.options.namespace(noxfile="${RESOURCES_PATH}/noxfile.py") noxfile_module = tasks.load_nox_module(config) + assert not isinstance(noxfile_module, int) assert noxfile_module.__file__ == os.path.join(RESOURCES, "noxfile.py") assert noxfile_module.SIGIL == "123" -def test_load_nox_module_not_found(caplog, tmp_path): +def test_load_nox_module_not_found( + caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: bogus_noxfile = tmp_path / "bogus.py" config = _options.options.namespace(noxfile=str(bogus_noxfile)) @@ -93,7 +115,7 @@ def test_load_nox_module_not_found(caplog, tmp_path): ) -def test_load_nox_module_os_error(caplog): +def test_load_nox_module_os_error(caplog: pytest.LogCaptureFixture) -> None: noxfile = os.path.join(RESOURCES, "noxfile.py") config = _options.options.namespace(noxfile=noxfile) with mock.patch("nox.tasks.check_nox_version", autospec=True) as version_checker: @@ -103,7 +125,7 @@ def test_load_nox_module_os_error(caplog): @pytest.fixture -def reset_needs_version(): +def reset_needs_version() -> Generator[None, None, None]: """Do not leak ``nox.needs_version`` between tests.""" try: yield @@ -112,11 +134,13 @@ def reset_needs_version(): @pytest.fixture -def reset_global_nox_options(): +def reset_global_nox_options() -> None: nox.options = _options.options.noxfile_namespace() -def test_load_nox_module_needs_version_static(reset_needs_version, tmp_path): +def test_load_nox_module_needs_version_static( + reset_needs_version: None, tmp_path: Path +) -> None: text = dedent( """ import nox @@ -129,7 +153,9 @@ def test_load_nox_module_needs_version_static(reset_needs_version, tmp_path): assert tasks.load_nox_module(config) == 2 -def test_load_nox_module_needs_version_dynamic(reset_needs_version, tmp_path): +def test_load_nox_module_needs_version_dynamic( + reset_needs_version: None, tmp_path: Path +) -> None: text = dedent( """ import nox @@ -145,26 +171,29 @@ def test_load_nox_module_needs_version_dynamic(reset_needs_version, tmp_path): assert nox.needs_version == ">=9999.99.99" -def test_discover_session_functions_decorator(): +def test_discover_session_functions_decorator() -> None: # Define sessions using the decorator. @nox.session - def foo(): + def foo() -> None: pass @nox.session - def bar(): + def bar() -> None: pass @nox.session(name="not-a-bar") - def not_a_bar(): + def not_a_bar() -> None: pass - def notasession(): + def notasession() -> None: pass # Mock up a noxfile.py module and configuration. - mock_module = argparse.Namespace( - __name__=foo.__module__, foo=foo, bar=bar, notasession=notasession + mock_module = typing.cast( + ModuleType, + argparse.Namespace( + __name__=foo.__module__, foo=foo, bar=bar, notasession=notasession + ), ) config = _options.options.namespace(sessions=(), keywords=(), posargs=[]) @@ -175,7 +204,7 @@ def notasession(): assert [i.friendly_name for i in sessions] == ["foo", "bar", "not-a-bar"] -def test_filter_manifest(): +def test_filter_manifest() -> None: config = _options.options.namespace( sessions=None, pythons=(), keywords=(), posargs=[] ) @@ -185,7 +214,7 @@ def test_filter_manifest(): assert len(manifest) == 2 -def test_filter_manifest_not_found(): +def test_filter_manifest_not_found() -> None: config = _options.options.namespace( sessions=("baz",), pythons=(), keywords=(), posargs=[] ) @@ -194,7 +223,7 @@ def test_filter_manifest_not_found(): assert return_value == 3 -def test_filter_manifest_pythons(): +def test_filter_manifest_pythons() -> None: config = _options.options.namespace( sessions=None, pythons=("3.8",), keywords=(), posargs=[] ) @@ -207,7 +236,7 @@ def test_filter_manifest_pythons(): assert len(manifest) == 1 -def test_filter_manifest_pythons_not_found(caplog): +def test_filter_manifest_pythons_not_found(caplog: pytest.LogCaptureFixture) -> None: config = _options.options.namespace( sessions=None, pythons=("1.2",), keywords=(), posargs=[] ) @@ -220,7 +249,7 @@ def test_filter_manifest_pythons_not_found(caplog): assert "Python version selection caused no sessions to be selected." in caplog.text -def test_filter_manifest_keywords(): +def test_filter_manifest_keywords() -> None: config = _options.options.namespace( sessions=None, pythons=(), keywords="foo or bar", posargs=[] ) @@ -232,7 +261,7 @@ def test_filter_manifest_keywords(): assert len(manifest) == 2 -def test_filter_manifest_keywords_not_found(caplog): +def test_filter_manifest_keywords_not_found(caplog: pytest.LogCaptureFixture) -> None: config = _options.options.namespace( sessions=None, pythons=(), keywords="mouse or python", posargs=[] ) @@ -244,7 +273,7 @@ def test_filter_manifest_keywords_not_found(caplog): assert "No sessions selected after filtering by keyword." in caplog.text -def test_filter_manifest_keywords_syntax_error(): +def test_filter_manifest_keywords_syntax_error() -> None: config = _options.options.namespace( sessions=None, pythons=(), keywords="foo:bar", posargs=[] ) @@ -266,27 +295,34 @@ def test_filter_manifest_keywords_syntax_error(): (["foo", "bar", "baz"], 8), ], ) -def test_filter_manifest_tags(tags, session_count): +def test_filter_manifest_tags( + tags: None | builtins.list[builtins.str], + session_count: builtins.int + | builtins.int + | builtins.int + | builtins.int + | builtins.int, +) -> None: @nox.session(tags=["foo"]) - def qux(): + def qux() -> None: pass @nox.session(tags=["bar"]) - def quux(): + def quux() -> None: pass @nox.session(tags=["foo", "bar"]) - def quuz(): + def quuz() -> None: pass @nox.session(tags=["foo", "bar", "baz"]) - def corge(): + def corge() -> None: pass @nox.session(tags=["foo"]) @nox.parametrize("a", [1, nox.param(2, tags=["bar"])]) @nox.parametrize("b", [3, 4], tags=[["baz"]]) - def grault(): + def grault() -> None: pass config = _options.options.namespace( @@ -318,9 +354,11 @@ def grault(): "tag-does-not-exist", ], ) -def test_filter_manifest_tags_not_found(tags, caplog): +def test_filter_manifest_tags_not_found( + tags: list[str], caplog: pytest.LogCaptureFixture +) -> None: @nox.session(tags=["foo"]) - def quux(): + def quux() -> None: pass config = _options.options.namespace( @@ -332,13 +370,15 @@ def quux(): assert "Tag selection caused no sessions to be selected." in caplog.text -def test_merge_sessions_and_tags(reset_global_nox_options, generate_noxfile_options): +def test_merge_sessions_and_tags( + reset_global_nox_options: None, generate_noxfile_options: Callable[..., str] +) -> None: @nox.session(tags=["foobar"]) - def test(): + def test() -> None: pass @nox.session(tags=["foobar"]) - def bar(): + def bar() -> None: pass noxfile_path = generate_noxfile_options(reuse_existing_virtualenvs=True) @@ -351,6 +391,7 @@ def bar(): ) nox_module = tasks.load_nox_module(config) + assert not isinstance(nox_module, int) tasks.merge_noxfile_options(nox_module, config) manifest = Manifest({"test": test, "bar": bar}, config) return_value = tasks.filter_manifest(manifest, config) @@ -359,21 +400,21 @@ def bar(): @pytest.mark.parametrize("selection", [None, ["qux"], ["quuz"], ["qux", "quuz"]]) -def test_default_false(selection): +def test_default_false(selection: None | builtins.list[builtins.str]) -> None: @nox.session() - def qux(): + def qux() -> None: pass @nox.session() - def quux(): + def quux() -> None: pass @nox.session(default=False) - def quuz(): + def quuz() -> None: pass @nox.session(default=False) - def corge(): + def corge() -> None: pass config = _options.options.namespace(sessions=selection, pythons=(), posargs=[]) @@ -392,9 +433,9 @@ def corge(): assert len(manifest) == expected -def test_honor_list_request_noop(): +def test_honor_list_request_noop() -> None: config = _options.options.namespace(list_sessions=False) - manifest = {"thing": mock.sentinel.THING} + manifest = typing.cast(Manifest, {"thing": mock.sentinel.THING}) return_value = tasks.honor_list_request(manifest, global_config=config) assert return_value is manifest @@ -408,7 +449,9 @@ def test_honor_list_request_noop(): ("Bar", "hello docstring"), ], ) -def test_honor_list_request(description, module_docstring): +def test_honor_list_request( + description: None | builtins.str, module_docstring: None | builtins.str +) -> None: config = _options.options.namespace( list_sessions=True, noxfile="noxfile.py", color=False ) @@ -421,7 +464,9 @@ def test_honor_list_request(description, module_docstring): assert return_value == 0 -def test_honor_list_request_skip_and_selected(capsys): +def test_honor_list_request_skip_and_selected( + capsys: pytest.CaptureFixture[builtins.str], +) -> None: config = _options.options.namespace( list_sessions=True, noxfile="noxfile.py", color=False ) @@ -440,7 +485,9 @@ def test_honor_list_request_skip_and_selected(capsys): assert "- bar" in out -def test_honor_list_request_prints_docstring_if_present(capsys): +def test_honor_list_request_prints_docstring_if_present( + capsys: pytest.CaptureFixture[builtins.str], +) -> None: config = _options.options.namespace( list_sessions=True, noxfile="noxfile.py", color=False ) @@ -459,7 +506,9 @@ def test_honor_list_request_prints_docstring_if_present(capsys): assert "Hello I'm a docstring" in out -def test_honor_list_request_doesnt_print_docstring_if_not_present(capsys): +def test_honor_list_request_doesnt_print_docstring_if_not_present( + capsys: pytest.CaptureFixture[builtins.str], +) -> None: config = _options.options.namespace( list_sessions=True, noxfile="noxfile.py", color=False ) @@ -478,7 +527,7 @@ def test_honor_list_request_doesnt_print_docstring_if_not_present(capsys): assert "Hello I'm a docstring" not in out -def test_honor_list_json_request(capsys): +def test_honor_list_json_request(capsys: pytest.CaptureFixture[builtins.str]) -> None: config = _options.options.namespace( list_sessions=True, noxfile="noxfile.py", json=True ) @@ -513,7 +562,7 @@ def test_honor_list_json_request(capsys): ] -def test_refuse_json_nolist_request(caplog): +def test_refuse_json_nolist_request(caplog: pytest.LogCaptureFixture) -> None: config = _options.options.namespace( list_sessions=False, noxfile="noxfile.py", json=True ) @@ -536,7 +585,9 @@ def test_refuse_json_nolist_request(caplog): assert record.message == "Must specify --list-sessions with --json" -def test_empty_session_list_in_noxfile(capsys): +def test_empty_session_list_in_noxfile( + capsys: pytest.CaptureFixture[builtins.str], +) -> None: config = _options.options.namespace(noxfile="noxfile.py", sessions=(), posargs=[]) manifest = Manifest({"session": session_func}, config) return_value = tasks.filter_manifest(manifest, global_config=config) @@ -544,28 +595,30 @@ def test_empty_session_list_in_noxfile(capsys): assert "No sessions selected." in capsys.readouterr().out -def test_empty_session_None_in_noxfile(capsys): +def test_empty_session_None_in_noxfile( + capsys: pytest.CaptureFixture[builtins.str], +) -> 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) assert return_value == manifest -def test_verify_manifest_empty(): +def test_verify_manifest_empty() -> None: config = _options.options.namespace(sessions=(), keywords=()) manifest = Manifest({}, config) return_value = tasks.filter_manifest(manifest, global_config=config) assert return_value == 3 -def test_verify_manifest_nonempty(): +def test_verify_manifest_nonempty() -> None: config = _options.options.namespace(sessions=None, keywords=(), posargs=[]) manifest = Manifest({"session": session_func}, config) return_value = tasks.filter_manifest(manifest, global_config=config) assert return_value == manifest -def test_verify_manifest_list(capsys): +def test_verify_manifest_list(capsys: pytest.CaptureFixture[builtins.str]) -> None: config = _options.options.namespace(sessions=(), keywords=(), posargs=[]) manifest = Manifest({"session": session_func}, config) return_value = tasks.filter_manifest(manifest, global_config=config) @@ -574,19 +627,19 @@ def test_verify_manifest_list(capsys): @pytest.mark.parametrize("with_warnings", [False, True], ids="with_warnings={}".format) -def test_run_manifest(with_warnings): +def test_run_manifest(with_warnings: builtins.bool) -> None: # Set up a valid manifest. config = _options.options.namespace(stop_on_first_error=False) sessions_ = [ - mock.Mock(spec=sessions.SessionRunner), - mock.Mock(spec=sessions.SessionRunner), + typing.cast(sessions.SessionRunner, mock.Mock(spec=sessions.SessionRunner)), + typing.cast(sessions.SessionRunner, mock.Mock(spec=sessions.SessionRunner)), ] manifest = Manifest({}, config) manifest._queue = copy.copy(sessions_) # Ensure each of the mocks returns a successful result for mock_session in sessions_: - mock_session.execute.return_value = sessions.Result( + mock_session.execute.return_value = sessions.Result( # type: ignore[attr-defined] session=mock_session, status=sessions.Status.SUCCESS ) # we need the should_warn attribute, add some func @@ -608,19 +661,19 @@ def test_run_manifest(with_warnings): assert result.status == sessions.Status.SUCCESS -def test_run_manifest_abort_on_first_failure(): +def test_run_manifest_abort_on_first_failure() -> None: # Set up a valid manifest. config = _options.options.namespace(stop_on_first_error=True) sessions_ = [ - mock.Mock(spec=sessions.SessionRunner), - mock.Mock(spec=sessions.SessionRunner), + typing.cast(sessions.SessionRunner, mock.Mock(spec=sessions.SessionRunner)), + typing.cast(sessions.SessionRunner, mock.Mock(spec=sessions.SessionRunner)), ] manifest = Manifest({}, config) manifest._queue = copy.copy(sessions_) # Ensure each of the mocks returns a successful result. for mock_session in sessions_: - mock_session.execute.return_value = sessions.Result( + mock_session.execute.return_value = sessions.Result( # type: ignore[attr-defined] session=mock_session, status=sessions.Status.FAILED ) # we need the should_warn attribute, add some func @@ -636,38 +689,42 @@ def test_run_manifest_abort_on_first_failure(): assert results[0].status == sessions.Status.FAILED # Verify that only the first session was called. - assert sessions_[0].execute.called - assert not sessions_[1].execute.called + assert sessions_[0].execute.called # type: ignore[attr-defined] + assert not sessions_[1].execute.called # type: ignore[attr-defined] -def test_print_summary_one_result(): +def test_print_summary_one_result() -> None: results = [mock.sentinel.RESULT] with mock.patch("nox.tasks.logger", autospec=True) as logger: - answer = tasks.print_summary(results, object()) + answer = tasks.print_summary(results, argparse.Namespace()) assert not logger.warning.called assert not logger.success.called assert not logger.error.called assert answer is results -def test_print_summary(): +def test_print_summary() -> None: results = [ sessions.Result( - session=argparse.Namespace(friendly_name="foo"), + session=typing.cast( + sessions.SessionRunner, argparse.Namespace(friendly_name="foo") + ), status=sessions.Status.SUCCESS, ), sessions.Result( - session=argparse.Namespace(friendly_name="bar"), + session=typing.cast( + sessions.SessionRunner, argparse.Namespace(friendly_name="bar") + ), status=sessions.Status.FAILED, ), ] with mock.patch.object(sessions.Result, "log", autospec=True) as log: - answer = tasks.print_summary(results, object()) + answer = tasks.print_summary(results, argparse.Namespace()) assert log.call_count == 2 assert answer is results -def test_create_report_noop(): +def test_create_report_noop() -> None: config = _options.options.namespace(report=None) with mock.patch.object(builtins, "open", autospec=True) as open_: results = tasks.create_report(mock.sentinel.RESULTS, config) @@ -675,12 +732,13 @@ def test_create_report_noop(): assert results is mock.sentinel.RESULTS -def test_create_report(): +def test_create_report() -> None: config = _options.options.namespace(report="/path/to/report") results = [ sessions.Result( - session=argparse.Namespace( - signatures=["foosig"], name="foo", func=object() + session=typing.cast( + sessions.SessionRunner, + argparse.Namespace(signatures=["foosig"], name="foo", func=object()), ), status=sessions.Status.SUCCESS, ) @@ -708,8 +766,10 @@ def test_create_report(): open_.assert_called_once_with("/path/to/report", "w") -def test_final_reduce(): - config = object() - assert tasks.final_reduce([True, True], config) == 0 - assert tasks.final_reduce([True, False], config) == 1 +def test_final_reduce() -> None: + config = argparse.Namespace() + true = typing.cast(sessions.Result, True) + false = typing.cast(sessions.Result, False) + 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_toml.py b/tests/test_toml.py index ef2e60ed..54f88681 100644 --- a/tests/test_toml.py +++ b/tests/test_toml.py @@ -101,7 +101,7 @@ def test_load_multiple_script_block(tmp_path: Path) -> None: nox.project.load_toml(filepath) -def test_load_non_recognised_extension(): +def test_load_non_recognised_extension() -> None: msg = "Extension must be .py or .toml, got .txt" with pytest.raises(ValueError, match=msg): nox.project.load_toml("some.txt") diff --git a/tests/test_tox_to_nox.py b/tests/test_tox_to_nox.py index 50ab4f25..c5110b2b 100644 --- a/tests/test_tox_to_nox.py +++ b/tests/test_tox_to_nox.py @@ -16,8 +16,11 @@ import sys import textwrap +from collections.abc import Callable +from typing import Any import pytest +from _pytest.compat import LEGACY_PATH tox_to_nox = pytest.importorskip("nox.tox_to_nox") @@ -27,21 +30,21 @@ @pytest.fixture -def makeconfig(tmpdir): - def makeconfig(toxini_content): +def makeconfig(tmpdir: LEGACY_PATH) -> Callable[[str], str]: + def makeconfig(toxini_content: str) -> str: tmpdir.join("tox.ini").write(toxini_content) old = tmpdir.chdir() try: sys.argv = [sys.executable] tox_to_nox.main() - return tmpdir.join("noxfile.py").read() + return tmpdir.join("noxfile.py").read() # type: ignore[no-any-return] finally: old.chdir() return makeconfig -def test_trivial(makeconfig): +def test_trivial(makeconfig: Callable[..., Any]) -> None: result = makeconfig( textwrap.dedent( f""" @@ -66,7 +69,7 @@ def py{PYTHON_VERSION_NODOT}(session): ) -def test_skipinstall(makeconfig): +def test_skipinstall(makeconfig: Callable[..., Any]) -> None: result = makeconfig( textwrap.dedent( f""" @@ -93,7 +96,7 @@ def py{PYTHON_VERSION_NODOT}(session): ) -def test_usedevelop(makeconfig): +def test_usedevelop(makeconfig: Callable[..., Any]) -> None: result = makeconfig( textwrap.dedent( f""" @@ -121,7 +124,7 @@ def py{PYTHON_VERSION_NODOT}(session): ) -def test_commands(makeconfig): +def test_commands(makeconfig: Callable[..., Any]) -> None: result = makeconfig( textwrap.dedent( f""" @@ -157,7 +160,7 @@ def lint(session): ) -def test_deps(makeconfig): +def test_deps(makeconfig: Callable[..., Any]) -> None: result = makeconfig( textwrap.dedent( f""" @@ -189,7 +192,7 @@ def lint(session): ) -def test_env(makeconfig): +def test_env(makeconfig: Callable[..., Any]) -> None: result = makeconfig( textwrap.dedent( f""" @@ -223,7 +226,7 @@ def lint(session): ) -def test_chdir(makeconfig): +def test_chdir(makeconfig: Callable[..., Any]) -> None: result = makeconfig( textwrap.dedent( f""" @@ -253,7 +256,7 @@ def lint(session): ) -def test_dash_in_envname(makeconfig): +def test_dash_in_envname(makeconfig: Callable[..., Any]) -> None: result = makeconfig( textwrap.dedent( f""" @@ -282,7 +285,9 @@ def test_with_dash(session): @pytest.mark.skipif(TOX4, reason="Not supported in tox 4.") -def test_non_identifier_in_envname(makeconfig, capfd): +def test_non_identifier_in_envname( + makeconfig: Callable[..., Any], capfd: pytest.CaptureFixture[str] +) -> None: result = makeconfig( textwrap.dedent( f""" @@ -317,7 +322,7 @@ def test_with_&(session): ) -def test_descriptions_into_docstrings(makeconfig): +def test_descriptions_into_docstrings(makeconfig: Callable[..., Any]) -> None: result = makeconfig( textwrap.dedent( f""" diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index 962f3e93..7c8e0204 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -21,16 +21,20 @@ 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 NamedTuple +from typing import 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 IS_WINDOWS = nox.virtualenv._SYSTEM == "Windows" HAS_CONDA = shutil.which("conda") is not None @@ -48,8 +52,14 @@ class TextProcessResult(NamedTuple): @pytest.fixture -def make_one(tmpdir): - def factory(*args, venv_backend: str = "virtualenv", **kwargs): +def make_one( + tmpdir: LEGACY_PATH, +) -> Callable[ + ..., tuple[nox.virtualenv.VirtualEnv | nox.virtualenv.ProcessEnv, LEGACY_PATH] +]: + def factory( + *args: Any, venv_backend: str = "virtualenv", **kwargs: Any + ) -> tuple[nox.virtualenv.VirtualEnv | nox.virtualenv.ProcessEnv, LEGACY_PATH]: location = tmpdir.join("venv") try: venv_fn = nox.virtualenv.ALL_VENVS[venv_backend] @@ -64,8 +74,8 @@ def factory(*args, venv_backend: str = "virtualenv", **kwargs): @pytest.fixture -def make_conda(tmpdir): - def factory(*args, **kwargs): +def make_conda(tmpdir: LEGACY_PATH) -> Callable[..., tuple[CondaEnv, LEGACY_PATH]]: + def factory(*args: Any, **kwargs: Any) -> tuple[CondaEnv, LEGACY_PATH]: location = tmpdir.join("condaenv") venv = nox.virtualenv.CondaEnv(location.strpath, *args, **kwargs) return (venv, location) @@ -74,12 +84,16 @@ def factory(*args, **kwargs): @pytest.fixture -def patch_sysfind(monkeypatch): +def patch_sysfind( + monkeypatch: pytest.MonkeyPatch, +) -> Callable[[tuple[str, ...], str | None, str], None]: """Provides a function to patch ``sysfind`` with parameters for tests related to locating a Python interpreter in the system ``PATH``. """ - def patcher(only_find, sysfind_result, sysexec_result): + def patcher( + only_find: tuple[str, ...], sysfind_result: str | None, sysexec_result: str + ) -> None: """Monkeypatches python discovery, causing specific results to be found. Args: @@ -93,7 +107,7 @@ def patcher(only_find, sysfind_result, sysexec_result): Use the global ``RAISE_ERROR`` to have ``sysexec`` fail. """ - def special_which(name, path=None): + def special_which(name: str, path: Any = None) -> str | None: if sysfind_result is None: return None if name.lower() in only_find: @@ -102,7 +116,7 @@ def special_which(name, path=None): monkeypatch.setattr(shutil, "which", special_which) - def special_run(cmd, *args, **kwargs): + def special_run(cmd: Any, *args: Any, **kwargs: Any) -> TextProcessResult: return TextProcessResult(sysexec_result) monkeypatch.setattr(subprocess, "run", special_run) @@ -110,7 +124,7 @@ def special_run(cmd, *args, **kwargs): return patcher -def test_process_env_constructor(): +def test_process_env_constructor() -> None: penv = nox.virtualenv.PassthroughEnv() assert not penv.bin_paths with pytest.raises( @@ -125,24 +139,32 @@ def test_process_env_constructor(): assert penv.bin == "/bin" -def test_process_env_create(): +def test_process_env_create() -> None: with pytest.raises(TypeError): - nox.virtualenv.ProcessEnv() + nox.virtualenv.ProcessEnv() # type: ignore[abstract] -def test_invalid_venv_create(make_one): +def test_invalid_venv_create( + make_one: Callable[ + ..., tuple[nox.virtualenv.VirtualEnv | nox.virtualenv.ProcessEnv, LEGACY_PATH] + ], +) -> None: with pytest.raises(ValueError): make_one(venv_backend="invalid") -def test_condaenv_constructor_defaults(make_conda): +def test_condaenv_constructor_defaults( + make_conda: Callable[..., tuple[CondaEnv, Any]], +) -> None: venv, _ = make_conda() assert venv.location assert venv.interpreter is None assert venv.reuse_existing is False -def test_condaenv_constructor_explicit(make_conda): +def test_condaenv_constructor_explicit( + make_conda: Callable[..., tuple[CondaEnv, Any]], +) -> None: venv, _ = make_conda(interpreter="3.5", reuse_existing=True) assert venv.location assert venv.interpreter == "3.5" @@ -150,7 +172,7 @@ def test_condaenv_constructor_explicit(make_conda): @has_conda -def test_condaenv_create(make_conda): +def test_condaenv_create(make_conda: Callable[..., tuple[CondaEnv, Any]]) -> None: venv, dir_ = make_conda() venv.create() @@ -179,7 +201,9 @@ def test_condaenv_create(make_conda): @has_conda -def test_condaenv_create_with_params(make_conda): +def test_condaenv_create_with_params( + make_conda: Callable[..., tuple[CondaEnv, Any]], +) -> None: venv, dir_ = make_conda(venv_params=["--verbose"]) venv.create() if IS_WINDOWS: @@ -191,7 +215,9 @@ def test_condaenv_create_with_params(make_conda): @has_conda -def test_condaenv_create_interpreter(make_conda): +def test_condaenv_create_interpreter( + make_conda: Callable[..., tuple[CondaEnv, Any]], +) -> None: venv, dir_ = make_conda(interpreter="3.8") venv.create() if IS_WINDOWS: @@ -205,7 +231,9 @@ def test_condaenv_create_interpreter(make_conda): @has_conda -def test_conda_env_create_verbose(make_conda): +def test_conda_env_create_verbose( + make_conda: Callable[..., tuple[CondaEnv, Any]], +) -> None: venv, dir_ = make_conda() with mock.patch("nox.virtualenv.nox.command.run") as mock_run: venv.create() @@ -222,7 +250,7 @@ def test_conda_env_create_verbose(make_conda): @mock.patch("nox.virtualenv._SYSTEM", new="Windows") -def test_condaenv_bin_windows(make_conda): +def test_condaenv_bin_windows(make_conda: Callable[..., tuple[CondaEnv, Any]]) -> None: venv, dir_ = make_conda() assert [ dir_.strpath, @@ -235,18 +263,20 @@ def test_condaenv_bin_windows(make_conda): @has_conda -def test_condaenv_(make_conda): +def test_condaenv_(make_conda: Callable[..., tuple[CondaEnv, Any]]) -> None: venv, dir_ = make_conda() assert not venv.is_offline() @has_conda -def test_condaenv_detection(make_conda): +def test_condaenv_detection(make_conda: Callable[..., tuple[CondaEnv, Any]]) -> None: venv, dir_ = make_conda() venv.create() + conda = shutil.which("conda") + assert conda proc_result = subprocess.run( - [shutil.which("conda"), "list"], + [conda, "list"], env=venv.env, check=True, capture_output=True, @@ -254,11 +284,15 @@ def test_condaenv_detection(make_conda): output = proc_result.stdout.decode() path_regex = re.compile(r"packages in environment at (?P.+):") - assert path_regex.search(output).group("env_dir") == dir_.strpath + output_match = path_regex.search(output) + assert output_match + assert output_match.group("env_dir") == dir_.strpath @has_uv -def test_uv_creation(make_one): +def test_uv_creation( + make_one: Callable[..., tuple[VirtualEnv, Any]], +) -> None: venv, _ = make_one(venv_backend="uv") assert venv.location assert venv.interpreter is None @@ -270,11 +304,15 @@ def test_uv_creation(make_one): @has_uv -def test_uv_managed_python(make_one): +def test_uv_managed_python( + make_one: Callable[..., tuple[VirtualEnv, Any]], +) -> None: make_one(interpreter="cpython3.12", venv_backend="uv") -def test_constructor_defaults(make_one): +def test_constructor_defaults( + make_one: Callable[..., tuple[VirtualEnv, Any]], +) -> None: venv, _ = make_one() assert venv.location assert venv.interpreter is None @@ -283,14 +321,19 @@ def test_constructor_defaults(make_one): @pytest.mark.skipif(IS_WINDOWS, reason="Not testing multiple interpreters on Windows.") -def test_constructor_explicit(make_one): +def test_constructor_explicit( + make_one: Callable[..., tuple[VirtualEnv, Any]], +) -> None: venv, _ = make_one(interpreter="python3.5", reuse_existing=True) assert venv.location assert venv.interpreter == "python3.5" assert venv.reuse_existing is True -def test_env(monkeypatch, make_one): +def test_env( + monkeypatch: pytest.MonkeyPatch, + make_one: Callable[..., tuple[VirtualEnv, Any]], +) -> None: monkeypatch.setenv("SIGIL", "123") venv, _ = make_one() assert venv.env["SIGIL"] == "123" @@ -299,7 +342,10 @@ def test_env(monkeypatch, make_one): assert venv.bin_paths[0] not in os.environ["PATH"] -def test_blacklisted_env(monkeypatch, make_one): +def test_blacklisted_env( + monkeypatch: pytest.MonkeyPatch, + make_one: Callable[..., tuple[VirtualEnv, Any]], +) -> None: monkeypatch.setenv("__PYVENV_LAUNCHER__", "meep") venv, _ = make_one() assert len(venv.bin_paths) == 1 @@ -307,7 +353,10 @@ def test_blacklisted_env(monkeypatch, make_one): assert "__PYVENV_LAUNCHER__" not in venv.bin -def test__clean_location(monkeypatch, make_one): +def test__clean_location( + monkeypatch: pytest.MonkeyPatch, + make_one: Callable[..., tuple[VirtualEnv, Any]], +) -> None: venv, dir_ = make_one() # Don't reuse existing, but doesn't currently exist. @@ -320,7 +369,7 @@ def test__clean_location(monkeypatch, make_one): "_check_reused_environment_interpreter", mock.MagicMock(), ) - monkeypatch.delattr(nox.virtualenv.shutil, "rmtree") + monkeypatch.delattr(nox.virtualenv.shutil, "rmtree") # type: ignore[attr-defined] assert not dir_.check() assert venv._clean_location() @@ -345,9 +394,12 @@ def test__clean_location(monkeypatch, make_one): assert venv._clean_location() -def test_bin_paths(make_one): +def test_bin_paths( + make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Any]], +) -> None: venv, dir_ = make_one() + assert venv.bin_paths assert len(venv.bin_paths) == 1 assert venv.bin_paths[0] == venv.bin @@ -358,14 +410,20 @@ def test_bin_paths(make_one): @mock.patch("nox.virtualenv._SYSTEM", new="Windows") -def test_bin_windows(make_one): +def test_bin_windows( + make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Any]], +) -> None: venv, dir_ = make_one() + assert venv.bin_paths assert len(venv.bin_paths) == 1 assert venv.bin_paths[0] == venv.bin assert dir_.join("Scripts").strpath == venv.bin -def test_create(monkeypatch, make_one): +def test_create( + monkeypatch: pytest.MonkeyPatch, + make_one: Callable[..., tuple[VirtualEnv, Any]], +) -> None: monkeypatch.setenv("CONDA_PREFIX", "no-prefix-allowed") monkeypatch.setenv("NOT_CONDA_PREFIX", "something-else") @@ -401,7 +459,9 @@ def test_create(monkeypatch, make_one): assert dir_.join("test.txt").check() -def test_create_reuse_environment(make_one): +def test_create_reuse_environment( + make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Any]], +) -> None: venv, location = make_one(reuse_existing=True) venv.create() @@ -410,7 +470,10 @@ def test_create_reuse_environment(make_one): assert reused -def test_create_reuse_environment_with_different_interpreter(make_one, monkeypatch): +def test_create_reuse_environment_with_different_interpreter( + make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Any]], + monkeypatch: pytest.MonkeyPatch, +) -> None: # Making the reuse requirement more strict monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1") @@ -430,7 +493,9 @@ def test_create_reuse_environment_with_different_interpreter(make_one, monkeypat @has_uv -def test_create_reuse_stale_venv_environment(make_one): +def test_create_reuse_stale_venv_environment( + make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Any]], +) -> None: venv, location = make_one(reuse_existing=True) venv.create() @@ -448,7 +513,12 @@ def test_create_reuse_stale_venv_environment(make_one): assert not reused -def test_not_stale_virtualenv_environment(make_one, monkeypatch): +def test_not_stale_virtualenv_environment( + make_one: Callable[ + ..., tuple[nox.virtualenv.VirtualEnv | nox.virtualenv.ProcessEnv, Any] + ], + monkeypatch: pytest.MonkeyPatch, +) -> None: # Making the reuse requirement more strict monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1") @@ -462,7 +532,9 @@ def test_not_stale_virtualenv_environment(make_one, monkeypatch): @has_conda -def test_stale_virtualenv_to_conda_environment(make_one): +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.create() @@ -475,7 +547,9 @@ def test_stale_virtualenv_to_conda_environment(make_one): @has_conda -def test_reuse_conda_environment(make_one): +def test_reuse_conda_environment( + make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Any]], +) -> None: venv, _ = make_one(reuse_existing=True, venv_backend="conda") venv.create() @@ -487,7 +561,10 @@ def test_reuse_conda_environment(make_one): # This mocks micromamba so that it doesn't need to be installed. @has_conda -def test_micromamba_environment(make_one, monkeypatch): +def test_micromamba_environment( + make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Any]], + monkeypatch: pytest.MonkeyPatch, +) -> None: conda_path = shutil.which("conda") which = shutil.which monkeypatch.setattr( @@ -509,7 +586,11 @@ def test_micromamba_environment(make_one, monkeypatch): [["--channel=default"], ["-cdefault"], ["-c", "default"], ["--channel", "default"]], ) @has_conda -def test_micromamba_channel_environment(make_one, monkeypatch, params): +def test_micromamba_channel_environment( + make_one: Callable[..., tuple[VirtualEnv, Any]], + monkeypatch: pytest.MonkeyPatch, + params: list[str], +) -> None: conda_path = shutil.which("conda") which = shutil.which monkeypatch.setattr( @@ -538,7 +619,13 @@ def test_micromamba_channel_environment(make_one, monkeypatch, params): pytest.param("conda", "virtualenv", False, marks=has_conda), ], ) -def test_stale_environment(make_one, frm, to, result, monkeypatch): +def test_stale_environment( + make_one: Callable[..., tuple[VirtualEnv, Any]], + frm: str, + to: str, + result: bool, + monkeypatch: pytest.MonkeyPatch, +) -> None: monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1") venv, _ = make_one(reuse_existing=True, venv_backend=frm) venv.create() @@ -551,14 +638,19 @@ def test_stale_environment(make_one, frm, to, result, monkeypatch): assert reused == result -def test_passthrough_environment_venv_backend(make_one): +def test_passthrough_environment_venv_backend( + make_one: Callable[..., tuple[ProcessEnv, Any]], +) -> None: venv, _ = make_one(venv_backend="none") venv.create() assert venv.venv_backend == "none" @has_uv -def test_create_reuse_stale_virtualenv_environment(make_one, monkeypatch): +def test_create_reuse_stale_virtualenv_environment( + make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Any]], + monkeypatch: pytest.MonkeyPatch, +) -> None: monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1") venv, location = make_one(reuse_existing=True, venv_backend="venv") venv.create() @@ -584,7 +676,9 @@ def test_create_reuse_stale_virtualenv_environment(make_one, monkeypatch): @has_uv -def test_create_reuse_uv_environment(make_one): +def test_create_reuse_uv_environment( + make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Any]], +) -> None: venv, location = make_one(reuse_existing=True, venv_backend="uv") venv.create() @@ -605,16 +699,22 @@ def test_create_reuse_uv_environment(make_one): ["which_result", "find_uv_bin_result", "found", "path"], [ ("/usr/bin/uv", UV_IN_PIPX_VENV, True, UV_IN_PIPX_VENV), - ("/usr/bin/uv", FileNotFoundError, True, "uv"), + ("/usr/bin/uv", None, True, "uv"), (None, UV_IN_PIPX_VENV, True, UV_IN_PIPX_VENV), - (None, FileNotFoundError, False, "uv"), + (None, None, False, "uv"), ], ) -def test_find_uv(monkeypatch, which_result, find_uv_bin_result, found, path): - def find_uv_bin(): - if find_uv_bin_result is FileNotFoundError: - raise FileNotFoundError - return find_uv_bin_result +def test_find_uv( + monkeypatch: pytest.MonkeyPatch, + which_result: str | None, + find_uv_bin_result: str | None, + found: bool, + path: str, +) -> None: + def find_uv_bin() -> str: + if find_uv_bin_result: + return find_uv_bin_result + raise FileNotFoundError() monkeypatch.setattr(shutil, "which", lambda _: which_result) monkeypatch.setattr(Path, "samefile", lambda a, b: a == b) @@ -633,8 +733,13 @@ def find_uv_bin(): (1, '{"version": "9.9.9", "commit_info": null}', "0.0"), ], ) -def test_uv_version(monkeypatch, return_code, stdout, expected_result): - def mock_run(*args, **kwargs): +def test_uv_version( + monkeypatch: pytest.MonkeyPatch, + return_code: int, + stdout: str | None, + expected_result: str, +) -> None: + def mock_run(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]: return subprocess.CompletedProcess( args=["uv", "version", "--output-format", "json"], stdout=stdout, @@ -645,8 +750,8 @@ def mock_run(*args, **kwargs): assert nox.virtualenv.uv_version() == version.Version(expected_result) -def test_uv_version_no_uv(monkeypatch): - def mock_exception(*args, **kwargs): +def test_uv_version_no_uv(monkeypatch: pytest.MonkeyPatch) -> None: + def mock_exception(*args: object, **kwargs: object) -> NoReturn: raise FileNotFoundError monkeypatch.setattr(subprocess, "run", mock_exception) @@ -665,11 +770,14 @@ def mock_exception(*args, **kwargs): ], ) @has_uv -def test_uv_install(requested_python, expected_result): +def test_uv_install(requested_python: str, expected_result: bool) -> None: assert nox.virtualenv.uv_install_python(requested_python) == expected_result -def test_create_reuse_venv_environment(make_one, monkeypatch): +def test_create_reuse_venv_environment( + make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Any]], + monkeypatch: pytest.MonkeyPatch, +) -> None: # Making the reuse requirement more strict monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1") @@ -687,7 +795,9 @@ def test_create_reuse_venv_environment(make_one, monkeypatch): @pytest.mark.skipif(IS_WINDOWS, reason="Avoid 'No pyvenv.cfg file' error on Windows.") -def test_create_reuse_oldstyle_virtualenv_environment(make_one): +def test_create_reuse_oldstyle_virtualenv_environment( + make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Any]], +) -> None: venv, location = make_one(reuse_existing=True) venv.create() @@ -705,7 +815,10 @@ def test_create_reuse_oldstyle_virtualenv_environment(make_one): @pytest.mark.skipif(IS_WINDOWS, reason="Avoid 'No pyvenv.cfg file' error on Windows.") -def test_inner_functions_reusing_venv(make_one, monkeypatch): +def test_inner_functions_reusing_venv( + make_one: Callable[..., tuple[VirtualEnv, Any]], + monkeypatch: pytest.MonkeyPatch, +) -> None: monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1") venv, location = make_one(reuse_existing=True) venv.create() @@ -719,7 +832,9 @@ def test_inner_functions_reusing_venv(make_one, monkeypatch): """ location.join("pyvenv.cfg").write(dedent(pyvenv_cfg)) - base_prefix = venv._read_pyvenv_cfg()["base-prefix"] + config = venv._read_pyvenv_cfg() + assert config + base_prefix = config["base-prefix"] assert base_prefix == "foo" reused_interpreter = venv._check_reused_environment_interpreter() @@ -731,7 +846,9 @@ def test_inner_functions_reusing_venv(make_one, monkeypatch): version.parse(VIRTUALENV_VERSION) >= version.parse("20.22.0"), reason="Python 2.7 unsupported for virtualenv>=20.22.0", ) -def test_create_reuse_python2_environment(make_one): +def test_create_reuse_python2_environment( + make_one: Callable[..., tuple[VirtualEnv, Any]], +) -> None: venv, location = make_one(reuse_existing=True, interpreter="2.7") try: @@ -744,20 +861,26 @@ def test_create_reuse_python2_environment(make_one): assert reused -def test_create_venv_backend(make_one): +def test_create_venv_backend( + make_one: Callable[..., tuple[VirtualEnv, Any]], +) -> None: venv, dir_ = make_one(venv_backend="venv") venv.create() @pytest.mark.skipif(IS_WINDOWS, reason="Not testing multiple interpreters on Windows.") -def test_create_interpreter(make_one): +def test_create_interpreter( + make_one: Callable[..., tuple[VirtualEnv, Any]], +) -> None: venv, dir_ = make_one(interpreter="python3") venv.create() assert dir_.join("bin", "python").check() assert dir_.join("bin", "python3").check() -def test__resolved_interpreter_none(make_one): +def test__resolved_interpreter_none( + make_one: Callable[..., tuple[VirtualEnv, Any]], +) -> None: # Establish that the _resolved_interpreter method is a no-op if the # interpreter is not set. venv, _ = make_one(interpreter=None) @@ -776,7 +899,12 @@ def test__resolved_interpreter_none(make_one): ) @mock.patch("nox.virtualenv._SYSTEM", new="Linux") @mock.patch.object(shutil, "which", return_value=True) -def test__resolved_interpreter_numerical_non_windows(which, make_one, input_, expected): +def test__resolved_interpreter_numerical_non_windows( + which: mock.Mock, + make_one: Callable[..., tuple[VirtualEnv, Any]], + input_: str, + expected: str, +) -> None: venv, _ = make_one(interpreter=input_) assert venv._resolved_interpreter == expected @@ -786,7 +914,11 @@ def test__resolved_interpreter_numerical_non_windows(which, make_one, input_, ex @pytest.mark.parametrize("input_", ["2.", "2.7."]) @mock.patch("nox.virtualenv._SYSTEM", new="Linux") @mock.patch.object(shutil, "which", return_value=False) -def test__resolved_interpreter_invalid_numerical_id(which, make_one, input_): +def test__resolved_interpreter_invalid_numerical_id( + which: mock.Mock, + make_one: Callable[..., tuple[VirtualEnv, Any]], + input_: str, +) -> None: venv, _ = make_one(interpreter=input_) with pytest.raises(nox.virtualenv.InterpreterNotFound): @@ -797,7 +929,9 @@ def test__resolved_interpreter_invalid_numerical_id(which, make_one, input_): @mock.patch("nox.virtualenv._SYSTEM", new="Linux") @mock.patch.object(shutil, "which", return_value=False) -def test__resolved_interpreter_32_bit_non_windows(which, make_one): +def test__resolved_interpreter_32_bit_non_windows( + which: mock.Mock, make_one: Callable[..., tuple[VirtualEnv, Any]] +) -> None: venv, _ = make_one(interpreter="3.6-32") with pytest.raises(nox.virtualenv.InterpreterNotFound): @@ -807,7 +941,9 @@ def test__resolved_interpreter_32_bit_non_windows(which, make_one): @mock.patch("nox.virtualenv._SYSTEM", new="Linux") @mock.patch.object(shutil, "which", return_value=True) -def test__resolved_interpreter_non_windows(which, make_one): +def test__resolved_interpreter_non_windows( + which: mock.Mock, make_one: Callable[..., tuple[VirtualEnv, Any]] +) -> None: # Establish that the interpreter is simply passed through resolution # on non-Windows. venv, _ = make_one(interpreter="python3.6") @@ -818,7 +954,9 @@ def test__resolved_interpreter_non_windows(which, make_one): @mock.patch("nox.virtualenv._SYSTEM", new="Windows") @mock.patch.object(shutil, "which") -def test__resolved_interpreter_windows_full_path(which, make_one): +def test__resolved_interpreter_windows_full_path( + which: mock.Mock, make_one: Callable[..., tuple[VirtualEnv, Any]] +) -> None: # Establish that if we get a fully-qualified system path (on Windows # or otherwise) and the path exists, that we accept it. venv, _ = make_one(interpreter=r"c:\Python36\python.exe") @@ -839,7 +977,13 @@ def test__resolved_interpreter_windows_full_path(which, make_one): @mock.patch("nox.virtualenv._SYSTEM", new="Windows") @mock.patch.object(subprocess, "run") @mock.patch.object(shutil, "which") -def test__resolved_interpreter_windows_pyexe(which, run, make_one, input_, expected): +def test__resolved_interpreter_windows_pyexe( + which: mock.Mock, + run: mock.Mock, + make_one: Callable[..., tuple[VirtualEnv, Any]], + input_: str, + expected: str, +) -> None: # Establish that if we get a standard pythonX.Y path, we look it # up via the py launcher on Windows. venv, _ = make_one(interpreter=input_) @@ -851,7 +995,7 @@ def test__resolved_interpreter_windows_pyexe(which, run, make_one, input_, expec # (it likely will on Unix). Also, when the system looks for the # py launcher, give it a dummy that returns our test value when # run. - def special_run(cmd, *args, **kwargs): + def special_run(cmd: str, *args: str, **kwargs: object) -> TextProcessResult: if cmd[0] == "py": return TextProcessResult(expected) return TextProcessResult("", 1) @@ -868,14 +1012,16 @@ def special_run(cmd, *args, **kwargs): @mock.patch("nox.virtualenv._SYSTEM", new="Windows") @mock.patch.object(subprocess, "run") @mock.patch.object(shutil, "which") -def test__resolved_interpreter_windows_pyexe_fails(which, run, make_one): +def test__resolved_interpreter_windows_pyexe_fails( + which: mock.Mock, run: mock.Mock, make_one: Callable[..., tuple[VirtualEnv, Any]] +) -> None: # Establish that if the py launcher fails, we give the right error. venv, _ = make_one(interpreter="python3.6") # 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, *args, **kwargs): + def special_run(cmd: str, *args: str, **kwargs: object) -> TextProcessResult: return TextProcessResult("", 1) run.side_effect = special_run @@ -890,7 +1036,10 @@ def special_run(cmd, *args, **kwargs): @mock.patch("nox.virtualenv._SYSTEM", new="Windows") @mock.patch("nox.virtualenv.UV_PYTHON_SUPPORT", new=False) -def test__resolved_interpreter_windows_path_and_version(make_one, patch_sysfind): +def test__resolved_interpreter_windows_path_and_version( + make_one: Callable[..., tuple[VirtualEnv, Any]], + patch_sysfind: Callable[..., None], +) -> None: # Establish that if we get a standard pythonX.Y path, we look it # up via the path on Windows. venv, _ = make_one(interpreter="3.7") @@ -917,8 +1066,12 @@ def test__resolved_interpreter_windows_path_and_version(make_one, patch_sysfind) @mock.patch("nox.virtualenv._SYSTEM", new="Windows") @mock.patch("nox.virtualenv.UV_PYTHON_SUPPORT", new=False) def test__resolved_interpreter_windows_path_and_version_fails( - input_, sysfind_result, sysexec_result, make_one, patch_sysfind -): + input_: str, + sysfind_result: None | str, + sysexec_result: str, + make_one: Callable[..., tuple[VirtualEnv, Any]], + patch_sysfind: Callable[..., None], +) -> None: # Establish that if we get a standard pythonX.Y path, we look it # up via the path on Windows. venv, _ = make_one(interpreter=input_) @@ -936,7 +1089,9 @@ def test__resolved_interpreter_windows_path_and_version_fails( @mock.patch("nox.virtualenv._SYSTEM", new="Windows") @mock.patch.object(shutil, "which") -def test__resolved_interpreter_not_found(which, make_one): +def test__resolved_interpreter_not_found( + which: mock.Mock, make_one: Callable[..., tuple[VirtualEnv, Any]] +) -> None: # Establish that if an interpreter cannot be found at a standard # location on Windows, we raise a useful error. venv, _ = make_one(interpreter="python3.6") @@ -950,8 +1105,10 @@ def test__resolved_interpreter_not_found(which, make_one): @mock.patch("nox.virtualenv._SYSTEM", new="Windows") -@mock.patch("nox.virtualenv.locate_via_py", new=lambda _: None) -def test__resolved_interpreter_nonstandard(make_one): +@mock.patch("nox.virtualenv.locate_via_py", new=lambda _: None) # type: ignore[misc] +def test__resolved_interpreter_nonstandard( + make_one: Callable[..., tuple[VirtualEnv, Any]], +) -> None: # Establish that we do not try to resolve non-standard locations # on Windows. venv, _ = make_one(interpreter="goofy") @@ -962,7 +1119,9 @@ def test__resolved_interpreter_nonstandard(make_one): @mock.patch("nox.virtualenv._SYSTEM", new="Linux") @mock.patch.object(shutil, "which", return_value=True) -def test__resolved_interpreter_cache_result(which, make_one): +def test__resolved_interpreter_cache_result( + which: mock.Mock, make_one: Callable[..., tuple[VirtualEnv, Any]] +) -> None: venv, _ = make_one(interpreter="3.6") assert venv._resolved is None @@ -976,7 +1135,9 @@ def test__resolved_interpreter_cache_result(which, make_one): @mock.patch("nox.virtualenv._SYSTEM", new="Linux") @mock.patch.object(shutil, "which", return_value=None) -def test__resolved_interpreter_cache_failure(which, make_one): +def test__resolved_interpreter_cache_failure( + which: mock.Mock, make_one: Callable[..., tuple[VirtualEnv, Any]] +) -> None: venv, _ = make_one(interpreter="3.7-32") assert venv._resolved is None @@ -987,6 +1148,6 @@ def test__resolved_interpreter_cache_failure(which, make_one): which.assert_called_once_with("3.7-32") # Check the cache and call again to make sure it is used. assert venv._resolved is caught - with pytest.raises(nox.virtualenv.InterpreterNotFound): + with pytest.raises(nox.virtualenv.InterpreterNotFound): # type: ignore[unreachable] print(venv._resolved_interpreter) assert which.call_count == 1 diff --git a/tests/test_workflow.py b/tests/test_workflow.py index 48899902..dc3917e2 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -19,7 +19,7 @@ from nox import workflow -def test_simple_workflow(): +def test_simple_workflow() -> None: # Set up functions for the workflow. function_a = mock.Mock(spec=()) function_b = mock.Mock(spec=()) @@ -45,7 +45,7 @@ def test_simple_workflow(): ) -def test_workflow_int_cutoff(): +def test_workflow_int_cutoff() -> None: # Set up functions for the workflow. function_a = mock.Mock(spec=()) function_b = mock.Mock(spec=()) @@ -73,7 +73,7 @@ def test_workflow_int_cutoff(): assert not function_c.called -def test_workflow_interrupted(): +def test_workflow_interrupted() -> None: # Set up functions for the workflow. function_a = mock.Mock(spec=()) function_b = mock.Mock(spec=())