Skip to content

Commit 03fb027

Browse files
authored
Raise informative error when path nodes point to directories. (#484)
1 parent 6d2c07f commit 03fb027

File tree

5 files changed

+61
-4
lines changed

5 files changed

+61
-4
lines changed

docs/source/changes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and
88
## 0.4.3 - 2023-11-xx
99

1010
- {pull}`483` simplifies the teardown of a task.
11+
- {pull}`484` raises more informative error when directories instead of files are used
12+
with path nodes.
1113
- {pull}`485` adds missing steps to unconfigure pytask after the job is done which
1214
caused flaky tests.
1315

src/_pytask/collect.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,10 @@ def pytask_collect_task(
303303
"""
304304

305305

306+
_TEMPLATE_ERROR_DIRECTORY: str = """\
307+
The path '{path}' points to a directory, although only files are allowed."""
308+
309+
306310
@hookimpl(trylast=True)
307311
def pytask_collect_node(session: Session, path: Path, node_info: NodeInfo) -> PNode:
308312
"""Collect a node of a task as a :class:`pytask.PNode`.
@@ -348,6 +352,9 @@ def pytask_collect_node(session: Session, path: Path, node_info: NodeInfo) -> PN
348352
node.path, session.config["paths"] or (session.config["root"],)
349353
)
350354

355+
if isinstance(node, PPathNode) and node.path.is_dir():
356+
raise ValueError(_TEMPLATE_ERROR_DIRECTORY.format(path=node.path))
357+
351358
if isinstance(node, PNode):
352359
return node
353360

@@ -362,6 +369,10 @@ def pytask_collect_node(session: Session, path: Path, node_info: NodeInfo) -> PN
362369
node, session.config["check_casing_of_paths"]
363370
)
364371
name = shorten_path(node, session.config["paths"] or (session.config["root"],))
372+
373+
if isinstance(node, Path) and node.is_dir():
374+
raise ValueError(_TEMPLATE_ERROR_DIRECTORY.format(path=path))
375+
365376
return PathNode(name=name, path=node)
366377

367378
node_name = create_name_of_python_node(node_info)

src/_pytask/execute.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def pytask_execute_task_setup(session: Session, task: PTask) -> None:
127127
for dependency in session.dag.predecessors(task.signature):
128128
node = session.dag.nodes[dependency]["node"]
129129
if not node.state():
130-
msg = f"{task.name} requires missing node {node.name}."
130+
msg = f"{task.name!r} requires missing node {node.name!r}."
131131
if IS_FILE_SYSTEM_CASE_SENSITIVE:
132132
msg += (
133133
"\n\n(Hint: Your file-system is case-sensitive. Check the paths' "

tests/test_collect.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,3 +572,47 @@ def task_example(path: Annotated[Path, Path("file.txt"), Product]) -> None: ...
572572
result = runner.invoke(cli, [tmp_path.as_posix()])
573573
assert result.exit_code == ExitCode.COLLECTION_FAILED
574574
assert "is defined twice" in result.output
575+
576+
577+
@pytest.mark.parametrize(
578+
"node",
579+
[
580+
"Path(__file__).parent",
581+
"PathNode(name='path', path=Path(__file__).parent)",
582+
"PickleNode(name='', path=Path(__file__).parent)",
583+
],
584+
)
585+
def test_error_when_path_dependency_is_directory(runner, tmp_path, node):
586+
source = f"""
587+
from pathlib import Path
588+
from pytask import PickleNode, PathNode
589+
590+
def task_example(path = {node}): ...
591+
"""
592+
tmp_path.joinpath("task_example.py").write_text(textwrap.dedent(source))
593+
result = runner.invoke(cli, [tmp_path.as_posix()])
594+
assert result.exit_code == ExitCode.COLLECTION_FAILED
595+
assert all(i in result.output for i in ("only", "files", "are", "allowed"))
596+
597+
598+
@pytest.mark.parametrize(
599+
"node",
600+
[
601+
"Path(__file__).parent",
602+
"PathNode(name='path', path=Path(__file__).parent)",
603+
"PickleNode(name='', path=Path(__file__).parent)",
604+
],
605+
)
606+
def test_error_when_path_product_is_directory(runner, tmp_path, node):
607+
source = f"""
608+
from pathlib import Path
609+
from pytask import PickleNode, Product, PathNode
610+
from typing_extensions import Annotated
611+
from typing import Any
612+
613+
def task_example(path: Annotated[Any, Product] = {node}): ...
614+
"""
615+
tmp_path.joinpath("task_example.py").write_text(textwrap.dedent(source))
616+
result = runner.invoke(cli, [tmp_path.as_posix()])
617+
assert result.exit_code == ExitCode.COLLECTION_FAILED
618+
assert all(i in result.output for i in ("only", "files", "are", "allowed"))

tests/test_execute.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,7 @@ def task_d(produces):
10041004
result = runner.invoke(cli, [tmp_path.as_posix()])
10051005

10061006
assert result.exit_code == ExitCode.FAILED
1007-
assert "NodeNotFoundError: task_d.py::task_d requires" in result.output
1007+
assert "NodeNotFoundError: 'task_d.py::task_d' requires" in result.output
10081008

10091009

10101010
@pytest.mark.end_to_end()
@@ -1024,7 +1024,7 @@ def task_e(in1_: Annotated[Path, node1], in2_: Annotated[Any, node2]): ...
10241024
result = runner.invoke(cli, [tmp_path.as_posix()])
10251025

10261026
assert result.exit_code == ExitCode.FAILED
1027-
assert "NodeNotFoundError: task_e.py::task_e requires" in result.output
1027+
assert "NodeNotFoundError: 'task_e.py::task_e' requires" in result.output
10281028
assert "input1" in result.output
10291029

10301030

@@ -1045,7 +1045,7 @@ def task_d(produces):
10451045
result = runner.invoke(cli, [tmp_path.joinpath("src").as_posix()])
10461046

10471047
assert result.exit_code == ExitCode.FAILED
1048-
assert "NodeNotFoundError: task_d.py::task_d requires" in result.output
1048+
assert "NodeNotFoundError: 'task_d.py::task_d' requires" in result.output
10491049
assert "bld/in.txt" in result.output
10501050

10511051

0 commit comments

Comments
 (0)