Skip to content

Commit 9bd30f4

Browse files
authored
Limit number of items in the execution table during the execution. (#151)
1 parent 90f5b5a commit 9bd30f4

File tree

7 files changed

+204
-17
lines changed

7 files changed

+204
-17
lines changed

Diff for: docs/source/changes.rst

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ all releases are available on `PyPI <https://pypi.org/project/pytask>`_ and
1414
table for the default verbosity level of 1. They are displayed at 2.
1515
- :gh:`144` adds tryceratops to the pre-commit hooks for catching issues with
1616
exceptions.
17+
- :gh:`150` adds a limit on the number of items displayed in the execution table which
18+
is also configurable with ``--n-entries-in-table`` on the cli and
19+
``n_entries_in_table`` in the configuration file.
1720
- :gh:`152` makes the duration of the execution readable by humans by separating it into
1821
days, hours, minutes and seconds.
1922

Diff for: docs/source/reference_guides/configuration.rst

+17
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,23 @@ The options
9797
wip: Work-in-progress. These are tasks which I am currently working on.
9898
9999
100+
.. confval:: n_entries_in_table
101+
102+
You can limit the number of entries displayed in the live table during the execution
103+
to make it more clear. Use either ``all`` or an integer greater or equal to one. On
104+
the command line use
105+
106+
.. code-block:: console
107+
108+
$ pytask build --n-entries-in-table 10
109+
110+
and in the configuration use
111+
112+
.. code-block:: ini
113+
114+
n_entries_in_table = all # default 15
115+
116+
100117
.. confval:: paths
101118

102119
If you want to collect tasks from specific paths without passing the names via the

Diff for: src/_pytask/live.py

+73-5
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,71 @@
11
from pathlib import Path
2+
from typing import Union
23

34
import attr
5+
import click
46
from _pytask.config import hookimpl
57
from _pytask.console import console
8+
from _pytask.shared import get_first_non_none_value
69
from _pytask.shared import reduce_node_name
710
from rich.live import Live
811
from rich.status import Status
912
from rich.table import Table
1013
from rich.text import Text
1114

1215

16+
@hookimpl
17+
def pytask_extend_command_line_interface(cli):
18+
"""Extend command line interface."""
19+
additional_parameters = [
20+
click.Option(
21+
["--n-entries-in-table"],
22+
default=None,
23+
help="How many entries to display in the table during the execution. "
24+
"Tasks which are running are always displayed. [default: 15]",
25+
),
26+
]
27+
cli.commands["build"].params.extend(additional_parameters)
28+
29+
30+
@hookimpl
31+
def pytask_parse_config(config, config_from_cli, config_from_file):
32+
config["n_entries_in_table"] = get_first_non_none_value(
33+
config_from_cli,
34+
config_from_file,
35+
key="n_entries_in_table",
36+
default=15,
37+
callback=_parse_n_entries_in_table,
38+
)
39+
40+
41+
def _parse_n_entries_in_table(value: Union[int, str, None]) -> int:
42+
if value in ["none", "None", None, ""]:
43+
out = None
44+
elif isinstance(value, int) and value >= 1:
45+
out = value
46+
elif isinstance(value, str) and value.isdigit() and int(value) >= 1:
47+
out = int(value)
48+
elif value == "all":
49+
out = 1_000_000
50+
else:
51+
raise ValueError(
52+
"'n_entries_in_table' can either be 'None' or an integer bigger than one."
53+
)
54+
return out
55+
56+
1357
@hookimpl
1458
def pytask_post_parse(config):
1559
live_manager = LiveManager()
1660
config["pm"].register(live_manager, "live_manager")
1761

1862
if config["verbose"] >= 1:
19-
live_execution = LiveExecution(live_manager, config["paths"], config["verbose"])
63+
live_execution = LiveExecution(
64+
live_manager,
65+
config["paths"],
66+
config["n_entries_in_table"],
67+
config["verbose"],
68+
)
2069
config["pm"].register(live_execution)
2170

2271
live_collection = LiveCollection(live_manager)
@@ -66,6 +115,7 @@ class LiveExecution:
66115

67116
_live_manager = attr.ib(type=LiveManager)
68117
_paths = attr.ib(type=Path)
118+
_n_entries_in_table = attr.ib(type=int)
69119
_verbose = attr.ib(type=int)
70120
_running_tasks = attr.ib(factory=set)
71121
_reports = attr.ib(factory=list)
@@ -74,6 +124,7 @@ class LiveExecution:
74124
def pytask_execute_build(self):
75125
self._live_manager.start()
76126
yield
127+
self._update_table(reduce_table=False)
77128
self._live_manager.stop(transient=False)
78129

79130
@hookimpl(tryfirst=True)
@@ -86,10 +137,27 @@ def pytask_execute_task_log_end(self, report):
86137
self.update_reports(report)
87138
return True
88139

89-
def _generate_table(self):
140+
def _generate_table(self, reduce_table: bool) -> Union[None, Table]:
141+
"""Generate the table.
142+
143+
First, display all completed tasks and, then, all running tasks.
144+
145+
The number of entries can be limited. All running tasks are always displayed and
146+
if more entries are requested, the list is filled up with completed tasks.
147+
148+
"""
90149
if self._running_tasks or self._reports:
150+
151+
n_reports_to_display = self._n_entries_in_table - len(self._running_tasks)
152+
if not reduce_table:
153+
relevant_reports = self._reports
154+
elif n_reports_to_display >= 1:
155+
relevant_reports = self._reports[-n_reports_to_display:]
156+
else:
157+
relevant_reports = []
158+
91159
table = Table("Task", "Outcome")
92-
for report in self._reports:
160+
for report in relevant_reports:
93161
if report["symbol"] in ("s", "p") and self._verbose < 2:
94162
pass
95163
else:
@@ -103,8 +171,8 @@ def _generate_table(self):
103171

104172
return table
105173

106-
def _update_table(self):
107-
table = self._generate_table()
174+
def _update_table(self, reduce_table: bool = True):
175+
table = self._generate_table(reduce_table)
108176
self._live_manager.update(table)
109177

110178
def update_running_tasks(self, new_running_task):

Diff for: tests/test_live.py

+101-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,43 @@
11
import textwrap
2+
from contextlib import ExitStack as does_not_raise # noqa: N813
23

34
import pytest
5+
from _pytask.live import _parse_n_entries_in_table
46
from _pytask.live import LiveExecution
57
from _pytask.live import LiveManager
68
from _pytask.nodes import PythonFunctionTask
79
from _pytask.report import ExecutionReport
810
from pytask import cli
911

1012

13+
@pytest.mark.unit
14+
@pytest.mark.parametrize(
15+
"value, expectation, expected",
16+
[
17+
pytest.param(None, does_not_raise(), None, id="none parsed"),
18+
pytest.param(3, does_not_raise(), 3, id="int parsed"),
19+
pytest.param("10", does_not_raise(), 10, id="string int parsed"),
20+
pytest.param("all", does_not_raise(), 1_000_000, id="all to large int"),
21+
pytest.param(
22+
-1,
23+
pytest.raises(ValueError, match="'n_entries_in_table' can"),
24+
None,
25+
id="negative int raises error",
26+
),
27+
pytest.param(
28+
"-1",
29+
pytest.raises(ValueError, match="'n_entries_in_table' can"),
30+
None,
31+
id="negative int raises error",
32+
),
33+
],
34+
)
35+
def test_parse_n_entries_in_table(value, expectation, expected):
36+
with expectation:
37+
result = _parse_n_entries_in_table(value)
38+
assert result == expected
39+
40+
1141
@pytest.mark.end_to_end
1242
@pytest.mark.parametrize("verbose", [False, True])
1343
def test_verbose_mode_execution(tmp_path, runner, verbose):
@@ -33,7 +63,7 @@ def test_live_execution_sequentially(capsys, tmp_path):
3363
)
3464

3565
live_manager = LiveManager()
36-
live = LiveExecution(live_manager, [tmp_path], 1)
66+
live = LiveExecution(live_manager, [tmp_path], 20, 1)
3767

3868
live_manager.start()
3969
live.update_running_tasks(task)
@@ -86,7 +116,7 @@ def test_live_execution_displays_skips_and_persists(capsys, tmp_path, verbose, s
86116
)
87117

88118
live_manager = LiveManager()
89-
live = LiveExecution(live_manager, [tmp_path], verbose)
119+
live = LiveExecution(live_manager, [tmp_path], 20, verbose)
90120

91121
live_manager.start()
92122
live.update_running_tasks(task)
@@ -113,3 +143,72 @@ def test_live_execution_displays_skips_and_persists(capsys, tmp_path, verbose, s
113143
assert f"│ {symbol}" in captured.out
114144

115145
assert "running" not in captured.out
146+
147+
148+
@pytest.mark.unit
149+
@pytest.mark.parametrize("n_entries_in_table", [1, 2])
150+
def test_live_execution_displays_subset_of_table(capsys, tmp_path, n_entries_in_table):
151+
path = tmp_path.joinpath("task_dummy.py")
152+
running_task = PythonFunctionTask(
153+
"task_running", path.as_posix() + "::task_running", path, None
154+
)
155+
156+
live_manager = LiveManager()
157+
live = LiveExecution(live_manager, [tmp_path], n_entries_in_table, 1)
158+
159+
live_manager.start()
160+
live.update_running_tasks(running_task)
161+
live_manager.stop()
162+
163+
captured = capsys.readouterr()
164+
assert "Task" in captured.out
165+
assert "Outcome" in captured.out
166+
assert "::task_running" in captured.out
167+
assert " running " in captured.out
168+
169+
completed_task = PythonFunctionTask(
170+
"task_completed", path.as_posix() + "::task_completed", path, None
171+
)
172+
live.update_running_tasks(completed_task)
173+
report = ExecutionReport(
174+
task=completed_task, success=True, exc_info=None, symbol=".", color="black"
175+
)
176+
177+
live_manager.resume()
178+
live.update_reports(report)
179+
live_manager.stop()
180+
181+
# Test that report is or is not included.
182+
captured = capsys.readouterr()
183+
assert "Task" in captured.out
184+
assert "Outcome" in captured.out
185+
assert "::task_running" in captured.out
186+
assert " running " in captured.out
187+
188+
if n_entries_in_table == 1:
189+
assert "task_dummy.py::task_completed" not in captured.out
190+
assert "│ ." not in captured.out
191+
else:
192+
assert "task_dummy.py::task_completed" in captured.out
193+
assert "│ ." in captured.out
194+
195+
196+
def test_full_execution_table_is_displayed_at_the_end_of_execution(tmp_path, runner):
197+
source = """
198+
import pytask
199+
200+
@pytask.mark.parametrize("produces", [f"{i}.txt" for i in range(4)])
201+
def task_create_file(produces):
202+
produces.touch()
203+
"""
204+
# Subfolder to reduce task id and be able to check the output later.
205+
tmp_path.joinpath("d").mkdir()
206+
tmp_path.joinpath("d", "task_t.py").write_text(textwrap.dedent(source))
207+
208+
result = runner.invoke(
209+
cli, [tmp_path.joinpath("d").as_posix(), "--n-entries-in-table=1"]
210+
)
211+
212+
assert result.exit_code == 0
213+
for i in range(4):
214+
assert f"{i}.txt" in result.output

Diff for: tests/test_mark.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def test_pytask_mark_notcallable() -> None:
2424
@pytest.mark.unit
2525
@pytest.mark.filterwarnings("ignore:Unknown pytask.mark.foo")
2626
def test_mark_with_param():
27-
def some_function(abc):
27+
def some_function():
2828
pass
2929

3030
class SomeClass:

Diff for: tests/test_mark_expression.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ def evaluate(input_: str, matcher: Callable[[str], bool]) -> bool:
1111

1212
@pytest.mark.unit
1313
def test_empty_is_false() -> None:
14-
assert not evaluate("", lambda ident: False)
15-
assert not evaluate("", lambda ident: True)
16-
assert not evaluate(" ", lambda ident: False)
17-
assert not evaluate("\t", lambda ident: False)
14+
assert not evaluate("", lambda ident: False) # noqa: U100
15+
assert not evaluate("", lambda ident: True) # noqa: U100
16+
assert not evaluate(" ", lambda ident: False) # noqa: U100
17+
assert not evaluate("\t", lambda ident: False) # noqa: U100
1818

1919

2020
@pytest.mark.unit
@@ -119,7 +119,7 @@ def test_syntax_oddeties(expr: str, expected: bool) -> None:
119119
)
120120
def test_syntax_errors(expr: str, column: int, message: str) -> None:
121121
with pytest.raises(ParseError) as excinfo:
122-
evaluate(expr, lambda ident: True)
122+
evaluate(expr, lambda ident: True) # noqa: U100
123123
assert excinfo.value.column == column
124124
assert excinfo.value.message == message
125125

@@ -183,4 +183,4 @@ def test_valid_idents(ident: str) -> None:
183183
)
184184
def test_invalid_idents(ident: str) -> None:
185185
with pytest.raises(ParseError):
186-
evaluate(ident, lambda ident: True)
186+
evaluate(ident, lambda ident: True) # noqa: U100

Diff for: tests/test_parametrize.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def session():
3333
@pytest.mark.integration
3434
def test_pytask_generate_tasks_0(session):
3535
@pytask.mark.parametrize("i", range(2))
36-
def func(i):
36+
def func(i): # noqa: U100
3737
pass
3838

3939
names_and_objs = pytask_parametrize_task(session, "func", func)
@@ -48,7 +48,7 @@ def func(i):
4848
def test_pytask_generate_tasks_1(session):
4949
@pytask.mark.parametrize("j", range(2))
5050
@pytask.mark.parametrize("i", range(2))
51-
def func(i, j):
51+
def func(i, j): # noqa: U100
5252
pass
5353

5454
names_and_objs = pytask_parametrize_task(session, "func", func)
@@ -66,7 +66,7 @@ def func(i, j):
6666
def test_pytask_generate_tasks_2(session):
6767
@pytask.mark.parametrize("j, k", itertools.product(range(2), range(2)))
6868
@pytask.mark.parametrize("i", range(2))
69-
def func(i, j, k):
69+
def func(i, j, k): # noqa: U100
7070
pass
7171

7272
names_and_objs = pytask_parametrize_task(session, "func", func)

0 commit comments

Comments
 (0)