cook-build 0.6.3__tar.gz → 0.6.4__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.3/src/cook_build.egg-info → cook_build-0.6.4}/PKG-INFO +1 -1
  2. {cook_build-0.6.3 → cook_build-0.6.4}/pyproject.toml +1 -1
  3. {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/__main__.py +9 -1
  4. {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/controller.py +77 -43
  5. {cook_build-0.6.3 → cook_build-0.6.4/src/cook_build.egg-info}/PKG-INFO +1 -1
  6. {cook_build-0.6.3 → cook_build-0.6.4}/tests/test_main.py +63 -0
  7. {cook_build-0.6.3 → cook_build-0.6.4}/LICENSE +0 -0
  8. {cook_build-0.6.3 → cook_build-0.6.4}/README.md +0 -0
  9. {cook_build-0.6.3 → cook_build-0.6.4}/README.rst +0 -0
  10. {cook_build-0.6.3 → cook_build-0.6.4}/setup.cfg +0 -0
  11. {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/__init__.py +0 -0
  12. {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/actions.py +0 -0
  13. {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/contexts.py +0 -0
  14. {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/manager.py +0 -0
  15. {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/task.py +0 -0
  16. {cook_build-0.6.3 → cook_build-0.6.4}/src/cook/util.py +0 -0
  17. {cook_build-0.6.3 → cook_build-0.6.4}/src/cook_build.egg-info/SOURCES.txt +0 -0
  18. {cook_build-0.6.3 → cook_build-0.6.4}/src/cook_build.egg-info/dependency_links.txt +0 -0
  19. {cook_build-0.6.3 → cook_build-0.6.4}/src/cook_build.egg-info/entry_points.txt +0 -0
  20. {cook_build-0.6.3 → cook_build-0.6.4}/src/cook_build.egg-info/requires.txt +0 -0
  21. {cook_build-0.6.3 → cook_build-0.6.4}/src/cook_build.egg-info/top_level.txt +0 -0
  22. {cook_build-0.6.3 → cook_build-0.6.4}/tests/test_actions.py +0 -0
  23. {cook_build-0.6.3 → cook_build-0.6.4}/tests/test_contexts.py +0 -0
  24. {cook_build-0.6.3 → cook_build-0.6.4}/tests/test_controller.py +0 -0
  25. {cook_build-0.6.3 → cook_build-0.6.4}/tests/test_examples.py +0 -0
  26. {cook_build-0.6.3 → cook_build-0.6.4}/tests/test_manager.py +0 -0
  27. {cook_build-0.6.3 → cook_build-0.6.4}/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.3
3
+ Version: 0.6.4
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.3"
3
+ version = "0.6.4"
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"}
@@ -122,6 +122,7 @@ class Command:
122
122
 
123
123
  class ExecArgs(Args):
124
124
  jobs: int
125
+ dry_run: bool
125
126
 
126
127
 
127
128
  class ExecCommand(Command):
@@ -135,11 +136,18 @@ class ExecCommand(Command):
135
136
  parser.add_argument(
136
137
  "--jobs", "-j", help="number of concurrent jobs", type=int, default=1
137
138
  )
139
+ parser.add_argument(
140
+ "--dry-run",
141
+ "-n",
142
+ help="show what \033[1mwould\033[0m be executed without running tasks",
143
+ action="store_true",
144
+ dest="dry_run",
145
+ )
138
146
  super().configure_parser(parser)
139
147
 
140
148
  def execute(self, controller: Controller, args: ExecArgs) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
141
149
  tasks = self.discover_tasks(controller, args)
142
- controller.execute(tasks, num_concurrent=args.jobs)
150
+ controller.execute(tasks, num_concurrent=args.jobs, dry_run=args.dry_run)
143
151
 
144
152
 
145
153
  class LsArgs(Args):
@@ -233,7 +233,11 @@ class Controller:
233
233
  return False
234
234
 
235
235
  def execute(
236
- self, tasks: "Task | list[Task]", num_concurrent: int = 1, interval: float = 1
236
+ self,
237
+ tasks: "Task | list[Task]",
238
+ num_concurrent: int = 1,
239
+ interval: float = 1,
240
+ dry_run: bool = False,
237
241
  ) -> None:
238
242
  """
239
243
  Execute one or more tasks.
@@ -241,6 +245,8 @@ class Controller:
241
245
  Args:
242
246
  tasks: Tasks to execute.
243
247
  num_concurrent: Number of concurrent threads to run.
248
+ interval: Interval for checking stop events.
249
+ dry_run: If True, show what would execute without running tasks.
244
250
  """
245
251
  if not isinstance(tasks, Sequence):
246
252
  tasks = [tasks]
@@ -256,7 +262,7 @@ class Controller:
256
262
  thread = threading.Thread(
257
263
  target=self._target,
258
264
  name=f"cook-thread-{i}",
259
- args=(stop, input_queue, output_queue),
265
+ args=(stop, input_queue, output_queue, dry_run),
260
266
  daemon=True,
261
267
  )
262
268
  thread.start()
@@ -288,30 +294,35 @@ class Controller:
288
294
  # Unpack the results.
289
295
  if event.kind == "fail":
290
296
  # Update the status in the database.
291
- params = {
292
- "name": event.task.name,
293
- "last_failed": event.timestamp,
294
- }
295
- self.connection.execute(QUERIES["upsert_task_failed"], params)
296
- self.connection.commit()
297
+ if not dry_run:
298
+ params = {
299
+ "name": event.task.name,
300
+ "last_failed": event.timestamp,
301
+ }
302
+ self.connection.execute(QUERIES["upsert_task_failed"], params)
303
+ self.connection.commit()
297
304
  ex = event.exc_info[1]
298
305
  raise util.FailedTaskError(ex, task=event.task) from ex
299
306
  elif event.kind == "complete":
300
307
  # Update the status in the database.
301
- params = {
302
- "name": event.task.name,
303
- "digest": event.digest,
304
- "last_completed": event.timestamp,
305
- }
306
- self.connection.execute(QUERIES["upsert_task_completed"], params)
307
- self.connection.commit()
308
+ if not dry_run:
309
+ params = {
310
+ "name": event.task.name,
311
+ "digest": event.digest,
312
+ "last_completed": event.timestamp,
313
+ }
314
+ self.connection.execute(
315
+ QUERIES["upsert_task_completed"], params
316
+ )
317
+ self.connection.commit()
308
318
  elif event.kind == "start":
309
- params = {
310
- "name": event.task.name,
311
- "last_started": event.timestamp,
312
- }
313
- self.connection.execute(QUERIES["upsert_task_started"], params)
314
- self.connection.commit()
319
+ if not dry_run:
320
+ params = {
321
+ "name": event.task.name,
322
+ "last_started": event.timestamp,
323
+ }
324
+ self.connection.execute(QUERIES["upsert_task_started"], params)
325
+ self.connection.commit()
315
326
  continue
316
327
  else:
317
328
  raise ValueError(event) # pragma: no cover
@@ -345,7 +356,11 @@ class Controller:
345
356
  raise RuntimeError(f"thread {thread} failed to join")
346
357
 
347
358
  def _target(
348
- self, stop: util.StopEvent, input_queue: Queue, output_queue: Queue
359
+ self,
360
+ stop: util.StopEvent,
361
+ input_queue: Queue,
362
+ output_queue: Queue,
363
+ dry_run: bool = False,
349
364
  ) -> None:
350
365
  LOGGER.debug(f"started thread `{threading.current_thread().name}`")
351
366
  while not stop.is_set():
@@ -365,12 +380,28 @@ class Controller:
365
380
 
366
381
  start = datetime.now()
367
382
  try:
368
- # Execute the task.
369
- LOGGER.log(
370
- logging.DEBUG if task.name.startswith("_") else logging.INFO,
371
- "executing %s ...",
372
- task,
373
- )
383
+ # Execute or simulate the task.
384
+ if dry_run:
385
+ LOGGER.log(
386
+ logging.DEBUG if task.name.startswith("_") else logging.INFO,
387
+ "would execute %s",
388
+ task,
389
+ )
390
+ if task.action:
391
+ LOGGER.log(
392
+ logging.DEBUG
393
+ if task.name.startswith("_")
394
+ else logging.INFO,
395
+ " action: %s",
396
+ task.action,
397
+ )
398
+ else:
399
+ LOGGER.log(
400
+ logging.DEBUG if task.name.startswith("_") else logging.INFO,
401
+ "executing %s ...",
402
+ task,
403
+ )
404
+
374
405
  output_queue.put(
375
406
  Event(
376
407
  kind="start",
@@ -380,15 +411,17 @@ class Controller:
380
411
  exc_info=(None, None, None),
381
412
  )
382
413
  )
383
- task.execute(stop)
384
414
 
385
- # Check that all targets were created.
386
- for target in task.targets:
387
- if not target.is_file():
388
- raise FileNotFoundError(
389
- f"task {task} did not create target {target}"
390
- )
391
- LOGGER.debug("%s created `%s`", task, target)
415
+ if not dry_run:
416
+ task.execute(stop)
417
+
418
+ # Check that all targets were created.
419
+ for target in task.targets:
420
+ if not target.is_file():
421
+ raise FileNotFoundError(
422
+ f"task {task} did not create target {target}"
423
+ )
424
+ LOGGER.debug("%s created `%s`", task, target)
392
425
 
393
426
  # Add the result to the output queue and report success.
394
427
  output_queue.put(
@@ -400,13 +433,14 @@ class Controller:
400
433
  exc_info=(None, None, None),
401
434
  )
402
435
  )
403
- delta = util.format_timedelta(datetime.now() - start)
404
- LOGGER.log(
405
- logging.DEBUG if task.name.startswith("_") else logging.INFO,
406
- "completed %s in %s",
407
- task,
408
- delta,
409
- )
436
+ if not dry_run:
437
+ delta = util.format_timedelta(datetime.now() - start)
438
+ LOGGER.log(
439
+ logging.DEBUG if task.name.startswith("_") else logging.INFO,
440
+ "completed %s in %s",
441
+ task,
442
+ delta,
443
+ )
410
444
  except: # noqa: E722
411
445
  exc_info = sys.exc_info()
412
446
  delta = util.format_timedelta(datetime.now() - start)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cook-build
3
- Version: 0.6.3
3
+ Version: 0.6.4
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
@@ -127,3 +127,66 @@ def test_terrible_recipe(caplog: pytest.LogCaptureFixture) -> None:
127
127
  with pytest.raises(SystemExit), caplog.at_level("CRITICAL"):
128
128
  __main__(["--recipe", str(RECIPES / "terrible.not-py"), "ls"])
129
129
  assert "failed to load recipe" in caplog.text
130
+
131
+
132
+ def test_dry_run_shows_tasks(tmp_wd: Path, caplog: pytest.LogCaptureFixture) -> None:
133
+ """Dry-run should show what would execute without running tasks."""
134
+ with caplog.at_level(logging.INFO):
135
+ __main__(["--recipe", str(RECIPES / "blah.py"), "exec", "--dry-run", "run"])
136
+
137
+ # Should show "would execute" messages
138
+ assert "would execute <task `create_source`" in caplog.text
139
+ assert "would execute <task `compile`" in caplog.text
140
+ assert "would execute <task `link`" in caplog.text
141
+ assert "would execute <task `run`" in caplog.text
142
+
143
+ # Should show actions
144
+ assert "action:" in caplog.text
145
+
146
+ # Should NOT show "executing" or "completed" messages
147
+ assert "executing <task" not in caplog.text
148
+ assert "completed <task" not in caplog.text
149
+
150
+
151
+ def test_dry_run_does_not_create_files(tmp_wd: Path) -> None:
152
+ """Dry-run should not create target files."""
153
+ __main__(["--recipe", str(RECIPES / "blah.py"), "exec", "--dry-run", "link"])
154
+
155
+ # Files should NOT be created
156
+ assert not (tmp_wd / "blah.c").exists()
157
+ assert not (tmp_wd / "blah.o").exists()
158
+ assert not (tmp_wd / "blah").exists()
159
+
160
+
161
+ def test_dry_run_does_not_update_database(tmp_wd: Path) -> None:
162
+ """Dry-run should not update the database."""
163
+ # First, run normally to create database entry
164
+ __main__(["--recipe", str(RECIPES / "blah.py"), "exec", "link"])
165
+
166
+ # Check the database was updated
167
+ __main__(["--recipe", str(RECIPES / "blah.py"), "info", "link"])
168
+
169
+ # Reset the task
170
+ __main__(["--recipe", str(RECIPES / "blah.py"), "reset", "link"])
171
+
172
+ # Now dry-run should not update last_completed
173
+ __main__(["--recipe", str(RECIPES / "blah.py"), "exec", "--dry-run", "link"])
174
+
175
+ # Task should still be stale (database not updated)
176
+ __main__(["--recipe", str(RECIPES / "blah.py"), "info", "--stale", "link"])
177
+
178
+
179
+ def test_dry_run_with_no_stale_tasks(
180
+ tmp_wd: Path, caplog: pytest.LogCaptureFixture
181
+ ) -> None:
182
+ """Dry-run with no stale tasks should be silent."""
183
+ # First, run normally
184
+ __main__(["--recipe", str(RECIPES / "blah.py"), "exec", "link"])
185
+
186
+ # Now dry-run with nothing stale
187
+ caplog.clear()
188
+ with caplog.at_level(logging.INFO):
189
+ __main__(["--recipe", str(RECIPES / "blah.py"), "exec", "--dry-run", "link"])
190
+
191
+ # Should be silent (no "would execute" messages)
192
+ assert "would execute" not in caplog.text
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes