Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog/4041.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Report an empty ``install_command`` or ``list_dependencies_command`` in a TOML configuration as a handled error instead
of an unhandled traceback - by :user:`dylanpulver`
4 changes: 1 addition & 3 deletions src/tox/config/loader/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,6 @@ def _to_typing(self, raw: T, of_type: type[V] | UnionType, factory: Factory[V])
if origin in {list, list}:
entry_type = type_args[0]
result = [self.to(i, entry_type, factory) for i in self.to_list(raw, entry_type)]
if isclass(entry_type) and issubclass(entry_type, Command):
result = [i for i in result if i is not None]
elif origin in {set, set}:
entry_type = type_args[0]
result = {self.to(i, entry_type, factory) for i in self.to_set(raw, entry_type)}
Expand Down Expand Up @@ -180,7 +178,7 @@ def to_path(value: T) -> Path:

@staticmethod
@abstractmethod
def to_command(value: T) -> Command | None:
def to_command(value: T) -> Command:
"""Convert to a command to execute.
:param value: the value to convert
Expand Down
2 changes: 1 addition & 1 deletion src/tox/config/loader/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def to_path(value: Any) -> Path:
return Path(value)

@staticmethod
def to_command(value: Any) -> Command | None:
def to_command(value: Any) -> Command:
if isinstance(value, Command):
return value
if isinstance(value, str):
Expand Down
2 changes: 1 addition & 1 deletion src/tox/config/loader/str_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def _win32_process_path_backslash(value: str, escape: str, special_chars: str) -
return "".join(result)

@staticmethod
def to_command(value: str) -> Command | None:
def to_command(value: str) -> Command:
"""At this point, ``value`` has already been substituted out, and all punctuation / escapes are final.

Value will typically be stripped of whitespace when coming from an ini file.
Expand Down
19 changes: 11 additions & 8 deletions src/tox/config/loader/toml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,15 @@ def to_bool(value: TomlTypes) -> bool:

@staticmethod
def to_list(value: TomlTypes, of_type: type[_T]) -> Iterator[_T]:
result = validate(value, cast("type[list[Any]]", GenericAlias(list, (of_type,))))
return iter(cast("list[_T]", result))
result = cast("list[_T]", validate(value, cast("type[list[Any]]", GenericAlias(list, (of_type,)))))
if inspect.isclass(of_type) and issubclass(of_type, Command):
# a generated command list leaves gaps rather than compacting itself, so drop them here (see #3388)
return iter([i for i in result if i])
return iter(result)

@staticmethod
def to_set(value: TomlTypes, of_type: type[_T]) -> Iterator[_T]:
result = validate(value, cast("type[list[Any]]", GenericAlias(list, (of_type,))))
return iter(cast("list[_T]", result))
return TomlLoader.to_list(value, of_type)

@staticmethod
def to_dict(value: TomlTypes, of_type: tuple[type[_T], type[_V]]) -> Iterator[tuple[_T, _V]]:
Expand All @@ -123,10 +125,11 @@ def to_path(value: TomlTypes) -> Path:
return Path(TomlLoader.to_str(value))

@staticmethod
def to_command(value: TomlTypes) -> Command | None:
if value:
return Command(args=cast("list[str]", value)) # validated during load in _ensure_type_correct
return None
def to_command(value: TomlTypes) -> Command:
if not value:
msg = f"attempting to parse {value!r} into a command failed"
raise ValueError(msg)
return Command(args=cast("list[str]", value)) # validated during load in _ensure_type_correct

@staticmethod
def to_env_list(value: TomlTypes) -> EnvList:
Expand Down
10 changes: 10 additions & 0 deletions tests/config/loader/test_toml_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ def test_toml_loader_command_nok() -> None:
perform_load([["a", 1]], list[Command])


def test_toml_loader_command_list_drops_empty() -> None:
commands = perform_load([[], ["c"]], list[Command])
assert [i.args for i in commands] == [["c"]]


def test_toml_loader_command_empty_nok() -> None:
with pytest.raises(HandledError, match=_PREFIX + r"attempting to parse \[\] into a command failed"):
perform_load([], Command)


def test_toml_loader_env_list_ok() -> None:
res = perform_load(["a", "b"], EnvList)
assert isinstance(res, EnvList)
Expand Down
21 changes: 21 additions & 0 deletions tests/tox_env/python/pip/test_pip_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
import pytest
from packaging.requirements import Requirement

from tox.report import HandledError
from tox.tox_env.errors import Fail

if TYPE_CHECKING:
from collections.abc import Callable
from pathlib import Path

from tox.pytest import CaptureFixture, SubRequest, ToxProject, ToxProjectCreator
Expand Down Expand Up @@ -50,6 +52,25 @@ def test_pip_install_empty_command_error(tox_project: ToxProjectCreator) -> None
pip.install([Requirement("name")], "section", "type")


@pytest.mark.parametrize(
("key", "use"),
[
pytest.param(
"install_command",
lambda pip: pip.install([Requirement("name")], "section", "type"),
id="install_command",
),
pytest.param("list_dependencies_command", lambda pip: pip.installed(), id="list_dependencies_command"),
],
)
def test_pip_toml_empty_command_error(tox_project: ToxProjectCreator, key: str, use: Callable[[Any], object]) -> None:
proj = tox_project({"tox.toml": f"[env.py]\n{key} = []"})
pip = proj.run("l").state.envs["py"].installer

with pytest.raises(HandledError, match=rf"failed to load py\.{key}: attempting to parse \[\] into a command"):
use(pip)


def test_pip_install_flags_only_error(tox_project: ToxProjectCreator) -> None:
proj = tox_project({"tox.ini": "[testenv:py]\ndeps=-i a"})
result = proj.run("r")
Expand Down