Skip to content

Commit 4f28cfd

Browse files
authored
Fix display issues with the programmatic interface. (#631)
1 parent a5daa93 commit 4f28cfd

File tree

8 files changed

+57
-15
lines changed

8 files changed

+57
-15
lines changed

docs/source/changes.md

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and
1818
module's name does not start with `task_`.
1919
- {pull}`628` fixes duplicated collection of task modules. Fixes {issue}`624`. Thanks to
2020
{user}`timmens` for the issue.
21+
- {pull}`631` fixes display issues with the programmatic interface by giving each
22+
{class}`~_pytask.live.LiveManager` its own {class}`~rich.live.Live`.
2123

2224
## 0.5.0 - 2024-05-26
2325

docs/source/tutorials/visualizing_the_dag.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ layouts, which are listed [here](https://graphviz.org/docs/layouts/).
3535

3636
## Programmatic Interface
3737

38-
The programmatic and interactive interface allows customizing the figure.
38+
The programmatic and interactive interface allows for customizing the figure.
3939

4040
Similar to {func}`pytask.build`, there exists {func}`pytask.build_dag` which returns the
4141
DAG as a {class}`networkx.DiGraph`.
4242

43+
Create an executable script that you can execute with `python script.py`.
44+
4345
```{literalinclude} ../../../docs_src/tutorials/visualizing_the_dag.py
4446
```
4547

docs_src/tutorials/visualizing_the_dag.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33
import networkx as nx
44
from my_project.config import BLD
55
from my_project.config import SRC
6-
from pytask import Product
76
from pytask import build_dag
8-
from typing_extensions import Annotated
97

108

11-
def task_draw_dag(path: Annotated[Path, Product] = BLD / "dag.svg") -> None:
9+
def draw_dag(path: Path = BLD / "dag.svg") -> None:
1210
dag = build_dag({"paths": SRC})
1311

1412
# Set shapes to hexagons.
@@ -17,3 +15,7 @@ def task_draw_dag(path: Annotated[Path, Product] = BLD / "dag.svg") -> None:
1715
# Export with pygraphviz and dot.
1816
graph = nx.nx_agraph.to_agraph(dag)
1917
graph.draw(path, prog="dot")
18+
19+
20+
if __name__ == "__main__":
21+
draw_dag()

pyproject.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ build-backend = "hatchling.build"
9090

9191
[tool.rye]
9292
managed = true
93-
dev-dependencies = ["tox-uv>=1.7.0"]
93+
dev-dependencies = [
94+
"tox-uv>=1.7.0",
95+
]
9496

9597
[tool.rye.scripts]
9698
clean-docs = { cmd = "rm -rf docs/build" }

src/_pytask/live.py

+15-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from attrs import define
1313
from attrs import field
1414
from rich.box import ROUNDED
15+
from rich.errors import LiveError
1516
from rich.live import Live
1617
from rich.status import Status
1718
from rich.style import Style
@@ -22,6 +23,7 @@
2223
from _pytask.console import format_task_name
2324
from _pytask.logging_utils import TaskExecutionStatus
2425
from _pytask.outcomes import CollectionOutcome
26+
from _pytask.outcomes import Exit
2527
from _pytask.outcomes import TaskOutcome
2628
from _pytask.pluginmanager import hookimpl
2729

@@ -102,10 +104,21 @@ class LiveManager:
102104
103105
"""
104106

105-
_live = Live(renderable=None, console=console, auto_refresh=False)
107+
_live: Live = field(
108+
factory=lambda: Live(renderable=None, console=console, auto_refresh=False)
109+
)
106110

107111
def start(self) -> None:
108-
self._live.start()
112+
try:
113+
self._live.start()
114+
except LiveError:
115+
msg = (
116+
"pytask tried to launch a second live display which is impossible. the "
117+
"issue occurs when you use pytask on the command line on a task module "
118+
"that uses the programmatic interface of pytask at the same time. "
119+
"Use either the command line or the programmatic interface."
120+
)
121+
raise Exit(msg) from None
109122

110123
def stop(self, transient: bool | None = None) -> None:
111124
if transient is not None:
@@ -319,8 +332,6 @@ def pytask_collect_log(self) -> Generator[None, None, None]:
319332

320333
def _update_statistics(self, reports: list[CollectionReport]) -> None:
321334
"""Update the statistics on collected tasks and errors."""
322-
if reports is None:
323-
reports = []
324335
for report in reports:
325336
if report.outcome == CollectionOutcome.SUCCESS:
326337
self._n_collected_tasks += 1

tests/test_dag_command.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import os
4+
import subprocess
45
import sys
56
import textwrap
67

@@ -65,7 +66,7 @@ def task_example(path=Path("input.txt")): ...
6566
@pytest.mark.parametrize("layout", _GRAPH_LAYOUTS)
6667
@pytest.mark.parametrize("format_", _TEST_FORMATS)
6768
@pytest.mark.parametrize("rankdir", [_RankDirection.LR.value, _RankDirection.TB])
68-
def test_create_graph_via_task(tmp_path, runner, format_, layout, rankdir):
69+
def test_create_graph_via_task(tmp_path, format_, layout, rankdir):
6970
if sys.platform == "win32" and format_ == "pdf": # pragma: no cover
7071
pytest.xfail("gvplugin_pango.dll might be missing on Github Actions.")
7172

@@ -78,20 +79,24 @@ def test_create_graph_via_task(tmp_path, runner, format_, layout, rankdir):
7879
7980
def task_example(path=Path("input.txt")): ...
8081
81-
def task_create_graph():
82+
def main():
8283
dag = pytask.build_dag({{"paths": Path(__file__).parent}})
8384
dag.graph = {{"rankdir": "{rankdir_str}"}}
8485
graph = nx.nx_agraph.to_agraph(dag)
8586
path = Path(__file__).parent.joinpath("dag.{format_}")
8687
graph.draw(path, prog="{layout}")
87-
"""
8888
89+
if __name__ == "__main__":
90+
main()
91+
"""
8992
tmp_path.joinpath("task_example.py").write_text(textwrap.dedent(source))
9093
tmp_path.joinpath("input.txt").touch()
9194

92-
result = runner.invoke(cli, [tmp_path.as_posix()])
95+
result = subprocess.run(
96+
("python", "task_example.py"), cwd=tmp_path, check=True, capture_output=True
97+
)
9398

94-
assert result.exit_code == ExitCode.OK
99+
assert result.returncode == ExitCode.OK
95100
assert tmp_path.joinpath(f"dag.{format_}").exists()
96101

97102

tests/test_execute.py

+18
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,24 @@ def create_file(
625625
assert tmp_path.joinpath("file.txt").read_text() == "This is the text."
626626

627627

628+
@pytest.mark.end_to_end()
629+
def test_pytask_on_a_module_that_uses_the_functional_api(tmp_path):
630+
source = """
631+
from pytask import task, ExitCode, build
632+
from pathlib import Path
633+
from typing_extensions import Annotated
634+
635+
def task_example(): pass
636+
637+
session = build(tasks=[task_example])
638+
assert session.exit_code == ExitCode.OK
639+
"""
640+
tmp_path.joinpath("task_example.py").write_text(textwrap.dedent(source))
641+
result = subprocess.run(("pytask",), cwd=tmp_path, capture_output=True, check=False)
642+
assert result.returncode == ExitCode.COLLECTION_FAILED
643+
assert "pytask tried to launch a second live display" in result.stdout.decode()
644+
645+
628646
@pytest.mark.end_to_end()
629647
def test_pass_non_task_to_functional_api_that_are_ignored():
630648
session = pytask.build(tasks=None)

tests/test_task.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ def task_second():
688688
"""
689689
tmp_path.joinpath("task_example.py").write_text(textwrap.dedent(source))
690690

691-
result = subprocess.run(("pytask",), cwd=tmp_path, capture_output=True, check=False)
691+
result = subprocess.run(("python",), cwd=tmp_path, capture_output=True, check=False)
692692
assert result.returncode == ExitCode.OK
693693

694694

0 commit comments

Comments
 (0)