Skip to content

Commit 809e10f

Browse files
Add bang to invert exit code (#3271)
Co-authored-by: Asger Gitz-Johansen <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 822c9d0 commit 809e10f

File tree

6 files changed

+41
-6
lines changed

6 files changed

+41
-6
lines changed

docs/changelog/3271.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for inverting exit code success criteria using bang (!)

docs/faq.rst

+11-1
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,22 @@ a given command add a ``-`` prefix to that line (similar syntax to how the GNU `
187187

188188
.. code-block:: ini
189189
190-
191190
[testenv]
192191
commands =
193192
- python -c 'import sys; sys.exit(1)'
194193
python --version
195194
195+
You can also choose to provide a ``!`` prefix instead to purposely invert the exit code, making the line fail if the
196+
command returned exit code 0. Any other exit code is considered a success.
197+
198+
.. code-block:: ini
199+
200+
[testenv]
201+
commands =
202+
! python -c 'import sys; sys.exit(1)'
203+
python --version
204+
205+
196206
Customizing virtual environment creation
197207
----------------------------------------
198208

src/tox/config/types.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@ def __init__(self, args: list[str]) -> None:
1616
:param args: the command line arguments (first value can be ``-`` to indicate ignore the exit code)
1717
"""
1818
self.ignore_exit_code: bool = args[0] == "-" #: a flag indicating if the exit code should be ignored
19-
self.args: list[str] = args[1:] if self.ignore_exit_code else args #: the command line arguments
19+
self.invert_exit_code: bool = args[0] == "!" #: a flag for flipped exit code (non-zero = success, 0 = error)
20+
self.args: list[str] = (
21+
args[1:] if self.ignore_exit_code or self.invert_exit_code else args
22+
) #: the command line arguments
2023

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

2428
def __eq__(self, other: object) -> bool:
25-
return type(self) == type(other) and (self.args, self.ignore_exit_code) == (
29+
return type(self) == type(other) and (self.args, self.ignore_exit_code, self.invert_exit_code) == (
2630
other.args, # type: ignore[attr-defined]
2731
other.ignore_exit_code, # type: ignore[attr-defined]
32+
other.invert_exit_code, # type: ignore[attr-defined]
2833
)
2934

3035
def __ne__(self, other: object) -> bool:

src/tox/execute/api.py

+6
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,12 @@ def assert_success(self) -> None:
252252
self._assert_fail()
253253
self.log_run_done(logging.INFO)
254254

255+
def assert_failure(self) -> None:
256+
"""Assert that the execution failed."""
257+
if self.exit_code is not None and self.exit_code == self.OK:
258+
self._assert_fail()
259+
self.log_run_done(logging.INFO)
260+
255261
def _assert_fail(self) -> NoReturn:
256262
if self.show_on_standard is False:
257263
if self.out:

src/tox/session/cmd/run/single.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ def run_command_set(
112112
)
113113
outcomes.append(current_outcome)
114114
try:
115-
current_outcome.assert_success()
115+
if cmd.invert_exit_code:
116+
current_outcome.assert_failure()
117+
else:
118+
current_outcome.assert_success()
116119
except SystemExit as exception:
117120
if cmd.ignore_exit_code:
118121
logging.warning("command failed but is marked ignore outcome so handling it as success")

tests/config/test_types.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,24 @@
66
def tests_command_repr() -> None:
77
cmd = Command(["python", "-m", "pip", "list"])
88
assert repr(cmd) == "Command(args=['python', '-m', 'pip', 'list'])"
9+
assert cmd.invert_exit_code is False
910
assert cmd.ignore_exit_code is False
1011

1112

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

1719

20+
def tests_command_repr_invert() -> None:
21+
cmd = Command(["!", "python", "-m", "pip", "list"])
22+
assert repr(cmd) == "Command(args=['!', 'python', '-m', 'pip', 'list'])"
23+
assert cmd.invert_exit_code is True
24+
assert cmd.ignore_exit_code is False
25+
26+
1827
def tests_command_eq() -> None:
1928
cmd_1 = Command(["python", "-m", "pip", "list"])
2029
cmd_2 = Command(["python", "-m", "pip", "list"])
@@ -24,7 +33,8 @@ def tests_command_eq() -> None:
2433
def tests_command_ne() -> None:
2534
cmd_1 = Command(["python", "-m", "pip", "list"])
2635
cmd_2 = Command(["-", "python", "-m", "pip", "list"])
27-
assert cmd_1 != cmd_2
36+
cmd_3 = Command(["!", "python", "-m", "pip", "list"])
37+
assert cmd_1 != cmd_2 != cmd_3
2838

2939

3040
def tests_env_list_repr() -> None:

0 commit comments

Comments
 (0)