cook-build 0.6.4__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/__init__.py +11 -0
- cook/__main__.py +392 -0
- cook/actions.py +215 -0
- cook/contexts.py +307 -0
- cook/controller.py +473 -0
- cook/manager.py +168 -0
- cook/task.py +50 -0
- cook/util.py +110 -0
- cook_build-0.6.4.dist-info/METADATA +110 -0
- cook_build-0.6.4.dist-info/RECORD +14 -0
- cook_build-0.6.4.dist-info/WHEEL +5 -0
- cook_build-0.6.4.dist-info/entry_points.txt +2 -0
- cook_build-0.6.4.dist-info/licenses/LICENSE +28 -0
- cook_build-0.6.4.dist-info/top_level.txt +1 -0
cook/contexts.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Contexts
|
|
3
|
+
--------
|
|
4
|
+
|
|
5
|
+
:class:`.Context`\ s can modify the creation of :class:`~.task.Task`\ s and are activated using the
|
|
6
|
+
:code:`with` keyword. Builtin contexts handle the majority of Cook's task creation logic, such as
|
|
7
|
+
:class:`.normalize_dependencies` and :class:`.create_group`. The innermost context is applied first.
|
|
8
|
+
|
|
9
|
+
Custom contexts can be implemented by inheriting from :class:`.Context` and implementing the
|
|
10
|
+
:meth:`~.Context.apply` method which receives a :class:`~.task.Task` and must return a
|
|
11
|
+
:class:`~.task.Task`. For example, the following context converts all task names to uppercase.
|
|
12
|
+
|
|
13
|
+
.. doctest::
|
|
14
|
+
|
|
15
|
+
>>> from cook import create_task
|
|
16
|
+
>>> from cook.contexts import Context
|
|
17
|
+
|
|
18
|
+
>>> class UpperCaseContext(Context):
|
|
19
|
+
... def apply(self, task):
|
|
20
|
+
... task.name = task.name.upper()
|
|
21
|
+
... return task
|
|
22
|
+
|
|
23
|
+
>>> # Using the contexts yields uppercase task names.
|
|
24
|
+
>>> with UpperCaseContext():
|
|
25
|
+
... create_task("foo")
|
|
26
|
+
<task `FOO` @ ...>
|
|
27
|
+
|
|
28
|
+
>>> # create_task behaves as usual after the context is exited.
|
|
29
|
+
>>> create_task("bar")
|
|
30
|
+
<task `bar` @ ...>
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from types import ModuleType
|
|
36
|
+
from typing import Callable, TYPE_CHECKING, TypeVar
|
|
37
|
+
import warnings
|
|
38
|
+
from . import actions
|
|
39
|
+
from . import manager as manager_
|
|
40
|
+
from . import task as task_
|
|
41
|
+
from . import util
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from .manager import Manager
|
|
46
|
+
from .task import Task
|
|
47
|
+
|
|
48
|
+
ContextT = TypeVar("ContextT", bound="Context")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Context:
|
|
52
|
+
"""
|
|
53
|
+
Context that is applied to tasks when they are created.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
manager: Manager to which the context is added.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, manager: "Manager | None" = None) -> None:
|
|
60
|
+
self.manager = manager or manager_.Manager.get_instance()
|
|
61
|
+
|
|
62
|
+
def __enter__(self: ContextT) -> ContextT:
|
|
63
|
+
self.manager.contexts.append(self)
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def __exit__(self, ex_type, ex_value, ex_traceback) -> None:
|
|
67
|
+
if not self.manager.contexts:
|
|
68
|
+
raise RuntimeError("exiting failed: no active contexts")
|
|
69
|
+
if self.manager.contexts[-1] is not self:
|
|
70
|
+
raise RuntimeError("exiting failed: unexpected context")
|
|
71
|
+
self.manager.contexts.pop()
|
|
72
|
+
|
|
73
|
+
def apply(self, task: "Task") -> "Task":
|
|
74
|
+
"""
|
|
75
|
+
Apply the context to a task.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
task: Task to modify.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Modified task.
|
|
82
|
+
"""
|
|
83
|
+
raise NotImplementedError
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class FunctionContext(Context):
|
|
87
|
+
"""
|
|
88
|
+
Context wrapping a function to modify or replace a task.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
func: Function to call which must accept a :class:`~.task.Task` as its first argument and
|
|
92
|
+
return a :class:`~.task.Task`.
|
|
93
|
+
*args: Additional positional arguments.
|
|
94
|
+
**kwargs: Keyword arguments.
|
|
95
|
+
|
|
96
|
+
.. note::
|
|
97
|
+
|
|
98
|
+
:code:`manager` is a reserved keyword for the constructor of all contexts and cannot be used
|
|
99
|
+
as a keyword argument for the wrapped function.
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
|
|
103
|
+
.. doctest::
|
|
104
|
+
|
|
105
|
+
>>> from cook import create_task
|
|
106
|
+
>>> from cook.contexts import FunctionContext
|
|
107
|
+
>>> from cook.task import Task
|
|
108
|
+
|
|
109
|
+
>>> def repeat(task: Task, n: int) -> Task:
|
|
110
|
+
... task.name = task.name * n
|
|
111
|
+
... return task
|
|
112
|
+
|
|
113
|
+
>>> with FunctionContext(repeat, 3):
|
|
114
|
+
... create_task("baz")
|
|
115
|
+
<task `bazbazbaz` @ ...>
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
func: Callable[["Task"], Task],
|
|
121
|
+
*args,
|
|
122
|
+
manager: "Manager | None" = None,
|
|
123
|
+
**kwargs,
|
|
124
|
+
) -> None:
|
|
125
|
+
super().__init__(manager)
|
|
126
|
+
self.func = func
|
|
127
|
+
self.args = args or ()
|
|
128
|
+
self.kwargs = kwargs or {}
|
|
129
|
+
|
|
130
|
+
def apply(self, task: "Task") -> "Task":
|
|
131
|
+
return self.func(task, *self.args, **self.kwargs)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class create_target_directories(Context):
|
|
135
|
+
"""
|
|
136
|
+
Create parent directories for all targets before the task is executed.
|
|
137
|
+
|
|
138
|
+
.. note::
|
|
139
|
+
|
|
140
|
+
This context is active by default.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def apply(self, task: "Task") -> Task:
|
|
144
|
+
for target in task.targets:
|
|
145
|
+
name = f"_create_target_directories:{target.parent}"
|
|
146
|
+
# No need to create a task if the parent already exists.
|
|
147
|
+
if target.parent.is_dir():
|
|
148
|
+
continue
|
|
149
|
+
# Create a task if necessary.
|
|
150
|
+
create = self.manager.tasks.get(name)
|
|
151
|
+
if create is None:
|
|
152
|
+
create = self.manager.create_task(
|
|
153
|
+
name,
|
|
154
|
+
action=actions.FunctionAction(
|
|
155
|
+
lambda _: target.parent.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
task.task_dependencies.append(create)
|
|
159
|
+
return task
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class normalize_action(Context):
|
|
163
|
+
"""
|
|
164
|
+
Normalize actions of tasks.
|
|
165
|
+
|
|
166
|
+
- If the action is a callable, it will be wrapped in a :class:`~.actions.FunctionAction`.
|
|
167
|
+
- If the action is a string, it will be executed using a :class:`~.actions.SubprocessAction`
|
|
168
|
+
with :code:`shell = True`.
|
|
169
|
+
- If the action is a list of actions, a :class:`~.actions.CompositeAction` will be created.
|
|
170
|
+
- If the action is a list and the first element is a module, a :class:`~.actions.ModuleAction`
|
|
171
|
+
will be created. A subsequent elements ared passed to the module as strings on the command
|
|
172
|
+
line.
|
|
173
|
+
- If the action is any other list, it will be executed using a
|
|
174
|
+
:class:`.actions.SubprocessAction` after converting elements to strings.
|
|
175
|
+
|
|
176
|
+
.. note::
|
|
177
|
+
|
|
178
|
+
This context is active by default.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def apply(self, task: "Task") -> "Task":
|
|
182
|
+
if isinstance(task.action, Callable):
|
|
183
|
+
task.action = actions.FunctionAction(task.action)
|
|
184
|
+
elif isinstance(task.action, str):
|
|
185
|
+
task.action = actions.SubprocessAction(task.action, shell=True)
|
|
186
|
+
elif isinstance(task.action, list):
|
|
187
|
+
if not task.action:
|
|
188
|
+
raise ValueError("action must not be an empty list")
|
|
189
|
+
if all(isinstance(x, actions.Action) for x in task.action):
|
|
190
|
+
task.action = actions.CompositeAction(*task.action)
|
|
191
|
+
elif isinstance(task.action[0], ModuleType):
|
|
192
|
+
task.action = actions.ModuleAction(task.action)
|
|
193
|
+
else:
|
|
194
|
+
task.action = actions.SubprocessAction(list(map(str, task.action)))
|
|
195
|
+
return task
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class normalize_dependencies(Context):
|
|
199
|
+
"""
|
|
200
|
+
Normalize dependencies of tasks.
|
|
201
|
+
|
|
202
|
+
- If a dependency is a string, it will be converted to a :class:`~pathlib.Path`.
|
|
203
|
+
- If a dependency is a :class:`~.task.Task` or :class:`.create_group`, it will be removed and
|
|
204
|
+
added to the :code:`task_dependencies` of the task.
|
|
205
|
+
- If a task dependency is a string, the corresponding task will be looked up by name.
|
|
206
|
+
- If a task dependency is a group, it will be replaced by the corresponding meta task.
|
|
207
|
+
|
|
208
|
+
.. note::
|
|
209
|
+
|
|
210
|
+
This context is active by default.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def apply(self, task: "Task") -> "Task":
|
|
214
|
+
# Move task and group dependencies to the task_dependencies if they appear in regular
|
|
215
|
+
# dependencies.
|
|
216
|
+
dependencies = []
|
|
217
|
+
task_dependencies = task.task_dependencies
|
|
218
|
+
for dependency in task.dependencies:
|
|
219
|
+
if isinstance(dependency, (task_.Task, create_group)):
|
|
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)
|
|
227
|
+
else:
|
|
228
|
+
dependencies.append(dependency)
|
|
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]
|
|
235
|
+
|
|
236
|
+
# Unpack group dependencies and look up tasks by name.
|
|
237
|
+
task_dependencies = []
|
|
238
|
+
for other in task.task_dependencies:
|
|
239
|
+
if isinstance(other, create_group):
|
|
240
|
+
other = other.task
|
|
241
|
+
elif isinstance(other, str):
|
|
242
|
+
other = self.manager.tasks[other]
|
|
243
|
+
task_dependencies.append(other)
|
|
244
|
+
task.task_dependencies = task_dependencies
|
|
245
|
+
|
|
246
|
+
return task
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class create_group(Context):
|
|
250
|
+
"""
|
|
251
|
+
Context for grouping tasks. A task with the same name will be created.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
name: Name of the group.
|
|
255
|
+
manager: Manager to which the context is added.
|
|
256
|
+
location: Location at which the group was created as a tuple :code:`(filename, lineno)`
|
|
257
|
+
(defaults to :func:`~.util.get_location`).
|
|
258
|
+
|
|
259
|
+
Example:
|
|
260
|
+
|
|
261
|
+
.. doctest::
|
|
262
|
+
|
|
263
|
+
>>> from cook import create_task
|
|
264
|
+
>>> from cook.contexts import create_group
|
|
265
|
+
|
|
266
|
+
>>> with create_group("my_group") as my_group:
|
|
267
|
+
... create_task("task1")
|
|
268
|
+
... create_task("task2")
|
|
269
|
+
<task `task1` @ ...>
|
|
270
|
+
<task `task2` @ ...>
|
|
271
|
+
|
|
272
|
+
>>> my_group
|
|
273
|
+
<group `my_group` @ ... with 2 tasks>
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
def __init__(
|
|
277
|
+
self,
|
|
278
|
+
name: str,
|
|
279
|
+
manager: "Manager | None" = None,
|
|
280
|
+
location: tuple[str, int] | None = None,
|
|
281
|
+
) -> None:
|
|
282
|
+
super().__init__(manager)
|
|
283
|
+
self.name = name
|
|
284
|
+
self.task: task_.Task | None = None
|
|
285
|
+
self.location = location or util.get_location()
|
|
286
|
+
|
|
287
|
+
def apply(self, task: "Task") -> "Task":
|
|
288
|
+
# Skip if we're creating the task for the group itself to avoid infinite recursion.
|
|
289
|
+
if task.name == self.name:
|
|
290
|
+
return task
|
|
291
|
+
if self.task is None:
|
|
292
|
+
self.task = self.manager.create_task(self.name)
|
|
293
|
+
self.task.task_dependencies.append(task)
|
|
294
|
+
return task
|
|
295
|
+
|
|
296
|
+
def __exit__(self, ex_type, ex_value, ex_traceback) -> None:
|
|
297
|
+
super().__exit__(ex_type, ex_value, ex_traceback)
|
|
298
|
+
# Raise an error if the group was successfully created but no tasks were added.
|
|
299
|
+
has_dependents = self.task and self.task.task_dependencies
|
|
300
|
+
if not has_dependents and not ex_value:
|
|
301
|
+
raise RuntimeError(f"group `{self.name}` has no tasks")
|
|
302
|
+
|
|
303
|
+
def __repr__(self) -> str:
|
|
304
|
+
filename, lineno = self.location
|
|
305
|
+
num_tasks = len(self.task.task_dependencies) if self.task else 0
|
|
306
|
+
desc = "task" if num_tasks == 1 else "tasks"
|
|
307
|
+
return f"<group `{self.name}` @ {filename}:{lineno} with {num_tasks} {desc}>"
|