Skip to content

Commit

Permalink
Add bang to invert exit code (#3271)
Browse files Browse the repository at this point in the history
Co-authored-by: Asger Gitz-Johansen <asgj@gomspace.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 26, 2024
1 parent 822c9d0 commit 809e10f
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/changelog/3271.feature.rst
@@ -0,0 +1 @@
Add support for inverting exit code success criteria using bang (!)
12 changes: 11 additions & 1 deletion docs/faq.rst
Expand Up @@ -187,12 +187,22 @@ a given command add a ``-`` prefix to that line (similar syntax to how the GNU `

.. code-block:: ini
[testenv]
commands =
- python -c 'import sys; sys.exit(1)'
python --version
You can also choose to provide a ``!`` prefix instead to purposely invert the exit code, making the line fail if the
command returned exit code 0. Any other exit code is considered a success.

.. code-block:: ini
[testenv]
commands =
! python -c 'import sys; sys.exit(1)'
python --version
Customizing virtual environment creation
----------------------------------------

Expand Down
11 changes: 8 additions & 3 deletions src/tox/config/types.py
Expand Up @@ -16,15 +16,20 @@ def __init__(self, args: list[str]) -> None:
:param args: the command line arguments (first value can be ``-`` to indicate ignore the exit code)
"""
self.ignore_exit_code: bool = args[0] == "-" #: a flag indicating if the exit code should be ignored
self.args: list[str] = args[1:] if self.ignore_exit_code else args #: the command line arguments
self.invert_exit_code: bool = args[0] == "!" #: a flag for flipped exit code (non-zero = success, 0 = error)
self.args: list[str] = (
args[1:] if self.ignore_exit_code or self.invert_exit_code else args
) #: the command line arguments

def __repr__(self) -> str:
return f"{type(self).__name__}(args={(['-'] if self.ignore_exit_code else []) + self.args!r})"
args = (["-"] if self.ignore_exit_code else ["!"] if self.invert_exit_code else []) + self.args
return f"{type(self).__name__}(args={args!r})"

def __eq__(self, other: object) -> bool:
return type(self) == type(other) and (self.args, self.ignore_exit_code) == (
return type(self) == type(other) and (self.args, self.ignore_exit_code, self.invert_exit_code) == (
other.args, # type: ignore[attr-defined]
other.ignore_exit_code, # type: ignore[attr-defined]
other.invert_exit_code, # type: ignore[attr-defined]
)

def __ne__(self, other: object) -> bool:
Expand Down
6 changes: 6 additions & 0 deletions src/tox/execute/api.py
Expand Up @@ -252,6 +252,12 @@ def assert_success(self) -> None:
self._assert_fail()
self.log_run_done(logging.INFO)

def assert_failure(self) -> None:
"""Assert that the execution failed."""
if self.exit_code is not None and self.exit_code == self.OK:
self._assert_fail()
self.log_run_done(logging.INFO)

def _assert_fail(self) -> NoReturn:
if self.show_on_standard is False:
if self.out:
Expand Down
5 changes: 4 additions & 1 deletion src/tox/session/cmd/run/single.py
Expand Up @@ -112,7 +112,10 @@ def run_command_set(
)
outcomes.append(current_outcome)
try:
current_outcome.assert_success()
if cmd.invert_exit_code:
current_outcome.assert_failure()
else:
current_outcome.assert_success()
except SystemExit as exception:
if cmd.ignore_exit_code:
logging.warning("command failed but is marked ignore outcome so handling it as success")
Expand Down
12 changes: 11 additions & 1 deletion tests/config/test_types.py
Expand Up @@ -6,15 +6,24 @@
def tests_command_repr() -> None:
cmd = Command(["python", "-m", "pip", "list"])
assert repr(cmd) == "Command(args=['python', '-m', 'pip', 'list'])"
assert cmd.invert_exit_code is False
assert cmd.ignore_exit_code is False


def tests_command_repr_ignore() -> None:
cmd = Command(["-", "python", "-m", "pip", "list"])
assert repr(cmd) == "Command(args=['-', 'python', '-m', 'pip', 'list'])"
assert cmd.invert_exit_code is False
assert cmd.ignore_exit_code is True


def tests_command_repr_invert() -> None:
cmd = Command(["!", "python", "-m", "pip", "list"])
assert repr(cmd) == "Command(args=['!', 'python', '-m', 'pip', 'list'])"
assert cmd.invert_exit_code is True
assert cmd.ignore_exit_code is False


def tests_command_eq() -> None:
cmd_1 = Command(["python", "-m", "pip", "list"])
cmd_2 = Command(["python", "-m", "pip", "list"])
Expand All @@ -24,7 +33,8 @@ def tests_command_eq() -> None:
def tests_command_ne() -> None:
cmd_1 = Command(["python", "-m", "pip", "list"])
cmd_2 = Command(["-", "python", "-m", "pip", "list"])
assert cmd_1 != cmd_2
cmd_3 = Command(["!", "python", "-m", "pip", "list"])
assert cmd_1 != cmd_2 != cmd_3


def tests_env_list_repr() -> None:
Expand Down

0 comments on commit 809e10f

Please sign in to comment.