Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bang to invert exit code #3271

Merged
merged 2 commits into from Apr 26, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"])
sillydan1 marked this conversation as resolved.
Show resolved Hide resolved
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