Skip to content

Commit

Permalink
Update ignore_fail option to support non-zero exit status from sequ…
Browse files Browse the repository at this point in the history
…ence (#40)

Allow two addition option value for `ingore_fail` option:
- `return_zero`: works like true
- `return_non_zero`: run all the subtasks regardless theirs exit status,
but returns non-zero exit status if any of subtasks failed
  • Loading branch information
tsimoshka authored Oct 26, 2021
1 parent cb17707 commit ec4e2cf
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 10 deletions.
12 changes: 10 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,21 @@ scripts (shell), and composite tasks (sequence).
]
release.default_item_type = "script"
A failure (non-zero result) will result in the rest of the tasks in the sequence not being executed, unless the :toml:`ignore_fail` option is set on the task like so:
A failure (non-zero result) will result in the rest of the tasks in the sequence not being executed, unless the :toml:`ignore_fail` option is set on the task to :toml:`true` or :toml:`return_zero` like so:

.. code-block:: toml
[tool.poe.tasks]
attempts.sequence = ["task1", "task2", "task3"]
attempts.ignore_fail = true
attempts.ignore_fail = "return_zero"
If you want to run all the subtasks in the sequence but return non-zero result in the end of the sequnce if any of the subtasks have failed you can set :toml:`ignore_fail` option to the :toml:`return_non_zero` value like so:

.. code-block:: toml
[tool.poe.tasks]
attempts.sequence = ["task1", "task2", "task3"]
attempts.ignore_fail = "return_non_zero"
Task level configuration
========================
Expand Down
2 changes: 1 addition & 1 deletion poethepoet/task/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class PoeTask(metaclass=MetaPoeTask):
content: TaskContent
options: Dict[str, Any]

__options__: Dict[str, Type] = {}
__options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {}
__content_type__: Type = str
__base_options: Dict[str, Union[Type, Tuple[Type, ...]]] = {
"args": (dict, list),
Expand Down
4 changes: 3 additions & 1 deletion poethepoet/task/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
MutableMapping,
Sequence,
Type,
Tuple,
TYPE_CHECKING,
Union,
)
from .base import PoeTask

Expand All @@ -26,7 +28,7 @@ class CmdTask(PoeTask):
content: str

__key__ = "cmd"
__options__: Dict[str, Type] = {}
__options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {}

def _handle_run(
self,
Expand Down
4 changes: 3 additions & 1 deletion poethepoet/task/ref.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
Optional,
Sequence,
Type,
Tuple,
TYPE_CHECKING,
Union,
)
from .base import PoeTask

Expand All @@ -24,7 +26,7 @@ class RefTask(PoeTask):
content: str

__key__ = "ref"
__options__: Dict[str, Type] = {}
__options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {}

def _handle_run(
self,
Expand Down
2 changes: 1 addition & 1 deletion poethepoet/task/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class ScriptTask(PoeTask):
content: str

__key__ = "script"
__options__: Dict[str, Type] = {}
__options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {}

def _handle_run(
self,
Expand Down
28 changes: 26 additions & 2 deletions poethepoet/task/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ class SequenceTask(PoeTask):

__key__ = "sequence"
__content_type__: Type = list
__options__: Dict[str, Type] = {"ignore_fail": bool, "default_item_type": str}
__options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {
"ignore_fail": (bool, str),
"default_item_type": str,
}

def __init__(
self,
Expand Down Expand Up @@ -70,12 +73,21 @@ def _handle_run(
# Indicate on the global context that there are multiple stages
context.multistage = True

ignore_fail = self.options.get("ignore_fail")
non_zero_subtasks: List[str] = list()
for subtask in self.subtasks:
task_result = subtask.run(context=context, extra_args=tuple(), env=env)
if task_result and not self.options.get("ignore_fail"):
if task_result and not ignore_fail:
raise ExecutionError(
f"Sequence aborted after failed subtask {subtask.name!r}"
)
if task_result:
non_zero_subtasks.append(subtask.name)

if non_zero_subtasks and ignore_fail == "return_non_zero":
raise ExecutionError(
f"Subtasks {', '.join(non_zero_subtasks)} returned non-zero exit status"
)
return 0

@classmethod
Expand All @@ -90,4 +102,16 @@ def _validate_task_def(
"Unsupported value for option `default_item_type` for task "
f"{task_name!r}. Expected one of {cls.get_task_types(content_type=str)}"
)
ignore_fail = task_def.get("ignore_fail")
if ignore_fail is not None and ignore_fail not in (
True,
False,
"return_zero",
"return_non_zero",
):
return (
"Unsupported value for option `ignore_fail` for task "
f'{task_name!r}. Expected one of (true, false, "return_zero", "return_non_zero")'
)

return None
4 changes: 2 additions & 2 deletions poethepoet/task/shell.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import shutil
import subprocess
from typing import Dict, MutableMapping, Sequence, Type, TYPE_CHECKING
from typing import Dict, MutableMapping, Sequence, Type, Tuple, TYPE_CHECKING, Union
from ..exceptions import PoeException
from .base import PoeTask

Expand All @@ -18,7 +18,7 @@ class ShellTask(PoeTask):
content: str

__key__ = "shell"
__options__: Dict[str, Type] = {}
__options__: Dict[str, Union[Type, Tuple[Type, ...]]] = {}

def _handle_run(
self,
Expand Down
68 changes: 68 additions & 0 deletions tests/test_ignore_fail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import pytest


@pytest.fixture
def generate_pyproject(tmp_path):
"""Return function which generates pyproject.toml with a given ignore_fail value."""

def generator(ignore_fail):
project_tmpl = """
[tool.poe.tasks]
task_1 = { shell = "echo 'task 1 error'; exit 1;" }
task_2 = { shell = "echo 'task 2 error'; exit 1;" }
task_3 = { shell = "echo 'task 3 success'; exit 0;" }
[tool.poe.tasks.all_tasks]
sequence = ["task_1", "task_2", "task_3"]
"""
if isinstance(ignore_fail, bool) and ignore_fail:
project_tmpl += "\nignore_fail = true"
elif not isinstance(ignore_fail, bool):
project_tmpl += f'\nignore_fail = "{ignore_fail}"'
with open(tmp_path / "pyproject.toml", "w") as fp:
fp.write(project_tmpl)

return tmp_path

return generator


@pytest.mark.parametrize("fail_value", [True, "return_zero"])
def test_full_ignore(generate_pyproject, run_poe, fail_value):
project_path = generate_pyproject(ignore_fail=fail_value)
result = run_poe("all_tasks", cwd=project_path)
assert result.code == 0, "Expected zero result"
assert "task 1 error" in result.capture, "Expected first task in log"
assert "task 2 error" in result.capture, "Expected second task in log"
assert "task 3 success" in result.capture, "Expected third task in log"


def test_without_ignore(generate_pyproject, run_poe):
project_path = generate_pyproject(ignore_fail=False)
result = run_poe("all_tasks", cwd=project_path)
assert result.code == 1, "Expected non-zero result"
assert "task 1 error" in result.capture, "Expected first task in log"
assert "task 2 error" not in result.capture, "Second task shouldn't run"
assert "task 3 success" not in result.capture, "Third task shouldn't run"
assert "Sequence aborted after failed subtask 'task_1'" in result.capture


def test_return_non_zero(generate_pyproject, run_poe):
project_path = generate_pyproject(ignore_fail="return_non_zero")
result = run_poe("all_tasks", cwd=project_path)
assert result.code == 1, "Expected non-zero result"
assert "task 1 error" in result.capture, "Expected first task in log"
assert "task 2 error" in result.capture, "Expected second task in log"
assert "task 3 success" in result.capture, "Expected third task in log"
assert "Subtasks task_1, task_2 returned non-zero exit status" in result.capture


def test_invalid_ingore_value(generate_pyproject, run_poe):
project_path = generate_pyproject(ignore_fail="invalid_value")
result = run_poe("all_tasks", cwd=project_path)
assert result.code == 1, "Expected non-zero result"
assert (
"Unsupported value for option `ignore_fail` for task 'all_tasks'."
' Expected one of (true, false, "return_zero", "return_non_zero")'
in result.capture
)

0 comments on commit ec4e2cf

Please sign in to comment.