Skip to content

Add a counter under the execution table to show completed tasks. #252

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 3 commits into from
Apr 2, 2022
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
2 changes: 2 additions & 0 deletions docs/source/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and
- {pull}`245` adds choices on the command line to the help pages as metavars and show
defaults.
- {pull}`246` formalizes choices for {class}`click.Choice` to {class}`enum.Enum`.
- {pull}`248` adds a counter at the bottom of the execution table to show how many tasks
have been processed.

## 0.1.9 - 2022-02-23

Expand Down
93 changes: 62 additions & 31 deletions src/_pytask/live.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Dict
from typing import Generator
from typing import List
from typing import Union

import attr
import click
Expand All @@ -16,9 +17,11 @@
from _pytask.outcomes import TaskOutcome
from _pytask.report import CollectionReport
from _pytask.report import ExecutionReport
from _pytask.session import Session
from _pytask.shared import get_first_non_none_value
from rich.live import Live
from rich.status import Status
from rich.style import Style
from rich.table import Table
from rich.text import Text

Expand Down Expand Up @@ -78,15 +81,21 @@ def pytask_post_parse(config: dict[str, Any]) -> None:

if config["verbose"] >= 1:
live_execution = LiveExecution(
live_manager,
config["n_entries_in_table"],
config["verbose"],
config["editor_url_scheme"],
live_manager=live_manager,
n_entries_in_table=config["n_entries_in_table"],
verbose=config["verbose"],
editor_url_scheme=config["editor_url_scheme"],
)
config["pm"].register(live_execution)
config["pm"].register(live_execution, "live_execution")

live_collection = LiveCollection(live_manager)
config["pm"].register(live_collection)
live_collection = LiveCollection(live_manager=live_manager)
config["pm"].register(live_collection, "live_collection")


@hookimpl(tryfirst=True)
def pytask_execute_build(session: Session) -> None:
live_execution = session.config["pm"].get_plugin("live_execution")
live_execution.n_tasks = len(session.tasks)


@attr.s(eq=False)
Expand Down Expand Up @@ -138,25 +147,28 @@ def is_started(self) -> None:
return self._live.is_started


@attr.s(eq=False)
@attr.s(eq=False, kw_only=True)
class LiveExecution:
"""A class for managing the table displaying task progress during the execution."""

_live_manager = attr.ib(type=LiveManager)
_n_entries_in_table = attr.ib(type=int)
_verbose = attr.ib(type=int)
_editor_url_scheme = attr.ib(type=str)
_running_tasks = attr.ib(factory=dict, type=Dict[str, Task])
live_manager = attr.ib(type=LiveManager)
n_entries_in_table = attr.ib(type=int)
verbose = attr.ib(type=int)
editor_url_scheme = attr.ib(type=str)
n_tasks = attr.ib(default="x", type=Union[int, str])
_reports = attr.ib(factory=list, type=List[Dict[str, Any]])
_running_tasks = attr.ib(factory=dict, type=Dict[str, Task])

@hookimpl(hookwrapper=True)
def pytask_execute_build(self) -> Generator[None, None, None]:
"""Wrap the execution with the live manager and yield a complete table at the
end."""
self._live_manager.start()
self.live_manager.start()
yield
self._live_manager.stop(transient=True)
table = self._generate_table(reduce_table=False, sort_table=True)
self.live_manager.stop(transient=True)
table = self._generate_table(
reduce_table=False, sort_table=True, add_caption=False
)
if table is not None:
console.print(table)

Expand All @@ -172,7 +184,9 @@ def pytask_execute_task_log_end(self, report: ExecutionReport) -> bool:
self.update_reports(report)
return True

def _generate_table(self, reduce_table: bool, sort_table: bool) -> Table | None:
def _generate_table(
self, reduce_table: bool, sort_table: bool, add_caption: bool
) -> Table | None:
"""Generate the table.

First, display all completed tasks and, then, all running tasks.
Expand All @@ -181,9 +195,9 @@ def _generate_table(self, reduce_table: bool, sort_table: bool) -> Table | None:
if more entries are requested, the list is filled up with completed tasks.

"""
n_reports_to_display = self._n_entries_in_table - len(self._running_tasks)
n_reports_to_display = self.n_entries_in_table - len(self._running_tasks)

if self._verbose < 2:
if self.verbose < 2:
reports = [
report
for report in self._reports
Expand All @@ -210,22 +224,34 @@ def _generate_table(self, reduce_table: bool, sort_table: bool) -> Table | None:
relevant_reports, key=lambda report: report["name"]
)

table = Table()
if add_caption:
caption_kwargs = {
"caption": Text(
f"Completed: {len(self._reports)}/{self.n_tasks}",
style=Style(dim=True, italic=False),
),
"caption_justify": "right",
"caption_style": None,
}
else:
caption_kwargs = {}

table = Table(**caption_kwargs)
table.add_column("Task", overflow="fold")
table.add_column("Outcome")
for report in relevant_reports:
table.add_row(
format_task_id(
report["task"],
editor_url_scheme=self._editor_url_scheme,
editor_url_scheme=self.editor_url_scheme,
short_name=True,
),
Text(report["outcome"].symbol, style=report["outcome"].style),
)
for task in self._running_tasks.values():
table.add_row(
format_task_id(
task, editor_url_scheme=self._editor_url_scheme, short_name=True
task, editor_url_scheme=self.editor_url_scheme, short_name=True
),
"running",
)
Expand All @@ -237,11 +263,16 @@ def _generate_table(self, reduce_table: bool, sort_table: bool) -> Table | None:
return table

def _update_table(
self, reduce_table: bool = True, sort_table: bool = False
self,
reduce_table: bool = True,
sort_table: bool = False,
add_caption: bool = True,
) -> None:
"""Regenerate the table."""
table = self._generate_table(reduce_table=reduce_table, sort_table=sort_table)
self._live_manager.update(table)
table = self._generate_table(
reduce_table=reduce_table, sort_table=sort_table, add_caption=add_caption
)
self.live_manager.update(table)

def update_running_tasks(self, new_running_task: Task) -> None:
"""Add a new running task."""
Expand All @@ -261,18 +292,18 @@ def update_reports(self, new_report: ExecutionReport) -> None:
self._update_table()


@attr.s(eq=False)
@attr.s(eq=False, kw_only=True)
class LiveCollection:
"""A class for managing the live status during the collection."""

_live_manager = attr.ib(type=LiveManager)
live_manager = attr.ib(type=LiveManager)
_n_collected_tasks = attr.ib(default=0, type=int)
_n_errors = attr.ib(default=0, type=int)

@hookimpl(hookwrapper=True)
def pytask_collect(self) -> Generator[None, None, None]:
"""Start the status of the cllection."""
self._live_manager.start()
"""Start the status of the collection."""
self.live_manager.start()
yield

@hookimpl
Expand All @@ -284,7 +315,7 @@ def pytask_collect_file_log(self, reports: list[CollectionReport]) -> None:
@hookimpl(hookwrapper=True)
def pytask_collect_log(self) -> Generator[None, None, None]:
"""Stop the live display when all tasks have been collected."""
self._live_manager.stop(transient=True)
self.live_manager.stop(transient=True)
yield

def _update_statistics(self, reports: list[CollectionReport]) -> None:
Expand All @@ -300,7 +331,7 @@ def _update_statistics(self, reports: list[CollectionReport]) -> None:
def _update_status(self) -> None:
"""Update the status."""
status = self._generate_status()
self._live_manager.update(status)
self.live_manager.update(status)

def _generate_status(self) -> Status:
"""Generate the status."""
Expand Down
34 changes: 30 additions & 4 deletions tests/test_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ def test_live_execution_sequentially(capsys, tmp_path):
task.short_name = "task_module.py::task_example"

live_manager = LiveManager()
live = LiveExecution(live_manager, 20, 1, "no_link")
live = LiveExecution(
live_manager=live_manager,
n_entries_in_table=20,
verbose=1,
editor_url_scheme="no_link",
)

live_manager.start()
live.update_running_tasks(task)
Expand All @@ -77,6 +82,7 @@ def test_live_execution_sequentially(capsys, tmp_path):
assert "Outcome" not in captured.out
assert "task_module.py::task_example" not in captured.out
assert "running" not in captured.out
assert "Completed: 0/x" not in captured.out

live_manager.resume()
live_manager.start()
Expand All @@ -88,6 +94,7 @@ def test_live_execution_sequentially(capsys, tmp_path):
assert "Outcome" in captured.out
assert "task_module.py::task_example" in captured.out
assert "running" in captured.out
assert "Completed: 0/x" in captured.out

live_manager.start()

Expand All @@ -104,6 +111,7 @@ def test_live_execution_sequentially(capsys, tmp_path):
assert "task_module.py::task_example" in captured.out
assert "running" not in captured.out
assert TaskOutcome.SUCCESS.symbol in captured.out
assert "Completed: 1/x" in captured.out


@pytest.mark.unit
Expand All @@ -115,7 +123,12 @@ def test_live_execution_displays_skips_and_persists(capsys, tmp_path, verbose, o
task.short_name = "task_module.py::task_example"

live_manager = LiveManager()
live = LiveExecution(live_manager, 20, verbose, "no_link")
live = LiveExecution(
live_manager=live_manager,
n_entries_in_table=20,
verbose=verbose,
editor_url_scheme="no_link",
)

live_manager.start()
live.update_running_tasks(task)
Expand Down Expand Up @@ -159,7 +172,13 @@ def test_live_execution_displays_subset_of_table(capsys, tmp_path, n_entries_in_
running_task.short_name = "task_module.py::task_running"

live_manager = LiveManager()
live = LiveExecution(live_manager, n_entries_in_table, 1, "no_link")
live = LiveExecution(
live_manager=live_manager,
n_entries_in_table=n_entries_in_table,
verbose=1,
editor_url_scheme="no_link",
n_tasks=2,
)

live_manager.start()
live.update_running_tasks(running_task)
Expand All @@ -170,6 +189,7 @@ def test_live_execution_displays_subset_of_table(capsys, tmp_path, n_entries_in_
assert "Outcome" in captured.out
assert "::task_running" in captured.out
assert " running " in captured.out
assert "Completed: 0/2" in captured.out

completed_task = Task(base_name="task_completed", path=path, function=lambda x: x)
completed_task.short_name = "task_module.py::task_completed"
Expand All @@ -188,6 +208,7 @@ def test_live_execution_displays_subset_of_table(capsys, tmp_path, n_entries_in_
assert "Outcome" in captured.out
assert "::task_running" in captured.out
assert " running " in captured.out
assert "Completed: 1/2" in captured.out

if n_entries_in_table == 1:
assert "task_module.py::task_completed" not in captured.out
Expand All @@ -204,7 +225,12 @@ def test_live_execution_skips_do_not_crowd_out_displayed_tasks(capsys, tmp_path)
task.short_name = "task_module.py::task_example"

live_manager = LiveManager()
live = LiveExecution(live_manager, 20, 1, "no_link")
live = LiveExecution(
live_manager=live_manager,
n_entries_in_table=20,
verbose=1,
editor_url_scheme="no_link",
)

live_manager.start()
live.update_running_tasks(task)
Expand Down