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.
Files changed (28) hide show
  1. {cook_build-0.6.4/src/cook_build.egg-info → cook_build-0.7.0}/PKG-INFO +1 -1
  2. {cook_build-0.6.4 → cook_build-0.7.0}/pyproject.toml +2 -1
  3. {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/__init__.py +1 -2
  4. {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/__main__.py +10 -7
  5. {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/actions.py +71 -50
  6. {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/contexts.py +5 -5
  7. {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/controller.py +118 -214
  8. {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/manager.py +12 -9
  9. {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/task.py +30 -7
  10. {cook_build-0.6.4 → cook_build-0.7.0}/src/cook/util.py +2 -13
  11. {cook_build-0.6.4 → cook_build-0.7.0/src/cook_build.egg-info}/PKG-INFO +1 -1
  12. cook_build-0.7.0/tests/test_actions.py +169 -0
  13. {cook_build-0.6.4 → cook_build-0.7.0}/tests/test_contexts.py +14 -10
  14. {cook_build-0.6.4 → cook_build-0.7.0}/tests/test_controller.py +116 -70
  15. {cook_build-0.6.4 → cook_build-0.7.0}/tests/test_examples.py +4 -2
  16. {cook_build-0.6.4 → cook_build-0.7.0}/tests/test_main.py +5 -4
  17. {cook_build-0.6.4 → cook_build-0.7.0}/tests/test_manager.py +19 -3
  18. {cook_build-0.6.4 → cook_build-0.7.0}/tests/test_util.py +3 -2
  19. cook_build-0.6.4/tests/test_actions.py +0 -104
  20. {cook_build-0.6.4 → cook_build-0.7.0}/LICENSE +0 -0
  21. {cook_build-0.6.4 → cook_build-0.7.0}/README.md +0 -0
  22. {cook_build-0.6.4 → cook_build-0.7.0}/README.rst +0 -0
  23. {cook_build-0.6.4 → cook_build-0.7.0}/setup.cfg +0 -0
  24. {cook_build-0.6.4 → cook_build-0.7.0}/src/cook_build.egg-info/SOURCES.txt +0 -0
  25. {cook_build-0.6.4 → cook_build-0.7.0}/src/cook_build.egg-info/dependency_links.txt +0 -0
  26. {cook_build-0.6.4 → cook_build-0.7.0}/src/cook_build.egg-info/entry_points.txt +0 -0
  27. {cook_build-0.6.4 → cook_build-0.7.0}/src/cook_build.egg-info/requires.txt +0 -0
  28. {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
  Metadata-Version: 2.4
2
2
  Name: cook-build
3
- Version: 0.6.4
3
+ Version: 0.7.0
4
4
  Summary: A task-centric build system with simple declarative recipes specified in Python
5
5
  Author: Till Hoffmann
6
6
  License: BSD-3-Clause
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cook-build"
3
- version = "0.6.4"
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,8 +1,7 @@
1
1
  from .controller import Controller
2
- from .manager import create_task, Manager
2
+ from .manager import Manager, create_task
3
3
  from .task import Task
4
4
 
5
-
6
5
  __all__ = [
7
6
  "Controller",
8
7
  "create_task",
@@ -1,29 +1,30 @@
1
1
  import argparse
2
- import colorama
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 Controller, QUERIES
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
- controller.execute(tasks, num_concurrent=args.jobs, dry_run=args.dry_run)
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 execute the
12
- action; its return value is ignored. For example, the following action waits for a specified time.
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
- >>> from time import sleep, time
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 Callable, TYPE_CHECKING
41
-
46
+ from typing import TYPE_CHECKING, Callable
42
47
 
43
48
  if TYPE_CHECKING:
44
49
  from .task import Task
45
- from .util import StopEvent
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 in its own thread.
57
+ Action to perform when a task is executed.
51
58
  """
52
59
 
53
- def execute(self, task: "Task", stop: "StopEvent | None" = None) -> None:
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", stop: "StopEvent | None" = None) -> None:
84
- self.func(task, *self.args, **self.kwargs)
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 :class:`subprocess.Popen`.
93
- **kwargs: Keyword arguments for :class:`subprocess.Popen`.
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", stop: "StopEvent | None" = None) -> None:
113
- # Repeatedly wait for the process to complete, checking the stop event after each poll.
114
- interval = stop.interval if stop else None
115
- process = subprocess.Popen(*self.args, **self.kwargs)
116
- while True:
117
- try:
118
- returncode = process.wait(interval)
119
- if returncode:
120
- raise subprocess.CalledProcessError(returncode, process.args)
121
- return
122
- except subprocess.TimeoutExpired:
123
- if stop and stop.is_set():
124
- break
125
-
126
- # Clean up the process by trying to terminate it and then killing it.
127
- for method in [process.terminate, process.kill]:
128
- method()
129
- try:
130
- returncode = process.wait(max(interval, 3) if interval else None)
131
- if returncode:
132
- raise subprocess.CalledProcessError(returncode, process.args)
133
- # The process managed to exit gracefully after the main loop. This is unlikely.
134
- return # pragma: no cover
135
- except subprocess.TimeoutExpired: # pragma: no cover
136
- pass
137
-
138
- # We couldn't kill the process. Also very unlikely.
139
- raise subprocess.SubprocessError(
140
- f"failed to shut down {process}"
141
- ) # pragma: no cover
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", stop: "StopEvent | None" = None) -> None:
193
+ async def execute(self, task: "Task") -> None:
173
194
  for action in self.actions:
174
- action.execute(task, stop)
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 Callable, TYPE_CHECKING, TypeVar
37
- import warnings
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