Skip to content

Cli #831

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

Merged
merged 13 commits into from
Jun 23, 2025
Merged

Cli #831

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions docs/source/tutorial/3-troubleshooting.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"The error pickle files can be loaded using the `cloudpickle` library, noting that it is\n",
"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",
"important to use the same Python version to load the files that was used to run the Pydra\n",
"workflow"
]
Expand All @@ -152,17 +152,28 @@
"metadata": {},
"outputs": [],
"source": [
"from pydra.utils.general import default_run_cache_root\n",
"import cloudpickle as cp\n",
"from pprint import pprint\n",
"from pydra.tasks.testing import Divide\n",
"from pydra.utils.general import default_run_cache_root\n",
"\n",
"with open(\n",
" default_run_cache_root / Divide(x=15, y=0)._checksum / \"_error.pklz\", \"rb\"\n",
") as f:\n",
" error = cp.load(f)\n",
"if __name__ == \"__main__\":\n",
" divide = Divide(x=15, y=0)\n",
" try:\n",
" divide(cache_root=default_run_cache_root)\n",
" except Exception:\n",
" pass\n",
"\n",
" errorfile = default_run_cache_root / divide._checksum / \"_error.pklz\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%bash -s \"$errorfile\"\n",
"\n",
"pprint(error)"
"pydracli crash $1"
]
},
{
Expand Down
38 changes: 31 additions & 7 deletions pydra/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import shutil
import os
import pytest
import typing as ty
from click.testing import CliRunner, Result as CliResult

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

Expand Down Expand Up @@ -41,17 +43,39 @@
metafunc.parametrize("any_worker", available_workers)


@pytest.fixture
def cli_runner(catch_cli_exceptions: bool) -> ty.Callable[..., ty.Any]:
def invoke(
*args: ty.Any, catch_exceptions: bool = catch_cli_exceptions, **kwargs: ty.Any
) -> CliResult:
runner = CliRunner()
result = runner.invoke(*args, catch_exceptions=catch_exceptions, **kwargs) # type: ignore[misc]
return result

Check warning on line 53 in pydra/conftest.py

View check run for this annotation

Codecov / codecov/patch

pydra/conftest.py#L53

Added line #L53 was not covered by tests

return invoke


# For debugging in IDE's don't catch raised exceptions and let the IDE
# break at it
if os.getenv("_PYTEST_RAISE", "0") != "0": # pragma: no cover
if os.getenv("_PYTEST_RAISE", "0") != "0":

@pytest.hookimpl(tryfirst=True)

Check warning on line 62 in pydra/conftest.py

View check run for this annotation

Codecov / codecov/patch

pydra/conftest.py#L62

Added line #L62 was not covered by tests
def pytest_exception_interact(call: pytest.CallInfo[ty.Any]) -> None:
if call.excinfo is not None:
raise call.excinfo.value

Check warning on line 65 in pydra/conftest.py

View check run for this annotation

Codecov / codecov/patch

pydra/conftest.py#L65

Added line #L65 was not covered by tests

@pytest.hookimpl(tryfirst=True)

Check warning on line 67 in pydra/conftest.py

View check run for this annotation

Codecov / codecov/patch

pydra/conftest.py#L67

Added line #L67 was not covered by tests
def pytest_internalerror(excinfo: pytest.ExceptionInfo[BaseException]) -> None:
raise excinfo.value

Check warning on line 69 in pydra/conftest.py

View check run for this annotation

Codecov / codecov/patch

pydra/conftest.py#L69

Added line #L69 was not covered by tests

CATCH_CLI_EXCEPTIONS = False

Check warning on line 71 in pydra/conftest.py

View check run for this annotation

Codecov / codecov/patch

pydra/conftest.py#L71

Added line #L71 was not covered by tests
else:
CATCH_CLI_EXCEPTIONS = True

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

@pytest.hookimpl(tryfirst=True) # pragma: no cover
def pytest_internalerror(excinfo): # pragma: no cover
raise excinfo.value # pragma: no cover
@pytest.fixture
def catch_cli_exceptions() -> bool:
return CATCH_CLI_EXCEPTIONS


# Example VSCode launch configuration for debugging unittests
Expand Down
63 changes: 63 additions & 0 deletions pydra/scripts/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from pathlib import Path
import pdb
import sys

import click
import cloudpickle as cp

CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
ExistingFilePath = click.Path(exists=True, dir_okay=False, resolve_path=True)


@click.group(context_settings=CONTEXT_SETTINGS)
def cli():
pass

Check warning on line 14 in pydra/scripts/cli.py

View check run for this annotation

Codecov / codecov/patch

pydra/scripts/cli.py#L14

Added line #L14 was not covered by tests


@cli.command(context_settings=CONTEXT_SETTINGS)
@click.argument("crashfile", type=ExistingFilePath)
@click.option(
"-r", "--rerun", is_flag=True, flag_value=True, help="Rerun crashed code."
)
@click.option(
"-d",
"--debugger",
type=click.Choice([None, "ipython", "pdb"]),
help="Debugger to use when rerunning",
)
def crash(crashfile, rerun, debugger=None):
"""Display a crash file and rerun if required."""
if crashfile.endswith(("pkl", "pklz")):
with open(crashfile, "rb") as f:
crash_content = cp.load(f)
print("".join(crash_content["error message"]))

if rerun:
jobfile = Path(crashfile).parent / "_job.pklz"
if jobfile.exists():
with open(jobfile, "rb") as f:
job_obj = cp.load(f)

if debugger == "ipython":
try:
from IPython.core import ultratb

Check warning on line 43 in pydra/scripts/cli.py

View check run for this annotation

Codecov / codecov/patch

pydra/scripts/cli.py#L42-L43

Added lines #L42 - L43 were not covered by tests

sys.excepthook = ultratb.FormattedTB(

Check warning on line 45 in pydra/scripts/cli.py

View check run for this annotation

Codecov / codecov/patch

pydra/scripts/cli.py#L45

Added line #L45 was not covered by tests
mode="Verbose", theme_name="Linux", call_pdb=True
)
except ImportError:
raise ImportError(

Check warning on line 49 in pydra/scripts/cli.py

View check run for this annotation

Codecov / codecov/patch

pydra/scripts/cli.py#L48-L49

Added lines #L48 - L49 were not covered by tests
"'Ipython' needs to be installed to use the 'ipython' debugger"
)

try:
job_obj.run(rerun=True)
except Exception: # noqa: E722
if debugger == "pdb":
pdb.post_mortem()
elif debugger == "ipython":
raise

Check warning on line 59 in pydra/scripts/cli.py

View check run for this annotation

Codecov / codecov/patch

pydra/scripts/cli.py#L59

Added line #L59 was not covered by tests
else:
raise FileNotFoundError(f"Job file {jobfile} not found")

Check warning on line 61 in pydra/scripts/cli.py

View check run for this annotation

Codecov / codecov/patch

pydra/scripts/cli.py#L61

Added line #L61 was not covered by tests
else:
raise ValueError("Only pickled crashfiles are supported")

Check warning on line 63 in pydra/scripts/cli.py

View check run for this annotation

Codecov / codecov/patch

pydra/scripts/cli.py#L63

Added line #L63 was not covered by tests
28 changes: 28 additions & 0 deletions pydra/scripts/tests/test_crash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import pytest
from pydra.scripts.cli import crash
from pydra.tasks.testing import Divide
from traceback import format_exception
import typing as ty


# @pytest.mark.xfail(reason="Need to fix a couple of things after syntax changes")
def test_crash_cli(cli_runner, tmp_path):
divide = Divide(x=15, y=0)
with pytest.raises(ZeroDivisionError):
divide(cache_root=tmp_path)

result = cli_runner(
crash,
[
f"{tmp_path}/{divide._checksum}/_error.pklz",
"--rerun",
"--debugger",
"pdb",
],
)
assert result.exit_code == 0, show_cli_trace(result)

Check warning on line 23 in pydra/scripts/tests/test_crash.py

View check run for this annotation

Codecov / codecov/patch

pydra/scripts/tests/test_crash.py#L23

Added line #L23 was not covered by tests


def show_cli_trace(result: ty.Any) -> str:
"Used in testing to show traceback of CLI output"
return "".join(format_exception(*result.exc_info))

Check warning on line 28 in pydra/scripts/tests/test_crash.py

View check run for this annotation

Codecov / codecov/patch

pydra/scripts/tests/test_crash.py#L28

Added line #L28 was not covered by tests
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ documentation = "https://nipype.github.io/pydra/"
homepage = "https://nipype.github.io/pydra/"
repository = "https://github.com/nipype/pydra.git"

[project.scripts]
pydracli = "pydra.scripts.cli:cli"

[tool.hatch.build]
packages = ["pydra"]
exclude = ["tests"]
Expand Down
Loading