cook-build 0.6.2__tar.gz → 0.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cook_build-0.6.2/src/cook_build.egg-info → cook_build-0.6.3}/PKG-INFO +1 -1
- {cook_build-0.6.2 → cook_build-0.6.3}/pyproject.toml +1 -1
- {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/contexts.py +18 -4
- {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/controller.py +6 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/manager.py +10 -4
- {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/task.py +1 -1
- {cook_build-0.6.2 → cook_build-0.6.3/src/cook_build.egg-info}/PKG-INFO +1 -1
- {cook_build-0.6.2 → cook_build-0.6.3}/tests/test_actions.py +10 -10
- {cook_build-0.6.2 → cook_build-0.6.3}/tests/test_contexts.py +26 -7
- {cook_build-0.6.2 → cook_build-0.6.3}/tests/test_controller.py +1 -1
- {cook_build-0.6.2 → cook_build-0.6.3}/tests/test_main.py +15 -7
- {cook_build-0.6.2 → cook_build-0.6.3}/tests/test_manager.py +1 -1
- {cook_build-0.6.2 → cook_build-0.6.3}/LICENSE +0 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/README.md +0 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/README.rst +0 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/setup.cfg +0 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/__init__.py +0 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/__main__.py +0 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/actions.py +0 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/util.py +0 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/src/cook_build.egg-info/SOURCES.txt +0 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/src/cook_build.egg-info/dependency_links.txt +0 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/src/cook_build.egg-info/entry_points.txt +0 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/src/cook_build.egg-info/requires.txt +0 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/src/cook_build.egg-info/top_level.txt +0 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/tests/test_examples.py +0 -0
- {cook_build-0.6.2 → cook_build-0.6.3}/tests/test_util.py +0 -0
|
@@ -33,7 +33,8 @@ Custom contexts can be implemented by inheriting from :class:`.Context` and impl
|
|
|
33
33
|
from __future__ import annotations
|
|
34
34
|
from pathlib import Path
|
|
35
35
|
from types import ModuleType
|
|
36
|
-
from typing import Callable, TYPE_CHECKING
|
|
36
|
+
from typing import Callable, TYPE_CHECKING, TypeVar
|
|
37
|
+
import warnings
|
|
37
38
|
from . import actions
|
|
38
39
|
from . import manager as manager_
|
|
39
40
|
from . import task as task_
|
|
@@ -44,6 +45,8 @@ if TYPE_CHECKING:
|
|
|
44
45
|
from .manager import Manager
|
|
45
46
|
from .task import Task
|
|
46
47
|
|
|
48
|
+
ContextT = TypeVar("ContextT", bound="Context")
|
|
49
|
+
|
|
47
50
|
|
|
48
51
|
class Context:
|
|
49
52
|
"""
|
|
@@ -56,7 +59,7 @@ class Context:
|
|
|
56
59
|
def __init__(self, manager: "Manager | None" = None) -> None:
|
|
57
60
|
self.manager = manager or manager_.Manager.get_instance()
|
|
58
61
|
|
|
59
|
-
def __enter__(self) ->
|
|
62
|
+
def __enter__(self: ContextT) -> ContextT:
|
|
60
63
|
self.manager.contexts.append(self)
|
|
61
64
|
return self
|
|
62
65
|
|
|
@@ -214,10 +217,21 @@ class normalize_dependencies(Context):
|
|
|
214
217
|
task_dependencies = task.task_dependencies
|
|
215
218
|
for dependency in task.dependencies:
|
|
216
219
|
if isinstance(dependency, (task_.Task, create_group)):
|
|
217
|
-
|
|
220
|
+
warnings.warn(
|
|
221
|
+
"Passing Task objects to 'dependencies' is deprecated. Use "
|
|
222
|
+
"'task_dependencies' instead.",
|
|
223
|
+
DeprecationWarning,
|
|
224
|
+
stacklevel=4,
|
|
225
|
+
)
|
|
226
|
+
task_dependencies.append(dependency)
|
|
218
227
|
else:
|
|
219
228
|
dependencies.append(dependency)
|
|
220
|
-
|
|
229
|
+
# Convert all remaining dependencies (strings) to Path objects.
|
|
230
|
+
# After normalization, dependencies list contains only Path objects, but the
|
|
231
|
+
# Task.dependencies attribute is typed as list[PathOrStr | Task] to accept broader
|
|
232
|
+
# input before normalization. The isinstance checks in controller.py and manager.py
|
|
233
|
+
# validate this assumption at runtime.
|
|
234
|
+
task.dependencies = [Path(x) for x in dependencies] # type: ignore[assignment]
|
|
221
235
|
|
|
222
236
|
# Unpack group dependencies and look up tasks by name.
|
|
223
237
|
task_dependencies = []
|
|
@@ -109,6 +109,12 @@ class Controller:
|
|
|
109
109
|
"""
|
|
110
110
|
dependencies = []
|
|
111
111
|
for dependency in task.dependencies:
|
|
112
|
+
# Dependencies should be Path or str after normalize_dependencies runs.
|
|
113
|
+
# Tasks should have been moved to task_dependencies.
|
|
114
|
+
assert isinstance(dependency, (Path, str)), (
|
|
115
|
+
f"Unexpected dependency type '{type(dependency)}'. Dependencies "
|
|
116
|
+
"should be 'Path' or 'str' after 'normalize_dependencies'."
|
|
117
|
+
)
|
|
112
118
|
dependency = Path(dependency).resolve()
|
|
113
119
|
if not dependency.is_file():
|
|
114
120
|
LOGGER.debug("dependency %s of %s is missing", dependency, task)
|
|
@@ -96,8 +96,14 @@ class Manager:
|
|
|
96
96
|
f"tasks {task} and {other} both have target {path}"
|
|
97
97
|
)
|
|
98
98
|
task_by_target[path] = task
|
|
99
|
-
for
|
|
100
|
-
|
|
99
|
+
for dependency in task.dependencies:
|
|
100
|
+
# Dependencies should be Path or str after normalize_dependencies runs.
|
|
101
|
+
# Tasks should have been moved to task_dependencies.
|
|
102
|
+
assert isinstance(dependency, (Path, str)), (
|
|
103
|
+
f"Unexpected dependency type '{type(dependency)}'. Dependencies "
|
|
104
|
+
"should be 'Path' or 'str' after 'normalize_dependencies'."
|
|
105
|
+
)
|
|
106
|
+
path = Path(dependency).resolve()
|
|
101
107
|
tasks_by_file_dependency.setdefault(path, set()).add(task)
|
|
102
108
|
|
|
103
109
|
# Build a directed graph of dependencies based on files produced and consumed by tasks.
|
|
@@ -132,8 +138,8 @@ def create_task(
|
|
|
132
138
|
name: str,
|
|
133
139
|
*,
|
|
134
140
|
action: "Action | str | None" = None,
|
|
135
|
-
targets: list["Path"] | None = None,
|
|
136
|
-
dependencies: list["Path"] | None = None,
|
|
141
|
+
targets: list["Path | str"] | None = None,
|
|
142
|
+
dependencies: list["Path | str | Task"] | None = None,
|
|
137
143
|
task_dependencies: list["Task"] | None = None,
|
|
138
144
|
location: tuple[str, int] | None = None,
|
|
139
145
|
) -> "Task":
|
|
@@ -19,7 +19,7 @@ class Task:
|
|
|
19
19
|
self,
|
|
20
20
|
name: str,
|
|
21
21
|
*,
|
|
22
|
-
dependencies: list["PathOrStr"] | None = None,
|
|
22
|
+
dependencies: list["PathOrStr | Task"] | None = None,
|
|
23
23
|
targets: list["PathOrStr"] | None = None,
|
|
24
24
|
action: Action | None = None,
|
|
25
25
|
task_dependencies: list[Task] | None = None,
|
|
@@ -11,7 +11,7 @@ from unittest import mock
|
|
|
11
11
|
|
|
12
12
|
def test_shell_action(tmp_wd: Path) -> None:
|
|
13
13
|
action = SubprocessAction("echo hello > world.txt", shell=True)
|
|
14
|
-
action.execute(None)
|
|
14
|
+
action.execute(None) # type: ignore[arg-type]
|
|
15
15
|
assert (tmp_wd / "world.txt").read_text().strip() == "hello"
|
|
16
16
|
|
|
17
17
|
|
|
@@ -27,7 +27,7 @@ def test_shell_action_timeout() -> None:
|
|
|
27
27
|
|
|
28
28
|
action = SubprocessAction(["sleep", "2"])
|
|
29
29
|
with Timer() as timer, pytest.raises(SubprocessError, match="SIGTERM"):
|
|
30
|
-
action.execute(None, stop)
|
|
30
|
+
action.execute(None, stop) # type: ignore[arg-type]
|
|
31
31
|
|
|
32
32
|
assert 1 < timer.duration < 2
|
|
33
33
|
assert not thread.is_alive()
|
|
@@ -35,14 +35,14 @@ def test_shell_action_timeout() -> None:
|
|
|
35
35
|
|
|
36
36
|
def test_subprocess_action(tmp_wd: Path) -> None:
|
|
37
37
|
action = SubprocessAction(["touch", "foo"])
|
|
38
|
-
action.execute(None)
|
|
38
|
+
action.execute(None) # type: ignore[arg-type]
|
|
39
39
|
assert (tmp_wd / "foo").is_file()
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def test_bad_subprocess_action() -> None:
|
|
43
43
|
action = SubprocessAction("false")
|
|
44
44
|
with pytest.raises(SubprocessError):
|
|
45
|
-
action.execute(None)
|
|
45
|
+
action.execute(None) # type: ignore[arg-type]
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def test_subprocess_action_repr() -> None:
|
|
@@ -54,7 +54,7 @@ def test_function_action() -> None:
|
|
|
54
54
|
args = []
|
|
55
55
|
|
|
56
56
|
action = FunctionAction(args.append)
|
|
57
|
-
action.execute(42)
|
|
57
|
+
action.execute(42) # type: ignore[arg-type]
|
|
58
58
|
|
|
59
59
|
assert args == [42]
|
|
60
60
|
|
|
@@ -63,13 +63,13 @@ def test_composite_action() -> None:
|
|
|
63
63
|
args = []
|
|
64
64
|
|
|
65
65
|
action = CompositeAction(FunctionAction(args.append), FunctionAction(args.append))
|
|
66
|
-
action.execute("hello")
|
|
66
|
+
action.execute("hello") # type: ignore[arg-type]
|
|
67
67
|
assert args == ["hello", "hello"]
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
def test_module_action() -> None:
|
|
71
71
|
action = ModuleAction([pytest, "-h"])
|
|
72
|
-
action.execute(None)
|
|
72
|
+
action.execute(None) # type: ignore[arg-type]
|
|
73
73
|
|
|
74
74
|
with pytest.raises(ValueError, match="shell execution"):
|
|
75
75
|
ModuleAction([pytest], shell=True)
|
|
@@ -84,12 +84,12 @@ def test_module_action() -> None:
|
|
|
84
84
|
def test_module_action_debug() -> None:
|
|
85
85
|
with mock.patch("subprocess.Popen") as Popen:
|
|
86
86
|
Popen().wait = mock.MagicMock(return_value=0)
|
|
87
|
-
ModuleAction([pytest], debug=True).execute(None)
|
|
87
|
+
ModuleAction([pytest], debug=True).execute(None) # type: ignore[arg-type]
|
|
88
88
|
Popen.assert_called_with([sys.executable, "-m", "pdb", "-m", "pytest"])
|
|
89
89
|
|
|
90
90
|
with mock.patch("subprocess.Popen") as Popen:
|
|
91
91
|
Popen().wait = mock.MagicMock(return_value=0)
|
|
92
|
-
ModuleAction([pytest], debug=False).execute(None)
|
|
92
|
+
ModuleAction([pytest], debug=False).execute(None) # type: ignore[arg-type]
|
|
93
93
|
Popen.assert_called_with([sys.executable, "-m", "pytest"])
|
|
94
94
|
|
|
95
95
|
|
|
@@ -100,5 +100,5 @@ def test_composite_digest() -> None:
|
|
|
100
100
|
]
|
|
101
101
|
assert CompositeAction(*actions).hexdigest
|
|
102
102
|
|
|
103
|
-
actions.append(FunctionAction(print))
|
|
103
|
+
actions.append(FunctionAction(print)) # type: ignore[arg-type]
|
|
104
104
|
assert CompositeAction(*actions).hexdigest is None
|
|
@@ -33,7 +33,7 @@ def test_function_context(m: Manager) -> None:
|
|
|
33
33
|
def test_missing_task_context(m: Manager) -> None:
|
|
34
34
|
with (
|
|
35
35
|
pytest.raises(ValueError, match="did not return a task"),
|
|
36
|
-
FunctionContext(lambda _: None),
|
|
36
|
+
FunctionContext(lambda _: None), # type: ignore[arg-type]
|
|
37
37
|
):
|
|
38
38
|
m.create_task("my-task")
|
|
39
39
|
|
|
@@ -42,7 +42,7 @@ def test_context_management(m: Manager) -> None:
|
|
|
42
42
|
with pytest.raises(RuntimeError, match="no active contexts"), Context():
|
|
43
43
|
m.contexts = []
|
|
44
44
|
with pytest.raises(RuntimeError, match="unexpected context"), Context():
|
|
45
|
-
m.contexts.append("something else")
|
|
45
|
+
m.contexts.append("something else") # type: ignore[arg-type]
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def test_create_target_directories(
|
|
@@ -59,17 +59,20 @@ def test_create_target_directories(
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
def test_create_target_directories_with_multiple_targets(
|
|
62
|
-
m: Manager, tmp_wd: Path, conn: sqlite3
|
|
62
|
+
m: Manager, tmp_wd: Path, conn: sqlite3.Connection
|
|
63
63
|
) -> None:
|
|
64
64
|
filenames = [
|
|
65
65
|
tmp_wd / "this/is/a/hierarchy.txt",
|
|
66
66
|
tmp_wd / "this/is/a/hierarchy2.txt",
|
|
67
67
|
]
|
|
68
|
+
filename: Path | None = None
|
|
69
|
+
task: Task | None = None
|
|
68
70
|
with normalize_action(), create_target_directories():
|
|
69
71
|
for filename in filenames:
|
|
70
72
|
task = m.create_task(
|
|
71
73
|
filename.name, targets=[filename], action=["touch", filename]
|
|
72
74
|
)
|
|
75
|
+
assert filename is not None and task is not None
|
|
73
76
|
assert not filename.parent.is_dir()
|
|
74
77
|
|
|
75
78
|
controller = Controller(m.resolve_dependencies(), conn)
|
|
@@ -131,11 +134,27 @@ def test_normalize_dependencies(m: Manager) -> None:
|
|
|
131
134
|
with create_group("g") as g:
|
|
132
135
|
base = m.create_task("base")
|
|
133
136
|
with normalize_dependencies():
|
|
134
|
-
task = m.create_task("
|
|
137
|
+
task = m.create_task("task3", task_dependencies=["g"])
|
|
135
138
|
assert task.task_dependencies == [g.task]
|
|
136
139
|
|
|
137
|
-
task = m.create_task("task2", dependencies=[base])
|
|
138
|
-
assert task.task_dependencies == [base]
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
def test_normalize_dependencies_deprecated_syntax(m: Manager) -> None:
|
|
142
|
+
"""Test that passing Tasks to dependencies parameter emits DeprecationWarning."""
|
|
143
|
+
with create_group("g") as g:
|
|
144
|
+
base = m.create_task("base")
|
|
145
|
+
with normalize_dependencies():
|
|
146
|
+
# Test with group
|
|
147
|
+
with pytest.warns(
|
|
148
|
+
DeprecationWarning,
|
|
149
|
+
match="Passing Task objects to 'dependencies' is deprecated",
|
|
150
|
+
):
|
|
151
|
+
task = m.create_task("task1", dependencies=[g])
|
|
141
152
|
assert task.task_dependencies == [g.task]
|
|
153
|
+
|
|
154
|
+
# Test with regular task
|
|
155
|
+
with pytest.warns(
|
|
156
|
+
DeprecationWarning,
|
|
157
|
+
match="Passing Task objects to 'dependencies' is deprecated",
|
|
158
|
+
):
|
|
159
|
+
task = m.create_task("task2", dependencies=[base])
|
|
160
|
+
assert task.task_dependencies == [base]
|
|
@@ -188,7 +188,7 @@ def test_digest_cache(m: Manager, conn: Connection, tmp_wd: Path) -> None:
|
|
|
188
188
|
|
|
189
189
|
|
|
190
190
|
def test_skip_if_no_stale_tasks(m: Manager, conn: Connection, tmp_wd: Path) -> None:
|
|
191
|
-
c = Controller(m, conn)
|
|
191
|
+
c = Controller(m.resolve_dependencies(), conn)
|
|
192
192
|
c.execute([])
|
|
193
193
|
|
|
194
194
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import colorama
|
|
1
2
|
from cook.__main__ import __main__, Formatter
|
|
2
3
|
import logging
|
|
3
4
|
from pathlib import Path
|
|
@@ -9,6 +10,13 @@ import sys
|
|
|
9
10
|
RECIPES = Path(__file__).parent / "recipes"
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
def strip_colors(text: str) -> str:
|
|
14
|
+
for ansi_codes in [colorama.Fore, colorama.Back]:
|
|
15
|
+
for code in vars(ansi_codes).values():
|
|
16
|
+
text = text.replace(code, "")
|
|
17
|
+
return text
|
|
18
|
+
|
|
19
|
+
|
|
12
20
|
def test_blah_recipe_run(tmp_wd: Path) -> None:
|
|
13
21
|
__main__(["--recipe", str(RECIPES / "blah.py"), "exec", "run"])
|
|
14
22
|
|
|
@@ -28,35 +36,35 @@ def test_blah_recipe_ls(
|
|
|
28
36
|
__main__(["--recipe", str(RECIPES / "blah.py"), "ls", *patterns])
|
|
29
37
|
out, _ = capsys.readouterr()
|
|
30
38
|
for task in expected:
|
|
31
|
-
assert f"<task `{task}` @ " in
|
|
39
|
+
assert f"<task `{task}` @ " in strip_colors(out)
|
|
32
40
|
|
|
33
41
|
|
|
34
42
|
def test_blah_recipe_info(tmp_wd: Path, capsys: pytest.CaptureFixture) -> None:
|
|
35
43
|
__main__(["--recipe", str(RECIPES / "blah.py"), "info", "link"])
|
|
36
44
|
stdout, _ = capsys.readouterr()
|
|
37
|
-
assert "status: stale" in
|
|
45
|
+
assert "status: stale" in strip_colors(stdout)
|
|
38
46
|
|
|
39
47
|
__main__(["--recipe", str(RECIPES / "blah.py"), "exec", "link"])
|
|
40
48
|
__main__(["--recipe", str(RECIPES / "blah.py"), "info", "link"])
|
|
41
49
|
stdout, _ = capsys.readouterr()
|
|
42
|
-
assert "status: current" in
|
|
50
|
+
assert "status: current" in strip_colors(stdout)
|
|
43
51
|
|
|
44
52
|
__main__(["--recipe", str(RECIPES / "blah.py"), "info", "run"])
|
|
45
53
|
stdout, _ = capsys.readouterr()
|
|
46
|
-
assert "targets: -" in
|
|
54
|
+
assert "targets: -" in strip_colors(stdout)
|
|
47
55
|
|
|
48
56
|
# Check filtering based on stale/current status.
|
|
49
57
|
__main__(["--recipe", str(RECIPES / "blah.py"), "info", "--stale"])
|
|
50
58
|
stdout, _ = capsys.readouterr()
|
|
51
|
-
assert "status: current" not in
|
|
59
|
+
assert "status: current" not in strip_colors(stdout)
|
|
52
60
|
|
|
53
61
|
__main__(["--recipe", str(RECIPES / "blah.py"), "info", "--current"])
|
|
54
62
|
stdout, _ = capsys.readouterr()
|
|
55
|
-
assert "status: stale" not in
|
|
63
|
+
assert "status: stale" not in strip_colors(stdout)
|
|
56
64
|
|
|
57
65
|
__main__(["--recipe", str(RECIPES / "blah.py"), "info"])
|
|
58
66
|
stdout, _ = capsys.readouterr()
|
|
59
|
-
stdout =
|
|
67
|
+
stdout = strip_colors(stdout)
|
|
60
68
|
assert "status: stale" in stdout and "status: current" in stdout
|
|
61
69
|
|
|
62
70
|
# Check only one can be given.
|
|
@@ -85,7 +85,7 @@ def test_get_manager_instance() -> None:
|
|
|
85
85
|
with Manager() as m:
|
|
86
86
|
assert Manager.get_instance() is m
|
|
87
87
|
with pytest.raises(RuntimeError, match="unexpected manager"), Manager():
|
|
88
|
-
Manager._INSTANCE = "asdf"
|
|
88
|
+
Manager._INSTANCE = "asdf" # pyright: ignore[reportAttributeAccessIssue]
|
|
89
89
|
with pytest.raises(ValueError, match="already active"), Manager(), Manager():
|
|
90
90
|
pass
|
|
91
91
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|