Skip to content

Commit 7fa412b

Browse files
authored
Merge pull request #831 from jadecci/cli
Thanks :)
2 parents 400cd69 + 864a386 commit 7fa412b

File tree

5 files changed

+145
-16
lines changed

5 files changed

+145
-16
lines changed

docs/source/tutorial/3-troubleshooting.ipynb

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@
141141
"cell_type": "markdown",
142142
"metadata": {},
143143
"source": [
144-
"The error pickle files can be loaded using the `cloudpickle` library, noting that it is\n",
144+
"The error pickle files can be viewed using the `pydracli crash` command, with the possibility of rerunning and debugging the job. Note that it is\n",
145145
"important to use the same Python version to load the files that was used to run the Pydra\n",
146146
"workflow"
147147
]
@@ -152,17 +152,28 @@
152152
"metadata": {},
153153
"outputs": [],
154154
"source": [
155-
"from pydra.utils.general import default_run_cache_root\n",
156-
"import cloudpickle as cp\n",
157-
"from pprint import pprint\n",
158155
"from pydra.tasks.testing import Divide\n",
156+
"from pydra.utils.general import default_run_cache_root\n",
159157
"\n",
160-
"with open(\n",
161-
" default_run_cache_root / Divide(x=15, y=0)._checksum / \"_error.pklz\", \"rb\"\n",
162-
") as f:\n",
163-
" error = cp.load(f)\n",
158+
"if __name__ == \"__main__\":\n",
159+
" divide = Divide(x=15, y=0)\n",
160+
" try:\n",
161+
" divide(cache_root=default_run_cache_root)\n",
162+
" except Exception:\n",
163+
" pass\n",
164+
"\n",
165+
" errorfile = default_run_cache_root / divide._checksum / \"_error.pklz\""
166+
]
167+
},
168+
{
169+
"cell_type": "code",
170+
"execution_count": null,
171+
"metadata": {},
172+
"outputs": [],
173+
"source": [
174+
"%%bash -s \"$errorfile\"\n",
164175
"\n",
165-
"pprint(error)"
176+
"pydracli crash $1"
166177
]
167178
},
168179
{

pydra/conftest.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import shutil
22
import os
33
import pytest
4+
import typing as ty
5+
from click.testing import CliRunner, Result as CliResult
46

57
os.environ["NO_ET"] = "true"
68

@@ -41,17 +43,39 @@ def pytest_generate_tests(metafunc):
4143
metafunc.parametrize("any_worker", available_workers)
4244

4345

46+
@pytest.fixture
47+
def cli_runner(catch_cli_exceptions: bool) -> ty.Callable[..., ty.Any]:
48+
def invoke(
49+
*args: ty.Any, catch_exceptions: bool = catch_cli_exceptions, **kwargs: ty.Any
50+
) -> CliResult:
51+
runner = CliRunner()
52+
result = runner.invoke(*args, catch_exceptions=catch_exceptions, **kwargs) # type: ignore[misc]
53+
return result
54+
55+
return invoke
56+
57+
4458
# For debugging in IDE's don't catch raised exceptions and let the IDE
4559
# break at it
46-
if os.getenv("_PYTEST_RAISE", "0") != "0": # pragma: no cover
60+
if os.getenv("_PYTEST_RAISE", "0") != "0":
61+
62+
@pytest.hookimpl(tryfirst=True)
63+
def pytest_exception_interact(call: pytest.CallInfo[ty.Any]) -> None:
64+
if call.excinfo is not None:
65+
raise call.excinfo.value
66+
67+
@pytest.hookimpl(tryfirst=True)
68+
def pytest_internalerror(excinfo: pytest.ExceptionInfo[BaseException]) -> None:
69+
raise excinfo.value
70+
71+
CATCH_CLI_EXCEPTIONS = False
72+
else:
73+
CATCH_CLI_EXCEPTIONS = True
4774

48-
@pytest.hookimpl(tryfirst=True) # pragma: no cover
49-
def pytest_exception_interact(call): # pragma: no cover
50-
raise call.excinfo.value # pragma: no cover
5175

52-
@pytest.hookimpl(tryfirst=True) # pragma: no cover
53-
def pytest_internalerror(excinfo): # pragma: no cover
54-
raise excinfo.value # pragma: no cover
76+
@pytest.fixture
77+
def catch_cli_exceptions() -> bool:
78+
return CATCH_CLI_EXCEPTIONS
5579

5680

5781
# Example VSCode launch configuration for debugging unittests

pydra/scripts/cli.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from pathlib import Path
2+
import pdb
3+
import sys
4+
5+
import click
6+
import cloudpickle as cp
7+
8+
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
9+
ExistingFilePath = click.Path(exists=True, dir_okay=False, resolve_path=True)
10+
11+
12+
@click.group(context_settings=CONTEXT_SETTINGS)
13+
def cli():
14+
pass
15+
16+
17+
@cli.command(context_settings=CONTEXT_SETTINGS)
18+
@click.argument("crashfile", type=ExistingFilePath)
19+
@click.option(
20+
"-r", "--rerun", is_flag=True, flag_value=True, help="Rerun crashed code."
21+
)
22+
@click.option(
23+
"-d",
24+
"--debugger",
25+
type=click.Choice([None, "ipython", "pdb"]),
26+
help="Debugger to use when rerunning",
27+
)
28+
def crash(crashfile, rerun, debugger=None):
29+
"""Display a crash file and rerun if required."""
30+
if crashfile.endswith(("pkl", "pklz")):
31+
with open(crashfile, "rb") as f:
32+
crash_content = cp.load(f)
33+
print("".join(crash_content["error message"]))
34+
35+
if rerun:
36+
jobfile = Path(crashfile).parent / "_job.pklz"
37+
if jobfile.exists():
38+
with open(jobfile, "rb") as f:
39+
job_obj = cp.load(f)
40+
41+
if debugger == "ipython":
42+
try:
43+
from IPython.core import ultratb
44+
45+
sys.excepthook = ultratb.FormattedTB(
46+
mode="Verbose", theme_name="Linux", call_pdb=True
47+
)
48+
except ImportError:
49+
raise ImportError(
50+
"'Ipython' needs to be installed to use the 'ipython' debugger"
51+
)
52+
53+
try:
54+
job_obj.run(rerun=True)
55+
except Exception: # noqa: E722
56+
if debugger == "pdb":
57+
pdb.post_mortem()
58+
elif debugger == "ipython":
59+
raise
60+
else:
61+
raise FileNotFoundError(f"Job file {jobfile} not found")
62+
else:
63+
raise ValueError("Only pickled crashfiles are supported")

pydra/scripts/tests/test_crash.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import pytest
2+
from pydra.scripts.cli import crash
3+
from pydra.tasks.testing import Divide
4+
from traceback import format_exception
5+
import typing as ty
6+
7+
8+
# @pytest.mark.xfail(reason="Need to fix a couple of things after syntax changes")
9+
def test_crash_cli(cli_runner, tmp_path):
10+
divide = Divide(x=15, y=0)
11+
with pytest.raises(ZeroDivisionError):
12+
divide(cache_root=tmp_path)
13+
14+
result = cli_runner(
15+
crash,
16+
[
17+
f"{tmp_path}/{divide._checksum}/_error.pklz",
18+
"--rerun",
19+
"--debugger",
20+
"pdb",
21+
],
22+
)
23+
assert result.exit_code == 0, show_cli_trace(result)
24+
25+
26+
def show_cli_trace(result: ty.Any) -> str:
27+
"Used in testing to show traceback of CLI output"
28+
return "".join(format_exception(*result.exc_info))

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ documentation = "https://nipype.github.io/pydra/"
114114
homepage = "https://nipype.github.io/pydra/"
115115
repository = "https://github.com/nipype/pydra.git"
116116

117+
[project.scripts]
118+
pydracli = "pydra.scripts.cli:cli"
119+
117120
[tool.hatch.build]
118121
packages = ["pydra"]
119122
exclude = ["tests"]

0 commit comments

Comments
 (0)