Skip to content

Commit

Permalink
fix: error with helpful message if invalid option is set via nox.opti…
Browse files Browse the repository at this point in the history
…ons (#871)

* fix: error with helpful message if invalid option is set via nox.options

Signed-off-by: Henry Schreiner <[email protected]>

* tests: include other branch for coverage

Signed-off-by: Henry Schreiner <[email protected]>

* fix: add static typing to nox.options

Signed-off-by: Henry Schreiner <[email protected]>

* fix: one bool was being set to None

Signed-off-by: Henry Schreiner <[email protected]>

---------

Signed-off-by: Henry Schreiner <[email protected]>
  • Loading branch information
henryiii authored Oct 29, 2024
1 parent 7a94886 commit 6edc697
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 19 deletions.
52 changes: 44 additions & 8 deletions nox/_option_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,51 @@

import argparse
import collections
import dataclasses
import functools
from argparse import ArgumentError as ArgumentError # noqa: PLC0414
from argparse import ArgumentParser, Namespace
from collections.abc import Callable, Iterable
from typing import Any
from typing import Any, Literal

import argcomplete


# Python 3.10+ has slots=True (or attrs does), also kwonly=True
@dataclasses.dataclass
class NoxOptions:
__slots__ = (
"default_venv_backend",
"envdir",
"error_on_external_run",
"error_on_missing_interpreters",
"force_venv_backend",
"keywords",
"pythons",
"report",
"reuse_existing_virtualenvs",
"reuse_venv",
"sessions",
"stop_on_first_error",
"tags",
"verbose",
)
default_venv_backend: None | str
envdir: None | str
error_on_external_run: bool
error_on_missing_interpreters: bool
force_venv_backend: None | str
keywords: None | list[str]
pythons: None | list[str]
report: None | str
reuse_existing_virtualenvs: bool
reuse_venv: None | Literal["no", "yes", "never", "always"]
sessions: None | list[str]
stop_on_first_error: bool
tags: None | list[str]
verbose: bool


class OptionGroup:
"""A single group for command-line options.
Expand Down Expand Up @@ -59,7 +95,7 @@ class Option:
help (str): The help string pass to argparse.
noxfile (bool): Whether or not this option can be set in the
configuration file.
merge_func (Callable[[Namespace, Namespace], Any]): A function that
merge_func (Callable[[Namespace, NoxOptions], Any]): A function that
can define custom behavior when merging the command-line options
with the configuration file options. The first argument is the
command-line options, the second is the configuration file options.
Expand All @@ -84,7 +120,7 @@ def __init__(
group: OptionGroup | None,
help: str | None = None,
noxfile: bool = False,
merge_func: Callable[[Namespace, Namespace], Any] | None = None,
merge_func: Callable[[Namespace, NoxOptions], Any] | None = None,
finalizer_func: Callable[[Any, Namespace], Any] | None = None,
default: (
bool | str | None | list[str] | Callable[[], bool | str | None | list[str]]
Expand Down Expand Up @@ -117,7 +153,7 @@ def flag_pair_merge_func(
enable_default: bool | Callable[[], bool],
disable_name: str,
command_args: Namespace,
noxfile_args: Namespace,
noxfile_args: NoxOptions,
) -> bool:
"""Merge function for flag pairs. If the flag is set in the Noxfile or
the command line params, return ``True`` *unless* the disable flag has been
Expand Down Expand Up @@ -305,19 +341,19 @@ def namespace(self, **kwargs: Any) -> argparse.Namespace:

return argparse.Namespace(**args)

def noxfile_namespace(self) -> Namespace:
def noxfile_namespace(self) -> NoxOptions:
"""Returns a namespace of options that can be set in the configuration
file."""
return argparse.Namespace(
return NoxOptions(
**{
option.name: option.default
for option in self.options.values()
if option.noxfile
}
} # type: ignore[arg-type]
)

def merge_namespaces(
self, command_args: Namespace, noxfile_args: Namespace
self, command_args: Namespace, noxfile_args: NoxOptions
) -> None:
"""Merges the command-line options with the Noxfile options."""
command_args_copy = Namespace(**vars(command_args))
Expand Down
24 changes: 13 additions & 11 deletions nox/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import argcomplete

from nox import _option_set
from nox._option_set import NoxOptions
from nox.tasks import discover_manifest, filter_manifest, load_nox_module
from nox.virtualenv import ALL_VENVS

Expand Down Expand Up @@ -72,7 +73,7 @@


def _sessions_merge_func(
key: str, command_args: argparse.Namespace, noxfile_args: argparse.Namespace
key: str, command_args: argparse.Namespace, noxfile_args: NoxOptions
) -> list[str]:
"""Only return the Noxfile value for sessions/keywords if neither sessions,
keywords or tags are specified on the command-line.
Expand All @@ -83,7 +84,7 @@ def _sessions_merge_func(
same function for both options.
command_args (_option_set.Namespace): The options specified on the
command-line.
noxfile_Args (_option_set.Namespace): The options specified in the
noxfile_Args (NoxOptions): The options specified in the
Noxfile."""
if (
not command_args.sessions
Expand All @@ -95,14 +96,14 @@ def _sessions_merge_func(


def _default_venv_backend_merge_func(
command_args: argparse.Namespace, noxfile_args: argparse.Namespace
command_args: argparse.Namespace, noxfile_args: NoxOptions
) -> str:
"""Merge default_venv_backend from command args and Noxfile. Default is "virtualenv".
Args:
command_args (_option_set.Namespace): The options specified on the
command-line.
noxfile_Args (_option_set.Namespace): The options specified in the
noxfile_Args (NoxOptions): The options specified in the
Noxfile.
"""
return (
Expand All @@ -113,14 +114,14 @@ def _default_venv_backend_merge_func(


def _force_venv_backend_merge_func(
command_args: argparse.Namespace, noxfile_args: argparse.Namespace
command_args: argparse.Namespace, noxfile_args: NoxOptions
) -> str:
"""Merge force_venv_backend from command args and Noxfile. Default is None.
Args:
command_args (_option_set.Namespace): The options specified on the
command-line.
noxfile_Args (_option_set.Namespace): The options specified in the
noxfile_Args (NoxOptions): The options specified in the
Noxfile.
"""
if command_args.no_venv:
Expand All @@ -132,33 +133,33 @@ def _force_venv_backend_merge_func(
"You can not use `--no-venv` with a non-none `--force-venv-backend`"
)
return "none"
return command_args.force_venv_backend or noxfile_args.force_venv_backend # type: ignore[no-any-return]
return command_args.force_venv_backend or noxfile_args.force_venv_backend # type: ignore[return-value]


def _envdir_merge_func(
command_args: argparse.Namespace, noxfile_args: argparse.Namespace
command_args: argparse.Namespace, noxfile_args: NoxOptions
) -> str:
"""Ensure that there is always some envdir.
Args:
command_args (_option_set.Namespace): The options specified on the
command-line.
noxfile_Args (_option_set.Namespace): The options specified in the
noxfile_Args (NoxOptions): The options specified in the
Noxfile.
"""
return command_args.envdir or noxfile_args.envdir or ".nox"


def _reuse_venv_merge_func(
command_args: argparse.Namespace, noxfile_args: argparse.Namespace
command_args: argparse.Namespace, noxfile_args: NoxOptions
) -> ReuseVenvType:
"""Merge reuse_venv from command args and Noxfile while maintaining
backwards compatibility with reuse_existing_virtualenvs. Default is "no".
Args:
command_args (_option_set.Namespace): The options specified on the
command-line.
noxfile_Args (_option_set.Namespace): The options specified in the
noxfile_Args (NoxOptions): The options specified in the
Noxfile.
"""
# back-compat scenario with no_reuse_existing_virtualenvs/reuse_existing_virtualenvs
Expand Down Expand Up @@ -397,6 +398,7 @@ def _tag_completer(
"--verbose",
group=options.groups["reporting"],
action="store_true",
default=False,
help="Logs the output of all commands run including commands marked silent.",
noxfile=True,
),
Expand Down
10 changes: 10 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -918,3 +918,13 @@ def test_main_noxfile_options_reuse_venv_compat_check(
nox.main()
config = honor_list_request.call_args[1]["global_config"]
assert config.reuse_venv == expected


def test_noxfile_options_cant_be_set():
with pytest.raises(AttributeError, match="reuse_venvs"):
nox.options.reuse_venvs = True


def test_noxfile_options_cant_be_set_long():
with pytest.raises(AttributeError, match="i_am_clearly_not_an_option"):
nox.options.i_am_clearly_not_an_option = True

0 comments on commit 6edc697

Please sign in to comment.