cook-build 0.5.1__tar.gz → 0.6.1__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 (33) hide show
  1. cook_build-0.6.1/PKG-INFO +91 -0
  2. cook_build-0.6.1/README.md +81 -0
  3. {cook-build-0.5.1 → cook_build-0.6.1}/README.rst +6 -6
  4. cook_build-0.6.1/pyproject.toml +25 -0
  5. cook_build-0.6.1/setup.cfg +4 -0
  6. {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/__main__.py +146 -63
  7. {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/actions.py +21 -13
  8. {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/contexts.py +33 -14
  9. {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/controller.py +157 -56
  10. {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/manager.py +41 -22
  11. {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/task.py +13 -13
  12. {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/util.py +14 -9
  13. cook_build-0.6.1/src/cook_build.egg-info/PKG-INFO +91 -0
  14. cook_build-0.6.1/src/cook_build.egg-info/SOURCES.txt +25 -0
  15. cook_build-0.6.1/src/cook_build.egg-info/requires.txt +2 -0
  16. {cook-build-0.5.1 → cook_build-0.6.1}/tests/test_contexts.py +45 -16
  17. {cook-build-0.5.1 → cook_build-0.6.1}/tests/test_controller.py +109 -37
  18. {cook-build-0.5.1 → cook_build-0.6.1}/tests/test_examples.py +6 -3
  19. {cook-build-0.5.1 → cook_build-0.6.1}/tests/test_main.py +27 -17
  20. {cook-build-0.5.1 → cook_build-0.6.1}/tests/test_manager.py +16 -7
  21. {cook-build-0.5.1 → cook_build-0.6.1}/tests/test_util.py +7 -2
  22. cook-build-0.5.1/PKG-INFO +0 -91
  23. cook-build-0.5.1/cook_build.egg-info/PKG-INFO +0 -91
  24. cook-build-0.5.1/cook_build.egg-info/SOURCES.txt +0 -25
  25. cook-build-0.5.1/cook_build.egg-info/requires.txt +0 -2
  26. cook-build-0.5.1/setup.cfg +0 -18
  27. cook-build-0.5.1/setup.py +0 -31
  28. {cook-build-0.5.1 → cook_build-0.6.1}/LICENSE +0 -0
  29. {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/__init__.py +0 -0
  30. {cook-build-0.5.1 → cook_build-0.6.1/src}/cook_build.egg-info/dependency_links.txt +0 -0
  31. {cook-build-0.5.1 → cook_build-0.6.1/src}/cook_build.egg-info/entry_points.txt +0 -0
  32. {cook-build-0.5.1 → cook_build-0.6.1/src}/cook_build.egg-info/top_level.txt +0 -0
  33. {cook-build-0.5.1 → cook_build-0.6.1}/tests/test_actions.py +0 -0
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: cook-build
3
+ Version: 0.6.1
4
+ Requires-Python: >=3.10
5
+ Description-Content-Type: text/markdown
6
+ License-File: LICENSE
7
+ Requires-Dist: colorama>=0.4.6
8
+ Requires-Dist: networkx>=3.2.1
9
+ Dynamic: license-file
10
+
11
+ # 🧑‍🍳 Cook [![](https://github.com/tillahoffmann/cook-build/actions/workflows/main.yaml/badge.svg)](https://github.com/tillahoffmann/cook-build/actions/workflows/main.yaml) [![](https://img.shields.io/pypi/v/cook-build)](https://pypi.org/project/cook-build)
12
+
13
+ Cook is a task-centric build system with simple declarative recipes specified in Python.
14
+
15
+ ## Getting Started
16
+
17
+ Tasks are declared in a `recipe.py` file using the `cook.manager.create_task` function. Each task must have a unique name, may depend on files or other tasks, and can execute an action, typically a shell command. The simple example below creates a C source file, compiles it, and executes the binary.
18
+
19
+ ```python
20
+ >>> from cook import create_task
21
+
22
+ >>> create_task("src", targets=["hello.c"],
23
+ ... action="echo 'int main() { return 0; }' > hello.c")
24
+ >>> create_task("cc", dependencies=["hello.c"], targets=["hello"],
25
+ ... action="cc -o hello hello.c")
26
+ >>> create_task("hello", dependencies=["hello"], action="./hello")
27
+ ```
28
+
29
+ Running `cook ls` from the command line lists all known tasks, e.g.,
30
+
31
+ ```bash
32
+ $ cook ls
33
+ <task `src` @ /.../recipe.py:3>
34
+ <task `cc` @ /.../recipe.py:6>
35
+ <task `hello` @ /.../recipe.py:9>
36
+ ```
37
+
38
+ Running `cook exec hello` creates the source file, compiles it, and executes the binary (using `--log-level=debug` can provide additional information).
39
+
40
+ ```bash
41
+ $ cook exec hello
42
+ INFO: executing <task `src` @ /.../recipe.py:3> ...
43
+ INFO: completed <task `src` @ /.../recipe.py:3> in 0:00:...
44
+ INFO: executing <task `cc` @ /.../recipe.py:6> ...
45
+ INFO: completed <task `cc` @ /.../recipe.py:6> in 0:00:...
46
+ INFO: executing <task `hello` @ /.../recipe.py:9> ...
47
+ INFO: completed <task `hello` @ /.../recipe.py:9> in 0:00:...
48
+ ```
49
+
50
+ To rerun a task, tell Cook to reset it.
51
+
52
+ ```bash
53
+ $ cook reset cc
54
+ INFO: reset 1 task
55
+ ```
56
+
57
+ The full set of available commands can be explored using `cook --help` as shown below.
58
+
59
+ ```bash
60
+ $ cook --help
61
+ usage: cook [-h] [--recipe RECIPE] [--module MODULE] [--db DB]
62
+ [--log-level {warning,error,info,debug}]
63
+ {exec,ls,info,reset} ...
64
+
65
+ positional arguments:
66
+ {exec,ls,info,reset}
67
+ exec Execute one or more tasks.
68
+ ls List tasks.
69
+ info Display information about one or more tasks.
70
+ reset Reset the status of one or more tasks.
71
+
72
+ options:
73
+ -h, --help show this help message and exit
74
+ --recipe RECIPE file containing declarative recipe for tasks
75
+ --module, -m MODULE module containing declarative recipe for tasks
76
+ --db DB database for keeping track of assets
77
+ --log-level {warning,error,info,debug}
78
+ log level
79
+ ```
80
+
81
+ ## Tasks Are Dumb; Contexts Are Smart
82
+
83
+ `cook.task.Task`s do not provide any functionality beyond storing metadata, including
84
+
85
+ - `targets`, the files generated by the task,
86
+ - `dependencies`, the files the task depends on,
87
+ - `action`, the `cook.actions.Action` to execute when the task is run,
88
+ - `task_dependencies`, other tasks that should be executed first,
89
+ - `location`, filename and line number where the task was defined.
90
+
91
+ All logic is handled by `cook.contexts.Context`s which are applied to each task when it is created. For example, `cook.contexts.create_group` adds all tasks created within the context to a group. This group can be executed to execute all child tasks.
@@ -0,0 +1,81 @@
1
+ # 🧑‍🍳 Cook [![](https://github.com/tillahoffmann/cook-build/actions/workflows/main.yaml/badge.svg)](https://github.com/tillahoffmann/cook-build/actions/workflows/main.yaml) [![](https://img.shields.io/pypi/v/cook-build)](https://pypi.org/project/cook-build)
2
+
3
+ Cook is a task-centric build system with simple declarative recipes specified in Python.
4
+
5
+ ## Getting Started
6
+
7
+ Tasks are declared in a `recipe.py` file using the `cook.manager.create_task` function. Each task must have a unique name, may depend on files or other tasks, and can execute an action, typically a shell command. The simple example below creates a C source file, compiles it, and executes the binary.
8
+
9
+ ```python
10
+ >>> from cook import create_task
11
+
12
+ >>> create_task("src", targets=["hello.c"],
13
+ ... action="echo 'int main() { return 0; }' > hello.c")
14
+ >>> create_task("cc", dependencies=["hello.c"], targets=["hello"],
15
+ ... action="cc -o hello hello.c")
16
+ >>> create_task("hello", dependencies=["hello"], action="./hello")
17
+ ```
18
+
19
+ Running `cook ls` from the command line lists all known tasks, e.g.,
20
+
21
+ ```bash
22
+ $ cook ls
23
+ <task `src` @ /.../recipe.py:3>
24
+ <task `cc` @ /.../recipe.py:6>
25
+ <task `hello` @ /.../recipe.py:9>
26
+ ```
27
+
28
+ Running `cook exec hello` creates the source file, compiles it, and executes the binary (using `--log-level=debug` can provide additional information).
29
+
30
+ ```bash
31
+ $ cook exec hello
32
+ INFO: executing <task `src` @ /.../recipe.py:3> ...
33
+ INFO: completed <task `src` @ /.../recipe.py:3> in 0:00:...
34
+ INFO: executing <task `cc` @ /.../recipe.py:6> ...
35
+ INFO: completed <task `cc` @ /.../recipe.py:6> in 0:00:...
36
+ INFO: executing <task `hello` @ /.../recipe.py:9> ...
37
+ INFO: completed <task `hello` @ /.../recipe.py:9> in 0:00:...
38
+ ```
39
+
40
+ To rerun a task, tell Cook to reset it.
41
+
42
+ ```bash
43
+ $ cook reset cc
44
+ INFO: reset 1 task
45
+ ```
46
+
47
+ The full set of available commands can be explored using `cook --help` as shown below.
48
+
49
+ ```bash
50
+ $ cook --help
51
+ usage: cook [-h] [--recipe RECIPE] [--module MODULE] [--db DB]
52
+ [--log-level {warning,error,info,debug}]
53
+ {exec,ls,info,reset} ...
54
+
55
+ positional arguments:
56
+ {exec,ls,info,reset}
57
+ exec Execute one or more tasks.
58
+ ls List tasks.
59
+ info Display information about one or more tasks.
60
+ reset Reset the status of one or more tasks.
61
+
62
+ options:
63
+ -h, --help show this help message and exit
64
+ --recipe RECIPE file containing declarative recipe for tasks
65
+ --module, -m MODULE module containing declarative recipe for tasks
66
+ --db DB database for keeping track of assets
67
+ --log-level {warning,error,info,debug}
68
+ log level
69
+ ```
70
+
71
+ ## Tasks Are Dumb; Contexts Are Smart
72
+
73
+ `cook.task.Task`s do not provide any functionality beyond storing metadata, including
74
+
75
+ - `targets`, the files generated by the task,
76
+ - `dependencies`, the files the task depends on,
77
+ - `action`, the `cook.actions.Action` to execute when the task is run,
78
+ - `task_dependencies`, other tasks that should be executed first,
79
+ - `location`, filename and line number where the task was defined.
80
+
81
+ All logic is handled by `cook.contexts.Context`s which are applied to each task when it is created. For example, `cook.contexts.create_group` adds all tasks created within the context to a group. This group can be executed to execute all child tasks.
@@ -30,8 +30,8 @@ Running :code:`cook ls` from the command line lists all known tasks, e.g.,
30
30
 
31
31
  $ cook ls
32
32
  <task `src` @ /.../recipe.py:3>
33
- <task `cc` @ /.../recipe.py:4>
34
- <task `hello` @ /.../recipe.py:5>
33
+ <task `cc` @ /.../recipe.py:6>
34
+ <task `hello` @ /.../recipe.py:9>
35
35
 
36
36
  Running :code:`cook exec hello` creates the source file, compiles it, and executes the binary (using :code:`--log-level=debug` can provide additional information).
37
37
 
@@ -42,10 +42,10 @@ Running :code:`cook exec hello` creates the source file, compiles it, and execut
42
42
  $ cook exec hello
43
43
  INFO: executing <task `src` @ /.../recipe.py:3> ...
44
44
  INFO: completed <task `src` @ /.../recipe.py:3> in 0:00:...
45
- INFO: executing <task `cc` @ /.../recipe.py:4> ...
46
- INFO: completed <task `cc` @ /.../recipe.py:4> in 0:00:...
47
- INFO: executing <task `hello` @ /.../recipe.py:5> ...
48
- INFO: completed <task `hello` @ /.../recipe.py:5> in 0:00:...
45
+ INFO: executing <task `cc` @ /.../recipe.py:6> ...
46
+ INFO: completed <task `cc` @ /.../recipe.py:6> in 0:00:...
47
+ INFO: executing <task `hello` @ /.../recipe.py:9> ...
48
+ INFO: completed <task `hello` @ /.../recipe.py:9> in 0:00:...
49
49
 
50
50
  To rerun a task, tell Cook to reset it.
51
51
 
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "cook-build"
3
+ version = "0.6.1"
4
+ readme = "README.md"
5
+ dependencies = [
6
+ "colorama>=0.4.6",
7
+ "networkx>=3.2.1",
8
+ ]
9
+ requires-python = ">=3.10"
10
+
11
+ [dependency-groups]
12
+ dev = [
13
+ "build>=1.3.0",
14
+ "furo>=2025.9.25",
15
+ "pyright>=1.1.406",
16
+ "pytest>=8.4.2",
17
+ "pytest-cov>=7.0.0",
18
+ "ruff>=0.13.3",
19
+ "sphinx>=7.4.7",
20
+ "sphinxcontrib-shtest>=0.5.0",
21
+ "twine>=6.2.0",
22
+ ]
23
+
24
+ [project.scripts]
25
+ cook = "cook.__main__:__main__"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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, List, Optional
14
+ from typing import Iterable
14
15
 
15
- from .contexts import create_target_directories, normalize_action, normalize_dependencies
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__(self, patterns: Iterable[re.Pattern], hidden_tasks_available: bool):
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
- patterns = [f"`{pattern}`" for pattern in patterns]
29
- if len(patterns) == 1:
30
- message = f"found no tasks matching pattern {patterns[0]}"
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
- *patterns, last = patterns
33
- message = "found no tasks matching patterns " + ", ".join(patterns) \
34
- + (", or " if len(patterns) > 1 else " or ") + last
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
- NAME: Optional[str] = None
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("--re", "-r", action="store_true",
55
- help="use regular expressions for pattern matching instead of glob")
56
- parser.add_argument("--all", "-a", action="store_true",
57
- help="include tasks starting with `_` prefix")
58
- parser.add_argument("tasks", nargs="*" if self.ALLOW_EMPTY_PATTERN else "+",
59
- help="task or tasks to execute as regular expressions or glob patterns")
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) -> List[Task]:
87
+ def discover_tasks(self, controller: Controller, args: Args) -> list[Task]:
65
88
  task: Task
66
- tasks: List[Task] = []
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(fnmatch.fnmatch(task.name, pattern) for pattern in args.tasks)
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 = [task for task in tasks if not task.name.startswith("_") or task.name in
87
- args.tasks]
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("--jobs", "-j", help="number of concurrent jobs", type=int, default=1)
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("--stale", "-s", action="store_true", help="only show stale tasks")
127
- parser.add_argument("--current", "-c", action="store_true", help="only show current tasks")
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 = [task.format(colorama.Fore.YELLOW if is_stale else colorama.Fore.GREEN)
133
- for is_stale, task in zip(controller.is_stale(tasks), tasks)]
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) -> List[Task]:
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("only one of `--stale` and `--current` may be given at the same time")
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 [task for stale, task in zip(controller.is_stale(tasks), tasks) if stale]
182
+ return [
183
+ task for stale, task in zip(controller.is_stale(tasks), tasks) if stale
184
+ ]
142
185
  elif args.current:
143
- return [task for stale, task in zip(controller.is_stale(tasks), tasks) if not stale]
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(f"last {key}: {format_timedelta(datetime.now() - value)} ago "
177
- f"({format_datetime(value)})")
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(sorted(controller.dependencies.successors(task),
180
- key=lambda t: t.name))
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(controller.is_stale(task_dependencies), task_dependencies)
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('\n'.join(parts), indent)
251
+ parts = textwrap.indent("\n".join(parts), indent)
199
252
  print(f"{task}\n{parts}")
200
253
 
201
254
 
202
- class ResetArgs(argparse.Namespace):
203
- tasks: Iterable[re.Pattern]
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: Optional[List[str]] = None) -> None:
289
+ def __main__(cli_args: list[str] | None = None) -> None:
237
290
  parser = argparse.ArgumentParser("cook")
238
- parser.add_argument("--recipe", help="file containing declarative recipe for tasks",
239
- default="recipe.py", type=Path)
240
- parser.add_argument("--module", "-m", help="module containing declarative recipe for tasks")
241
- parser.add_argument("--db", help="database for keeping track of assets", default=".cook")
242
- parser.add_argument("--log-level", help="log level", default="info",
243
- choices={"error", "warning", "info", "debug"})
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
- create_target_directories(),
263
- normalize_action(),
264
- normalize_dependencies(),
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("recipe file or module must be specified; default recipe.py not "
280
- "found")
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 sqlite3.connect(args.db, detect_types=sqlite3.PARSE_DECLTYPES) as connection:
286
- connection.executescript(QUERIES["schema"])
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__()