cook-build 0.5.1__py3-none-any.whl → 0.6.1__py3-none-any.whl
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/__main__.py +146 -63
- cook/actions.py +21 -13
- cook/contexts.py +33 -14
- cook/controller.py +157 -56
- cook/manager.py +41 -22
- cook/task.py +13 -13
- cook/util.py +14 -9
- cook_build-0.6.1.dist-info/METADATA +91 -0
- cook_build-0.6.1.dist-info/RECORD +14 -0
- cook_build-0.5.1.dist-info/METADATA +0 -11
- cook_build-0.5.1.dist-info/RECORD +0 -14
- {cook_build-0.5.1.dist-info → cook_build-0.6.1.dist-info}/WHEEL +0 -0
- {cook_build-0.5.1.dist-info → cook_build-0.6.1.dist-info}/entry_points.txt +0 -0
- {cook_build-0.5.1.dist-info → cook_build-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {cook_build-0.5.1.dist-info → cook_build-0.6.1.dist-info}/top_level.txt +0 -0
cook/__main__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import colorama
|
|
3
|
+
from contextlib import closing
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
import fnmatch
|
|
5
6
|
import importlib.util
|
|
@@ -10,9 +11,13 @@ import re
|
|
|
10
11
|
import sqlite3
|
|
11
12
|
import sys
|
|
12
13
|
import textwrap
|
|
13
|
-
from typing import Iterable
|
|
14
|
+
from typing import Iterable
|
|
14
15
|
|
|
15
|
-
from .contexts import
|
|
16
|
+
from .contexts import (
|
|
17
|
+
create_target_directories,
|
|
18
|
+
normalize_action,
|
|
19
|
+
normalize_dependencies,
|
|
20
|
+
)
|
|
16
21
|
from .controller import Controller, QUERIES
|
|
17
22
|
from .manager import Manager
|
|
18
23
|
from .task import Task
|
|
@@ -23,22 +28,28 @@ LOGGER = logging.getLogger("cook")
|
|
|
23
28
|
|
|
24
29
|
|
|
25
30
|
class NoMatchingTaskError(ValueError):
|
|
26
|
-
def __init__(
|
|
31
|
+
def __init__(
|
|
32
|
+
self, patterns: Iterable[re.Pattern | str], hidden_tasks_available: bool
|
|
33
|
+
) -> None:
|
|
27
34
|
self.hidden_tasks_available = hidden_tasks_available
|
|
28
|
-
|
|
29
|
-
if len(
|
|
30
|
-
message = f"found no tasks matching pattern {
|
|
35
|
+
formatted_patterns = [f"`{pattern}`" for pattern in patterns]
|
|
36
|
+
if len(formatted_patterns) == 1:
|
|
37
|
+
message = f"found no tasks matching pattern {formatted_patterns[0]}"
|
|
31
38
|
else:
|
|
32
|
-
*
|
|
33
|
-
message =
|
|
34
|
-
|
|
39
|
+
*formatted_patterns, last = formatted_patterns
|
|
40
|
+
message = (
|
|
41
|
+
"found no tasks matching patterns "
|
|
42
|
+
+ ", ".join(formatted_patterns)
|
|
43
|
+
+ (", or " if len(formatted_patterns) > 1 else " or ")
|
|
44
|
+
+ last
|
|
45
|
+
)
|
|
35
46
|
if hidden_tasks_available:
|
|
36
47
|
message = message + "; use --all or -a to include hidden tasks"
|
|
37
48
|
super().__init__(message)
|
|
38
49
|
|
|
39
50
|
|
|
40
51
|
class Args:
|
|
41
|
-
tasks: Iterable[re.Pattern]
|
|
52
|
+
tasks: Iterable[re.Pattern | str]
|
|
42
53
|
re: bool
|
|
43
54
|
all: bool
|
|
44
55
|
|
|
@@ -47,23 +58,35 @@ class Command:
|
|
|
47
58
|
"""
|
|
48
59
|
Abstract base class for commands.
|
|
49
60
|
"""
|
|
50
|
-
|
|
61
|
+
|
|
62
|
+
NAME: str | None = None
|
|
51
63
|
ALLOW_EMPTY_PATTERN: bool = False
|
|
52
64
|
|
|
53
65
|
def configure_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
54
|
-
parser.add_argument(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--re",
|
|
68
|
+
"-r",
|
|
69
|
+
action="store_true",
|
|
70
|
+
help="use regular expressions for pattern matching instead of glob",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--all",
|
|
74
|
+
"-a",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="include tasks starting with `_` prefix",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"tasks",
|
|
80
|
+
nargs="*" if self.ALLOW_EMPTY_PATTERN else "+",
|
|
81
|
+
help="task or tasks to execute as regular expressions or glob patterns",
|
|
82
|
+
)
|
|
60
83
|
|
|
61
84
|
def execute(self, controller: Controller, args: argparse.Namespace) -> None:
|
|
62
85
|
raise NotImplementedError
|
|
63
86
|
|
|
64
|
-
def discover_tasks(self, controller: Controller, args: Args) ->
|
|
87
|
+
def discover_tasks(self, controller: Controller, args: Args) -> list[Task]:
|
|
65
88
|
task: Task
|
|
66
|
-
tasks:
|
|
89
|
+
tasks: list[Task] = []
|
|
67
90
|
|
|
68
91
|
# Get tasks based on the pattern matching.
|
|
69
92
|
if not args.tasks:
|
|
@@ -73,7 +96,10 @@ class Command:
|
|
|
73
96
|
if args.re:
|
|
74
97
|
match = any(re.match(pattern, task.name) for pattern in args.tasks)
|
|
75
98
|
else:
|
|
76
|
-
match = any(
|
|
99
|
+
match = any(
|
|
100
|
+
fnmatch.fnmatch(task.name, pattern) # pyright: ignore[reportArgumentType]
|
|
101
|
+
for pattern in args.tasks
|
|
102
|
+
)
|
|
77
103
|
if match:
|
|
78
104
|
tasks.append(task)
|
|
79
105
|
|
|
@@ -83,8 +109,11 @@ class Command:
|
|
|
83
109
|
# Filter out hidden tasks if desired unless the name is an exact match to a specified
|
|
84
110
|
# pattern.
|
|
85
111
|
if not args.all:
|
|
86
|
-
tasks = [
|
|
87
|
-
|
|
112
|
+
tasks = [
|
|
113
|
+
task
|
|
114
|
+
for task in tasks
|
|
115
|
+
if not task.name.startswith("_") or task.name in args.tasks
|
|
116
|
+
]
|
|
88
117
|
|
|
89
118
|
if not tasks:
|
|
90
119
|
raise NoMatchingTaskError(args.tasks, not args.all and has_hidden_task)
|
|
@@ -99,13 +128,16 @@ class ExecCommand(Command):
|
|
|
99
128
|
"""
|
|
100
129
|
Execute one or more tasks.
|
|
101
130
|
"""
|
|
131
|
+
|
|
102
132
|
NAME = "exec"
|
|
103
133
|
|
|
104
134
|
def configure_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
105
|
-
parser.add_argument(
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--jobs", "-j", help="number of concurrent jobs", type=int, default=1
|
|
137
|
+
)
|
|
106
138
|
super().configure_parser(parser)
|
|
107
139
|
|
|
108
|
-
def execute(self, controller: Controller, args: ExecArgs) -> None:
|
|
140
|
+
def execute(self, controller: Controller, args: ExecArgs) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
109
141
|
tasks = self.discover_tasks(controller, args)
|
|
110
142
|
controller.execute(tasks, num_concurrent=args.jobs)
|
|
111
143
|
|
|
@@ -119,28 +151,43 @@ class LsCommand(Command):
|
|
|
119
151
|
"""
|
|
120
152
|
List tasks.
|
|
121
153
|
"""
|
|
154
|
+
|
|
122
155
|
NAME = "ls"
|
|
123
156
|
ALLOW_EMPTY_PATTERN = True
|
|
124
157
|
|
|
125
158
|
def configure_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
126
|
-
parser.add_argument(
|
|
127
|
-
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
"--stale", "-s", action="store_true", help="only show stale tasks"
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--current", "-c", action="store_true", help="only show current tasks"
|
|
164
|
+
)
|
|
128
165
|
super().configure_parser(parser)
|
|
129
166
|
|
|
130
|
-
def execute(self, controller: Controller, args: LsArgs) -> None:
|
|
167
|
+
def execute(self, controller: Controller, args: LsArgs) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
131
168
|
tasks = self.discover_tasks(controller, args)
|
|
132
|
-
tasks = [
|
|
133
|
-
|
|
169
|
+
tasks = [
|
|
170
|
+
task.format(colorama.Fore.YELLOW if is_stale else colorama.Fore.GREEN)
|
|
171
|
+
for is_stale, task in zip(controller.is_stale(tasks), tasks)
|
|
172
|
+
]
|
|
134
173
|
print("\n".join(tasks))
|
|
135
174
|
|
|
136
|
-
def discover_tasks(self, controller: Controller, args: LsArgs) ->
|
|
175
|
+
def discover_tasks(self, controller: Controller, args: LsArgs) -> list[Task]: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
137
176
|
if args.current and args.stale:
|
|
138
|
-
raise ValueError(
|
|
177
|
+
raise ValueError(
|
|
178
|
+
"only one of `--stale` and `--current` may be given at the same time"
|
|
179
|
+
)
|
|
139
180
|
tasks = super().discover_tasks(controller, args)
|
|
140
181
|
if args.stale:
|
|
141
|
-
return [
|
|
182
|
+
return [
|
|
183
|
+
task for stale, task in zip(controller.is_stale(tasks), tasks) if stale
|
|
184
|
+
]
|
|
142
185
|
elif args.current:
|
|
143
|
-
return [
|
|
186
|
+
return [
|
|
187
|
+
task
|
|
188
|
+
for stale, task in zip(controller.is_stale(tasks), tasks)
|
|
189
|
+
if not stale
|
|
190
|
+
]
|
|
144
191
|
return tasks
|
|
145
192
|
|
|
146
193
|
|
|
@@ -148,6 +195,7 @@ class InfoCommand(LsCommand):
|
|
|
148
195
|
"""
|
|
149
196
|
Display information about one or more tasks.
|
|
150
197
|
"""
|
|
198
|
+
|
|
151
199
|
NAME = "info"
|
|
152
200
|
|
|
153
201
|
def execute(self, controller: Controller, args: LsArgs) -> None:
|
|
@@ -166,21 +214,26 @@ class InfoCommand(LsCommand):
|
|
|
166
214
|
]
|
|
167
215
|
# Show when the task last completed and failed.
|
|
168
216
|
last = controller.connection.execute(
|
|
169
|
-
"SELECT last_completed, last_failed FROM tasks WHERE name = :name",
|
|
170
|
-
{"name": task.name}
|
|
171
|
-
).fetchone() or (None, None)
|
|
172
|
-
for key, value in zip(["completed", "failed"], last):
|
|
217
|
+
"SELECT last_started, last_completed, last_failed FROM tasks WHERE name = :name",
|
|
218
|
+
{"name": task.name},
|
|
219
|
+
).fetchone() or (None, None, None)
|
|
220
|
+
for key, value in zip(["started", "completed", "failed"], last):
|
|
173
221
|
if value is None:
|
|
174
222
|
parts.append(f"last {key}: -")
|
|
175
223
|
continue
|
|
176
|
-
parts.append(
|
|
177
|
-
|
|
224
|
+
parts.append(
|
|
225
|
+
f"last {key}: {format_timedelta(datetime.now() - value)} ago "
|
|
226
|
+
f"({format_datetime(value)})"
|
|
227
|
+
)
|
|
178
228
|
# Show dependencies and targets.
|
|
179
|
-
task_dependencies = list(
|
|
180
|
-
|
|
229
|
+
task_dependencies = list(
|
|
230
|
+
sorted(controller.dependencies.successors(task), key=lambda t: t.name)
|
|
231
|
+
)
|
|
181
232
|
task_dependencies = [
|
|
182
233
|
dep.format(colorama.Fore.YELLOW if is_stale else colorama.Fore.GREEN)
|
|
183
|
-
for is_stale, dep in zip(
|
|
234
|
+
for is_stale, dep in zip(
|
|
235
|
+
controller.is_stale(task_dependencies), task_dependencies
|
|
236
|
+
)
|
|
184
237
|
]
|
|
185
238
|
items = [
|
|
186
239
|
("dependencies", task.dependencies),
|
|
@@ -195,22 +248,22 @@ class InfoCommand(LsCommand):
|
|
|
195
248
|
parts.append(f"{key}: -")
|
|
196
249
|
parts.append(f"action: {task.action if task.action else '-'}")
|
|
197
250
|
|
|
198
|
-
parts = textwrap.indent(
|
|
251
|
+
parts = textwrap.indent("\n".join(parts), indent)
|
|
199
252
|
print(f"{task}\n{parts}")
|
|
200
253
|
|
|
201
254
|
|
|
202
|
-
class ResetArgs(
|
|
203
|
-
|
|
204
|
-
re: bool
|
|
255
|
+
class ResetArgs(Args):
|
|
256
|
+
pass
|
|
205
257
|
|
|
206
258
|
|
|
207
259
|
class ResetCommand(Command):
|
|
208
260
|
"""
|
|
209
261
|
Reset the status of one or more tasks.
|
|
210
262
|
"""
|
|
263
|
+
|
|
211
264
|
NAME = "reset"
|
|
212
265
|
|
|
213
|
-
def execute(self, controller: Controller, args: ResetArgs) -> None:
|
|
266
|
+
def execute(self, controller: Controller, args: ResetArgs) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
214
267
|
tasks = self.discover_tasks(controller, args)
|
|
215
268
|
controller.reset(*tasks)
|
|
216
269
|
|
|
@@ -233,14 +286,26 @@ class Formatter(logging.Formatter):
|
|
|
233
286
|
return formatted
|
|
234
287
|
|
|
235
288
|
|
|
236
|
-
def __main__(cli_args:
|
|
289
|
+
def __main__(cli_args: list[str] | None = None) -> None:
|
|
237
290
|
parser = argparse.ArgumentParser("cook")
|
|
238
|
-
parser.add_argument(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
291
|
+
parser.add_argument(
|
|
292
|
+
"--recipe",
|
|
293
|
+
help="file containing declarative recipe for tasks",
|
|
294
|
+
default="recipe.py",
|
|
295
|
+
type=Path,
|
|
296
|
+
)
|
|
297
|
+
parser.add_argument(
|
|
298
|
+
"--module", "-m", help="module containing declarative recipe for tasks"
|
|
299
|
+
)
|
|
300
|
+
parser.add_argument(
|
|
301
|
+
"--db", help="database for keeping track of assets", default=".cook"
|
|
302
|
+
)
|
|
303
|
+
parser.add_argument(
|
|
304
|
+
"--log-level",
|
|
305
|
+
help="log level",
|
|
306
|
+
default="info",
|
|
307
|
+
choices={"error", "warning", "info", "debug"},
|
|
308
|
+
)
|
|
244
309
|
subparsers = parser.add_subparsers()
|
|
245
310
|
subparsers.required = True
|
|
246
311
|
|
|
@@ -258,11 +323,13 @@ def __main__(cli_args: Optional[List[str]] = None) -> None:
|
|
|
258
323
|
|
|
259
324
|
with Manager() as manager:
|
|
260
325
|
try:
|
|
261
|
-
manager.contexts.extend(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
326
|
+
manager.contexts.extend(
|
|
327
|
+
[
|
|
328
|
+
create_target_directories(),
|
|
329
|
+
normalize_action(),
|
|
330
|
+
normalize_dependencies(),
|
|
331
|
+
]
|
|
332
|
+
)
|
|
266
333
|
if args.module:
|
|
267
334
|
# Temporarily add the current working directory to the path.
|
|
268
335
|
try:
|
|
@@ -273,17 +340,23 @@ def __main__(cli_args: Optional[List[str]] = None) -> None:
|
|
|
273
340
|
elif args.recipe.is_file():
|
|
274
341
|
# Parse the recipe.
|
|
275
342
|
spec = importlib.util.spec_from_file_location("recipe", args.recipe)
|
|
343
|
+
assert spec, f"Could not load spec for '{args.recipe}'."
|
|
276
344
|
recipe = importlib.util.module_from_spec(spec)
|
|
345
|
+
assert spec.loader, f"Could not load recipe '{args.recipe}'."
|
|
277
346
|
spec.loader.exec_module(recipe)
|
|
278
347
|
else: # pragma: no cover
|
|
279
|
-
raise ValueError(
|
|
280
|
-
|
|
348
|
+
raise ValueError(
|
|
349
|
+
"recipe file or module must be specified; default recipe.py not "
|
|
350
|
+
"found"
|
|
351
|
+
)
|
|
281
352
|
except: # noqa: E722
|
|
282
353
|
LOGGER.fatal("failed to load recipe", exc_info=sys.exc_info())
|
|
283
354
|
sys.exit(1)
|
|
284
355
|
|
|
285
|
-
with
|
|
286
|
-
|
|
356
|
+
with closing(
|
|
357
|
+
sqlite3.connect(args.db, detect_types=sqlite3.PARSE_DECLTYPES)
|
|
358
|
+
) as connection:
|
|
359
|
+
_setup_schema(connection)
|
|
287
360
|
controller = Controller(manager.resolve_dependencies(), connection)
|
|
288
361
|
command: Command = args.command
|
|
289
362
|
try:
|
|
@@ -297,5 +370,15 @@ def __main__(cli_args: Optional[List[str]] = None) -> None:
|
|
|
297
370
|
sys.exit(1)
|
|
298
371
|
|
|
299
372
|
|
|
373
|
+
def _setup_schema(connection: sqlite3.Connection) -> None:
|
|
374
|
+
connection.executescript(QUERIES["schema"])
|
|
375
|
+
# Attempt to add the column which may not be present for cook versions <0.6.
|
|
376
|
+
try:
|
|
377
|
+
connection.execute("ALTER TABLE tasks ADD COLUMN last_started TIMESTAMP")
|
|
378
|
+
except sqlite3.OperationalError as ex:
|
|
379
|
+
if "duplicate column name" not in ex.args[0]:
|
|
380
|
+
raise # pragma: no cover
|
|
381
|
+
|
|
382
|
+
|
|
300
383
|
if __name__ == "__main__":
|
|
301
384
|
__main__()
|
cook/actions.py
CHANGED
|
@@ -30,13 +30,14 @@ action; its return value is ignored. For example, the following action waits for
|
|
|
30
30
|
>>> action.execute(None)
|
|
31
31
|
time: 0.1...
|
|
32
32
|
"""
|
|
33
|
+
|
|
33
34
|
import hashlib
|
|
34
35
|
import os
|
|
35
36
|
import shlex
|
|
36
37
|
import subprocess
|
|
37
38
|
import sys
|
|
38
39
|
from types import ModuleType
|
|
39
|
-
from typing import Callable,
|
|
40
|
+
from typing import Callable, TYPE_CHECKING
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
if TYPE_CHECKING:
|
|
@@ -48,14 +49,15 @@ class Action:
|
|
|
48
49
|
"""
|
|
49
50
|
Action to perform when a task is executed in its own thread.
|
|
50
51
|
"""
|
|
51
|
-
|
|
52
|
+
|
|
53
|
+
def execute(self, task: "Task", stop: "StopEvent | None" = None) -> None:
|
|
52
54
|
"""
|
|
53
55
|
Execute the action.
|
|
54
56
|
"""
|
|
55
57
|
raise NotImplementedError
|
|
56
58
|
|
|
57
59
|
@property
|
|
58
|
-
def hexdigest(self) ->
|
|
60
|
+
def hexdigest(self) -> str | None:
|
|
59
61
|
"""
|
|
60
62
|
Optional digest to check if an action changed.
|
|
61
63
|
"""
|
|
@@ -71,13 +73,14 @@ class FunctionAction(Action):
|
|
|
71
73
|
*args: Additional positional arguments.
|
|
72
74
|
**kwargs: Keyword arguments.
|
|
73
75
|
"""
|
|
76
|
+
|
|
74
77
|
def __init__(self, func: Callable, *args, **kwargs) -> None:
|
|
75
78
|
super().__init__()
|
|
76
79
|
self.func = func
|
|
77
80
|
self.args = args
|
|
78
81
|
self.kwargs = kwargs
|
|
79
82
|
|
|
80
|
-
def execute(self, task: "Task", stop:
|
|
83
|
+
def execute(self, task: "Task", stop: "StopEvent | None" = None) -> None:
|
|
81
84
|
self.func(task, *self.args, **self.kwargs)
|
|
82
85
|
|
|
83
86
|
|
|
@@ -101,11 +104,12 @@ class SubprocessAction(Action):
|
|
|
101
104
|
>>> Path("hello.txt").is_file()
|
|
102
105
|
True
|
|
103
106
|
"""
|
|
107
|
+
|
|
104
108
|
def __init__(self, *args, **kwargs) -> None:
|
|
105
109
|
self.args = args
|
|
106
110
|
self.kwargs = kwargs
|
|
107
111
|
|
|
108
|
-
def execute(self, task: "Task", stop:
|
|
112
|
+
def execute(self, task: "Task", stop: "StopEvent | None" = None) -> None:
|
|
109
113
|
# Repeatedly wait for the process to complete, checking the stop event after each poll.
|
|
110
114
|
interval = stop.interval if stop else None
|
|
111
115
|
process = subprocess.Popen(*self.args, **self.kwargs)
|
|
@@ -116,14 +120,14 @@ class SubprocessAction(Action):
|
|
|
116
120
|
raise subprocess.CalledProcessError(returncode, process.args)
|
|
117
121
|
return
|
|
118
122
|
except subprocess.TimeoutExpired:
|
|
119
|
-
if stop.is_set():
|
|
123
|
+
if stop and stop.is_set():
|
|
120
124
|
break
|
|
121
125
|
|
|
122
126
|
# Clean up the process by trying to terminate it and then killing it.
|
|
123
127
|
for method in [process.terminate, process.kill]:
|
|
124
128
|
method()
|
|
125
129
|
try:
|
|
126
|
-
returncode = process.wait(max(interval, 3))
|
|
130
|
+
returncode = process.wait(max(interval, 3) if interval else None)
|
|
127
131
|
if returncode:
|
|
128
132
|
raise subprocess.CalledProcessError(returncode, process.args)
|
|
129
133
|
# The process managed to exit gracefully after the main loop. This is unlikely.
|
|
@@ -132,12 +136,14 @@ class SubprocessAction(Action):
|
|
|
132
136
|
pass
|
|
133
137
|
|
|
134
138
|
# We couldn't kill the process. Also very unlikely.
|
|
135
|
-
raise subprocess.SubprocessError(
|
|
139
|
+
raise subprocess.SubprocessError(
|
|
140
|
+
f"failed to shut down {process}"
|
|
141
|
+
) # pragma: no cover
|
|
136
142
|
|
|
137
143
|
@property
|
|
138
144
|
def hexdigest(self) -> str:
|
|
139
145
|
hasher = hashlib.sha1()
|
|
140
|
-
args, = self.args
|
|
146
|
+
(args,) = self.args
|
|
141
147
|
if isinstance(args, str):
|
|
142
148
|
hasher.update(args.encode())
|
|
143
149
|
else:
|
|
@@ -148,7 +154,7 @@ class SubprocessAction(Action):
|
|
|
148
154
|
def __repr__(self) -> str:
|
|
149
155
|
args, *_ = self.args
|
|
150
156
|
if not isinstance(args, str):
|
|
151
|
-
args =
|
|
157
|
+
args = " ".join(map(shlex.quote, args))
|
|
152
158
|
return f"{self.__class__.__name__}({repr(args)})"
|
|
153
159
|
|
|
154
160
|
|
|
@@ -159,15 +165,16 @@ class CompositeAction(Action):
|
|
|
159
165
|
Args:
|
|
160
166
|
*actions: Actions to execute.
|
|
161
167
|
"""
|
|
168
|
+
|
|
162
169
|
def __init__(self, *actions: Action) -> None:
|
|
163
170
|
self.actions = actions
|
|
164
171
|
|
|
165
|
-
def execute(self, task: "Task", stop:
|
|
172
|
+
def execute(self, task: "Task", stop: "StopEvent | None" = None) -> None:
|
|
166
173
|
for action in self.actions:
|
|
167
174
|
action.execute(task, stop)
|
|
168
175
|
|
|
169
176
|
@property
|
|
170
|
-
def hexdigest(self) ->
|
|
177
|
+
def hexdigest(self) -> str | None:
|
|
171
178
|
parts = []
|
|
172
179
|
for action in self.actions:
|
|
173
180
|
hexdigest = action.hexdigest
|
|
@@ -188,7 +195,8 @@ class ModuleAction(SubprocessAction):
|
|
|
188
195
|
being set).
|
|
189
196
|
**kwargs: Keyword arguments for :class:`subprocess.Popen`.
|
|
190
197
|
"""
|
|
191
|
-
|
|
198
|
+
|
|
199
|
+
def __init__(self, args: list, debug: bool | None = None, **kwargs) -> None:
|
|
192
200
|
if kwargs.get("shell"):
|
|
193
201
|
raise ValueError("shell execution is not supported by `ModuleAction`")
|
|
194
202
|
if not args:
|
cook/contexts.py
CHANGED
|
@@ -29,10 +29,11 @@ Custom contexts can be implemented by inheriting from :class:`.Context` and impl
|
|
|
29
29
|
>>> create_task("bar")
|
|
30
30
|
<task `bar` @ ...>
|
|
31
31
|
"""
|
|
32
|
+
|
|
32
33
|
from __future__ import annotations
|
|
33
34
|
from pathlib import Path
|
|
34
35
|
from types import ModuleType
|
|
35
|
-
from typing import Callable,
|
|
36
|
+
from typing import Callable, TYPE_CHECKING
|
|
36
37
|
from . import actions
|
|
37
38
|
from . import manager as manager_
|
|
38
39
|
from . import task as task_
|
|
@@ -51,14 +52,15 @@ class Context:
|
|
|
51
52
|
Args:
|
|
52
53
|
manager: Manager to which the context is added.
|
|
53
54
|
"""
|
|
54
|
-
|
|
55
|
+
|
|
56
|
+
def __init__(self, manager: "Manager | None" = None) -> None:
|
|
55
57
|
self.manager = manager or manager_.Manager.get_instance()
|
|
56
58
|
|
|
57
59
|
def __enter__(self) -> Context:
|
|
58
60
|
self.manager.contexts.append(self)
|
|
59
61
|
return self
|
|
60
62
|
|
|
61
|
-
def __exit__(self,
|
|
63
|
+
def __exit__(self, ex_type, ex_value, ex_traceback) -> None:
|
|
62
64
|
if not self.manager.contexts:
|
|
63
65
|
raise RuntimeError("exiting failed: no active contexts")
|
|
64
66
|
if self.manager.contexts[-1] is not self:
|
|
@@ -109,8 +111,14 @@ class FunctionContext(Context):
|
|
|
109
111
|
... create_task("baz")
|
|
110
112
|
<task `bazbazbaz` @ ...>
|
|
111
113
|
"""
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
func: Callable[["Task"], Task],
|
|
118
|
+
*args,
|
|
119
|
+
manager: "Manager | None" = None,
|
|
120
|
+
**kwargs,
|
|
121
|
+
) -> None:
|
|
114
122
|
super().__init__(manager)
|
|
115
123
|
self.func = func
|
|
116
124
|
self.args = args or ()
|
|
@@ -128,6 +136,7 @@ class create_target_directories(Context):
|
|
|
128
136
|
|
|
129
137
|
This context is active by default.
|
|
130
138
|
"""
|
|
139
|
+
|
|
131
140
|
def apply(self, task: "Task") -> Task:
|
|
132
141
|
for target in task.targets:
|
|
133
142
|
name = f"_create_target_directories:{target.parent}"
|
|
@@ -137,9 +146,12 @@ class create_target_directories(Context):
|
|
|
137
146
|
# Create a task if necessary.
|
|
138
147
|
create = self.manager.tasks.get(name)
|
|
139
148
|
if create is None:
|
|
140
|
-
create = self.manager.create_task(
|
|
141
|
-
|
|
142
|
-
|
|
149
|
+
create = self.manager.create_task(
|
|
150
|
+
name,
|
|
151
|
+
action=actions.FunctionAction(
|
|
152
|
+
lambda _: target.parent.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
),
|
|
154
|
+
)
|
|
143
155
|
task.task_dependencies.append(create)
|
|
144
156
|
return task
|
|
145
157
|
|
|
@@ -162,12 +174,13 @@ class normalize_action(Context):
|
|
|
162
174
|
|
|
163
175
|
This context is active by default.
|
|
164
176
|
"""
|
|
177
|
+
|
|
165
178
|
def apply(self, task: "Task") -> "Task":
|
|
166
179
|
if isinstance(task.action, Callable):
|
|
167
180
|
task.action = actions.FunctionAction(task.action)
|
|
168
181
|
elif isinstance(task.action, str):
|
|
169
182
|
task.action = actions.SubprocessAction(task.action, shell=True)
|
|
170
|
-
elif isinstance(task.action,
|
|
183
|
+
elif isinstance(task.action, list):
|
|
171
184
|
if not task.action:
|
|
172
185
|
raise ValueError("action must not be an empty list")
|
|
173
186
|
if all(isinstance(x, actions.Action) for x in task.action):
|
|
@@ -193,6 +206,7 @@ class normalize_dependencies(Context):
|
|
|
193
206
|
|
|
194
207
|
This context is active by default.
|
|
195
208
|
"""
|
|
209
|
+
|
|
196
210
|
def apply(self, task: "Task") -> "Task":
|
|
197
211
|
# Move task and group dependencies to the task_dependencies if they appear in regular
|
|
198
212
|
# dependencies.
|
|
@@ -200,10 +214,10 @@ class normalize_dependencies(Context):
|
|
|
200
214
|
task_dependencies = task.task_dependencies
|
|
201
215
|
for dependency in task.dependencies:
|
|
202
216
|
if isinstance(dependency, (task_.Task, create_group)):
|
|
203
|
-
task_dependencies.append(dependency)
|
|
217
|
+
task_dependencies.append(dependency) # pyright: ignore[reportArgumentType]
|
|
204
218
|
else:
|
|
205
219
|
dependencies.append(dependency)
|
|
206
|
-
task.dependencies = [Path(x) for x in dependencies]
|
|
220
|
+
task.dependencies = [Path(x) for x in dependencies] # pyright: ignore[reportAttributeAccessIssue]
|
|
207
221
|
|
|
208
222
|
# Unpack group dependencies and look up tasks by name.
|
|
209
223
|
task_dependencies = []
|
|
@@ -244,11 +258,16 @@ class create_group(Context):
|
|
|
244
258
|
>>> my_group
|
|
245
259
|
<group `my_group` @ ... with 2 tasks>
|
|
246
260
|
"""
|
|
247
|
-
|
|
248
|
-
|
|
261
|
+
|
|
262
|
+
def __init__(
|
|
263
|
+
self,
|
|
264
|
+
name: str,
|
|
265
|
+
manager: "Manager | None" = None,
|
|
266
|
+
location: tuple[str, int] | None = None,
|
|
267
|
+
) -> None:
|
|
249
268
|
super().__init__(manager)
|
|
250
269
|
self.name = name
|
|
251
|
-
self.task:
|
|
270
|
+
self.task: task_.Task | None = None
|
|
252
271
|
self.location = location or util.get_location()
|
|
253
272
|
|
|
254
273
|
def apply(self, task: "Task") -> "Task":
|