cook-build 0.5.1__py3-none-any.whl → 0.6.1__py3-none-any.whl

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/__main__.py CHANGED
@@ -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__()
cook/actions.py CHANGED
@@ -30,13 +30,14 @@ action; its return value is ignored. For example, the following action waits for
30
30
  >>> action.execute(None)
31
31
  time: 0.1...
32
32
  """
33
+
33
34
  import hashlib
34
35
  import os
35
36
  import shlex
36
37
  import subprocess
37
38
  import sys
38
39
  from types import ModuleType
39
- from typing import Callable, List, Optional, TYPE_CHECKING
40
+ from typing import Callable, TYPE_CHECKING
40
41
 
41
42
 
42
43
  if TYPE_CHECKING:
@@ -48,14 +49,15 @@ class Action:
48
49
  """
49
50
  Action to perform when a task is executed in its own thread.
50
51
  """
51
- def execute(self, task: "Task", stop: Optional["StopEvent"] = None) -> None:
52
+
53
+ def execute(self, task: "Task", stop: "StopEvent | None" = None) -> None:
52
54
  """
53
55
  Execute the action.
54
56
  """
55
57
  raise NotImplementedError
56
58
 
57
59
  @property
58
- def hexdigest(self) -> Optional[str]:
60
+ def hexdigest(self) -> str | None:
59
61
  """
60
62
  Optional digest to check if an action changed.
61
63
  """
@@ -71,13 +73,14 @@ class FunctionAction(Action):
71
73
  *args: Additional positional arguments.
72
74
  **kwargs: Keyword arguments.
73
75
  """
76
+
74
77
  def __init__(self, func: Callable, *args, **kwargs) -> None:
75
78
  super().__init__()
76
79
  self.func = func
77
80
  self.args = args
78
81
  self.kwargs = kwargs
79
82
 
80
- def execute(self, task: "Task", stop: Optional["StopEvent"] = None) -> None:
83
+ def execute(self, task: "Task", stop: "StopEvent | None" = None) -> None:
81
84
  self.func(task, *self.args, **self.kwargs)
82
85
 
83
86
 
@@ -101,11 +104,12 @@ class SubprocessAction(Action):
101
104
  >>> Path("hello.txt").is_file()
102
105
  True
103
106
  """
107
+
104
108
  def __init__(self, *args, **kwargs) -> None:
105
109
  self.args = args
106
110
  self.kwargs = kwargs
107
111
 
108
- def execute(self, task: "Task", stop: Optional["StopEvent"] = None) -> None:
112
+ def execute(self, task: "Task", stop: "StopEvent | None" = None) -> None:
109
113
  # Repeatedly wait for the process to complete, checking the stop event after each poll.
110
114
  interval = stop.interval if stop else None
111
115
  process = subprocess.Popen(*self.args, **self.kwargs)
@@ -116,14 +120,14 @@ class SubprocessAction(Action):
116
120
  raise subprocess.CalledProcessError(returncode, process.args)
117
121
  return
118
122
  except subprocess.TimeoutExpired:
119
- if stop.is_set():
123
+ if stop and stop.is_set():
120
124
  break
121
125
 
122
126
  # Clean up the process by trying to terminate it and then killing it.
123
127
  for method in [process.terminate, process.kill]:
124
128
  method()
125
129
  try:
126
- returncode = process.wait(max(interval, 3))
130
+ returncode = process.wait(max(interval, 3) if interval else None)
127
131
  if returncode:
128
132
  raise subprocess.CalledProcessError(returncode, process.args)
129
133
  # The process managed to exit gracefully after the main loop. This is unlikely.
@@ -132,12 +136,14 @@ class SubprocessAction(Action):
132
136
  pass
133
137
 
134
138
  # We couldn't kill the process. Also very unlikely.
135
- raise subprocess.SubprocessError(f"failed to shut down {process}") # pragma: no cover
139
+ raise subprocess.SubprocessError(
140
+ f"failed to shut down {process}"
141
+ ) # pragma: no cover
136
142
 
137
143
  @property
138
144
  def hexdigest(self) -> str:
139
145
  hasher = hashlib.sha1()
140
- args, = self.args
146
+ (args,) = self.args
141
147
  if isinstance(args, str):
142
148
  hasher.update(args.encode())
143
149
  else:
@@ -148,7 +154,7 @@ class SubprocessAction(Action):
148
154
  def __repr__(self) -> str:
149
155
  args, *_ = self.args
150
156
  if not isinstance(args, str):
151
- args = ' '.join(map(shlex.quote, args))
157
+ args = " ".join(map(shlex.quote, args))
152
158
  return f"{self.__class__.__name__}({repr(args)})"
153
159
 
154
160
 
@@ -159,15 +165,16 @@ class CompositeAction(Action):
159
165
  Args:
160
166
  *actions: Actions to execute.
161
167
  """
168
+
162
169
  def __init__(self, *actions: Action) -> None:
163
170
  self.actions = actions
164
171
 
165
- def execute(self, task: "Task", stop: Optional["StopEvent"] = None) -> None:
172
+ def execute(self, task: "Task", stop: "StopEvent | None" = None) -> None:
166
173
  for action in self.actions:
167
174
  action.execute(task, stop)
168
175
 
169
176
  @property
170
- def hexdigest(self) -> Optional[str]:
177
+ def hexdigest(self) -> str | None:
171
178
  parts = []
172
179
  for action in self.actions:
173
180
  hexdigest = action.hexdigest
@@ -188,7 +195,8 @@ class ModuleAction(SubprocessAction):
188
195
  being set).
189
196
  **kwargs: Keyword arguments for :class:`subprocess.Popen`.
190
197
  """
191
- def __init__(self, args: List, debug: Optional[bool] = None, **kwargs) -> None:
198
+
199
+ def __init__(self, args: list, debug: bool | None = None, **kwargs) -> None:
192
200
  if kwargs.get("shell"):
193
201
  raise ValueError("shell execution is not supported by `ModuleAction`")
194
202
  if not args:
cook/contexts.py CHANGED
@@ -29,10 +29,11 @@ Custom contexts can be implemented by inheriting from :class:`.Context` and impl
29
29
  >>> create_task("bar")
30
30
  <task `bar` @ ...>
31
31
  """
32
+
32
33
  from __future__ import annotations
33
34
  from pathlib import Path
34
35
  from types import ModuleType
35
- from typing import Callable, List, Optional, Tuple, TYPE_CHECKING
36
+ from typing import Callable, TYPE_CHECKING
36
37
  from . import actions
37
38
  from . import manager as manager_
38
39
  from . import task as task_
@@ -51,14 +52,15 @@ class Context:
51
52
  Args:
52
53
  manager: Manager to which the context is added.
53
54
  """
54
- def __init__(self, manager: Optional["Manager"] = None) -> None:
55
+
56
+ def __init__(self, manager: "Manager | None" = None) -> None:
55
57
  self.manager = manager or manager_.Manager.get_instance()
56
58
 
57
59
  def __enter__(self) -> Context:
58
60
  self.manager.contexts.append(self)
59
61
  return self
60
62
 
61
- def __exit__(self, *_) -> None:
63
+ def __exit__(self, ex_type, ex_value, ex_traceback) -> None:
62
64
  if not self.manager.contexts:
63
65
  raise RuntimeError("exiting failed: no active contexts")
64
66
  if self.manager.contexts[-1] is not self:
@@ -109,8 +111,14 @@ class FunctionContext(Context):
109
111
  ... create_task("baz")
110
112
  <task `bazbazbaz` @ ...>
111
113
  """
112
- def __init__(self, func: Callable[["Task"], Task], *args, manager: Optional["Manager"] = None,
113
- **kwargs) -> None:
114
+
115
+ def __init__(
116
+ self,
117
+ func: Callable[["Task"], Task],
118
+ *args,
119
+ manager: "Manager | None" = None,
120
+ **kwargs,
121
+ ) -> None:
114
122
  super().__init__(manager)
115
123
  self.func = func
116
124
  self.args = args or ()
@@ -128,6 +136,7 @@ class create_target_directories(Context):
128
136
 
129
137
  This context is active by default.
130
138
  """
139
+
131
140
  def apply(self, task: "Task") -> Task:
132
141
  for target in task.targets:
133
142
  name = f"_create_target_directories:{target.parent}"
@@ -137,9 +146,12 @@ class create_target_directories(Context):
137
146
  # Create a task if necessary.
138
147
  create = self.manager.tasks.get(name)
139
148
  if create is None:
140
- create = self.manager.create_task(name, action=actions.FunctionAction(
141
- lambda _: target.parent.mkdir(parents=True, exist_ok=True)
142
- ))
149
+ create = self.manager.create_task(
150
+ name,
151
+ action=actions.FunctionAction(
152
+ lambda _: target.parent.mkdir(parents=True, exist_ok=True)
153
+ ),
154
+ )
143
155
  task.task_dependencies.append(create)
144
156
  return task
145
157
 
@@ -162,12 +174,13 @@ class normalize_action(Context):
162
174
 
163
175
  This context is active by default.
164
176
  """
177
+
165
178
  def apply(self, task: "Task") -> "Task":
166
179
  if isinstance(task.action, Callable):
167
180
  task.action = actions.FunctionAction(task.action)
168
181
  elif isinstance(task.action, str):
169
182
  task.action = actions.SubprocessAction(task.action, shell=True)
170
- elif isinstance(task.action, List):
183
+ elif isinstance(task.action, list):
171
184
  if not task.action:
172
185
  raise ValueError("action must not be an empty list")
173
186
  if all(isinstance(x, actions.Action) for x in task.action):
@@ -193,6 +206,7 @@ class normalize_dependencies(Context):
193
206
 
194
207
  This context is active by default.
195
208
  """
209
+
196
210
  def apply(self, task: "Task") -> "Task":
197
211
  # Move task and group dependencies to the task_dependencies if they appear in regular
198
212
  # dependencies.
@@ -200,10 +214,10 @@ class normalize_dependencies(Context):
200
214
  task_dependencies = task.task_dependencies
201
215
  for dependency in task.dependencies:
202
216
  if isinstance(dependency, (task_.Task, create_group)):
203
- task_dependencies.append(dependency)
217
+ task_dependencies.append(dependency) # pyright: ignore[reportArgumentType]
204
218
  else:
205
219
  dependencies.append(dependency)
206
- task.dependencies = [Path(x) for x in dependencies]
220
+ task.dependencies = [Path(x) for x in dependencies] # pyright: ignore[reportAttributeAccessIssue]
207
221
 
208
222
  # Unpack group dependencies and look up tasks by name.
209
223
  task_dependencies = []
@@ -244,11 +258,16 @@ class create_group(Context):
244
258
  >>> my_group
245
259
  <group `my_group` @ ... with 2 tasks>
246
260
  """
247
- def __init__(self, name: str, manager: Optional["Manager"] = None,
248
- location: Optional[Tuple[str, int]] = None) -> None:
261
+
262
+ def __init__(
263
+ self,
264
+ name: str,
265
+ manager: "Manager | None" = None,
266
+ location: tuple[str, int] | None = None,
267
+ ) -> None:
249
268
  super().__init__(manager)
250
269
  self.name = name
251
- self.task: Optional[task_.Task] = None
270
+ self.task: task_.Task | None = None
252
271
  self.location = location or util.get_location()
253
272
 
254
273
  def apply(self, task: "Task") -> "Task":