Skip to content

Commit 8dbdfbb

Browse files
authored
Normalize programmatic build_dag config defaults (#767)
1 parent dbf0236 commit 8dbdfbb

3 files changed

Lines changed: 89 additions & 87 deletions

File tree

src/_pytask/build.py

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import json
66
import sys
77
from contextlib import suppress
8-
from pathlib import Path
98
from typing import TYPE_CHECKING
109
from typing import Any
1110
from typing import Literal
@@ -16,8 +15,7 @@
1615
from _pytask.capture_utils import CaptureMethod
1716
from _pytask.capture_utils import ShowCapture
1817
from _pytask.click import ColoredCommand
19-
from _pytask.config_utils import find_project_root_and_config
20-
from _pytask.config_utils import read_config
18+
from _pytask.config_utils import normalize_programmatic_config
2119
from _pytask.console import console
2220
from _pytask.dag import create_dag
2321
from _pytask.exceptions import CollectionError
@@ -30,13 +28,12 @@
3028
from _pytask.pluginmanager import hookimpl
3129
from _pytask.pluginmanager import storage
3230
from _pytask.session import Session
33-
from _pytask.shared import parse_paths
34-
from _pytask.shared import to_list
3531
from _pytask.traceback import Traceback
3632

3733
if TYPE_CHECKING:
3834
from collections.abc import Callable
3935
from collections.abc import Iterable
36+
from pathlib import Path
4037
from typing import NoReturn
4138

4239
from _pytask.node_protocols import PTask
@@ -66,7 +63,7 @@ def pytask_unconfigure(session: Session) -> None:
6663
path.write_text(json.dumps(HashPathCache._cache))
6764

6865

69-
def build( # noqa: C901, PLR0912, PLR0913, PLR0915
66+
def build( # noqa: PLR0913
7067
*,
7168
capture: Literal["fd", "no", "sys", "tee-sys"] | CaptureMethod = CaptureMethod.FD,
7269
check_casing_of_paths: bool = True,
@@ -225,47 +222,14 @@ def build( # noqa: C901, PLR0912, PLR0913, PLR0915
225222

226223
# If someone called the programmatic interface, we need to do some parsing.
227224
if "command" not in raw_config:
228-
raw_config["command"] = "build"
229225
# Add defaults from cli.
230226
from _pytask.cli import DEFAULTS_FROM_CLI # noqa: PLC0415
231227

232-
raw_config = {**DEFAULTS_FROM_CLI, **raw_config}
233-
234-
paths_value = raw_config["paths"]
235-
# Convert tuple to list since parse_paths expects Path | list[Path]
236-
if isinstance(paths_value, tuple):
237-
paths_value = list(paths_value)
238-
if not isinstance(paths_value, (Path, list)):
239-
msg = f"paths must be Path or list, got {type(paths_value)}"
240-
raise TypeError(msg) # noqa: TRY301
241-
# Cast is justified - we validated at runtime
242-
raw_config["paths"] = parse_paths(cast("Path | list[Path]", paths_value))
243-
244-
if raw_config["config"] is not None:
245-
config_value = raw_config["config"]
246-
if not isinstance(config_value, (str, Path)):
247-
msg = f"config must be str or Path, got {type(config_value)}"
248-
raise TypeError(msg) # noqa: TRY301
249-
raw_config["config"] = Path(config_value).resolve()
250-
raw_config["root"] = raw_config["config"].parent
251-
else:
252-
(
253-
raw_config["root"],
254-
raw_config["config"],
255-
) = find_project_root_and_config(raw_config["paths"])
256-
257-
if raw_config["config"] is not None:
258-
config_from_file = read_config(raw_config["config"])
259-
260-
if "paths" in config_from_file:
261-
paths = config_from_file["paths"]
262-
paths = [
263-
raw_config["config"].parent.joinpath(path).resolve()
264-
for path in to_list(paths)
265-
]
266-
config_from_file["paths"] = paths
267-
268-
raw_config = {**raw_config, **config_from_file}
228+
raw_config = normalize_programmatic_config(
229+
raw_config,
230+
command="build",
231+
defaults_from_cli=cast("dict[str, Any]", DEFAULTS_FROM_CLI),
232+
)
269233

270234
config_ = pm.hook.pytask_configure(pm=pm, raw_config=raw_config)
271235

src/_pytask/config_utils.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import enum
56
import os
67
import sys
78
from pathlib import Path
@@ -12,6 +13,7 @@
1213
import click
1314

1415
from _pytask.shared import parse_paths
16+
from _pytask.shared import to_list
1517

1618
if TYPE_CHECKING:
1719
from collections.abc import Sequence
@@ -22,7 +24,12 @@
2224
import tomli as tomllib # ty: ignore[unresolved-import]
2325

2426

25-
__all__ = ["find_project_root_and_config", "read_config", "set_defaults_from_config"]
27+
__all__ = [
28+
"find_project_root_and_config",
29+
"normalize_programmatic_config",
30+
"read_config",
31+
"set_defaults_from_config",
32+
]
2633

2734

2835
def set_defaults_from_config(
@@ -78,6 +85,73 @@ def set_defaults_from_config(
7885
return context.params["config"]
7986

8087

88+
def normalize_programmatic_config(
89+
raw_config: dict[str, Any],
90+
*,
91+
command: str,
92+
defaults_from_cli: dict[str, Any],
93+
) -> dict[str, Any]:
94+
"""Normalize programmatic raw_config inputs."""
95+
raw_config = {**defaults_from_cli, **raw_config}
96+
raw_config["command"] = command
97+
raw_config["paths"] = _normalize_paths_value(raw_config["paths"])
98+
_normalize_config_value(raw_config)
99+
100+
if raw_config["config"] is not None:
101+
config_from_file = read_config(raw_config["config"])
102+
103+
if "paths" in config_from_file:
104+
paths = config_from_file["paths"]
105+
paths = [
106+
raw_config["config"].parent.joinpath(path).resolve()
107+
for path in to_list(paths)
108+
]
109+
config_from_file["paths"] = paths
110+
111+
raw_config = {**raw_config, **config_from_file}
112+
113+
return raw_config
114+
115+
116+
def _normalize_paths_value(paths_value: Any) -> list[Path]:
117+
"""Normalize paths from programmatic inputs."""
118+
if isinstance(paths_value, tuple):
119+
paths_value = list(paths_value)
120+
if not isinstance(paths_value, (Path, list)):
121+
msg = f"paths must be Path or list, got {type(paths_value)}"
122+
raise TypeError(msg)
123+
# Cast is justified - we validated at runtime
124+
return parse_paths(cast("Path | list[Path]", paths_value))
125+
126+
127+
def _normalize_config_value(raw_config: dict[str, Any]) -> None:
128+
"""Normalize config value from programmatic inputs."""
129+
config_value = raw_config["config"]
130+
if _is_click_sentinel(config_value):
131+
config_value = None
132+
raw_config["config"] = None
133+
if config_value is not None:
134+
if not isinstance(config_value, (str, Path)):
135+
msg = f"config must be str or Path, got {type(config_value)}"
136+
raise TypeError(msg)
137+
raw_config["config"] = Path(config_value).resolve()
138+
raw_config["root"] = raw_config["config"].parent
139+
else:
140+
raw_config["root"], raw_config["config"] = find_project_root_and_config(
141+
raw_config["paths"]
142+
)
143+
144+
145+
def _is_click_sentinel(value: Any) -> bool:
146+
"""Return True if value looks like Click's Sentinel.UNSET."""
147+
return (
148+
isinstance(value, enum.Enum)
149+
and value.name == "UNSET"
150+
and value.__class__.__name__ == "Sentinel"
151+
and value.__class__.__module__ == "click._utils"
152+
)
153+
154+
81155
def find_project_root_and_config(
82156
paths: Sequence[Path] | None,
83157
) -> tuple[Path, Path | None]:

src/_pytask/dag_command.py

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
from _pytask.click import EnumChoice
1717
from _pytask.compat import check_for_optional_program
1818
from _pytask.compat import import_optional_dependency
19-
from _pytask.config_utils import find_project_root_and_config
20-
from _pytask.config_utils import read_config
19+
from _pytask.config_utils import normalize_programmatic_config
2120
from _pytask.console import console
2221
from _pytask.dag import create_dag
2322
from _pytask.exceptions import CollectionError
@@ -28,9 +27,7 @@
2827
from _pytask.pluginmanager import hookimpl
2928
from _pytask.pluginmanager import storage
3029
from _pytask.session import Session
31-
from _pytask.shared import parse_paths
3230
from _pytask.shared import reduce_names_of_multiple_nodes
33-
from _pytask.shared import to_list
3431
from _pytask.traceback import Traceback
3532

3633

@@ -150,47 +147,14 @@ def build_dag(raw_config: dict[str, Any]) -> nx.DiGraph:
150147

151148
# If someone called the programmatic interface, we need to do some parsing.
152149
if "command" not in raw_config:
153-
raw_config["command"] = "dag"
154150
# Add defaults from cli.
155151
from _pytask.cli import DEFAULTS_FROM_CLI # noqa: PLC0415
156152

157-
raw_config = {**DEFAULTS_FROM_CLI, **raw_config} # ty: ignore[invalid-assignment]
158-
159-
paths_value = raw_config["paths"]
160-
# Convert tuple to list since parse_paths expects Path | list[Path]
161-
if isinstance(paths_value, tuple):
162-
paths_value = list(paths_value)
163-
if not isinstance(paths_value, (Path, list)):
164-
msg = f"paths must be Path or list, got {type(paths_value)}"
165-
raise TypeError(msg) # noqa: TRY301
166-
# Cast is justified - we validated at runtime
167-
raw_config["paths"] = parse_paths(cast("Path | list[Path]", paths_value))
168-
169-
if raw_config["config"] is not None:
170-
config_value = raw_config["config"]
171-
if not isinstance(config_value, (str, Path)):
172-
msg = f"config must be str or Path, got {type(config_value)}"
173-
raise TypeError(msg) # noqa: TRY301
174-
raw_config["config"] = Path(config_value).resolve()
175-
raw_config["root"] = raw_config["config"].parent
176-
else:
177-
(
178-
raw_config["root"],
179-
raw_config["config"],
180-
) = find_project_root_and_config(raw_config["paths"])
181-
182-
if raw_config["config"] is not None:
183-
config_from_file = read_config(raw_config["config"])
184-
185-
if "paths" in config_from_file:
186-
paths = config_from_file["paths"]
187-
paths = [
188-
raw_config["config"].parent.joinpath(path).resolve()
189-
for path in to_list(paths)
190-
]
191-
config_from_file["paths"] = paths
192-
193-
raw_config = {**raw_config, **config_from_file}
153+
raw_config = normalize_programmatic_config(
154+
raw_config,
155+
command="dag",
156+
defaults_from_cli=cast("dict[str, Any]", DEFAULTS_FROM_CLI),
157+
)
194158

195159
config = pm.hook.pytask_configure(pm=pm, raw_config=raw_config)
196160

0 commit comments

Comments
 (0)