Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ repos:
rev: "0.46"
hooks:
- id: check-manifest
- repo: https://github.com/guilatrova/tryceratops
rev: v0.6.0
hooks:
- id: tryceratops
exclude: (console\.py|test_mark_expression\.py)
- repo: meta
hooks:
- id: check-hooks-apply
Expand Down
2 changes: 2 additions & 0 deletions docs/source/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ all releases are available on `PyPI <https://pypi.org/project/pytask>`_ and

- :gh:`142` removes the display of skipped and persisted tasks from the live execution
table for the default verbosity level of 1. They are displayed at 2.
- :gh:`144` adds tryceratops to the pre-commit hooks for catching issues with
exceptions.


0.1.1 - 2021-08-25
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.0"]

[tool.setuptools_scm]
write_to = "src/_pytask/_version.py"


[tool.tryceratops]
ignore = ["TC003"]
2 changes: 1 addition & 1 deletion src/_pytask/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def pytask_collect_node(session, path, node):
):
case_sensitive_path = find_case_sensitive_path(node, "win32")
if str(node) != str(case_sensitive_path):
raise Exception(_TEMPLATE_ERROR.format(node, case_sensitive_path))
raise ValueError(_TEMPLATE_ERROR.format(node, case_sensitive_path))

return FilePathNode.from_path(node)

Expand Down
2 changes: 1 addition & 1 deletion src/_pytask/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ def _find_project_root_and_ini(paths: List[Path]):
if path.exists():
try:
_read_config(path)
except Exception:
except KeyError:
pass
else:
config_path = path
Expand Down
9 changes: 5 additions & 4 deletions src/_pytask/debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from _pytask.config import hookimpl
from _pytask.console import console
from _pytask.nodes import PythonFunctionTask
from _pytask.outcomes import Exit
from _pytask.shared import convert_truthy_or_falsy_to_bool
from _pytask.shared import get_first_non_none_value
from _pytask.traceback import remove_internal_traceback_frames_from_exc_info
Expand Down Expand Up @@ -238,7 +239,7 @@ def do_quit(self, arg):
ret = super().do_quit(arg)

if cls._recursive_debug == 0:
raise Exception("Quitting debugger")
raise Exit("Quitting debugger")

return ret

Expand Down Expand Up @@ -341,7 +342,7 @@ def wrapper(*args, **kwargs):
try:
task_function(*args, **kwargs)

except Exception as e:
except Exception:
# Order is important! Pausing the live object before the capturemanager
# would flush the table to stdout and it will be visible in the captured
# output.
Expand Down Expand Up @@ -371,7 +372,7 @@ def wrapper(*args, **kwargs):
live_manager.resume()
capman.resume()

raise e
raise

task.function = wrapper

Expand Down Expand Up @@ -432,4 +433,4 @@ def post_mortem(t) -> None:
p.reset()
p.interaction(None, t)
if p.quitting:
raise Exception("Quitting debugger")
raise Exit("Quitting debugger")
20 changes: 15 additions & 5 deletions src/_pytask/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
from _pytask.exceptions import NodeNotFoundError
from _pytask.mark import Mark
from _pytask.nodes import FilePathNode
from _pytask.outcomes import Exit
from _pytask.outcomes import Persisted
from _pytask.outcomes import Skipped
from _pytask.report import ExecutionReport
from _pytask.shared import reduce_node_name
from _pytask.traceback import format_exception_without_traceback
from _pytask.traceback import remove_traceback_from_exc_info
from _pytask.traceback import render_exc_info

Expand Down Expand Up @@ -96,10 +98,10 @@ def pytask_execute_task_setup(session, task):
node = session.dag.nodes[dependency]["node"]
try:
node.state()
except NodeNotFoundError:
except NodeNotFoundError as e:
raise NodeNotFoundError(
f"{node.name} is missing and required for {task.name}."
)
) from e

# Create directory for product if it does not exist. Maybe this should be a `setup`
# method for the node classes.
Expand All @@ -122,8 +124,10 @@ def pytask_execute_task_teardown(session, task):
node = session.dag.nodes[product]["node"]
try:
node.state()
except NodeNotFoundError:
raise NodeNotFoundError(f"{node.name} was not produced by {task.name}.")
except NodeNotFoundError as e:
raise NodeNotFoundError(
f"{node.name} was not produced by {task.name}."
) from e


@hookimpl(trylast=True)
Expand Down Expand Up @@ -156,6 +160,9 @@ def pytask_execute_task_process_report(session, report):
if session.n_tasks_failed >= session.config["max_failures"]:
session.should_stop = True

if report.exc_info and isinstance(report.exc_info[1], Exit):
session.should_stop = True

return True


Expand Down Expand Up @@ -198,7 +205,10 @@ def pytask_execute_log_end(session, reports):

console.print()

console.print(render_exc_info(*report.exc_info, show_locals))
if report.exc_info and isinstance(report.exc_info[1], Exit):
console.print(format_exception_without_traceback(report.exc_info))
else:
console.print(render_exc_info(*report.exc_info, show_locals))

console.print()
show_capture = session.config["show_capture"]
Expand Down
14 changes: 14 additions & 0 deletions src/_pytask/outcomes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Optional


class PytaskOutcome(Exception):
"""Base outcome of a task."""

Expand All @@ -16,3 +19,14 @@ class SkippedUnchanged(PytaskOutcome):

class Persisted(PytaskOutcome):
"""Outcome if task should persist."""


class Exit(Exception):
"""Raised for immediate program exits (no tracebacks/summaries)."""

def __init__(
self, msg: str = "unknown reason", returncode: Optional[int] = None
) -> None:
self.msg = msg
self.returncode = returncode
super().__init__(msg)
5 changes: 5 additions & 0 deletions src/_pytask/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ def render_exc_info(exc_type, exc_value, traceback, show_locals=False):
return renderable


def format_exception_without_traceback(exc_info):
"""Format an exception without displaying the traceback."""
return f"[red bold]{exc_info[0].__name__}:[/] {exc_info[1]}"


def remove_traceback_from_exc_info(exc_info):
"""Remove traceback from exception."""
return (*exc_info[:2], None)
Expand Down
21 changes: 21 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import os
import textwrap
from contextlib import ExitStack as does_not_raise # noqa: N813

import pytest
from _pytask.config import _find_project_root_and_ini
from _pytask.config import _read_config
from pytask import main


Expand Down Expand Up @@ -131,3 +133,22 @@ def test_passing_paths_via_configuration_file(tmp_path, config_path, file_or_fol

assert session.exit_code == 0
assert len(session.tasks) == 1


@pytest.mark.unit
@pytest.mark.parametrize(
"file_exists, content, expectation, expected",
[
(False, None, pytest.raises(KeyError), None),
(True, "[pytask]", does_not_raise(), {}),
(True, "[pytask]\nvalue = 1", does_not_raise(), {"value": "1"}),
],
)
def test_read_config(tmp_path, file_exists, content, expectation, expected):
path = tmp_path / "config.ini"
if file_exists:
path.write_text(content)

with expectation:
result = _read_config(path)
assert result == expected