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.
- cook_build-0.6.0/PKG-INFO +91 -0
- cook_build-0.6.0/README.md +81 -0
- {cook-build-0.5.0 → cook_build-0.6.0}/README.rst +6 -6
- cook_build-0.6.0/pyproject.toml +29 -0
- cook_build-0.6.0/setup.cfg +4 -0
- {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/__main__.py +157 -68
- {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/actions.py +21 -13
- {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/contexts.py +38 -17
- {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/controller.py +157 -55
- {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/manager.py +41 -22
- {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/task.py +13 -13
- {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/util.py +14 -9
- cook_build-0.6.0/src/cook_build.egg-info/PKG-INFO +91 -0
- cook_build-0.6.0/src/cook_build.egg-info/SOURCES.txt +25 -0
- cook_build-0.6.0/src/cook_build.egg-info/requires.txt +2 -0
- {cook-build-0.5.0 → cook_build-0.6.0}/tests/test_contexts.py +45 -16
- {cook-build-0.5.0 → cook_build-0.6.0}/tests/test_controller.py +109 -37
- {cook-build-0.5.0 → cook_build-0.6.0}/tests/test_examples.py +6 -3
- {cook-build-0.5.0 → cook_build-0.6.0}/tests/test_main.py +27 -16
- {cook-build-0.5.0 → cook_build-0.6.0}/tests/test_manager.py +16 -7
- {cook-build-0.5.0 → cook_build-0.6.0}/tests/test_util.py +7 -2
- cook-build-0.5.0/PKG-INFO +0 -91
- cook-build-0.5.0/cook_build.egg-info/PKG-INFO +0 -91
- cook-build-0.5.0/cook_build.egg-info/SOURCES.txt +0 -25
- cook-build-0.5.0/cook_build.egg-info/requires.txt +0 -2
- cook-build-0.5.0/setup.cfg +0 -18
- cook-build-0.5.0/setup.py +0 -31
- {cook-build-0.5.0 → cook_build-0.6.0}/LICENSE +0 -0
- {cook-build-0.5.0 → cook_build-0.6.0/src}/cook/__init__.py +0 -0
- {cook-build-0.5.0 → cook_build-0.6.0/src}/cook_build.egg-info/dependency_links.txt +0 -0
- {cook-build-0.5.0 → cook_build-0.6.0/src}/cook_build.egg-info/entry_points.txt +0 -0
- {cook-build-0.5.0 → cook_build-0.6.0/src}/cook_build.egg-info/top_level.txt +0 -0
- {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) [](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) [](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:
|
|
34
|
-
<task `hello` @ /.../recipe.py:
|
|
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:
|
|
46
|
-
INFO: completed <task `cc` @ /.../recipe.py:
|
|
47
|
-
INFO: executing <task `hello` @ /.../recipe.py:
|
|
48
|
-
INFO: completed <task `hello` @ /.../recipe.py:
|
|
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__"
|
|
@@ -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
|
|
14
|
+
from typing import Iterable
|
|
14
15
|
|
|
15
|
-
from .contexts import
|
|
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__(
|
|
27
|
-
patterns
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
*
|
|
32
|
-
message =
|
|
33
|
-
|
|
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
|
-
|
|
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(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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) ->
|
|
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
|
-
|
|
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(
|
|
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(
|
|
111
|
-
|
|
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 = [
|
|
117
|
-
|
|
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) ->
|
|
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(
|
|
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 [
|
|
182
|
+
return [
|
|
183
|
+
task for stale, task in zip(controller.is_stale(tasks), tasks) if stale
|
|
184
|
+
]
|
|
126
185
|
elif args.current:
|
|
127
|
-
return [
|
|
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(
|
|
161
|
-
|
|
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(
|
|
164
|
-
|
|
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(
|
|
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(
|
|
251
|
+
parts = textwrap.indent("\n".join(parts), indent)
|
|
183
252
|
print(f"{task}\n{parts}")
|
|
184
253
|
|
|
185
254
|
|
|
186
|
-
class ResetArgs(
|
|
187
|
-
|
|
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:
|
|
289
|
+
def __main__(cli_args: list[str] | None = None) -> None:
|
|
221
290
|
parser = argparse.ArgumentParser("cook")
|
|
222
|
-
parser.add_argument(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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(
|
|
264
|
-
|
|
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
|
|
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
|