cook-build 0.6.2__tar.gz → 0.6.3__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 (27) hide show
  1. {cook_build-0.6.2/src/cook_build.egg-info → cook_build-0.6.3}/PKG-INFO +1 -1
  2. {cook_build-0.6.2 → cook_build-0.6.3}/pyproject.toml +1 -1
  3. {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/contexts.py +18 -4
  4. {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/controller.py +6 -0
  5. {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/manager.py +10 -4
  6. {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/task.py +1 -1
  7. {cook_build-0.6.2 → cook_build-0.6.3/src/cook_build.egg-info}/PKG-INFO +1 -1
  8. {cook_build-0.6.2 → cook_build-0.6.3}/tests/test_actions.py +10 -10
  9. {cook_build-0.6.2 → cook_build-0.6.3}/tests/test_contexts.py +26 -7
  10. {cook_build-0.6.2 → cook_build-0.6.3}/tests/test_controller.py +1 -1
  11. {cook_build-0.6.2 → cook_build-0.6.3}/tests/test_main.py +15 -7
  12. {cook_build-0.6.2 → cook_build-0.6.3}/tests/test_manager.py +1 -1
  13. {cook_build-0.6.2 → cook_build-0.6.3}/LICENSE +0 -0
  14. {cook_build-0.6.2 → cook_build-0.6.3}/README.md +0 -0
  15. {cook_build-0.6.2 → cook_build-0.6.3}/README.rst +0 -0
  16. {cook_build-0.6.2 → cook_build-0.6.3}/setup.cfg +0 -0
  17. {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/__init__.py +0 -0
  18. {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/__main__.py +0 -0
  19. {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/actions.py +0 -0
  20. {cook_build-0.6.2 → cook_build-0.6.3}/src/cook/util.py +0 -0
  21. {cook_build-0.6.2 → cook_build-0.6.3}/src/cook_build.egg-info/SOURCES.txt +0 -0
  22. {cook_build-0.6.2 → cook_build-0.6.3}/src/cook_build.egg-info/dependency_links.txt +0 -0
  23. {cook_build-0.6.2 → cook_build-0.6.3}/src/cook_build.egg-info/entry_points.txt +0 -0
  24. {cook_build-0.6.2 → cook_build-0.6.3}/src/cook_build.egg-info/requires.txt +0 -0
  25. {cook_build-0.6.2 → cook_build-0.6.3}/src/cook_build.egg-info/top_level.txt +0 -0
  26. {cook_build-0.6.2 → cook_build-0.6.3}/tests/test_examples.py +0 -0
  27. {cook_build-0.6.2 → cook_build-0.6.3}/tests/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cook-build
3
- Version: 0.6.2
3
+ Version: 0.6.3
4
4
  Summary: A task-centric build system with simple declarative recipes specified in Python
5
5
  Author: Till Hoffmann
6
6
  License: BSD-3-Clause
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cook-build"
3
- version = "0.6.2"
3
+ version = "0.6.3"
4
4
  description = "A task-centric build system with simple declarative recipes specified in Python"
5
5
  readme = "README.md"
6
6
  license = {text = "BSD-3-Clause"}
@@ -33,7 +33,8 @@ Custom contexts can be implemented by inheriting from :class:`.Context` and impl
33
33
  from __future__ import annotations
34
34
  from pathlib import Path
35
35
  from types import ModuleType
36
- from typing import Callable, TYPE_CHECKING
36
+ from typing import Callable, TYPE_CHECKING, TypeVar
37
+ import warnings
37
38
  from . import actions
38
39
  from . import manager as manager_
39
40
  from . import task as task_
@@ -44,6 +45,8 @@ if TYPE_CHECKING:
44
45
  from .manager import Manager
45
46
  from .task import Task
46
47
 
48
+ ContextT = TypeVar("ContextT", bound="Context")
49
+
47
50
 
48
51
  class Context:
49
52
  """
@@ -56,7 +59,7 @@ class Context:
56
59
  def __init__(self, manager: "Manager | None" = None) -> None:
57
60
  self.manager = manager or manager_.Manager.get_instance()
58
61
 
59
- def __enter__(self) -> Context:
62
+ def __enter__(self: ContextT) -> ContextT:
60
63
  self.manager.contexts.append(self)
61
64
  return self
62
65
 
@@ -214,10 +217,21 @@ class normalize_dependencies(Context):
214
217
  task_dependencies = task.task_dependencies
215
218
  for dependency in task.dependencies:
216
219
  if isinstance(dependency, (task_.Task, create_group)):
217
- task_dependencies.append(dependency) # pyright: ignore[reportArgumentType]
220
+ warnings.warn(
221
+ "Passing Task objects to 'dependencies' is deprecated. Use "
222
+ "'task_dependencies' instead.",
223
+ DeprecationWarning,
224
+ stacklevel=4,
225
+ )
226
+ task_dependencies.append(dependency)
218
227
  else:
219
228
  dependencies.append(dependency)
220
- task.dependencies = [Path(x) for x in dependencies] # pyright: ignore[reportAttributeAccessIssue]
229
+ # Convert all remaining dependencies (strings) to Path objects.
230
+ # After normalization, dependencies list contains only Path objects, but the
231
+ # Task.dependencies attribute is typed as list[PathOrStr | Task] to accept broader
232
+ # input before normalization. The isinstance checks in controller.py and manager.py
233
+ # validate this assumption at runtime.
234
+ task.dependencies = [Path(x) for x in dependencies] # type: ignore[assignment]
221
235
 
222
236
  # Unpack group dependencies and look up tasks by name.
223
237
  task_dependencies = []
@@ -109,6 +109,12 @@ class Controller:
109
109
  """
110
110
  dependencies = []
111
111
  for dependency in task.dependencies:
112
+ # Dependencies should be Path or str after normalize_dependencies runs.
113
+ # Tasks should have been moved to task_dependencies.
114
+ assert isinstance(dependency, (Path, str)), (
115
+ f"Unexpected dependency type '{type(dependency)}'. Dependencies "
116
+ "should be 'Path' or 'str' after 'normalize_dependencies'."
117
+ )
112
118
  dependency = Path(dependency).resolve()
113
119
  if not dependency.is_file():
114
120
  LOGGER.debug("dependency %s of %s is missing", dependency, task)
@@ -96,8 +96,14 @@ class Manager:
96
96
  f"tasks {task} and {other} both have target {path}"
97
97
  )
98
98
  task_by_target[path] = task
99
- for path in task.dependencies:
100
- path = Path(path).resolve()
99
+ for dependency in task.dependencies:
100
+ # Dependencies should be Path or str after normalize_dependencies runs.
101
+ # Tasks should have been moved to task_dependencies.
102
+ assert isinstance(dependency, (Path, str)), (
103
+ f"Unexpected dependency type '{type(dependency)}'. Dependencies "
104
+ "should be 'Path' or 'str' after 'normalize_dependencies'."
105
+ )
106
+ path = Path(dependency).resolve()
101
107
  tasks_by_file_dependency.setdefault(path, set()).add(task)
102
108
 
103
109
  # Build a directed graph of dependencies based on files produced and consumed by tasks.
@@ -132,8 +138,8 @@ def create_task(
132
138
  name: str,
133
139
  *,
134
140
  action: "Action | str | None" = None,
135
- targets: list["Path"] | None = None,
136
- dependencies: list["Path"] | None = None,
141
+ targets: list["Path | str"] | None = None,
142
+ dependencies: list["Path | str | Task"] | None = None,
137
143
  task_dependencies: list["Task"] | None = None,
138
144
  location: tuple[str, int] | None = None,
139
145
  ) -> "Task":
@@ -19,7 +19,7 @@ class Task:
19
19
  self,
20
20
  name: str,
21
21
  *,
22
- dependencies: list["PathOrStr"] | None = None,
22
+ dependencies: list["PathOrStr | Task"] | None = None,
23
23
  targets: list["PathOrStr"] | None = None,
24
24
  action: Action | None = None,
25
25
  task_dependencies: list[Task] | None = None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cook-build
3
- Version: 0.6.2
3
+ Version: 0.6.3
4
4
  Summary: A task-centric build system with simple declarative recipes specified in Python
5
5
  Author: Till Hoffmann
6
6
  License: BSD-3-Clause
@@ -11,7 +11,7 @@ from unittest import mock
11
11
 
12
12
  def test_shell_action(tmp_wd: Path) -> None:
13
13
  action = SubprocessAction("echo hello > world.txt", shell=True)
14
- action.execute(None)
14
+ action.execute(None) # type: ignore[arg-type]
15
15
  assert (tmp_wd / "world.txt").read_text().strip() == "hello"
16
16
 
17
17
 
@@ -27,7 +27,7 @@ def test_shell_action_timeout() -> None:
27
27
 
28
28
  action = SubprocessAction(["sleep", "2"])
29
29
  with Timer() as timer, pytest.raises(SubprocessError, match="SIGTERM"):
30
- action.execute(None, stop)
30
+ action.execute(None, stop) # type: ignore[arg-type]
31
31
 
32
32
  assert 1 < timer.duration < 2
33
33
  assert not thread.is_alive()
@@ -35,14 +35,14 @@ def test_shell_action_timeout() -> None:
35
35
 
36
36
  def test_subprocess_action(tmp_wd: Path) -> None:
37
37
  action = SubprocessAction(["touch", "foo"])
38
- action.execute(None)
38
+ action.execute(None) # type: ignore[arg-type]
39
39
  assert (tmp_wd / "foo").is_file()
40
40
 
41
41
 
42
42
  def test_bad_subprocess_action() -> None:
43
43
  action = SubprocessAction("false")
44
44
  with pytest.raises(SubprocessError):
45
- action.execute(None)
45
+ action.execute(None) # type: ignore[arg-type]
46
46
 
47
47
 
48
48
  def test_subprocess_action_repr() -> None:
@@ -54,7 +54,7 @@ def test_function_action() -> None:
54
54
  args = []
55
55
 
56
56
  action = FunctionAction(args.append)
57
- action.execute(42)
57
+ action.execute(42) # type: ignore[arg-type]
58
58
 
59
59
  assert args == [42]
60
60
 
@@ -63,13 +63,13 @@ def test_composite_action() -> None:
63
63
  args = []
64
64
 
65
65
  action = CompositeAction(FunctionAction(args.append), FunctionAction(args.append))
66
- action.execute("hello")
66
+ action.execute("hello") # type: ignore[arg-type]
67
67
  assert args == ["hello", "hello"]
68
68
 
69
69
 
70
70
  def test_module_action() -> None:
71
71
  action = ModuleAction([pytest, "-h"])
72
- action.execute(None)
72
+ action.execute(None) # type: ignore[arg-type]
73
73
 
74
74
  with pytest.raises(ValueError, match="shell execution"):
75
75
  ModuleAction([pytest], shell=True)
@@ -84,12 +84,12 @@ def test_module_action() -> None:
84
84
  def test_module_action_debug() -> None:
85
85
  with mock.patch("subprocess.Popen") as Popen:
86
86
  Popen().wait = mock.MagicMock(return_value=0)
87
- ModuleAction([pytest], debug=True).execute(None)
87
+ ModuleAction([pytest], debug=True).execute(None) # type: ignore[arg-type]
88
88
  Popen.assert_called_with([sys.executable, "-m", "pdb", "-m", "pytest"])
89
89
 
90
90
  with mock.patch("subprocess.Popen") as Popen:
91
91
  Popen().wait = mock.MagicMock(return_value=0)
92
- ModuleAction([pytest], debug=False).execute(None)
92
+ ModuleAction([pytest], debug=False).execute(None) # type: ignore[arg-type]
93
93
  Popen.assert_called_with([sys.executable, "-m", "pytest"])
94
94
 
95
95
 
@@ -100,5 +100,5 @@ def test_composite_digest() -> None:
100
100
  ]
101
101
  assert CompositeAction(*actions).hexdigest
102
102
 
103
- actions.append(FunctionAction(print))
103
+ actions.append(FunctionAction(print)) # type: ignore[arg-type]
104
104
  assert CompositeAction(*actions).hexdigest is None
@@ -33,7 +33,7 @@ def test_function_context(m: Manager) -> None:
33
33
  def test_missing_task_context(m: Manager) -> None:
34
34
  with (
35
35
  pytest.raises(ValueError, match="did not return a task"),
36
- FunctionContext(lambda _: None),
36
+ FunctionContext(lambda _: None), # type: ignore[arg-type]
37
37
  ):
38
38
  m.create_task("my-task")
39
39
 
@@ -42,7 +42,7 @@ def test_context_management(m: Manager) -> None:
42
42
  with pytest.raises(RuntimeError, match="no active contexts"), Context():
43
43
  m.contexts = []
44
44
  with pytest.raises(RuntimeError, match="unexpected context"), Context():
45
- m.contexts.append("something else")
45
+ m.contexts.append("something else") # type: ignore[arg-type]
46
46
 
47
47
 
48
48
  def test_create_target_directories(
@@ -59,17 +59,20 @@ def test_create_target_directories(
59
59
 
60
60
 
61
61
  def test_create_target_directories_with_multiple_targets(
62
- m: Manager, tmp_wd: Path, conn: sqlite3
62
+ m: Manager, tmp_wd: Path, conn: sqlite3.Connection
63
63
  ) -> None:
64
64
  filenames = [
65
65
  tmp_wd / "this/is/a/hierarchy.txt",
66
66
  tmp_wd / "this/is/a/hierarchy2.txt",
67
67
  ]
68
+ filename: Path | None = None
69
+ task: Task | None = None
68
70
  with normalize_action(), create_target_directories():
69
71
  for filename in filenames:
70
72
  task = m.create_task(
71
73
  filename.name, targets=[filename], action=["touch", filename]
72
74
  )
75
+ assert filename is not None and task is not None
73
76
  assert not filename.parent.is_dir()
74
77
 
75
78
  controller = Controller(m.resolve_dependencies(), conn)
@@ -131,11 +134,27 @@ def test_normalize_dependencies(m: Manager) -> None:
131
134
  with create_group("g") as g:
132
135
  base = m.create_task("base")
133
136
  with normalize_dependencies():
134
- task = m.create_task("task1", dependencies=[g])
137
+ task = m.create_task("task3", task_dependencies=["g"])
135
138
  assert task.task_dependencies == [g.task]
136
139
 
137
- task = m.create_task("task2", dependencies=[base])
138
- assert task.task_dependencies == [base]
139
140
 
140
- task = m.create_task("task3", task_dependencies=["g"])
141
+ def test_normalize_dependencies_deprecated_syntax(m: Manager) -> None:
142
+ """Test that passing Tasks to dependencies parameter emits DeprecationWarning."""
143
+ with create_group("g") as g:
144
+ base = m.create_task("base")
145
+ with normalize_dependencies():
146
+ # Test with group
147
+ with pytest.warns(
148
+ DeprecationWarning,
149
+ match="Passing Task objects to 'dependencies' is deprecated",
150
+ ):
151
+ task = m.create_task("task1", dependencies=[g])
141
152
  assert task.task_dependencies == [g.task]
153
+
154
+ # Test with regular task
155
+ with pytest.warns(
156
+ DeprecationWarning,
157
+ match="Passing Task objects to 'dependencies' is deprecated",
158
+ ):
159
+ task = m.create_task("task2", dependencies=[base])
160
+ assert task.task_dependencies == [base]
@@ -188,7 +188,7 @@ def test_digest_cache(m: Manager, conn: Connection, tmp_wd: Path) -> None:
188
188
 
189
189
 
190
190
  def test_skip_if_no_stale_tasks(m: Manager, conn: Connection, tmp_wd: Path) -> None:
191
- c = Controller(m, conn)
191
+ c = Controller(m.resolve_dependencies(), conn)
192
192
  c.execute([])
193
193
 
194
194
 
@@ -1,3 +1,4 @@
1
+ import colorama
1
2
  from cook.__main__ import __main__, Formatter
2
3
  import logging
3
4
  from pathlib import Path
@@ -9,6 +10,13 @@ import sys
9
10
  RECIPES = Path(__file__).parent / "recipes"
10
11
 
11
12
 
13
+ def strip_colors(text: str) -> str:
14
+ for ansi_codes in [colorama.Fore, colorama.Back]:
15
+ for code in vars(ansi_codes).values():
16
+ text = text.replace(code, "")
17
+ return text
18
+
19
+
12
20
  def test_blah_recipe_run(tmp_wd: Path) -> None:
13
21
  __main__(["--recipe", str(RECIPES / "blah.py"), "exec", "run"])
14
22
 
@@ -28,35 +36,35 @@ def test_blah_recipe_ls(
28
36
  __main__(["--recipe", str(RECIPES / "blah.py"), "ls", *patterns])
29
37
  out, _ = capsys.readouterr()
30
38
  for task in expected:
31
- assert f"<task `{task}` @ " in pytest.shared.strip_colors(out)
39
+ assert f"<task `{task}` @ " in strip_colors(out)
32
40
 
33
41
 
34
42
  def test_blah_recipe_info(tmp_wd: Path, capsys: pytest.CaptureFixture) -> None:
35
43
  __main__(["--recipe", str(RECIPES / "blah.py"), "info", "link"])
36
44
  stdout, _ = capsys.readouterr()
37
- assert "status: stale" in pytest.shared.strip_colors(stdout)
45
+ assert "status: stale" in strip_colors(stdout)
38
46
 
39
47
  __main__(["--recipe", str(RECIPES / "blah.py"), "exec", "link"])
40
48
  __main__(["--recipe", str(RECIPES / "blah.py"), "info", "link"])
41
49
  stdout, _ = capsys.readouterr()
42
- assert "status: current" in pytest.shared.strip_colors(stdout)
50
+ assert "status: current" in strip_colors(stdout)
43
51
 
44
52
  __main__(["--recipe", str(RECIPES / "blah.py"), "info", "run"])
45
53
  stdout, _ = capsys.readouterr()
46
- assert "targets: -" in pytest.shared.strip_colors(stdout)
54
+ assert "targets: -" in strip_colors(stdout)
47
55
 
48
56
  # Check filtering based on stale/current status.
49
57
  __main__(["--recipe", str(RECIPES / "blah.py"), "info", "--stale"])
50
58
  stdout, _ = capsys.readouterr()
51
- assert "status: current" not in pytest.shared.strip_colors(stdout)
59
+ assert "status: current" not in strip_colors(stdout)
52
60
 
53
61
  __main__(["--recipe", str(RECIPES / "blah.py"), "info", "--current"])
54
62
  stdout, _ = capsys.readouterr()
55
- assert "status: stale" not in pytest.shared.strip_colors(stdout)
63
+ assert "status: stale" not in strip_colors(stdout)
56
64
 
57
65
  __main__(["--recipe", str(RECIPES / "blah.py"), "info"])
58
66
  stdout, _ = capsys.readouterr()
59
- stdout = pytest.shared.strip_colors(stdout)
67
+ stdout = strip_colors(stdout)
60
68
  assert "status: stale" in stdout and "status: current" in stdout
61
69
 
62
70
  # Check only one can be given.
@@ -85,7 +85,7 @@ def test_get_manager_instance() -> None:
85
85
  with Manager() as m:
86
86
  assert Manager.get_instance() is m
87
87
  with pytest.raises(RuntimeError, match="unexpected manager"), Manager():
88
- Manager._INSTANCE = "asdf"
88
+ Manager._INSTANCE = "asdf" # pyright: ignore[reportAttributeAccessIssue]
89
89
  with pytest.raises(ValueError, match="already active"), Manager(), Manager():
90
90
  pass
91
91
 
File without changes
File without changes
File without changes
File without changes
File without changes