Skip to content

Commit 9f3e340

Browse files
authored
Implement ExecutionReports. (#3)
1 parent 61be848 commit 9f3e340

8 files changed

Lines changed: 99 additions & 87 deletions

File tree

.github/workflows/continuous-integration-workflow.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,6 @@ jobs:
3131
shell: bash -l {0}
3232
run: conda install -c conda-forge tox-conda coverage
3333

34-
- name: Validate codecov.yml
35-
if: runner.os == 'Linux' && matrix.python-version == '3.8'
36-
shell: bash -l {0}
37-
run: cat codecov.yml | curl --data-binary @- https://codecov.io/validate
38-
3934
# Unit, integration, and end-to-end tests.
4035

4136
- name: Run unit tests and doctests.
@@ -65,6 +60,11 @@ jobs:
6560
shell: bash -l {0}
6661
run: bash <(curl -s https://codecov.io/bash) -F end_to_end -c
6762

63+
- name: Validate codecov.yml
64+
if: runner.os == 'Linux' && matrix.python-version == '3.7'
65+
shell: bash -l {0}
66+
run: cat codecov.yml | curl --data-binary @- https://codecov.io/validate
67+
6868

6969
pre-commit:
7070

docs/changes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ all releases are available on `Anaconda.org <https://anaconda.org/pytask/pytask>
1010
------------------
1111

1212
- :gh:`2` provided multiple small changes.
13+
- :gh:`3` implements a class which holds the execution report of one task.
1314

1415

1516
0.0.1 - 2020-06-29

src/pytask/config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pathlib import Path
88

99
import click
10-
from pytask import hookimpl
10+
import pytask
1111
from pytask.shared import to_list
1212

1313

@@ -19,7 +19,7 @@
1919
]
2020

2121

22-
@hookimpl
22+
@pytask.hookimpl
2323
def pytask_configure(pm, config_from_cli):
2424
config = {"pm": pm, "terminal_width": _get_terminal_width()}
2525

@@ -41,7 +41,7 @@ def pytask_configure(pm, config_from_cli):
4141
return config
4242

4343

44-
@hookimpl
44+
@pytask.hookimpl
4545
def pytask_parse_config(config, config_from_cli, config_from_file):
4646
config["ignore"] = (
4747
_get_first_not_none_value(

src/pytask/execute.py

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,18 @@
1313
from pytask.exceptions import NodeNotFoundError
1414
from pytask.mark import Mark
1515
from pytask.nodes import FilePathNode
16+
from pytask.report import ExecutionReport
1617
from pytask.report import format_execute_footer
1718

1819

1920
@hookimpl
2021
def pytask_execute(session):
2122
session.hook.pytask_execute_log_start(session=session)
2223
session.scheduler = session.hook.pytask_execute_create_scheduler(session=session)
23-
session.results = session.hook.pytask_execute_build(session=session)
24-
session.hook.pytask_execute_log_end(session=session, reports=session.results)
24+
session.execution_reports = session.hook.pytask_execute_build(session=session)
25+
session.hook.pytask_execute_log_end(
26+
session=session, reports=session.execution_reports
27+
)
2528

2629

2730
@hookimpl
@@ -41,12 +44,12 @@ def pytask_execute_create_scheduler(session):
4144

4245
@hookimpl
4346
def pytask_execute_build(session):
44-
results = []
47+
reports = []
4548
for task in session.scheduler:
46-
result = session.hook.pytask_execute_task_protocol(session=session, task=task)
47-
results.append(result)
49+
report = session.hook.pytask_execute_task_protocol(session=session, task=task)
50+
reports.append(report)
4851

49-
return results
52+
return reports
5053

5154

5255
@hookimpl
@@ -57,19 +60,13 @@ def pytask_execute_task_protocol(session, task):
5760
session.hook.pytask_execute_task(session=session, task=task)
5861
session.hook.pytask_execute_task_teardown(session=session, task=task)
5962
except Exception:
60-
etype, value, tb = sys.exc_info()
61-
result = {
62-
"task": task,
63-
"etype": etype,
64-
"value": value,
65-
"traceback": tb,
66-
}
63+
report = ExecutionReport.from_task_and_exception(task, sys.exc_info())
6764
else:
68-
result = {"task": task, "value": None}
69-
session.hook.pytask_execute_task_process_result(session=session, result=result)
70-
session.hook.pytask_execute_task_log_end(session=session, task=task, result=result)
65+
report = ExecutionReport.from_task(task)
66+
session.hook.pytask_execute_task_process_report(session=session, report=report)
67+
session.hook.pytask_execute_task_log_end(session=session, task=task, report=report)
7168

72-
return result
69+
return report
7370

7471

7572
@hookimpl(trylast=True)
@@ -114,13 +111,11 @@ def pytask_execute_task_teardown(session, task):
114111

115112

116113
@hookimpl(trylast=True)
117-
def pytask_execute_task_process_result(session, result):
118-
task = result["task"]
119-
if result["value"] is None:
120-
result["success"] = True
114+
def pytask_execute_task_process_report(session, report):
115+
task = report.task
116+
if report.success:
121117
_update_states_in_database(session.dag, task.name)
122118
else:
123-
result["success"] = False
124119
for descending_task_name in task_and_descending_tasks(task.name, session.dag):
125120
descending_task = session.dag.nodes[descending_task_name]["task"]
126121
descending_task.markers.append(
@@ -135,8 +130,8 @@ def pytask_execute_task_process_result(session, result):
135130

136131

137132
@hookimpl(trylast=True)
138-
def pytask_execute_task_log_end(result):
139-
if result["success"]:
133+
def pytask_execute_task_log_end(report):
134+
if report.success:
140135
click.secho(".", fg="green", nl=False)
141136
else:
142137
click.secho("F", fg="red", nl=False)
@@ -149,19 +144,15 @@ def pytask_execute_log_end(session, reports):
149144
session.execution_end = time.time()
150145
click.echo("")
151146

152-
n_successful = sum(result["success"] for result in reports)
147+
n_successful = sum(report.success for report in reports)
153148
n_failed = len(reports) - n_successful
154149
tm_width = session.config["terminal_width"]
155150

156151
for report in reports:
157-
if not report["success"]:
158-
click.echo(
159-
f"{{:=^{tm_width}}}".format(f" Task {report['task'].name} failed ")
160-
)
152+
if not report.success:
153+
click.echo(f"{{:=^{tm_width}}}".format(f" Task {report.task.name} failed "))
161154
click.echo("")
162-
traceback.print_exception(
163-
report["etype"], report["value"], report["traceback"]
164-
)
155+
traceback.print_exception(*report.exc_info)
165156
click.echo("")
166157
click.echo("=" * tm_width)
167158

src/pytask/hookspecs.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,12 @@ def pytask_execute_task_teardown(session, task):
170170

171171

172172
@hookspec(firstresult=True)
173-
def pytask_execute_task_process_result(session, result):
174-
"""Process the result of a task."""
173+
def pytask_execute_task_process_report(session, report):
174+
"""Process the report of a task."""
175175

176176

177177
@hookspec(firstresult=True)
178-
def pytask_execute_task_log_end(session, task, result):
178+
def pytask_execute_task_log_end(session, task, report):
179179
"""End logging of task execution."""
180180

181181

src/pytask/report.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,6 @@
66
import click
77

88

9-
@attr.s
10-
class ResolvingDependenciesReport:
11-
exc_info = attr.ib()
12-
13-
149
@attr.s
1510
class CollectionReportTask:
1611
path = attr.ib(type=Path)
@@ -51,6 +46,26 @@ def format_title(self):
5146
return f" Collection of file '{self.path}' failed "
5247

5348

49+
@attr.s
50+
class ResolvingDependenciesReport:
51+
exc_info = attr.ib()
52+
53+
54+
@attr.s
55+
class ExecutionReport:
56+
task = attr.ib()
57+
success = attr.ib(type=bool)
58+
exc_info = attr.ib(default=None)
59+
60+
@classmethod
61+
def from_task_and_exception(cls, task, exc_info):
62+
return cls(task, False, exc_info)
63+
64+
@classmethod
65+
def from_task(cls, task):
66+
return cls(task, True)
67+
68+
5469
def format_execute_footer(n_successful, n_failed, duration, terminal_width):
5570
message = []
5671
if n_successful:

src/pytask/skipping.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,38 +25,45 @@ def pytask_execute_task_setup(task):
2525

2626

2727
@pytask.hookimpl
28-
def pytask_execute_task_process_result(session, result):
29-
if isinstance(result["value"], SkippedUnchanged):
30-
result["success"] = True
28+
def pytask_execute_task_process_report(session, report):
29+
if report.exc_info and isinstance(report.exc_info[1], SkippedUnchanged):
30+
report.success = True
3131

32-
elif isinstance(result["value"], Skipped):
33-
result["success"] = True
32+
elif report.exc_info and isinstance(report.exc_info[1], Skipped):
33+
report.success = True
3434
for descending_task_name in task_and_descending_tasks(
35-
result["task"].name, session.dag
35+
report.task.name, session.dag
3636
):
3737
descending_task = session.dag.nodes[descending_task_name]["task"]
3838
descending_task.markers.append(Mark("skip", (), {},))
3939

40-
elif isinstance(result["value"], SkippedAncestorFailed):
41-
result["success"] = False
42-
result["traceback"] = None
40+
elif report.exc_info and isinstance(report.exc_info[1], SkippedAncestorFailed):
41+
report.success = False
42+
report.exc_info = _remove_traceback_from_exc_info(report.exc_info)
4343

44-
if isinstance(result["value"], (Skipped, SkippedUnchanged, SkippedAncestorFailed)):
44+
if report.exc_info and isinstance(
45+
report.exc_info[1], (Skipped, SkippedUnchanged, SkippedAncestorFailed)
46+
):
4547
return True
4648

4749

4850
@pytask.hookimpl
49-
def pytask_execute_task_log_end(result):
50-
value = result["value"]
51-
if result["success"]:
52-
if isinstance(value, Skipped):
51+
def pytask_execute_task_log_end(report):
52+
if report.success:
53+
if report.exc_info and isinstance(report.exc_info[1], Skipped):
5354
click.secho("s", fg="yellow", nl=False)
54-
elif isinstance(value, SkippedUnchanged):
55+
elif report.exc_info and isinstance(report.exc_info[1], SkippedUnchanged):
5556
click.secho("s", fg="green", nl=False)
5657
else:
57-
if isinstance(value, SkippedAncestorFailed):
58+
if report.exc_info and isinstance(report.exc_info[1], SkippedAncestorFailed):
5859
click.secho("s", fg="red", nl=False)
5960

60-
if isinstance(value, (Skipped, SkippedUnchanged, SkippedAncestorFailed)):
61+
if report.exc_info and isinstance(
62+
report.exc_info[1], (Skipped, SkippedUnchanged, SkippedAncestorFailed)
63+
):
6164
# Return non-None value so that the task is not logged again.
6265
return True
66+
67+
68+
def _remove_traceback_from_exc_info(exc_info):
69+
return (*exc_info[:2], None)

tests/test_skipping.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
import os
22
import textwrap
3-
from contextlib import contextmanager
3+
from contextlib import ExitStack as does_not_raise # noqa: N813
44

55
import pytest
66
from pytask.main import main
77
from pytask.mark import Mark
88
from pytask.outcomes import Skipped
99
from pytask.outcomes import SkippedAncestorFailed
1010
from pytask.outcomes import SkippedUnchanged
11+
from pytask.report import ExecutionReport
1112
from pytask.skipping import pytask_execute_task_log_end
1213
from pytask.skipping import pytask_execute_task_setup
1314

1415

15-
@contextmanager
16-
def does_not_raise():
17-
yield
18-
19-
2016
@pytest.mark.end_to_end
2117
def test_skip_unchanged(tmp_path):
2218
source = """
@@ -26,10 +22,10 @@ def task_dummy():
2622
tmp_path.joinpath("task_dummy.py").write_text(textwrap.dedent(source))
2723

2824
session = main({"paths": tmp_path})
29-
assert session.results[0]["success"]
25+
assert session.execution_reports[0].success
3026

3127
session = main({"paths": tmp_path})
32-
assert isinstance(session.results[0]["value"], SkippedUnchanged)
28+
assert isinstance(session.execution_reports[0].exc_info[1], SkippedUnchanged)
3329

3430

3531
@pytest.mark.end_to_end
@@ -51,12 +47,12 @@ def task_dummy():
5147
os.chdir(tmp_path)
5248
session = main({"paths": tmp_path})
5349

54-
assert session.results[0]["success"]
50+
assert session.execution_reports[0].success
5551
assert tmp_path.joinpath("out.txt").read_text() == "Original content of in.txt."
5652

5753
session = main({"paths": tmp_path})
5854

59-
assert isinstance(session.results[0]["value"], SkippedUnchanged)
55+
assert isinstance(session.execution_reports[0].exc_info[1], SkippedUnchanged)
6056
assert tmp_path.joinpath("out.txt").read_text() == "Original content of in.txt."
6157

6258

@@ -81,10 +77,10 @@ def task_second():
8177
os.chdir(tmp_path)
8278
session = main({"paths": tmp_path})
8379

84-
assert not session.results[0]["success"]
85-
assert isinstance(session.results[0]["value"], Exception)
86-
assert not session.results[1]["success"]
87-
assert isinstance(session.results[1]["value"], SkippedAncestorFailed)
80+
assert not session.execution_reports[0].success
81+
assert isinstance(session.execution_reports[0].exc_info[1], Exception)
82+
assert not session.execution_reports[1].success
83+
assert isinstance(session.execution_reports[1].exc_info[1], SkippedAncestorFailed)
8884

8985

9086
def test_if_skip_decorator_is_applied(tmp_path):
@@ -108,10 +104,10 @@ def task_second():
108104
os.chdir(tmp_path)
109105
session = main({"paths": tmp_path})
110106

111-
assert session.results[0]["success"]
112-
assert isinstance(session.results[0]["value"], Skipped)
113-
assert session.results[1]["success"]
114-
assert isinstance(session.results[1]["value"], Skipped)
107+
assert session.execution_reports[0].success
108+
assert isinstance(session.execution_reports[0].exc_info[1], Skipped)
109+
assert session.execution_reports[1].success
110+
assert isinstance(session.execution_reports[1].exc_info[1], Skipped)
115111

116112

117113
@pytest.mark.unit
@@ -148,13 +144,15 @@ class Task:
148144
)
149145
def test_pytask_execute_task_log_end(capsys, exception, character):
150146
if isinstance(exception, (Skipped, SkippedUnchanged)):
151-
result = {"success": False, "value": exception()}
147+
report = ExecutionReport.from_task_and_exception((), exception())
148+
report.success = True
152149
elif isinstance(exception, SkippedAncestorFailed):
153-
result = {"success": True, "value": SkippedAncestorFailed(), "reason": ""}
150+
report = ExecutionReport.from_task_and_exception((), SkippedAncestorFailed)
151+
report.success = True
154152
else:
155-
result = {"success": True, "value": None}
153+
report = ExecutionReport.from_task(())
156154

157-
out = pytask_execute_task_log_end(result)
155+
out = pytask_execute_task_log_end(report)
158156

159157
captured = capsys.readouterr()
160158
if isinstance(exception, (Skipped, SkippedUnchanged)):

0 commit comments

Comments
 (0)