cook-build 0.5.0__tar.gz → 0.6.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 (33) hide show
  1. cook_build-0.6.0/PKG-INFO +91 -0
  2. cook_build-0.6.0/README.md +81 -0
  3. {cook-build-0.5.0 → cook_build-0.6.0}/README.rst +6 -6
  4. cook_build-0.6.0/pyproject.toml +29 -0
  5. cook_build-0.6.0/setup.cfg +4 -0
  6. {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/__main__.py +157 -68
  7. {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/actions.py +21 -13
  8. {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/contexts.py +38 -17
  9. {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/controller.py +157 -55
  10. {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/manager.py +41 -22
  11. {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/task.py +13 -13
  12. {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/util.py +14 -9
  13. cook_build-0.6.0/src/cook_build.egg-info/PKG-INFO +91 -0
  14. cook_build-0.6.0/src/cook_build.egg-info/SOURCES.txt +25 -0
  15. cook_build-0.6.0/src/cook_build.egg-info/requires.txt +2 -0
  16. {cook-build-0.5.0 → cook_build-0.6.0}/tests/test_contexts.py +45 -16
  17. {cook-build-0.5.0 → cook_build-0.6.0}/tests/test_controller.py +109 -37
  18. {cook-build-0.5.0 → cook_build-0.6.0}/tests/test_examples.py +6 -3
  19. {cook-build-0.5.0 → cook_build-0.6.0}/tests/test_main.py +27 -16
  20. {cook-build-0.5.0 → cook_build-0.6.0}/tests/test_manager.py +16 -7
  21. {cook-build-0.5.0 → cook_build-0.6.0}/tests/test_util.py +7 -2
  22. cook-build-0.5.0/PKG-INFO +0 -91
  23. cook-build-0.5.0/cook_build.egg-info/PKG-INFO +0 -91
  24. cook-build-0.5.0/cook_build.egg-info/SOURCES.txt +0 -25
  25. cook-build-0.5.0/cook_build.egg-info/requires.txt +0 -2
  26. cook-build-0.5.0/setup.cfg +0 -18
  27. cook-build-0.5.0/setup.py +0 -31
  28. {cook-build-0.5.0 → cook_build-0.6.0}/LICENSE +0 -0
  29. {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/__init__.py +0 -0
  30. {cook-build-0.5.0 → cook_build-0.6.0/src}/cook_build.egg-info/dependency_links.txt +0 -0
  31. {cook-build-0.5.0 → cook_build-0.6.0/src}/cook_build.egg-info/entry_points.txt +0 -0
  32. {cook-build-0.5.0 → cook_build-0.6.0/src}/cook_build.egg-info/top_level.txt +0 -0
  33. {cook-build-0.5.0 → cook_build-0.6.0}/tests/test_actions.py +0 -0
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: cook-build
3
+ Version: 0.6.0
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,29 @@
1
+ [project]
2
+ name = "cook-build"
3
+ version = "0.6.0"
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
+ [build-system]
25
+ requires = ["setuptools>=61.0"]
26
+ build-backend = "setuptools.build_meta"
27
+
28
+ [project.scripts]
29
+ 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,19 +28,28 @@ LOGGER = logging.getLogger("cook")
23
28
 
24
29
 
25
30
  class NoMatchingTaskError(ValueError):
26
- def __init__(self, patterns: Iterable[re.Pattern]):
27
- patterns = [f"`{pattern}`" for pattern in patterns]
28
- if len(patterns) == 1:
29
- message = f"found no tasks matching pattern {patterns[0]}"
31
+ def __init__(
32
+ self, patterns: Iterable[re.Pattern | str], hidden_tasks_available: bool
33
+ ) -> None:
34
+ self.hidden_tasks_available = hidden_tasks_available
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]}"
30
38
  else:
31
- *patterns, last = patterns
32
- message = "found no tasks matching patterns " + ", ".join(patterns) \
33
- + (", 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
+ )
46
+ if hidden_tasks_available:
47
+ message = message + "; use --all or -a to include hidden tasks"
34
48
  super().__init__(message)
35
49
 
36
50
 
37
51
  class Args:
38
- tasks: Iterable[re.Pattern]
52
+ tasks: Iterable[re.Pattern | str]
39
53
  re: bool
40
54
  all: bool
41
55
 
@@ -44,34 +58,65 @@ class Command:
44
58
  """
45
59
  Abstract base class for commands.
46
60
  """
47
- NAME: Optional[str] = None
61
+
62
+ NAME: str | None = None
48
63
  ALLOW_EMPTY_PATTERN: bool = False
49
64
 
50
65
  def configure_parser(self, parser: argparse.ArgumentParser) -> None:
51
- parser.add_argument("--re", "-r", action="store_true",
52
- help="use regular expressions for pattern matching instead of glob")
53
- parser.add_argument("--all", "-a", action="store_true",
54
- help="include tasks starting with `_` prefix")
55
- parser.add_argument("tasks", nargs="*" if self.ALLOW_EMPTY_PATTERN else "+",
56
- 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
+ )
57
83
 
58
84
  def execute(self, controller: Controller, args: argparse.Namespace) -> None:
59
85
  raise NotImplementedError
60
86
 
61
- def discover_tasks(self, controller: Controller, args: Args) -> List[Task]:
87
+ def discover_tasks(self, controller: Controller, args: Args) -> list[Task]:
88
+ task: Task
89
+ tasks: list[Task] = []
90
+
91
+ # Get tasks based on the pattern matching.
62
92
  if not args.tasks:
63
- return list(controller.dependencies)
93
+ tasks = list(controller.dependencies)
94
+ else:
95
+ for task in controller.dependencies:
96
+ if args.re:
97
+ match = any(re.match(pattern, task.name) for pattern in args.tasks)
98
+ else:
99
+ match = any(
100
+ fnmatch.fnmatch(task.name, pattern) # pyright: ignore[reportArgumentType]
101
+ for pattern in args.tasks
102
+ )
103
+ if match:
104
+ tasks.append(task)
105
+
106
+ # Store whether any of the candidates are hidden by default.
107
+ has_hidden_task = any(task.name.startswith("_") for task in tasks)
108
+
109
+ # Filter out hidden tasks if desired unless the name is an exact match to a specified
110
+ # pattern.
111
+ if not args.all:
112
+ tasks = [
113
+ task
114
+ for task in tasks
115
+ if not task.name.startswith("_") or task.name in args.tasks
116
+ ]
64
117
 
65
- tasks: List[Task] = []
66
- task: Task
67
- for task in controller.dependencies:
68
- match = any(re.match(pattern, task.name) for pattern in args.tasks) if args.re else \
69
- any(fnmatch.fnmatch(task.name, pattern) for pattern in args.tasks)
70
- match = match and (args.all or not task.name.startswith("_"))
71
- if match:
72
- tasks.append(task)
73
118
  if not tasks:
74
- raise NoMatchingTaskError(args.tasks)
119
+ raise NoMatchingTaskError(args.tasks, not args.all and has_hidden_task)
75
120
  return tasks
76
121
 
77
122
 
@@ -83,13 +128,16 @@ class ExecCommand(Command):
83
128
  """
84
129
  Execute one or more tasks.
85
130
  """
131
+
86
132
  NAME = "exec"
87
133
 
88
134
  def configure_parser(self, parser: argparse.ArgumentParser) -> None:
89
- 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
+ )
90
138
  super().configure_parser(parser)
91
139
 
92
- def execute(self, controller: Controller, args: ExecArgs) -> None:
140
+ def execute(self, controller: Controller, args: ExecArgs) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
93
141
  tasks = self.discover_tasks(controller, args)
94
142
  controller.execute(tasks, num_concurrent=args.jobs)
95
143
 
@@ -103,28 +151,43 @@ class LsCommand(Command):
103
151
  """
104
152
  List tasks.
105
153
  """
154
+
106
155
  NAME = "ls"
107
156
  ALLOW_EMPTY_PATTERN = True
108
157
 
109
158
  def configure_parser(self, parser: argparse.ArgumentParser) -> None:
110
- parser.add_argument("--stale", "-s", action="store_true", help="only show stale tasks")
111
- 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
+ )
112
165
  super().configure_parser(parser)
113
166
 
114
- def execute(self, controller: Controller, args: LsArgs) -> None:
167
+ def execute(self, controller: Controller, args: LsArgs) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
115
168
  tasks = self.discover_tasks(controller, args)
116
- tasks = [task.format(colorama.Fore.YELLOW if is_stale else colorama.Fore.GREEN)
117
- 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
+ ]
118
173
  print("\n".join(tasks))
119
174
 
120
- def discover_tasks(self, controller: Controller, args: LsArgs) -> List[Task]:
175
+ def discover_tasks(self, controller: Controller, args: LsArgs) -> list[Task]: # pyright: ignore[reportIncompatibleMethodOverride]
121
176
  if args.current and args.stale:
122
- 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
+ )
123
180
  tasks = super().discover_tasks(controller, args)
124
181
  if args.stale:
125
- 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
+ ]
126
185
  elif args.current:
127
- 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
+ ]
128
191
  return tasks
129
192
 
130
193
 
@@ -132,6 +195,7 @@ class InfoCommand(LsCommand):
132
195
  """
133
196
  Display information about one or more tasks.
134
197
  """
198
+
135
199
  NAME = "info"
136
200
 
137
201
  def execute(self, controller: Controller, args: LsArgs) -> None:
@@ -150,21 +214,26 @@ class InfoCommand(LsCommand):
150
214
  ]
151
215
  # Show when the task last completed and failed.
152
216
  last = controller.connection.execute(
153
- "SELECT last_completed, last_failed FROM tasks WHERE name = :name",
154
- {"name": task.name}
155
- ).fetchone() or (None, None)
156
- 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):
157
221
  if value is None:
158
222
  parts.append(f"last {key}: -")
159
223
  continue
160
- parts.append(f"last {key}: {format_timedelta(datetime.now() - value)} ago "
161
- f"({format_datetime(value)})")
224
+ parts.append(
225
+ f"last {key}: {format_timedelta(datetime.now() - value)} ago "
226
+ f"({format_datetime(value)})"
227
+ )
162
228
  # Show dependencies and targets.
163
- task_dependencies = list(sorted(controller.dependencies.successors(task),
164
- key=lambda t: t.name))
229
+ task_dependencies = list(
230
+ sorted(controller.dependencies.successors(task), key=lambda t: t.name)
231
+ )
165
232
  task_dependencies = [
166
233
  dep.format(colorama.Fore.YELLOW if is_stale else colorama.Fore.GREEN)
167
- 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
+ )
168
237
  ]
169
238
  items = [
170
239
  ("dependencies", task.dependencies),
@@ -179,22 +248,22 @@ class InfoCommand(LsCommand):
179
248
  parts.append(f"{key}: -")
180
249
  parts.append(f"action: {task.action if task.action else '-'}")
181
250
 
182
- parts = textwrap.indent('\n'.join(parts), indent)
251
+ parts = textwrap.indent("\n".join(parts), indent)
183
252
  print(f"{task}\n{parts}")
184
253
 
185
254
 
186
- class ResetArgs(argparse.Namespace):
187
- tasks: Iterable[re.Pattern]
188
- re: bool
255
+ class ResetArgs(Args):
256
+ pass
189
257
 
190
258
 
191
259
  class ResetCommand(Command):
192
260
  """
193
261
  Reset the status of one or more tasks.
194
262
  """
263
+
195
264
  NAME = "reset"
196
265
 
197
- def execute(self, controller: Controller, args: ResetArgs) -> None:
266
+ def execute(self, controller: Controller, args: ResetArgs) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
198
267
  tasks = self.discover_tasks(controller, args)
199
268
  controller.reset(*tasks)
200
269
 
@@ -217,14 +286,26 @@ class Formatter(logging.Formatter):
217
286
  return formatted
218
287
 
219
288
 
220
- def __main__(cli_args: Optional[List[str]] = None) -> None:
289
+ def __main__(cli_args: list[str] | None = None) -> None:
221
290
  parser = argparse.ArgumentParser("cook")
222
- parser.add_argument("--recipe", help="file containing declarative recipe for tasks",
223
- default="recipe.py", type=Path)
224
- parser.add_argument("--module", "-m", help="module containing declarative recipe for tasks")
225
- parser.add_argument("--db", help="database for keeping track of assets", default=".cook")
226
- parser.add_argument("--log-level", help="log level", default="info",
227
- 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
+ )
228
309
  subparsers = parser.add_subparsers()
229
310
  subparsers.required = True
230
311
 
@@ -242,11 +323,13 @@ def __main__(cli_args: Optional[List[str]] = None) -> None:
242
323
 
243
324
  with Manager() as manager:
244
325
  try:
245
- manager.contexts.extend([
246
- create_target_directories(),
247
- normalize_action(),
248
- normalize_dependencies(),
249
- ])
326
+ manager.contexts.extend(
327
+ [
328
+ create_target_directories(),
329
+ normalize_action(),
330
+ normalize_dependencies(),
331
+ ]
332
+ )
250
333
  if args.module:
251
334
  # Temporarily add the current working directory to the path.
252
335
  try:
@@ -257,16 +340,22 @@ def __main__(cli_args: Optional[List[str]] = None) -> None:
257
340
  elif args.recipe.is_file():
258
341
  # Parse the recipe.
259
342
  spec = importlib.util.spec_from_file_location("recipe", args.recipe)
343
+ assert spec, f"Could not load spec for '{args.recipe}'."
260
344
  recipe = importlib.util.module_from_spec(spec)
345
+ assert spec.loader, f"Could not load recipe '{args.recipe}'."
261
346
  spec.loader.exec_module(recipe)
262
347
  else: # pragma: no cover
263
- raise ValueError("recipe file or module must be specified; default recipe.py not "
264
- "found")
348
+ raise ValueError(
349
+ "recipe file or module must be specified; default recipe.py not "
350
+ "found"
351
+ )
265
352
  except: # noqa: E722
266
353
  LOGGER.fatal("failed to load recipe", exc_info=sys.exc_info())
267
354
  sys.exit(1)
268
355
 
269
- with sqlite3.connect(args.db, detect_types=sqlite3.PARSE_DECLTYPES) as connection:
356
+ with closing(
357
+ sqlite3.connect(args.db, detect_types=sqlite3.PARSE_DECLTYPES)
358
+ ) as connection:
270
359
  connection.executescript(QUERIES["schema"])
271
360
  controller = Controller(manager.resolve_dependencies(), connection)
272
361
  command: Command = args.command