cook-build 0.7.1__tar.gz → 0.7.2__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.7.1/src/cook_build.egg-info → cook_build-0.7.2}/PKG-INFO +1 -1
  2. {cook_build-0.7.1 → cook_build-0.7.2}/pyproject.toml +1 -1
  3. {cook_build-0.7.1 → cook_build-0.7.2}/src/cook/actions.py +4 -1
  4. {cook_build-0.7.1 → cook_build-0.7.2}/src/cook/contexts.py +1 -1
  5. {cook_build-0.7.1 → cook_build-0.7.2}/src/cook/controller.py +12 -14
  6. {cook_build-0.7.1 → cook_build-0.7.2}/src/cook/manager.py +1 -1
  7. {cook_build-0.7.1 → cook_build-0.7.2/src/cook_build.egg-info}/PKG-INFO +1 -1
  8. {cook_build-0.7.1 → cook_build-0.7.2}/tests/test_actions.py +12 -0
  9. {cook_build-0.7.1 → cook_build-0.7.2}/tests/test_contexts.py +21 -0
  10. {cook_build-0.7.1 → cook_build-0.7.2}/tests/test_controller.py +30 -0
  11. {cook_build-0.7.1 → cook_build-0.7.2}/LICENSE +0 -0
  12. {cook_build-0.7.1 → cook_build-0.7.2}/README.md +0 -0
  13. {cook_build-0.7.1 → cook_build-0.7.2}/README.rst +0 -0
  14. {cook_build-0.7.1 → cook_build-0.7.2}/setup.cfg +0 -0
  15. {cook_build-0.7.1 → cook_build-0.7.2}/src/cook/__init__.py +0 -0
  16. {cook_build-0.7.1 → cook_build-0.7.2}/src/cook/__main__.py +0 -0
  17. {cook_build-0.7.1 → cook_build-0.7.2}/src/cook/task.py +0 -0
  18. {cook_build-0.7.1 → cook_build-0.7.2}/src/cook/util.py +0 -0
  19. {cook_build-0.7.1 → cook_build-0.7.2}/src/cook_build.egg-info/SOURCES.txt +0 -0
  20. {cook_build-0.7.1 → cook_build-0.7.2}/src/cook_build.egg-info/dependency_links.txt +0 -0
  21. {cook_build-0.7.1 → cook_build-0.7.2}/src/cook_build.egg-info/entry_points.txt +0 -0
  22. {cook_build-0.7.1 → cook_build-0.7.2}/src/cook_build.egg-info/requires.txt +0 -0
  23. {cook_build-0.7.1 → cook_build-0.7.2}/src/cook_build.egg-info/top_level.txt +0 -0
  24. {cook_build-0.7.1 → cook_build-0.7.2}/tests/test_examples.py +0 -0
  25. {cook_build-0.7.1 → cook_build-0.7.2}/tests/test_main.py +0 -0
  26. {cook_build-0.7.1 → cook_build-0.7.2}/tests/test_manager.py +0 -0
  27. {cook_build-0.7.1 → cook_build-0.7.2}/tests/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cook-build
3
- Version: 0.7.1
3
+ Version: 0.7.2
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.7.1"
3
+ version = "0.7.2"
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"}
@@ -36,6 +36,7 @@ executor with a deprecation warning.
36
36
  """
37
37
 
38
38
  import asyncio
39
+ import functools
39
40
  import hashlib
40
41
  import logging
41
42
  import os
@@ -94,7 +95,9 @@ class FunctionAction(Action):
94
95
  else:
95
96
  # Run sync function in executor
96
97
  loop = asyncio.get_running_loop()
97
- await loop.run_in_executor(None, self.func, task, *self.args, **self.kwargs)
98
+ await loop.run_in_executor(
99
+ None, functools.partial(self.func, task, *self.args, **self.kwargs)
100
+ )
98
101
 
99
102
 
100
103
  class SubprocessAction(Action):
@@ -152,7 +152,7 @@ class create_target_directories(Context):
152
152
  create = self.manager.create_task(
153
153
  name,
154
154
  action=actions.FunctionAction(
155
- lambda _: target.parent.mkdir(parents=True, exist_ok=True)
155
+ lambda _, p=target.parent: p.mkdir(parents=True, exist_ok=True)
156
156
  ),
157
157
  )
158
158
  task.task_dependencies.append(create)
@@ -290,8 +290,8 @@ class Controller:
290
290
  dep_futures = [task_futures[dep] for dep in dep_tasks]
291
291
  await asyncio.gather(*dep_futures)
292
292
 
293
- start = datetime.now()
294
293
  digest = self._evaluate_task_hexdigest(task)
294
+ start: datetime | None = None
295
295
 
296
296
  try:
297
297
  # Log what we're doing
@@ -307,22 +307,19 @@ class Controller:
307
307
  " action: %s",
308
308
  task.action,
309
309
  )
310
- else:
311
- LOGGER.log(
312
- logging.DEBUG if task.name.startswith("_") else logging.INFO,
313
- "executing %s ...",
314
- task,
315
- )
316
-
317
- # Update DB for start
318
- if not dry_run:
319
- params = {"name": task.name, "last_started": start}
320
- self.connection.execute(QUERIES["upsert_task_started"], params)
321
- self.connection.commit()
322
310
 
323
311
  # Execute the task
324
312
  if not dry_run:
325
313
  async with semaphore:
314
+ start = datetime.now()
315
+ LOGGER.log(
316
+ logging.DEBUG if task.name.startswith("_") else logging.INFO,
317
+ "executing %s ...",
318
+ task,
319
+ )
320
+ params = {"name": task.name, "last_started": start}
321
+ self.connection.execute(QUERIES["upsert_task_started"], params)
322
+ self.connection.commit()
326
323
  await task.execute()
327
324
 
328
325
  # Check that all targets were created
@@ -344,6 +341,7 @@ class Controller:
344
341
  self.connection.commit()
345
342
 
346
343
  # Log completion
344
+ assert start is not None
347
345
  delta = util.format_timedelta(datetime.now() - start)
348
346
  LOGGER.log(
349
347
  logging.DEBUG if task.name.startswith("_") else logging.INFO,
@@ -362,7 +360,7 @@ class Controller:
362
360
  self.connection.execute(QUERIES["upsert_task_failed"], params)
363
361
  self.connection.commit()
364
362
 
365
- delta = util.format_timedelta(datetime.now() - start)
363
+ delta = util.format_timedelta(datetime.now() - start) if start else "?"
366
364
  LOGGER.exception("failed to execute %s after %s", task, delta)
367
365
  raise util.FailedTaskError(ex, task=task) from ex
368
366
 
@@ -150,7 +150,7 @@ def create_task(
150
150
 
151
151
  Args:
152
152
  name: Name of the new task. Defaults to the string representation of the first
153
- dependency if not provided.
153
+ target if not provided.
154
154
  action: Action to execute or a string for shell commands.
155
155
  targets: Paths for files to be generated.
156
156
  dependencies: Paths to files on which this task depends.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cook-build
3
- Version: 0.7.1
3
+ Version: 0.7.2
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
@@ -158,6 +158,18 @@ async def test_module_action_debug() -> None:
158
158
  create_subprocess.assert_called_with(sys.executable, "-m", "pytest")
159
159
 
160
160
 
161
+ @pytest.mark.asyncio
162
+ async def test_sync_function_action_with_kwargs() -> None:
163
+ calls = []
164
+
165
+ def func(task, *, key):
166
+ calls.append((task, key))
167
+
168
+ action = FunctionAction(func, key="value")
169
+ await action.execute("my_task") # type: ignore[arg-type]
170
+ assert calls == [("my_task", "value")]
171
+
172
+
161
173
  def test_composite_digest() -> None:
162
174
  actions = [
163
175
  SubprocessAction("hello"),
@@ -84,6 +84,27 @@ async def test_create_target_directories_with_multiple_targets(
84
84
  assert filename.parent.is_dir()
85
85
 
86
86
 
87
+ @pytest.mark.asyncio
88
+ async def test_create_target_directories_different_parents(
89
+ m: Manager, tmp_wd: Path, conn: sqlite3.Connection
90
+ ) -> None:
91
+ # Targets in two different directories: the closure in create_target_directories
92
+ # must create both parents, not just the last one from the loop.
93
+ targets = [tmp_wd / "dir_a" / "file.txt", tmp_wd / "dir_b" / "file.txt"]
94
+ with normalize_action(), create_target_directories():
95
+ task = m.create_task(
96
+ "multi_dir",
97
+ targets=targets,
98
+ action=FunctionAction(
99
+ lambda t: [target.write_text("ok") for target in t.targets]
100
+ ),
101
+ )
102
+ controller = Controller(m.resolve_dependencies(), conn)
103
+ await controller.execute(task)
104
+ for target in targets:
105
+ assert target.is_file(), f"{target} was not created"
106
+
107
+
87
108
  def test_normalize_action(m: Manager) -> None:
88
109
  with normalize_action():
89
110
  task = m.create_task("foo", action="bar")
@@ -420,6 +420,36 @@ async def test_last_started_completed(m: Manager, conn: Connection) -> None:
420
420
  assert delta.total_seconds() > 1
421
421
 
422
422
 
423
+ @pytest.mark.asyncio
424
+ async def test_last_started_reflects_actual_start(m: Manager, conn: Connection) -> None:
425
+ delay = 0.5
426
+ task1 = m.create_task(
427
+ "task1",
428
+ action=SubprocessAction(f"sleep {delay} && touch task1.txt", shell=True),
429
+ targets=["task1.txt"],
430
+ )
431
+ task2 = m.create_task(
432
+ "task2",
433
+ action=SubprocessAction("touch task2.txt", shell=True),
434
+ targets=["task2.txt"],
435
+ )
436
+ result = m.create_task("result", dependencies=[task1.targets[0], task2.targets[0]])
437
+
438
+ # With num_concurrent=1, task2 must wait for task1 to finish. Its last_started
439
+ # should reflect when it actually began executing, not when it was scheduled.
440
+ await Controller(m.resolve_dependencies(), conn).execute(result, num_concurrent=1)
441
+ rows = dict(
442
+ conn.execute(
443
+ "SELECT name, last_started FROM tasks WHERE name IN ('task1', 'task2')"
444
+ ).fetchall()
445
+ )
446
+ delta = (rows["task2"] - rows["task1"]).total_seconds()
447
+ assert delta >= delay, (
448
+ f"task2 started only {delta:.3f}s after task1, expected >= {delay}s; "
449
+ "last_started is recorded at scheduling time, not actual execution time"
450
+ )
451
+
452
+
423
453
  @pytest.mark.asyncio
424
454
  async def test_sync_action_backwards_compat(
425
455
  m: Manager, conn: Connection, caplog: pytest.LogCaptureFixture
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes