Skip to content

Commit 7d20db2

Browse files
authored
Remove checks for missing root nodes. (#480)
1 parent 2eb480e commit 7d20db2

File tree

12 files changed

+97
-400
lines changed

12 files changed

+97
-400
lines changed

docs/source/changes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and
2626
- {pull}`477` updates the PyPI action.
2727
- {pull}`478` replaces black with ruff-format.
2828
- {pull}`479` gives skips a higher precedence as an outcome than ancestor failed.
29+
- {pull}`480` removes the check for missing root nodes from the generation of the DAG.
30+
It is delegated to the check during the execution.
2931

3032
## 0.4.1 - 2023-10-11
3133

docs/source/reference_guides/hookspecs.md

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,12 @@ hooks are allowed to raise exceptions which are handled and stored in a report.
2323

2424
```{eval-rst}
2525
.. autofunction:: pytask_add_hooks
26-
2726
```
2827

2928
## Command Line Interface
3029

3130
```{eval-rst}
3231
.. autofunction:: pytask_extend_command_line_interface
33-
3432
```
3533

3634
## Configuration
@@ -41,20 +39,9 @@ together.
4139

4240
```{eval-rst}
4341
.. autofunction:: pytask_configure
44-
```
45-
46-
```{eval-rst}
4742
.. autofunction:: pytask_parse_config
48-
```
49-
50-
```{eval-rst}
5143
.. autofunction:: pytask_post_parse
52-
53-
```
54-
55-
```{eval-rst}
5644
.. autofunction:: pytask_unconfigure
57-
5845
```
5946

6047
## Collection
@@ -63,47 +50,16 @@ The following hooks traverse directories and collect tasks from files.
6350

6451
```{eval-rst}
6552
.. autofunction:: pytask_collect
66-
```
67-
68-
```{eval-rst}
6953
.. autofunction:: pytask_ignore_collect
70-
```
71-
72-
```{eval-rst}
7354
.. autofunction:: pytask_collect_modify_tasks
74-
```
75-
76-
```{eval-rst}
7755
.. autofunction:: pytask_collect_file_protocol
78-
```
79-
80-
```{eval-rst}
8156
.. autofunction:: pytask_collect_file
82-
```
83-
84-
```{eval-rst}
8557
.. autofunction:: pytask_collect_task_protocol
86-
```
87-
88-
```{eval-rst}
8958
.. autofunction:: pytask_collect_task_setup
90-
```
91-
92-
```{eval-rst}
9359
.. autofunction:: pytask_collect_task
94-
```
95-
96-
```{eval-rst}
9760
.. autofunction:: pytask_collect_task_teardown
98-
```
99-
100-
```{eval-rst}
10161
.. autofunction:: pytask_collect_node
102-
```
103-
104-
```{eval-rst}
10562
.. autofunction:: pytask_collect_log
106-
10763
```
10864

10965
## Resolving Dependencies
@@ -120,21 +76,8 @@ your plugin.
12076

12177
```{eval-rst}
12278
.. autofunction:: pytask_dag
123-
```
124-
125-
```{eval-rst}
12679
.. autofunction:: pytask_dag_create_dag
127-
```
128-
129-
```{eval-rst}
130-
.. autofunction:: pytask_dag_validate_dag
131-
```
132-
133-
```{eval-rst}
13480
.. autofunction:: pytask_dag_select_execution_dag
135-
```
136-
137-
```{eval-rst}
13881
.. autofunction:: pytask_dag_log
13982
14083
```
@@ -145,48 +88,15 @@ The following hooks execute the tasks and log information on the result in the t
14588

14689
```{eval-rst}
14790
.. autofunction:: pytask_execute
148-
```
149-
150-
```{eval-rst}
15191
.. autofunction:: pytask_execute_log_start
152-
```
153-
154-
```{eval-rst}
15592
.. autofunction:: pytask_execute_create_scheduler
156-
```
157-
158-
```{eval-rst}
15993
.. autofunction:: pytask_execute_build
160-
```
161-
162-
```{eval-rst}
16394
.. autofunction:: pytask_execute_task_protocol
164-
```
165-
166-
```{eval-rst}
16795
.. autofunction:: pytask_execute_task_log_start
168-
```
169-
170-
```{eval-rst}
17196
.. autofunction:: pytask_execute_task_setup
172-
```
173-
174-
```{eval-rst}
17597
.. autofunction:: pytask_execute_task
176-
```
177-
178-
```{eval-rst}
17998
.. autofunction:: pytask_execute_task_teardown
180-
```
181-
182-
```{eval-rst}
18399
.. autofunction:: pytask_execute_task_process_report
184-
```
185-
186-
```{eval-rst}
187100
.. autofunction:: pytask_execute_task_log_end
188-
```
189-
190-
```{eval-rst}
191101
.. autofunction:: pytask_execute_log_end
192102
```

src/_pytask/dag.py

Lines changed: 3 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33

44
import itertools
55
import sys
6-
from typing import Sequence
76
from typing import TYPE_CHECKING
87

98
import networkx as nx
109
from _pytask.config import hookimpl
11-
from _pytask.config import IS_FILE_SYSTEM_CASE_SENSITIVE
1210
from _pytask.console import ARROW_DOWN_ICON
1311
from _pytask.console import console
1412
from _pytask.console import FILE_ICON
@@ -23,8 +21,6 @@
2321
from _pytask.database_utils import State
2422
from _pytask.exceptions import ResolvingDependenciesError
2523
from _pytask.mark import Mark
26-
from _pytask.mark_utils import get_marks
27-
from _pytask.mark_utils import has_mark
2824
from _pytask.node_protocols import PNode
2925
from _pytask.node_protocols import PTask
3026
from _pytask.nodes import PythonNode
@@ -48,13 +44,12 @@ def pytask_dag(session: Session) -> bool | None:
4844
session=session, tasks=session.tasks
4945
)
5046
session.hook.pytask_dag_modify_dag(session=session, dag=session.dag)
51-
session.hook.pytask_dag_validate_dag(session=session, dag=session.dag)
5247
session.hook.pytask_dag_select_execution_dag(session=session, dag=session.dag)
5348

5449
except Exception: # noqa: BLE001
5550
report = DagReport.from_exception(sys.exc_info())
5651
session.hook.pytask_dag_log(session=session, report=report)
57-
session.dag_reports = report
52+
session.dag_report = report
5853

5954
raise ResolvingDependenciesError from None
6055

@@ -63,7 +58,7 @@ def pytask_dag(session: Session) -> bool | None:
6358

6459

6560
@hookimpl
66-
def pytask_dag_create_dag(tasks: list[PTask]) -> nx.DiGraph:
61+
def pytask_dag_create_dag(session: Session, tasks: list[PTask]) -> nx.DiGraph:
6762
"""Create the DAG from tasks, dependencies and products."""
6863

6964
def _add_dependency(dag: nx.DiGraph, task: PTask, node: PNode) -> None:
@@ -101,6 +96,7 @@ def _add_product(dag: nx.DiGraph, task: PTask, node: PNode) -> None:
10196
)
10297

10398
_check_if_dag_has_cycles(dag)
99+
_check_if_tasks_have_the_same_products(dag, session.config["paths"])
104100

105101
return dag
106102

@@ -123,13 +119,6 @@ def pytask_dag_select_execution_dag(session: Session, dag: nx.DiGraph) -> None:
123119
)
124120

125121

126-
@hookimpl
127-
def pytask_dag_validate_dag(session: Session, dag: nx.DiGraph) -> None:
128-
"""Validate the DAG."""
129-
_check_if_root_nodes_are_available(dag, session.config["paths"])
130-
_check_if_tasks_have_the_same_products(dag, session.config["paths"])
131-
132-
133122
def _have_task_or_neighbors_changed(
134123
session: Session, dag: nx.DiGraph, task: PTask
135124
) -> bool:
@@ -198,98 +187,6 @@ def _format_cycles(dag: nx.DiGraph, cycles: list[tuple[str, ...]]) -> str:
198187
return "\n".join(lines[:-1])
199188

200189

201-
_TEMPLATE_ERROR: str = (
202-
"Some dependencies do not exist or are not produced by any task. See the following "
203-
"tree which shows which dependencies are missing for which tasks.\n\n{}"
204-
)
205-
if IS_FILE_SYSTEM_CASE_SENSITIVE:
206-
_TEMPLATE_ERROR += (
207-
"\n\n(Hint: Your file-system is case-sensitive. Check the paths' "
208-
"capitalization carefully.)"
209-
)
210-
211-
212-
def _check_if_root_nodes_are_available(dag: nx.DiGraph, paths: Sequence[Path]) -> None:
213-
__tracebackhide__ = True
214-
215-
missing_root_nodes = []
216-
is_task_skipped: dict[str, bool] = {}
217-
218-
for node in dag.nodes:
219-
is_node = "node" in dag.nodes[node]
220-
is_without_parents = len(list(dag.predecessors(node))) == 0
221-
if is_node and is_without_parents:
222-
are_all_tasks_skipped, is_task_skipped = _check_if_tasks_are_skipped(
223-
node, dag, is_task_skipped
224-
)
225-
if not are_all_tasks_skipped:
226-
try:
227-
node_exists = dag.nodes[node]["node"].state()
228-
except Exception as e: # noqa: BLE001
229-
msg = _format_exception_from_failed_node_state(node, dag, paths)
230-
raise ResolvingDependenciesError(msg) from e
231-
if not node_exists:
232-
missing_root_nodes.append(node)
233-
234-
if missing_root_nodes:
235-
dictionary = {}
236-
for node in missing_root_nodes:
237-
short_node_name = format_node_name(dag.nodes[node]["node"], paths).plain
238-
not_skipped_successors = [
239-
task for task in dag.successors(node) if not is_task_skipped[task]
240-
]
241-
short_successors = reduce_names_of_multiple_nodes(
242-
not_skipped_successors, dag, paths
243-
)
244-
dictionary[short_node_name] = short_successors
245-
246-
text = _format_dictionary_to_tree(dictionary, "Missing dependencies:")
247-
raise ResolvingDependenciesError(_TEMPLATE_ERROR.format(text)) from None
248-
249-
250-
def _format_exception_from_failed_node_state(
251-
node_signature: str, dag: nx.DiGraph, paths: Sequence[Path]
252-
) -> str:
253-
"""Format message when ``node.state()`` threw an exception."""
254-
tasks = [dag.nodes[i]["task"] for i in dag.successors(node_signature)]
255-
names = [task.name for task in tasks]
256-
successors = ", ".join([f"{name!r}" for name in names])
257-
node_name = format_node_name(dag.nodes[node_signature]["node"], paths).plain
258-
return (
259-
f"While checking whether dependency {node_name!r} from task(s) "
260-
f"{successors} exists, an error was raised."
261-
)
262-
263-
264-
def _check_if_tasks_are_skipped(
265-
node: PNode, dag: nx.DiGraph, is_task_skipped: dict[str, bool]
266-
) -> tuple[bool, dict[str, bool]]:
267-
"""Check for a given node whether it is only used by skipped tasks."""
268-
are_all_tasks_skipped = []
269-
for successor in dag.successors(node):
270-
if successor not in is_task_skipped:
271-
is_task_skipped[successor] = _check_if_task_is_skipped(successor, dag)
272-
are_all_tasks_skipped.append(is_task_skipped[successor])
273-
274-
return all(are_all_tasks_skipped), is_task_skipped
275-
276-
277-
def _check_if_task_is_skipped(task_name: str, dag: nx.DiGraph) -> bool:
278-
task = dag.nodes[task_name]["task"]
279-
is_skipped = has_mark(task, "skip")
280-
281-
if is_skipped:
282-
return True
283-
284-
skip_if_markers = get_marks(task, "skipif")
285-
return any(_skipif(*marker.args, **marker.kwargs)[0] for marker in skip_if_markers)
286-
287-
288-
def _skipif(condition: bool, *, reason: str) -> tuple[bool, str]:
289-
"""Shameless copy to circumvent circular imports."""
290-
return condition, reason
291-
292-
293190
def _format_dictionary_to_tree(dict_: dict[str, list[str]], title: str) -> str:
294191
"""Format missing root nodes."""
295192
tree = Tree(title)

src/_pytask/dag_command.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import sys
66
from pathlib import Path
77
from typing import Any
8-
from typing import TYPE_CHECKING
98

109
import click
1110
import networkx as nx
@@ -31,10 +30,6 @@
3130
from rich.traceback import Traceback
3231

3332

34-
if TYPE_CHECKING:
35-
from typing import NoReturn
36-
37-
3833
class _RankDirection(enum.Enum):
3934
TB = "TB"
4035
LR = "LR"
@@ -82,7 +77,7 @@ def pytask_extend_command_line_interface(cli: click.Group) -> None:
8277
help=_HELP_TEXT_RANK_DIRECTION,
8378
default=_RankDirection.TB,
8479
)
85-
def dag(**raw_config: Any) -> NoReturn:
80+
def dag(**raw_config: Any) -> int:
8681
"""Create a visualization of the project's directed acyclic graph."""
8782
try:
8883
pm = get_plugin_manager()

src/_pytask/execute.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import TYPE_CHECKING
99

1010
from _pytask.config import hookimpl
11+
from _pytask.config import IS_FILE_SYSTEM_CASE_SENSITIVE
1112
from _pytask.console import console
1213
from _pytask.console import create_summary_panel
1314
from _pytask.console import create_url_style_for_task
@@ -36,6 +37,7 @@
3637
from _pytask.tree_util import tree_structure
3738
from rich.text import Text
3839

40+
3941
if TYPE_CHECKING:
4042
from _pytask.session import Session
4143

@@ -125,7 +127,12 @@ def pytask_execute_task_setup(session: Session, task: PTask) -> None:
125127
for dependency in session.dag.predecessors(task.signature):
126128
node = session.dag.nodes[dependency]["node"]
127129
if not node.state():
128-
msg = f"{node.name} is missing and required for {task.name}."
130+
msg = f"{task.name} requires missing node {node.name}."
131+
if IS_FILE_SYSTEM_CASE_SENSITIVE:
132+
msg += (
133+
"\n\n(Hint: Your file-system is case-sensitive. Check the paths' "
134+
"capitalization carefully.)"
135+
)
129136
raise NodeNotFoundError(msg)
130137

131138
# Create directory for product if it does not exist. Maybe this should be a `setup`

0 commit comments

Comments
 (0)