cook-build 0.6.4__tar.gz → 0.7.0__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.4/src/cook_build.egg-info → cook_build-0.7.0}/PKG-INFO +1 -1
- {cook_build-0.6.4 → cook_build-0.7.0}/pyproject.toml +2 -1
- {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/__init__.py +1 -2
- {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/__main__.py +10 -7
- {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/actions.py +71 -50
- {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/contexts.py +5 -5
- {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/controller.py +118 -214
- {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/manager.py +12 -9
- {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/task.py +30 -7
- {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/util.py +2 -13
- {cook_build-0.6.4 → cook_build-0.7.0/src/cook_build.egg-info}/PKG-INFO +1 -1
- cook_build-0.7.0/tests/test_actions.py +169 -0
- {cook_build-0.6.4 → cook_build-0.7.0}/tests/test_contexts.py +14 -10
- {cook_build-0.6.4 → cook_build-0.7.0}/tests/test_controller.py +116 -70
- {cook_build-0.6.4 → cook_build-0.7.0}/tests/test_examples.py +4 -2
- {cook_build-0.6.4 → cook_build-0.7.0}/tests/test_main.py +5 -4
- {cook_build-0.6.4 → cook_build-0.7.0}/tests/test_manager.py +19 -3
- {cook_build-0.6.4 → cook_build-0.7.0}/tests/test_util.py +3 -2
- cook_build-0.6.4/tests/test_actions.py +0 -104
- {cook_build-0.6.4 → cook_build-0.7.0}/LICENSE +0 -0
- {cook_build-0.6.4 → cook_build-0.7.0}/README.md +0 -0
- {cook_build-0.6.4 → cook_build-0.7.0}/README.rst +0 -0
- {cook_build-0.6.4 → cook_build-0.7.0}/setup.cfg +0 -0
- {cook_build-0.6.4 → cook_build-0.7.0}/src/cook_build.egg-info/SOURCES.txt +0 -0
- {cook_build-0.6.4 → cook_build-0.7.0}/src/cook_build.egg-info/dependency_links.txt +0 -0
- {cook_build-0.6.4 → cook_build-0.7.0}/src/cook_build.egg-info/entry_points.txt +0 -0
- {cook_build-0.6.4 → cook_build-0.7.0}/src/cook_build.egg-info/requires.txt +0 -0
- {cook_build-0.6.4 → cook_build-0.7.0}/src/cook_build.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cook-build"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "A task-centric build system with simple declarative recipes specified in Python"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "BSD-3-Clause"}
|
|
@@ -39,6 +39,7 @@ dev = [
|
|
|
39
39
|
"furo>=2025.9.25",
|
|
40
40
|
"pyright>=1.1.406",
|
|
41
41
|
"pytest>=8.4.2",
|
|
42
|
+
"pytest-asyncio>=0.24.0",
|
|
42
43
|
"pytest-cov>=7.0.0",
|
|
43
44
|
"ruff>=0.13.3",
|
|
44
45
|
"sphinx>=7.4.7",
|
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
import
|
|
3
|
-
from contextlib import closing
|
|
4
|
-
from datetime import datetime
|
|
2
|
+
import asyncio
|
|
5
3
|
import fnmatch
|
|
6
4
|
import importlib.util
|
|
7
5
|
import logging
|
|
8
6
|
import os
|
|
9
|
-
from pathlib import Path
|
|
10
7
|
import re
|
|
11
8
|
import sqlite3
|
|
12
9
|
import sys
|
|
13
10
|
import textwrap
|
|
11
|
+
from contextlib import closing
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
14
|
from typing import Iterable
|
|
15
15
|
|
|
16
|
+
import colorama
|
|
17
|
+
|
|
16
18
|
from .contexts import (
|
|
17
19
|
create_target_directories,
|
|
18
20
|
normalize_action,
|
|
19
21
|
normalize_dependencies,
|
|
20
22
|
)
|
|
21
|
-
from .controller import
|
|
23
|
+
from .controller import QUERIES, Controller
|
|
22
24
|
from .manager import Manager
|
|
23
25
|
from .task import Task
|
|
24
26
|
from .util import FailedTaskError, format_datetime, format_timedelta
|
|
25
27
|
|
|
26
|
-
|
|
27
28
|
LOGGER = logging.getLogger("cook")
|
|
28
29
|
|
|
29
30
|
|
|
@@ -147,7 +148,9 @@ class ExecCommand(Command):
|
|
|
147
148
|
|
|
148
149
|
def execute(self, controller: Controller, args: ExecArgs) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
149
150
|
tasks = self.discover_tasks(controller, args)
|
|
150
|
-
|
|
151
|
+
asyncio.run(
|
|
152
|
+
controller.execute(tasks, num_concurrent=args.jobs, dry_run=args.dry_run)
|
|
153
|
+
)
|
|
151
154
|
|
|
152
155
|
|
|
153
156
|
class LsArgs(Args):
|
|
@@ -8,51 +8,58 @@ multiple actions using :class:`.CompositeAction`, and executing modules as scrip
|
|
|
8
8
|
:class:`.ModuleAction`.
|
|
9
9
|
|
|
10
10
|
Custom actions can be implemented by inheriting from :class:`.Action` and implementing the
|
|
11
|
-
:meth:`~.Action.execute` method which receives a :class:`~.task.Task`. The method should
|
|
12
|
-
|
|
11
|
+
:meth:`~.Action.execute` method which receives a :class:`~.task.Task`. The method should be async
|
|
12
|
+
and its return value is ignored. For example, the following action waits for a specified time.
|
|
13
13
|
|
|
14
14
|
.. doctest::
|
|
15
15
|
|
|
16
16
|
>>> from cook.actions import Action
|
|
17
17
|
>>> from cook.task import Task
|
|
18
|
-
>>>
|
|
18
|
+
>>> import asyncio
|
|
19
|
+
>>> from time import time
|
|
19
20
|
|
|
20
21
|
>>> class SleepAction(Action):
|
|
21
22
|
... def __init__(self, delay: float) -> None:
|
|
22
23
|
... self.delay = delay
|
|
23
24
|
...
|
|
24
|
-
... def execute(self, task: Task) -> None:
|
|
25
|
+
... async def execute(self, task: Task) -> None:
|
|
25
26
|
... start = time()
|
|
26
|
-
... sleep(self.delay)
|
|
27
|
+
... await asyncio.sleep(self.delay)
|
|
27
28
|
... print(f"time: {time() - start:.3f}")
|
|
28
29
|
|
|
29
30
|
>>> action = SleepAction(0.1)
|
|
30
|
-
>>> action.execute(None)
|
|
31
|
+
>>> asyncio.run(action.execute(None))
|
|
31
32
|
time: 0.1...
|
|
33
|
+
|
|
34
|
+
For backwards compatibility, synchronous execute methods are also supported but will run in an
|
|
35
|
+
executor with a deprecation warning.
|
|
32
36
|
"""
|
|
33
37
|
|
|
38
|
+
import asyncio
|
|
34
39
|
import hashlib
|
|
40
|
+
import logging
|
|
35
41
|
import os
|
|
36
42
|
import shlex
|
|
37
43
|
import subprocess
|
|
38
44
|
import sys
|
|
39
45
|
from types import ModuleType
|
|
40
|
-
from typing import
|
|
41
|
-
|
|
46
|
+
from typing import TYPE_CHECKING, Callable
|
|
42
47
|
|
|
43
48
|
if TYPE_CHECKING:
|
|
44
49
|
from .task import Task
|
|
45
|
-
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
LOGGER = logging.getLogger(__name__)
|
|
46
53
|
|
|
47
54
|
|
|
48
55
|
class Action:
|
|
49
56
|
"""
|
|
50
|
-
Action to perform when a task is executed
|
|
57
|
+
Action to perform when a task is executed.
|
|
51
58
|
"""
|
|
52
59
|
|
|
53
|
-
def execute(self, task: "Task"
|
|
60
|
+
async def execute(self, task: "Task") -> None:
|
|
54
61
|
"""
|
|
55
|
-
Execute the action.
|
|
62
|
+
Execute the action asynchronously.
|
|
56
63
|
"""
|
|
57
64
|
raise NotImplementedError
|
|
58
65
|
|
|
@@ -80,17 +87,23 @@ class FunctionAction(Action):
|
|
|
80
87
|
self.args = args
|
|
81
88
|
self.kwargs = kwargs
|
|
82
89
|
|
|
83
|
-
def execute(self, task: "Task"
|
|
84
|
-
|
|
90
|
+
async def execute(self, task: "Task") -> None:
|
|
91
|
+
# Check if the function is already async
|
|
92
|
+
if asyncio.iscoroutinefunction(self.func):
|
|
93
|
+
await self.func(task, *self.args, **self.kwargs)
|
|
94
|
+
else:
|
|
95
|
+
# Run sync function in executor
|
|
96
|
+
loop = asyncio.get_running_loop()
|
|
97
|
+
await loop.run_in_executor(None, self.func, task, *self.args, **self.kwargs)
|
|
85
98
|
|
|
86
99
|
|
|
87
100
|
class SubprocessAction(Action):
|
|
88
101
|
"""
|
|
89
|
-
Run a subprocess.
|
|
102
|
+
Run a subprocess asynchronously.
|
|
90
103
|
|
|
91
104
|
Args:
|
|
92
|
-
*args: Positional arguments for
|
|
93
|
-
**kwargs: Keyword arguments for
|
|
105
|
+
*args: Positional arguments for subprocess execution.
|
|
106
|
+
**kwargs: Keyword arguments for subprocess execution.
|
|
94
107
|
|
|
95
108
|
Example:
|
|
96
109
|
|
|
@@ -98,47 +111,55 @@ class SubprocessAction(Action):
|
|
|
98
111
|
|
|
99
112
|
>>> from cook.actions import SubprocessAction
|
|
100
113
|
>>> from pathlib import Path
|
|
114
|
+
>>> import asyncio
|
|
101
115
|
|
|
102
116
|
>>> action = SubprocessAction(["touch", "hello.txt"])
|
|
103
|
-
>>> action.execute(None)
|
|
117
|
+
>>> asyncio.run(action.execute(None))
|
|
104
118
|
>>> Path("hello.txt").is_file()
|
|
105
119
|
True
|
|
106
120
|
"""
|
|
107
121
|
|
|
108
122
|
def __init__(self, *args, **kwargs) -> None:
|
|
123
|
+
# Validate shell argument early
|
|
124
|
+
if kwargs.get("shell", False) and args and not isinstance(args[0], str):
|
|
125
|
+
raise ValueError("shell=True requires string args")
|
|
109
126
|
self.args = args
|
|
110
127
|
self.kwargs = kwargs
|
|
111
128
|
|
|
112
|
-
def execute(self, task: "Task"
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
129
|
+
async def execute(self, task: "Task") -> None:
|
|
130
|
+
# Get the command arguments
|
|
131
|
+
(args,) = self.args
|
|
132
|
+
shell = self.kwargs.get("shell", False)
|
|
133
|
+
other_kwargs = {k: v for k, v in self.kwargs.items() if k != "shell"}
|
|
134
|
+
|
|
135
|
+
# Create the subprocess
|
|
136
|
+
if shell:
|
|
137
|
+
process = await asyncio.create_subprocess_shell(args, **other_kwargs)
|
|
138
|
+
else:
|
|
139
|
+
# Exec mode: args can be a string (single command) or list
|
|
140
|
+
if isinstance(args, str):
|
|
141
|
+
# Single command string - treat as program name with no arguments
|
|
142
|
+
process = await asyncio.create_subprocess_exec(args, **other_kwargs)
|
|
143
|
+
else:
|
|
144
|
+
# List of arguments
|
|
145
|
+
process = await asyncio.create_subprocess_exec(*args, **other_kwargs)
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
# Wait for the process to complete
|
|
149
|
+
returncode = await process.wait()
|
|
150
|
+
if returncode:
|
|
151
|
+
raise subprocess.CalledProcessError(returncode, args)
|
|
152
|
+
|
|
153
|
+
except asyncio.CancelledError:
|
|
154
|
+
# Task was cancelled - terminate the subprocess
|
|
155
|
+
if process.returncode is None:
|
|
156
|
+
process.terminate()
|
|
157
|
+
try:
|
|
158
|
+
await asyncio.wait_for(process.wait(), timeout=3)
|
|
159
|
+
except asyncio.TimeoutError:
|
|
160
|
+
process.kill()
|
|
161
|
+
await process.wait()
|
|
162
|
+
raise
|
|
142
163
|
|
|
143
164
|
@property
|
|
144
165
|
def hexdigest(self) -> str:
|
|
@@ -169,9 +190,9 @@ class CompositeAction(Action):
|
|
|
169
190
|
def __init__(self, *actions: Action) -> None:
|
|
170
191
|
self.actions = actions
|
|
171
192
|
|
|
172
|
-
def execute(self, task: "Task"
|
|
193
|
+
async def execute(self, task: "Task") -> None:
|
|
173
194
|
for action in self.actions:
|
|
174
|
-
action.execute(task
|
|
195
|
+
await action.execute(task)
|
|
175
196
|
|
|
176
197
|
@property
|
|
177
198
|
def hexdigest(self) -> str | None:
|
|
@@ -31,15 +31,15 @@ Custom contexts can be implemented by inheriting from :class:`.Context` and impl
|
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
33
|
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import warnings
|
|
34
36
|
from pathlib import Path
|
|
35
37
|
from types import ModuleType
|
|
36
|
-
from typing import
|
|
37
|
-
|
|
38
|
-
from . import actions
|
|
38
|
+
from typing import TYPE_CHECKING, Callable, TypeVar
|
|
39
|
+
|
|
40
|
+
from . import actions, util
|
|
39
41
|
from . import manager as manager_
|
|
40
42
|
from . import task as task_
|
|
41
|
-
from . import util
|
|
42
|
-
|
|
43
43
|
|
|
44
44
|
if TYPE_CHECKING:
|
|
45
45
|
from .manager import Manager
|