Skip to content

Commit f79f1c8

Browse files
authored
Merge c34b17e into 7885402
2 parents 7885402 + c34b17e commit f79f1c8

File tree

3 files changed

+70
-69
lines changed

3 files changed

+70
-69
lines changed

docs/source/changes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and
1717
- {pull}`489` simplifies parsing products and does not raise an error when a product
1818
annotation is used with the argument name `produces`. And, allow `produces` to intake
1919
any node.
20+
- {pull}`490` refactors and better tests parsing of dependencies.
2021

2122
## 0.4.2 - 2023-11-8
2223

src/_pytask/collect_utils.py

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -267,21 +267,6 @@ def parse_dependencies_from_task_function(
267267
# Parse dependencies from task when @task is used.
268268
if "depends_on" in kwargs:
269269
has_depends_on_argument = True
270-
dependencies["depends_on"] = tree_map(
271-
lambda x: _collect_decorator_node(
272-
session,
273-
node_path,
274-
task_name,
275-
NodeInfo(
276-
arg_name="depends_on",
277-
path=(),
278-
value=x,
279-
task_path=task_path,
280-
task_name=task_name,
281-
),
282-
),
283-
kwargs["depends_on"],
284-
)
285270

286271
if has_depends_on_decorator and has_depends_on_argument:
287272
raise NodeNotCollectedError(_ERROR_MULTIPLE_DEPENDENCY_DEFINITIONS)
@@ -308,9 +293,6 @@ def parse_dependencies_from_task_function(
308293
):
309294
continue
310295

311-
if parameter_name == "depends_on":
312-
continue
313-
314296
nodes = tree_map_with_path(
315297
lambda p, x: _collect_dependency(
316298
session,
@@ -461,7 +443,6 @@ def parse_products_from_task_function( # noqa: C901
461443
task_path=task_path,
462444
task_name=task_name,
463445
),
464-
convert_string_to_path=parameter_name == "produces", # noqa: B023
465446
),
466447
value,
467448
)
@@ -481,7 +462,6 @@ def parse_products_from_task_function( # noqa: C901
481462
task_path=task_path,
482463
task_name=task_name,
483464
),
484-
convert_string_to_path=False,
485465
),
486466
parameters_with_node_annot["return"],
487467
)
@@ -502,7 +482,6 @@ def parse_products_from_task_function( # noqa: C901
502482
task_path=task_path,
503483
task_name=task_name,
504484
),
505-
convert_string_to_path=False,
506485
),
507486
task_produces,
508487
)
@@ -606,6 +585,16 @@ def _collect_dependency(
606585
"""
607586
node = node_info.value
608587

588+
# If we encounter a string and the argument name is 'depends_on', we convert it.
589+
if isinstance(node, str) and node_info.arg_name == "depends_on":
590+
warnings.warn(
591+
_WARNING_STRING_DEPRECATED.format(kind="dependency", node=node),
592+
category=FutureWarning,
593+
stacklevel=1,
594+
)
595+
node = Path(node)
596+
node_info = node_info._replace(value=node)
597+
609598
if isinstance(node, PythonNode) and node.value is no_default:
610599
# If a node is a dependency and its value is not set, the node is a product in
611600
# another task and the value will be set there. Thus, we wrap the original node
@@ -629,7 +618,6 @@ def _collect_product(
629618
path: Path,
630619
task_name: str,
631620
node_info: NodeInfo,
632-
convert_string_to_path: bool = False,
633621
) -> PNode:
634622
"""Collect products for a task.
635623
@@ -644,8 +632,13 @@ def _collect_product(
644632
"""
645633
node = node_info.value
646634

647-
# If we encounter a string and it is allowed, convert it to a path.
648-
if isinstance(node, str) and convert_string_to_path:
635+
# If we encounter a string and the argument name is 'produces', we convert it.
636+
if isinstance(node, str) and node_info.arg_name == "produces":
637+
warnings.warn(
638+
_WARNING_STRING_DEPRECATED.format(kind="product", node=node),
639+
category=FutureWarning,
640+
stacklevel=1,
641+
)
649642
node = Path(node)
650643
node_info = node_info._replace(value=node)
651644

tests/test_collect.py

Lines changed: 52 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,20 @@
1919

2020

2121
@pytest.mark.end_to_end()
22-
def test_collect_filepathnode_with_relative_path(tmp_path):
23-
source = """
22+
@pytest.mark.parametrize(
23+
("depends_on", "produces"),
24+
[
25+
("'in.txt'", "'out.txt'"),
26+
("Path('in.txt')", "Path('out.txt')"),
27+
],
28+
)
29+
def test_collect_file_with_relative_path(tmp_path, depends_on, produces):
30+
source = f"""
2431
import pytask
32+
from pathlib import Path
2533
26-
@pytask.mark.depends_on("in.txt")
27-
@pytask.mark.produces("out.txt")
34+
@pytask.mark.depends_on({depends_on})
35+
@pytask.mark.produces({produces})
2836
def task_write_text(depends_on, produces):
2937
produces.write_text(depends_on.read_text())
3038
"""
@@ -37,6 +45,27 @@ def task_write_text(depends_on, produces):
3745
assert tmp_path.joinpath("out.txt").read_text() == "Relative paths work."
3846

3947

48+
@pytest.mark.end_to_end()
49+
def test_relative_path_of_path_node(runner, tmp_path):
50+
source = """
51+
from pathlib import Path
52+
from typing_extensions import Annotated
53+
from pytask import Product, PathNode
54+
55+
def task_example(
56+
in_path: Annotated[Path, PathNode(path=Path("in.txt"))],
57+
path: Annotated[Path, Product, PathNode(path=Path("out.txt"))],
58+
) -> None:
59+
path.write_text("text")
60+
"""
61+
tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
62+
tmp_path.joinpath("in.txt").touch()
63+
64+
result = runner.invoke(cli, [tmp_path.as_posix()])
65+
assert result.exit_code == ExitCode.OK
66+
assert tmp_path.joinpath("out.txt").exists()
67+
68+
4069
@pytest.mark.end_to_end()
4170
def test_collect_depends_on_that_is_not_str_or_path(capsys, tmp_path):
4271
"""If a node cannot be parsed because unknown type, raise an error."""
@@ -348,30 +377,22 @@ def task_my_task():
348377

349378

350379
@pytest.mark.end_to_end()
351-
def test_collect_string_product_with_task_decorator(runner, tmp_path):
352-
source = """
353-
import pytask
354-
355-
@pytask.mark.task
356-
def task_write_text(produces="out.txt"):
357-
produces.touch()
358-
"""
359-
tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
360-
result = runner.invoke(cli, [tmp_path.as_posix()])
361-
assert result.exit_code == ExitCode.OK
362-
assert tmp_path.joinpath("out.txt").exists()
363-
380+
@pytest.mark.parametrize("decorator", ["", "@task"])
381+
def test_collect_string_product_with_or_without_task_decorator(
382+
runner, tmp_path, decorator
383+
):
384+
source = f"""
385+
from pytask import task
364386
365-
@pytest.mark.end_to_end()
366-
def test_collect_string_product_as_function_default(runner, tmp_path):
367-
source = """
387+
{decorator}
368388
def task_write_text(produces="out.txt"):
369389
produces.touch()
370390
"""
371391
tmp_path.joinpath("task_module.py").write_text(textwrap.dedent(source))
372392
result = runner.invoke(cli, [tmp_path.as_posix()])
373393
assert result.exit_code == ExitCode.OK
374394
assert tmp_path.joinpath("out.txt").exists()
395+
assert "FutureWarning" in result.output
375396

376397

377398
@pytest.mark.end_to_end()
@@ -443,12 +464,19 @@ def task_example(
443464

444465

445466
@pytest.mark.end_to_end()
446-
def test_deprecation_warning_for_strings_in_depends_on(runner, tmp_path):
447-
source = """
467+
@pytest.mark.parametrize(
468+
("depends_on", "produces"),
469+
[("'in.txt'", "Path('out.txt')"), ("Path('in.txt')", "'out.txt'")],
470+
)
471+
def test_deprecation_warning_for_strings_in_former_decorator_args(
472+
runner, tmp_path, depends_on, produces
473+
):
474+
source = f"""
448475
import pytask
476+
from pathlib import Path
449477
450-
@pytask.mark.depends_on("in.txt")
451-
@pytask.mark.produces("out.txt")
478+
@pytask.mark.depends_on({depends_on})
479+
@pytask.mark.produces({produces})
452480
def task_write_text(depends_on, produces):
453481
produces.touch()
454482
"""
@@ -472,7 +500,6 @@ def task_write_text(depends_on, produces=Path("out.txt")):
472500

473501
result = runner.invoke(cli, [tmp_path.as_posix()])
474502
assert "FutureWarning" not in result.output
475-
assert "Using 'produces' as an argument name" not in result.output
476503

477504

478505
@pytest.mark.end_to_end()
@@ -537,26 +564,6 @@ def task_example(path: Annotated[Path, Product, node]) -> None:
537564
assert "ValueError: The value for the parameter 'path'" in result.output
538565

539566

540-
@pytest.mark.end_to_end()
541-
def test_relative_path_of_path_node(runner, tmp_path):
542-
source = """
543-
from pathlib import Path
544-
from typing_extensions import Annotated
545-
from pytask import Product, PathNode
546-
547-
def task_example(
548-
path: Annotated[Path, Product, PathNode(path=Path("out.txt"), name="product")],
549-
) -> None:
550-
path.write_text("text")
551-
"""
552-
tmp_path.joinpath("subfolder").mkdir()
553-
tmp_path.joinpath("subfolder", "task_module.py").write_text(textwrap.dedent(source))
554-
555-
result = runner.invoke(cli, [tmp_path.as_posix()])
556-
assert result.exit_code == ExitCode.OK
557-
assert tmp_path.joinpath("subfolder", "out.txt").exists()
558-
559-
560567
@pytest.mark.end_to_end()
561568
def test_error_when_using_kwargs_and_node_in_annotation(runner, tmp_path):
562569
source = """

0 commit comments

Comments
 (0)