From 30832a48503d9a9c484eab7b0990ab73979d2439 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Wed, 29 Sep 2021 15:44:50 +0200 Subject: [PATCH 1/6] Add tryceratops. --- .pre-commit-config.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7ea00c84..d0897f7a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -90,6 +90,10 @@ repos: rev: "0.46" hooks: - id: check-manifest +- repo: https://github.com/guilatrova/tryceratops + rev: v0.6.0 + hooks: + - id: tryceratops - repo: meta hooks: - id: check-hooks-apply From b5abfc07a82ebd9e5306077d43cfab40a3e4a9e2 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Wed, 29 Sep 2021 18:09:37 +0200 Subject: [PATCH 2/6] Fix first exception. --- .pre-commit-config.yaml | 1 + src/_pytask/config.py | 2 +- tests/test_config.py | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d0897f7a..a8a72023 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -94,6 +94,7 @@ repos: rev: v0.6.0 hooks: - id: tryceratops + exclude: (console\.py|test_mark_expression\.py) - repo: meta hooks: - id: check-hooks-apply diff --git a/src/_pytask/config.py b/src/_pytask/config.py index 7e3b8711..68409777 100644 --- a/src/_pytask/config.py +++ b/src/_pytask/config.py @@ -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 diff --git a/tests/test_config.py b/tests/test_config.py index d26128dd..2295452f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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 @@ -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 From 7719d1ba7082060b7d5730988eca8516e1f082c0 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Wed, 29 Sep 2021 18:23:01 +0200 Subject: [PATCH 3/6] fix execute.py. --- src/_pytask/execute.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/_pytask/execute.py b/src/_pytask/execute.py index 08d2d735..ef28f4a6 100644 --- a/src/_pytask/execute.py +++ b/src/_pytask/execute.py @@ -96,10 +96,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. @@ -122,8 +122,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) From 923b63196c45f6231421c45f99ae1cd74aa8a21b Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 18 Oct 2021 23:30:14 +0200 Subject: [PATCH 4/6] Fix remaining issues. --- pyproject.toml | 4 ++++ src/_pytask/collect.py | 2 +- src/_pytask/debugging.py | 9 +++++---- src/_pytask/execute.py | 10 +++++++++- src/_pytask/outcomes.py | 14 ++++++++++++++ src/_pytask/traceback.py | 5 +++++ 6 files changed, 38 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1046f433..82173650 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/src/_pytask/collect.py b/src/_pytask/collect.py index b7bc3882..ace38820 100644 --- a/src/_pytask/collect.py +++ b/src/_pytask/collect.py @@ -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) diff --git a/src/_pytask/debugging.py b/src/_pytask/debugging.py index b6f999ce..f715f039 100644 --- a/src/_pytask/debugging.py +++ b/src/_pytask/debugging.py @@ -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 @@ -238,7 +239,7 @@ def do_quit(self, arg): ret = super().do_quit(arg) if cls._recursive_debug == 0: - raise Exception("Quitting debugger") + Exit("Quitting debugger") return ret @@ -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. @@ -371,7 +372,7 @@ def wrapper(*args, **kwargs): live_manager.resume() capman.resume() - raise e + raise task.function = wrapper @@ -432,4 +433,4 @@ def post_mortem(t) -> None: p.reset() p.interaction(None, t) if p.quitting: - raise Exception("Quitting debugger") + Exit("Quitting debugger") diff --git a/src/_pytask/execute.py b/src/_pytask/execute.py index ef28f4a6..2098f0c8 100644 --- a/src/_pytask/execute.py +++ b/src/_pytask/execute.py @@ -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 @@ -158,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 @@ -200,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"] diff --git a/src/_pytask/outcomes.py b/src/_pytask/outcomes.py index 678e8a03..1de32d80 100644 --- a/src/_pytask/outcomes.py +++ b/src/_pytask/outcomes.py @@ -1,3 +1,6 @@ +from typing import Optional + + class PytaskOutcome(Exception): """Base outcome of a task.""" @@ -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) diff --git a/src/_pytask/traceback.py b/src/_pytask/traceback.py index 22321736..ad160cd4 100644 --- a/src/_pytask/traceback.py +++ b/src/_pytask/traceback.py @@ -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) From 8f61358bf321998cf50424aa795676d99579b1e9 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Mon, 18 Oct 2021 23:56:56 +0200 Subject: [PATCH 5/6] Fix tests. --- src/_pytask/debugging.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytask/debugging.py b/src/_pytask/debugging.py index f715f039..d9bf34ef 100644 --- a/src/_pytask/debugging.py +++ b/src/_pytask/debugging.py @@ -239,7 +239,7 @@ def do_quit(self, arg): ret = super().do_quit(arg) if cls._recursive_debug == 0: - Exit("Quitting debugger") + raise Exit("Quitting debugger") return ret @@ -433,4 +433,4 @@ def post_mortem(t) -> None: p.reset() p.interaction(None, t) if p.quitting: - Exit("Quitting debugger") + raise Exit("Quitting debugger") From 256641482f8109a48c7c13d822a83bbc0e398279 Mon Sep 17 00:00:00 2001 From: Tobias Raabe Date: Tue, 19 Oct 2021 00:11:39 +0200 Subject: [PATCH 6/6] Add to changes. --- docs/source/changes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/changes.rst b/docs/source/changes.rst index cebe2bfd..80c2740e 100644 --- a/docs/source/changes.rst +++ b/docs/source/changes.rst @@ -12,6 +12,8 @@ all releases are available on `PyPI `_ 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