Skip to content

Commit 655f62b

Browse files
authored
Forbid strings as values for paths in config file. (#553)
1 parent 2bad52f commit 655f62b

File tree

8 files changed

+78
-35
lines changed

8 files changed

+78
-35
lines changed

docs/source/changes.md

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and
1212
- {pull}`551` removes the deprecated `@pytask.mark.depends_on` and
1313
`@pytask.mark.produces`.
1414
- {pull}`552` removes the deprecated `@pytask.mark.task`.
15+
- {pull}`553` deprecates `paths` as a string in configuration and ensures that paths
16+
passed via the command line are relative to CWD and paths in the configuration
17+
relative to the config file.
1518

1619
## 0.4.5 - 2024-01-09
1720

docs/source/reference_guides/configuration.md

-6
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,7 @@ command line, you can add the paths to the configuration file. Paths passed via
187187
command line will overwrite the configuration value.
188188
189189
```toml
190-
191-
# For single entries only.
192-
paths = "src"
193-
194-
# Or single and multiple entries.
195190
paths = ["folder_1", "folder_2/task_2.py"]
196-
197191
```
198192
````
199193

src/_pytask/build.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def build( # noqa: C901, PLR0912, PLR0913, PLR0915
8080
marker_expression: str = "",
8181
max_failures: float = float("inf"),
8282
n_entries_in_table: int = 15,
83-
paths: str | Path | Iterable[str | Path] = (),
83+
paths: Path | Iterable[Path] = (),
8484
pdb: bool = False,
8585
pdb_cls: str = "",
8686
s: bool = False,
@@ -225,13 +225,12 @@ def build( # noqa: C901, PLR0912, PLR0913, PLR0915
225225

226226
raw_config = {**DEFAULTS_FROM_CLI, **raw_config}
227227

228-
raw_config["paths"] = parse_paths(raw_config.get("paths"))
228+
raw_config["paths"] = parse_paths(raw_config["paths"])
229229

230230
if raw_config["config"] is not None:
231231
raw_config["config"] = Path(raw_config["config"]).resolve()
232232
raw_config["root"] = raw_config["config"].parent
233233
else:
234-
raw_config["paths"] = parse_paths(raw_config["paths"])
235234
(
236235
raw_config["root"],
237236
raw_config["config"],

src/_pytask/config_utils.py

+9
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,13 @@ def read_config(
140140
for section in sections_:
141141
config = config[section]
142142

143+
# Only convert paths when possible. Otherwise, we defer the error until the click
144+
# takes over.
145+
if (
146+
"paths" in config
147+
and isinstance(config["paths"], list)
148+
and all(isinstance(p, str) for p in config["paths"])
149+
):
150+
config["paths"] = [path.parent.joinpath(p).resolve() for p in config["paths"]]
151+
143152
return config

src/_pytask/dag_command.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -154,16 +154,12 @@ def build_dag(raw_config: dict[str, Any]) -> nx.DiGraph:
154154

155155
raw_config = {**DEFAULTS_FROM_CLI, **raw_config}
156156

157-
raw_config["paths"] = parse_paths(raw_config.get("paths"))
157+
raw_config["paths"] = parse_paths(raw_config["paths"])
158158

159159
if raw_config["config"] is not None:
160160
raw_config["config"] = Path(raw_config["config"]).resolve()
161161
raw_config["root"] = raw_config["config"].parent
162162
else:
163-
if raw_config["paths"] is None:
164-
raw_config["paths"] = (Path.cwd(),)
165-
166-
raw_config["paths"] = parse_paths(raw_config["paths"])
167163
(
168164
raw_config["root"],
169165
raw_config["config"],

src/_pytask/parameters.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
_PATH_ARGUMENT = click.Argument(
5757
["paths"],
5858
nargs=-1,
59-
type=click.Path(exists=True, resolve_path=True),
59+
type=click.Path(exists=True, resolve_path=True, path_type=Path),
6060
is_eager=True,
6161
)
6262
"""click.Argument: An argument for paths."""
@@ -180,9 +180,11 @@ def pytask_add_hooks(pm: PluginManager) -> None:
180180
def pytask_extend_command_line_interface(cli: click.Group) -> None:
181181
"""Register general markers."""
182182
for command in ("build", "clean", "collect", "dag", "profile"):
183-
cli.commands[command].params.extend((_PATH_ARGUMENT, _DATABASE_URL_OPTION))
183+
cli.commands[command].params.extend((_DATABASE_URL_OPTION,))
184184
for command in ("build", "clean", "collect", "dag", "markers", "profile"):
185-
cli.commands[command].params.extend((_CONFIG_OPTION, _HOOK_MODULE_OPTION))
185+
cli.commands[command].params.extend(
186+
(_CONFIG_OPTION, _HOOK_MODULE_OPTION, _PATH_ARGUMENT)
187+
)
186188
for command in ("build", "clean", "collect", "profile"):
187189
cli.commands[command].params.extend([_IGNORE_OPTION, _EDITOR_URL_SCHEME_OPTION])
188190
for command in ("build",):

src/_pytask/shared.py

+1-14
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from __future__ import annotations
33

44
import glob
5-
import warnings
65
from pathlib import Path
76
from typing import Any
87
from typing import Iterable
@@ -56,20 +55,8 @@ def to_list(scalar_or_iter: Any) -> list[Any]:
5655
)
5756

5857

59-
def parse_paths(x: Any | None) -> list[Path] | None:
58+
def parse_paths(x: Path | list[Path]) -> list[Path]:
6059
"""Parse paths."""
61-
if x is None:
62-
return None
63-
64-
if isinstance(x, str):
65-
msg = (
66-
"Specifying paths as a string in 'pyproject.toml' is deprecated and will "
67-
"result in an error in v0.5. Please use a list of strings instead: "
68-
f'["{x}"].'
69-
)
70-
warnings.warn(msg, category=FutureWarning, stacklevel=1)
71-
x = [x]
72-
7360
paths = [Path(p) for p in to_list(x)]
7461
for p in paths:
7562
if not p.exists():

tests/test_config.py

+57-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from __future__ import annotations
22

3+
import os
4+
import subprocess
5+
import sys
36
import textwrap
47

58
import pytest
@@ -46,7 +49,7 @@ def test_pass_config_to_cli(tmp_path):
4649
def test_passing_paths_via_configuration_file(tmp_path, file_or_folder):
4750
config = f"""
4851
[tool.pytask.ini_options]
49-
paths = "{file_or_folder}"
52+
paths = ["{file_or_folder}"]
5053
"""
5154
tmp_path.joinpath("pyproject.toml").write_text(textwrap.dedent(config))
5255

@@ -65,10 +68,60 @@ def test_passing_paths_via_configuration_file(tmp_path, file_or_folder):
6568
def test_not_existing_path_in_config(runner, tmp_path):
6669
config = """
6770
[tool.pytask.ini_options]
68-
paths = "not_existing_path"
71+
paths = ["not_existing_path"]
6972
"""
7073
tmp_path.joinpath("pyproject.toml").write_text(textwrap.dedent(config))
7174

72-
with pytest.warns(FutureWarning, match="Specifying paths as a string"):
73-
result = runner.invoke(cli, [tmp_path.as_posix()])
75+
result = runner.invoke(cli, [tmp_path.as_posix()])
7476
assert result.exit_code == ExitCode.CONFIGURATION_FAILED
77+
78+
79+
def test_paths_are_relative_to_configuration_file_cli(tmp_path):
80+
tmp_path.joinpath("src").mkdir()
81+
tmp_path.joinpath("tasks").mkdir()
82+
config = """
83+
[tool.pytask.ini_options]
84+
paths = ["../tasks"]
85+
"""
86+
tmp_path.joinpath("src", "pyproject.toml").write_text(textwrap.dedent(config))
87+
88+
source = "def task_example(): ..."
89+
tmp_path.joinpath("tasks", "task_example.py").write_text(source)
90+
91+
result = subprocess.run(
92+
("pytask", "src"), cwd=tmp_path, check=False, capture_output=True
93+
)
94+
95+
assert result.returncode == ExitCode.OK
96+
assert "1 Succeeded" in result.stdout.decode()
97+
98+
99+
@pytest.mark.skipif(
100+
sys.platform == "win32" and os.environ.get("CI") == "true",
101+
reason="Windows does not pick up the right Python interpreter.",
102+
)
103+
def test_paths_are_relative_to_configuration_file(tmp_path):
104+
tmp_path.joinpath("src").mkdir()
105+
tmp_path.joinpath("tasks").mkdir()
106+
config = """
107+
[tool.pytask.ini_options]
108+
paths = ["../tasks"]
109+
"""
110+
tmp_path.joinpath("src", "pyproject.toml").write_text(textwrap.dedent(config))
111+
112+
source = "def task_example(): ..."
113+
tmp_path.joinpath("tasks", "task_example.py").write_text(source)
114+
115+
source = """
116+
from pytask import build
117+
from pathlib import Path
118+
119+
session = build(paths=[Path("src")])
120+
"""
121+
tmp_path.joinpath("script.py").write_text(textwrap.dedent(source))
122+
result = subprocess.run(
123+
("python", "script.py"), cwd=tmp_path, check=False, capture_output=True
124+
)
125+
126+
assert result.returncode == ExitCode.OK
127+
assert "1 Succeeded" in result.stdout.decode()

0 commit comments

Comments
 (0)