Skip to content

Commit 7592664

Browse files
authored
Fix handling of names and signatures of PythonNodes. (#482)
1 parent 4e39b68 commit 7592664

10 files changed

+125
-23
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ repos:
4747
- repo: https://github.com/astral-sh/ruff-pre-commit
4848
rev: v0.1.4
4949
hooks:
50+
- id: ruff-format
5051
- id: ruff
5152
args: [--unsafe-fixes]
52-
- id: ruff-format
5353
- repo: https://github.com/dosisod/refurb
5454
rev: v1.22.1
5555
hooks:

docs/source/changes.md

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and
2929
- {pull}`480` removes the check for missing root nodes from the generation of the DAG.
3030
It is delegated to the check during the execution.
3131
- {pull}`481` improves coverage.
32+
- {pull}`482` correctly handles names and signatures of {class}`~pytask.PythonNode`.
3233

3334
## 0.4.1 - 2023-10-11
3435

src/_pytask/_hashlib.py

+7
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,14 @@ def hash_value(value: Any) -> int | str:
219219
Compute the hash of paths, strings, and bytes with a hash function or otherwise the
220220
hashes are salted.
221221
222+
The hash of None constant https://github.com/python/cpython/pull/99541 starting with
223+
Python 3.12.
224+
222225
"""
226+
if value is None:
227+
return 0xFCA86420
228+
if isinstance(value, (tuple, list)):
229+
value = "".join(str(hash_value(i)) for i in value)
223230
if isinstance(value, Path):
224231
value = str(value)
225232
if isinstance(value, str):

src/_pytask/collect.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,9 @@ def pytask_collect_node(session: Session, path: Path, node_info: NodeInfo) -> PN
325325
node = node_info.value
326326

327327
if isinstance(node, PythonNode):
328-
node.name = create_name_of_python_node(node_info)
328+
node.node_info = node_info
329+
if not node.name:
330+
node.name = create_name_of_python_node(node_info)
329331
return node
330332

331333
if isinstance(node, PPathNode) and not node.path.is_absolute():
@@ -363,7 +365,7 @@ def pytask_collect_node(session: Session, path: Path, node_info: NodeInfo) -> PN
363365
return PathNode(name=name, path=node)
364366

365367
node_name = create_name_of_python_node(node_info)
366-
return PythonNode(value=node, name=node_name)
368+
return PythonNode(value=node, name=node_name, node_info=node_info)
367369

368370

369371
def _raise_error_if_casing_of_path_is_wrong(

src/_pytask/collect_utils.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -684,11 +684,7 @@ def _collect_product(
684684

685685
def create_name_of_python_node(node_info: NodeInfo) -> str:
686686
"""Create name of PythonNode."""
687-
prefix = (
688-
node_info.task_path.as_posix() + "::" + node_info.task_name
689-
if node_info.task_path
690-
else node_info.task_name
691-
)
687+
prefix = node_info.task_name if node_info.task_path else node_info.task_name
692688
node_name = prefix + "::" + node_info.arg_name
693689
if node_info.path:
694690
suffix = "-".join(map(str, node_info.path))

src/_pytask/nodes.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323

2424
if TYPE_CHECKING:
25+
from _pytask.models import NodeInfo
2526
from _pytask.tree_util import PyTree
2627
from _pytask.mark import Mark
2728

@@ -211,6 +212,8 @@ class PythonNode(PNode):
211212
objects that are hashable like strings and tuples. For dictionaries and other
212213
non-hashable objects, you need to provide a function that can hash these
213214
objects.
215+
node_info
216+
The infos acquired while collecting the node.
214217
signature
215218
The signature of the node.
216219
@@ -229,11 +232,19 @@ class PythonNode(PNode):
229232
name: str = ""
230233
value: Any | NoDefault = no_default
231234
hash: bool | Callable[[Any], bool] = False # noqa: A003
235+
node_info: NodeInfo | None = None
232236

233237
@property
234238
def signature(self) -> str:
235239
"""The unique signature of the node."""
236-
raw_key = str(hash_value(self.name))
240+
raw_key = (
241+
"".join(
242+
str(hash_value(getattr(self.node_info, name)))
243+
for name in ("arg_name", "path", "task_name", "task_path")
244+
)
245+
if self.node_info
246+
else str(hash_value(self.node_info))
247+
)
237248
return hashlib.sha256(raw_key.encode()).hexdigest()
238249

239250
def load(self, is_product: bool = False) -> Any:

tests/__snapshots__/test_collect_command.ambr

+22-12
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,28 @@
99
Collected tasks:
1010
└── 🐍 <Module test_more_nested_pytree_and_py0/task_module.py>
1111
└── 📝 <Function task_module.py::task_example>
12-
├── 📄 <Product
13-
│ test_more_nested_pytree_and_py0/task_module.py::task_example::return
14-
│ ::0>
15-
├── 📄 <Product
16-
│ test_more_nested_pytree_and_py0/task_module.py::task_example::return
17-
│ ::1-0>
18-
├── 📄 <Product
19-
│ test_more_nested_pytree_and_py0/task_module.py::task_example::return
20-
│ ::1-1>
21-
└── 📄 <Product
22-
test_more_nested_pytree_and_py0/task_module.py::task_example::return
23-
::2>
12+
├── 📄 <Product task_example::return::0>
13+
├── 📄 <Product task_example::return::1-0>
14+
├── 📄 <Product task_example::return::1-1>
15+
└── 📄 <Product task_example::return::2>
16+
17+
────────────────────────────────────────────────────────────────────────────────
18+
'''
19+
# ---
20+
# name: test_more_nested_pytree_and_python_node_as_return_with_names
21+
'''
22+
───────────────────────────── Start pytask session ─────────────────────────────
23+
Platform: <platform> -- Python <version>, pytask <version>, pluggy <version>
24+
Root: <path>
25+
Collected 1 task.
26+
27+
Collected tasks:
28+
└── 🐍 <Module test_more_nested_pytree_and_py1/task_module.py>
29+
└── 📝 <Function task_module.py::task_example>
30+
├── 📄 <Product dict>
31+
├── 📄 <Product int>
32+
├── 📄 <Product tuple1>
33+
└── 📄 <Product tuple2>
2434

2535
────────────────────────────────────────────────────────────────────────────────
2636
'''

tests/test_collect_command.py

+27
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,33 @@ def test_more_nested_pytree_and_python_node_as_return(runner, snapshot_cli, tmp_
619619
from pytask import PythonNode
620620
from typing import Dict
621621
622+
nodes = [
623+
PythonNode(),
624+
(PythonNode(), PythonNode()),
625+
PythonNode()
626+
]
627+
628+
def task_example() -> Annotated[Dict[str, str], nodes]:
629+
return [{"first": "a", "second": "b"}, (1, 2), 1]
630+
"""
631+
tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
632+
result = runner.invoke(cli, ["collect", "--nodes", tmp_path.as_posix()])
633+
assert result.exit_code == ExitCode.OK
634+
if sys.platform != "win32":
635+
assert result.output == snapshot_cli()
636+
637+
638+
@pytest.mark.end_to_end()
639+
def test_more_nested_pytree_and_python_node_as_return_with_names(
640+
runner, snapshot_cli, tmp_path
641+
):
642+
source = """
643+
from pathlib import Path
644+
from typing import Any
645+
from typing_extensions import Annotated
646+
from pytask import PythonNode
647+
from typing import Dict
648+
622649
nodes = [
623650
PythonNode(name="dict"),
624651
(PythonNode(name="tuple1"), PythonNode(name="tuple2")),

tests/test_execute.py

+34-1
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,7 @@ def task_example() -> Annotated[Dict[str, str], PythonNode(name="result")]:
738738

739739

740740
@pytest.mark.end_to_end()
741-
def test_more_nested_pytree_and_python_node_as_return(runner, tmp_path):
741+
def test_more_nested_pytree_and_python_node_as_return_with_names(runner, tmp_path):
742742
source = """
743743
from pathlib import Path
744744
from typing import Any
@@ -758,6 +758,39 @@ def task_example() -> Annotated[Dict[str, str], nodes]:
758758
tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
759759
result = runner.invoke(cli, [tmp_path.as_posix()])
760760
assert result.exit_code == ExitCode.OK
761+
assert "1 Succeeded" in result.output
762+
763+
result = runner.invoke(cli, [tmp_path.as_posix()])
764+
assert result.exit_code == ExitCode.OK
765+
assert "1 Skipped" in result.output
766+
767+
768+
@pytest.mark.end_to_end()
769+
def test_more_nested_pytree_and_python_node_as_return(runner, tmp_path):
770+
source = """
771+
from pathlib import Path
772+
from typing import Any
773+
from typing_extensions import Annotated
774+
from pytask import PythonNode
775+
from typing import Dict
776+
777+
nodes = [
778+
PythonNode(),
779+
(PythonNode(), PythonNode()),
780+
PythonNode()
781+
]
782+
783+
def task_example() -> Annotated[Dict[str, str], nodes]:
784+
return [{"first": "a", "second": "b"}, (1, 2), 1]
785+
"""
786+
tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
787+
result = runner.invoke(cli, [tmp_path.as_posix()])
788+
assert result.exit_code == ExitCode.OK
789+
assert "1 Succeeded" in result.output
790+
791+
result = runner.invoke(cli, [tmp_path.as_posix()])
792+
assert result.exit_code == ExitCode.OK
793+
assert "1 Skipped" in result.output
761794

762795

763796
@pytest.mark.end_to_end()

tests/test_nodes.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pathlib import Path
55

66
import pytest
7+
from pytask import NodeInfo
78
from pytask import PathNode
89
from pytask import PickleNode
910
from pytask import PNode
@@ -39,7 +40,21 @@ def test_hash_of_python_node(value, hash_, expected):
3940
),
4041
(
4142
PythonNode(name="name", value=None),
42-
"c8265d64828f9e007a9108251883a2b63954c326c678fca23c49a0b08ea7c925",
43+
"a1f217807169de3253d1176ea5c45be20f3db2e2e81ea26c918f6008d2eb715b",
44+
),
45+
(
46+
PythonNode(
47+
name="name",
48+
value=None,
49+
node_info=NodeInfo(
50+
arg_name="arg_name",
51+
path=(0, 1, "dict_key"),
52+
value=None,
53+
task_path=Path("task_example.py"),
54+
task_name="task_example",
55+
),
56+
),
57+
"7284475a87b8f1aa49c40126c5064269f0ba926265b8fe9158a39a882c6a1512",
4358
),
4459
(
4560
Task(base_name="task", path=Path("task.py"), function=None),

0 commit comments

Comments
 (0)