From ed660794da4113a3f3d26f0ffd7a17f4c4be697b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 05:38:58 +0000 Subject: [PATCH 1/9] chore(deps): update wislertt/zerv action to v0.8.14 --- .github/workflows/cd.yml | 6 +++--- .github/workflows/ci.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 063a865..3cb33a2 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -17,7 +17,7 @@ jobs: # ==================================================== semantic-release: name: semantic-release - uses: wislertt/zerv/.github/workflows/shared-semantic-release.yml@643777b9a3573497fd9f8edabe340f256acce981 # v0.8.13 + uses: wislertt/zerv/.github/workflows/shared-semantic-release.yml@19299eac1a2c1b20717f1bf5c1f2a19ffe17f741 # v0.8.14 with: allowed_workflow_dispatch_branches: '["main"]' fail_on_invalid_workflow_dispatch_ref: true @@ -25,11 +25,11 @@ jobs: zerv-versioning: needs: semantic-release if: needs.semantic-release.outputs.is_valid_semantic_release == 'true' - uses: wislertt/zerv/.github/workflows/shared-zerv-versioning.yml@643777b9a3573497fd9f8edabe340f256acce981 # v0.8.13 + uses: wislertt/zerv/.github/workflows/shared-zerv-versioning.yml@19299eac1a2c1b20717f1bf5c1f2a19ffe17f741 # v0.8.14 create-version-prefix-tags: needs: zerv-versioning - uses: wislertt/zerv/.github/workflows/shared-create-tags.yml@643777b9a3573497fd9f8edabe340f256acce981 # v0.8.13 + uses: wislertt/zerv/.github/workflows/shared-create-tags.yml@19299eac1a2c1b20717f1bf5c1f2a19ffe17f741 # v0.8.14 with: tags: >- [ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c0e527..6fedf02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: # ==================================================== check-pre-release: name: check-pre-release - uses: wislertt/zerv/.github/workflows/shared-check-pr-label-and-branch.yml@643777b9a3573497fd9f8edabe340f256acce981 # v0.8.13 + uses: wislertt/zerv/.github/workflows/shared-check-pr-label-and-branch.yml@19299eac1a2c1b20717f1bf5c1f2a19ffe17f741 # v0.8.14 with: target_label: "pre-release" branch_prefixes: '["release/"]' @@ -23,7 +23,7 @@ jobs: zerv-versioning: name: zerv-versioning needs: check-pre-release - uses: wislertt/zerv/.github/workflows/shared-zerv-versioning.yml@643777b9a3573497fd9f8edabe340f256acce981 # v0.8.13 + uses: wislertt/zerv/.github/workflows/shared-zerv-versioning.yml@19299eac1a2c1b20717f1bf5c1f2a19ffe17f741 # v0.8.14 with: schema: >- ${{ (needs.check-pre-release.outputs.is_valid == 'true' @@ -34,7 +34,7 @@ jobs: name: tag-pre-release needs: [zerv-versioning, check-pre-release] if: needs.check-pre-release.outputs.is_valid == 'true' - uses: wislertt/zerv/.github/workflows/shared-create-tags.yml@643777b9a3573497fd9f8edabe340f256acce981 # v0.8.13 + uses: wislertt/zerv/.github/workflows/shared-create-tags.yml@19299eac1a2c1b20717f1bf5c1f2a19ffe17f741 # v0.8.14 with: tags: '["${{ fromJson(needs.zerv-versioning.outputs.versions).v_semver }}"]' From 1427b904c5d8ca8f6febf42fc05a552275c525cf Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Thu, 16 Apr 2026 16:02:26 +0700 Subject: [PATCH 2/9] fix: fix sigint for run --- bakefile.py | 10 ++ src/bake/__init__.py | 2 + src/bake/ui/run/main.py | 93 ++++++++++---- tests/unit/bake/ui/run/test_run.py | 194 ++++++++++++++++++++++++++++- uv.lock | 10 +- 5 files changed, 277 insertions(+), 32 deletions(-) diff --git a/bakefile.py b/bakefile.py index 9ab6815..7a4b51d 100644 --- a/bakefile.py +++ b/bakefile.py @@ -64,6 +64,16 @@ def uvx_install_bake_local( bakebook = MyBakebook() +@bakebook.command() +def test_child_kill(): + """Reproduce Ctrl+C child process not being killed bug. + + Runs _test_child_script.py via ctx.run(). Press Ctrl+C to test. + If [CHILD] lines keep appearing after [PARENT] exits, the bug exists. + """ + bakebook.ctx.run("python _test_child_script.py") + + @bakebook.command() def uvx_install_bake(): bakebook.ctx.run("uv tool install 'bakefile[lib]' --reinstall") diff --git a/src/bake/__init__.py b/src/bake/__init__.py index f5e80cc..c3b6018 100644 --- a/src/bake/__init__.py +++ b/src/bake/__init__.py @@ -30,6 +30,7 @@ DEFAULT_BAKE_LOG_PRETTY, DEFAULT_BAKE_LOG_VERBOSITY, ) +from bake.utils.settings import bake_settings from bake.utils.unwrap import unwrap __version__ = _get_version() @@ -48,6 +49,7 @@ "LogKey", "__version__", "argv_to_multiline_cmd", + "bake_settings", "capsys_to_logs", "capsys_to_logs_pretty", "capture_to_logs", diff --git a/src/bake/ui/run/main.py b/src/bake/ui/run/main.py index e79533f..66fa6d0 100644 --- a/src/bake/ui/run/main.py +++ b/src/bake/ui/run/main.py @@ -8,6 +8,7 @@ import tempfile import threading import time +import types from dataclasses import dataclass from pathlib import Path from typing import Literal, overload @@ -599,13 +600,14 @@ def _run_with_split( **kwargs, ) - try: - setup.proc.wait(timeout=timeout) - except (subprocess.TimeoutExpired, KeyboardInterrupt): - _kill_process_tree(setup.proc) - setup.proc.wait() - setup.splitter.finalize(setup.threads) - raise + with _sigint_guard(setup.proc): + try: + setup.proc.wait(timeout=timeout) + except (subprocess.TimeoutExpired, KeyboardInterrupt): + _kill_process_tree(setup.proc) + setup.proc.wait() + setup.splitter.finalize(setup.threads) + raise setup.splitter.finalize(setup.threads) @@ -613,10 +615,17 @@ def _run_with_split( def _kill_process_tree(proc: subprocess.Popen) -> None: - """Kill a process and all its children (cross-platform).""" + """Kill a process and all its children. + + On Unix, sends SIGTERM to the process group (because ``sh -c`` does not + forward signals to children), then escalates to SIGKILL after 5s. + On Windows, uses ``taskkill /F /T`` to kill the process tree. + Idempotent — safe to call on already-dead processes. + """ + if proc.poll() is not None: + return + if sys.platform == "win32": - # On Windows, proc.kill() only kills the parent, not children. - # Use taskkill to kill the entire process tree. try: subprocess.run( ["taskkill", "/F", "/T", "/PID", str(proc.pid)], @@ -626,20 +635,49 @@ def _kill_process_tree(proc: subprocess.Popen) -> None: except (subprocess.TimeoutExpired, FileNotFoundError): # pragma: no cover proc.kill() # pragma: no cover else: - # On Unix, kill the entire process group to handle grandchildren. - # The process group ID (PGID) is the same as the process ID for the leader. try: pgid = os.getpgid(proc.pid) os.killpg(pgid, signal.SIGTERM) - # Give processes a moment to terminate gracefully - time.sleep(0.01) - # If still running, force kill - with contextlib.suppress(ProcessLookupError): - os.killpg(pgid, signal.SIGKILL) except (ProcessLookupError, PermissionError): - # Process already terminated or permission denied for process group. - # Fall back to killing the process directly. proc.kill() + return + + try: + proc.wait(timeout=5) + return + except subprocess.TimeoutExpired: + pass + except KeyboardInterrupt: + pass + + try: + os.killpg(pgid, signal.SIGKILL) + except (ProcessLookupError, PermissionError): + proc.kill() + + +@contextlib.contextmanager +def _sigint_guard(proc: subprocess.Popen): + """Install SIGINT handler to kill the process tree on Ctrl+C. + + Needed because ``proc.wait()``/``proc.communicate()`` don't reliably + raise ``KeyboardInterrupt`` when ``capture_output=False``. + """ + + def _on_sigint(signum: int, frame: types.FrameType | None) -> None: + _ = signum, frame + _kill_process_tree(proc) + raise KeyboardInterrupt + + if threading.current_thread() is not threading.main_thread(): + yield + return + + old_handler = signal.signal(signal.SIGINT, _on_sigint) + try: + yield + finally: + signal.signal(signal.SIGINT, old_handler) def _run_without_split( @@ -668,12 +706,17 @@ def _run_without_split( **kwargs, ) - try: - stdout_bytes, stderr_bytes = proc.communicate(timeout=timeout) - except (subprocess.TimeoutExpired, KeyboardInterrupt): - _kill_process_tree(proc) - proc.wait() - raise + with _sigint_guard(proc): + try: + stdout_bytes, stderr_bytes = proc.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + _kill_process_tree(proc) + proc.wait() + raise + except KeyboardInterrupt: + # SIGINT handler already called _kill_process_tree, just wait for proc. + proc.wait() + raise # Handle output based on capture_output and encoding if capture_output: diff --git a/tests/unit/bake/ui/run/test_run.py b/tests/unit/bake/ui/run/test_run.py index 9611936..17b2d73 100644 --- a/tests/unit/bake/ui/run/test_run.py +++ b/tests/unit/bake/ui/run/test_run.py @@ -1,6 +1,8 @@ +import contextlib import inspect import logging import os +import signal import subprocess import sys from pathlib import Path @@ -1241,6 +1243,9 @@ def test_ctrl_c_with_stream_true_kills_process_tree(self) -> None: with ( mock.patch.object(main.subprocess, "Popen", return_value=mock_proc), mock.patch.object(main, "_kill_process_tree") as mock_kill, + mock.patch.object( + main, "_sigint_guard", side_effect=lambda _: contextlib.nullcontext() + ), ): with pytest.raises(KeyboardInterrupt): run(["echo", "test"], stream=True, capture_output=True, echo=False) @@ -1248,7 +1253,8 @@ def test_ctrl_c_with_stream_true_kills_process_tree(self) -> None: mock_kill.assert_called_once_with(mock_proc) def test_ctrl_c_with_stream_false_kills_process_tree(self) -> None: - """Test that KeyboardInterrupt during run() with stream=False calls _kill_process_tree.""" + """Test that KeyboardInterrupt during run() with stream=False + waits for proc and re-raises.""" mock_proc = mock.Mock(spec=subprocess.Popen) mock_proc.communicate.side_effect = KeyboardInterrupt() mock_proc.pid = 12345 @@ -1258,9 +1264,193 @@ def test_ctrl_c_with_stream_false_kills_process_tree(self) -> None: with ( mock.patch.object(main.subprocess, "Popen", return_value=mock_proc), mock.patch.object(main, "_kill_process_tree") as mock_kill, + mock.patch.object( + main, "_sigint_guard", side_effect=lambda _: contextlib.nullcontext() + ), ): with pytest.raises(KeyboardInterrupt): run(["echo", "test"], stream=False, capture_output=True, echo=False) - mock_kill.assert_called_once_with(mock_proc) + # _kill_process_tree is called by _sigint_guard's signal handler, + # not by the except block (which expects the guard to have done it). + # With passthrough guard, only proc.wait is called. + mock_kill.assert_not_called() mock_proc.wait.assert_called_once() + + +# ============================================================================ +# _sigint_guard Tests +# ============================================================================ + + +class TestSigintGuard: + def test_sigint_guard_calls_kill_process_tree(self) -> None: + mock_proc = mock.Mock(spec=subprocess.Popen) + mock_proc.poll.return_value = None + + with ( + mock.patch("bake.ui.run.main._kill_process_tree") as mock_kill, + mock.patch("bake.ui.run.main.signal") as mock_signal, + ): + installed_handler = None + + def capture_handler(sig, handler): + _ = sig + nonlocal installed_handler + installed_handler = handler + return mock.Mock() + + mock_signal.signal.side_effect = capture_handler + + with main._sigint_guard(mock_proc): + assert installed_handler is not None + with pytest.raises(KeyboardInterrupt): + installed_handler(signal.SIGINT, None) + + mock_kill.assert_called_once_with(mock_proc) + mock_signal.signal.assert_called() + + def test_sigint_guard_idempotent_on_dead_process(self) -> None: + mock_proc = mock.Mock(spec=subprocess.Popen) + mock_proc.poll.return_value = 0 + + with mock.patch("bake.ui.run.main.signal") as mock_signal: + installed_handler = None + + def capture_handler(sig, handler): + _ = sig + nonlocal installed_handler + installed_handler = handler + return mock.Mock() + + mock_signal.signal.side_effect = capture_handler + + with main._sigint_guard(mock_proc), pytest.raises(KeyboardInterrupt): + assert installed_handler is not None + installed_handler(signal.SIGINT, None) + + def test_sigint_guard_restores_old_handler(self) -> None: + mock_proc = mock.Mock(spec=subprocess.Popen) + old_handler = mock.Mock() + call_order = [] + + def mock_signal_fn(sig, handler): + call_order.append(("install", sig, handler)) + return old_handler + + with mock.patch("bake.ui.run.main.signal") as mock_signal: + mock_signal.SIGINT = signal.SIGINT + mock_signal.signal.side_effect = mock_signal_fn + + with main._sigint_guard(mock_proc): + pass + + assert call_order[0][0] == "install" + assert call_order[1][0] == "install" + assert call_order[1][2] is old_handler + + def test_sigint_guard_restores_handler_on_exception(self) -> None: + mock_proc = mock.Mock(spec=subprocess.Popen) + old_handler = mock.Mock() + + with mock.patch("bake.ui.run.main.signal") as mock_signal: + mock_signal.signal.return_value = old_handler + + with pytest.raises(RuntimeError), main._sigint_guard(mock_proc): + raise RuntimeError("test") + + last_call = mock_signal.signal.call_args_list[-1] + assert last_call[0][1] is old_handler + + +# ============================================================================ +# _kill_process_tree Tests +# ============================================================================ + + +class TestKillProcessTree: + def test_returns_early_if_process_already_dead(self) -> None: + mock_proc = mock.Mock(spec=subprocess.Popen) + mock_proc.poll.return_value = 0 + + main._kill_process_tree(mock_proc) + + mock_proc.kill.assert_not_called() + + @pytest.mark.skipif(sys.platform == "win32", reason="Unix-only") + def test_unix_sends_sigterm_then_sigkill(self) -> None: + mock_proc = mock.Mock(spec=subprocess.Popen) + mock_proc.poll.return_value = None + mock_proc.pid = 12345 + mock_proc.wait.side_effect = subprocess.TimeoutExpired("cmd", 5) + + with ( + mock.patch("os.getpgid", return_value=12345), + mock.patch("os.killpg") as mock_killpg, + ): + main._kill_process_tree(mock_proc) + + assert mock_killpg.call_count == 2 + mock_killpg.assert_any_call(12345, signal.SIGTERM) + mock_killpg.assert_any_call(12345, signal.SIGKILL) + + @pytest.mark.skipif(sys.platform == "win32", reason="Unix-only") + def test_unix_no_sigkill_if_process_exits_gracefully(self) -> None: + mock_proc = mock.Mock(spec=subprocess.Popen) + mock_proc.poll.return_value = None + mock_proc.pid = 12345 + mock_proc.wait.return_value = 0 + + with ( + mock.patch("os.getpgid", return_value=12345), + mock.patch("os.killpg") as mock_killpg, + ): + main._kill_process_tree(mock_proc) + + mock_killpg.assert_called_once_with(12345, signal.SIGTERM) + + @pytest.mark.skipif(sys.platform == "win32", reason="Unix-only") + def test_unix_second_ctrl_c_escalates_to_sigkill(self) -> None: + mock_proc = mock.Mock(spec=subprocess.Popen) + mock_proc.poll.return_value = None + mock_proc.pid = 12345 + mock_proc.wait.side_effect = KeyboardInterrupt() + + with ( + mock.patch("os.getpgid", return_value=12345), + mock.patch("os.killpg") as mock_killpg, + ): + main._kill_process_tree(mock_proc) + + assert mock_killpg.call_count == 2 + mock_killpg.assert_any_call(12345, signal.SIGTERM) + mock_killpg.assert_any_call(12345, signal.SIGKILL) + + @pytest.mark.skipif(sys.platform == "win32", reason="Unix-only") + def test_unix_fallback_to_proc_kill_on_permission_error(self) -> None: + mock_proc = mock.Mock(spec=subprocess.Popen) + mock_proc.poll.return_value = None + mock_proc.pid = 12345 + + with mock.patch("os.getpgid", side_effect=ProcessLookupError): + main._kill_process_tree(mock_proc) + mock_proc.kill.assert_called_once() + + @pytest.mark.skipif(sys.platform == "win32", reason="Unix-only") + def test_unix_sigkill_fallback_on_permission_error(self) -> None: + mock_proc = mock.Mock(spec=subprocess.Popen) + mock_proc.poll.return_value = None + mock_proc.pid = 12345 + mock_proc.wait.side_effect = subprocess.TimeoutExpired("cmd", 5) + + def killpg_side_effect(pgid, sig): + _ = pgid + if sig == signal.SIGKILL: + raise PermissionError("not allowed") + + with ( + mock.patch("os.getpgid", return_value=12345), + mock.patch("os.killpg", side_effect=killpg_side_effect), + ): + main._kill_process_tree(mock_proc) + mock_proc.kill.assert_called_once() diff --git a/uv.lock b/uv.lock index 75605cd..42a1253 100644 --- a/uv.lock +++ b/uv.lock @@ -132,7 +132,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "(python_full_version < '3.11' and implementation_name != 'PyPy' and sys_platform == 'emscripten') or (python_full_version < '3.11' and implementation_name != 'PyPy' and sys_platform == 'win32') or (implementation_name != 'PyPy' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -333,7 +333,7 @@ name = "cryptography" version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "cffi", marker = "(python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'emscripten') or (python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'win32') or (platform_python_implementation != 'PyPy' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } @@ -460,7 +460,7 @@ name = "importlib-metadata" version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } wheels = [ @@ -1394,8 +1394,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, + { name = "cryptography", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "jeepney", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ From 80ab607f923766533ab84b86deff8c4372167d99 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Thu, 16 Apr 2026 16:35:56 +0700 Subject: [PATCH 3/9] feat: add BakebookMixin --- bakefile.py | 23 +-- src/bake/__init__.py | 3 +- src/bake/bakebook/bakebook.py | 24 +++ .../unit/bake/bakebook/test_bakebook_mixin.py | 172 ++++++++++++++++++ 4 files changed, 205 insertions(+), 17 deletions(-) create mode 100644 tests/unit/bake/bakebook/test_bakebook_mixin.py diff --git a/bakefile.py b/bakefile.py index 7a4b51d..2b2bfac 100644 --- a/bakefile.py +++ b/bakefile.py @@ -5,9 +5,14 @@ import typer import zerv -from pydantic import SecretBytes, SecretStr -from bake import DEFAULT_BAKE_LOG, DEFAULT_BAKE_LOG_PRETTY, command, console, params +from bake import ( + DEFAULT_BAKE_LOG, + DEFAULT_BAKE_LOG_PRETTY, + command, + console, + params, +) from bakelib import GitHubActionsTools, PythonLibSpace logger = logging.getLogger(__name__) @@ -18,10 +23,6 @@ class MyBakebook(GitHubActionsTools, PythonLibSpace): bake_log_verbosity: params.BakeLogVerbosityField = 3 bake_log_pretty: bool = DEFAULT_BAKE_LOG_PRETTY - # Secret - some_secret_str: SecretStr = SecretStr("my_str_secret") - some_secret_bytes: SecretBytes = SecretBytes(b"my_bytes_secret") - def _get_mise_tools(self) -> set[str]: mise_tools = super()._get_mise_tools() mise_tools.remove("pipx:bakefile") @@ -64,16 +65,6 @@ def uvx_install_bake_local( bakebook = MyBakebook() -@bakebook.command() -def test_child_kill(): - """Reproduce Ctrl+C child process not being killed bug. - - Runs _test_child_script.py via ctx.run(). Press Ctrl+C to test. - If [CHILD] lines keep appearing after [PARENT] exits, the bug exists. - """ - bakebook.ctx.run("python _test_child_script.py") - - @bakebook.command() def uvx_install_bake(): bakebook.ctx.run("uv tool install 'bakefile[lib]' --reinstall") diff --git a/src/bake/__init__.py b/src/bake/__init__.py index c3b6018..afffa71 100644 --- a/src/bake/__init__.py +++ b/src/bake/__init__.py @@ -1,5 +1,5 @@ from bake import _params as params -from bake.bakebook.bakebook import Bakebook, GroupKwargs +from bake.bakebook.bakebook import Bakebook, BakebookMixin, GroupKwargs from bake.bakebook.decorator import command from bake.bakebook.utils import parse_bake_log, serialize_bake_log from bake.cli.common.context import BakeCommand, Context, context @@ -42,6 +42,7 @@ "UNPARSABLE_LINE", "BakeCommand", "Bakebook", + "BakebookMixin", "Context", "GCPJsonSink", "GCPLogKey", diff --git a/src/bake/bakebook/bakebook.py b/src/bake/bakebook/bakebook.py index 6c1642a..8fb9625 100644 --- a/src/bake/bakebook/bakebook.py +++ b/src/bake/bakebook/bakebook.py @@ -144,6 +144,30 @@ def registered_commands(self) -> set[str]: bakebook_model_config_type = ClassVar[SettingsConfigDict] +class BakebookMixin(BaseSettings): + """Base class for composable Bakebook mixins. + + Use instead of inheriting from Bakebook when creating mixin classes. + Multiple BakebookMixin subclasses can be composed with a single Bakebook + subclass without MRO conflicts. + + Supports ``@command()`` decorator for contributing commands. + """ + + def __init__(self, **kwargs: Any) -> None: + if not isinstance(self, Bakebook): + raise TypeError("BakebookMixin can only be used with Bakebook subclasses") + super().__init__(**kwargs) + + @property + def ctx(self) -> Context: + if not isinstance(self, Bakebook): + raise TypeError( # pragma: no cover + "BakebookMixin can only be used with Bakebook subclasses" + ) + return Bakebook.ctx.fget(self) + + class Bakebook(BaseSettings): model_config: bakebook_model_config_type = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore" diff --git a/tests/unit/bake/bakebook/test_bakebook_mixin.py b/tests/unit/bake/bakebook/test_bakebook_mixin.py new file mode 100644 index 0000000..6ca8c3c --- /dev/null +++ b/tests/unit/bake/bakebook/test_bakebook_mixin.py @@ -0,0 +1,172 @@ +import types + +import pytest + +from bake import Bakebook, BakebookMixin, Context, command +from tests.unit.bake.bakebook.utils import ExpectedCommand, assert_commands + + +def test_mixin_fields_accessible() -> None: + class ServiceMixin(BakebookMixin): + service_name: str = "my-service" + + class MyBakebook(ServiceMixin, Bakebook): + pass + + bakebook = MyBakebook() + assert bakebook.service_name == "my-service" + + +def test_multiple_mixins() -> None: + class EnvMixin(BakebookMixin): + env: str = "dev" + + class RegionMixin(BakebookMixin): + region: str = "us-central1" + + class MyBakebook(EnvMixin, RegionMixin, Bakebook): + pass + + bakebook = MyBakebook() + assert bakebook.env == "dev" + assert bakebook.region == "us-central1" + + +def test_mixin_with_command() -> None: + class DeployMixin(BakebookMixin): + deploy_env: str = "staging" + + @command() + def deploy(self) -> str: + return f"deploying to {self.deploy_env}" + + class MyBakebook(DeployMixin, Bakebook): + pass + + bakebook = MyBakebook() + assert bakebook.deploy() == "deploying to staging" + + assert_commands( + bakebook, + { + "deploy": ExpectedCommand( + name="deploy", command_type=types.MethodType, output="deploying to staging" + ), + }, + ) + + +def test_multiple_mixins_with_commands() -> None: + class BuildMixin(BakebookMixin): + @command() + def build(self) -> str: + return "built" + + class TestMixin(BakebookMixin): + @command() + def test(self) -> str: + return "tested" + + class MyBakebook(BuildMixin, TestMixin, Bakebook): + pass + + bakebook = MyBakebook() + assert bakebook.build() == "built" + assert bakebook.test() == "tested" + + assert_commands( + bakebook, + { + "build": ExpectedCommand(name="build", command_type=types.MethodType, output="built"), + "test": ExpectedCommand(name="test", command_type=types.MethodType, output="tested"), + }, + ) + + +def test_mixin_fields_overridable() -> None: + class EnvMixin(BakebookMixin): + env: str = "dev" + + class MyBakebook(EnvMixin, Bakebook): + pass + + bakebook = MyBakebook(env="prod") + assert bakebook.env == "prod" + + +def test_mixin_with_bakebook_subclass() -> None: + class BaseBakebook(Bakebook): + base_field: str = "base" + + @command() + def base_cmd(self) -> str: + return f"base: {self.base_field}" + + class ServiceMixin(BakebookMixin): + service: str = "api" + + class FinalBakebook(ServiceMixin, BaseBakebook): + pass + + bakebook = FinalBakebook() + assert bakebook.base_field == "base" + assert bakebook.service == "api" + + assert_commands( + bakebook, + { + "base_cmd": ExpectedCommand( + name="base_cmd", command_type=types.MethodType, output="base: base" + ), + }, + ) + + +def test_mixin_mro_order() -> None: + class LeftMixin(BakebookMixin): + value: str = "left" + + class RightMixin(BakebookMixin): + value: str = "right" + + class MyBakebook(LeftMixin, RightMixin, Bakebook): + pass + + bakebook = MyBakebook() + assert bakebook.value == "left" + + +def test_mixin_without_bakebook_raises() -> None: + class BadMixin(BakebookMixin): + field: str = "bad" + + with pytest.raises(TypeError, match="BakebookMixin can only be used with Bakebook subclasses"): + BadMixin() + + +def test_mixin_ctx_returns_context(mock_ctx: Context) -> None: + class MyMixin(BakebookMixin): + pass + + class MyBakebook(MyMixin, Bakebook): + pass + + bakebook = MyBakebook() + with mock_ctx: + result = bakebook.ctx + assert result is mock_ctx + assert isinstance(result, Context) + + +def test_mixin_ctx_raises_without_click_context() -> None: + class MyMixin(BakebookMixin): + pass + + class MyBakebook(MyMixin, Bakebook): + pass + + bakebook = MyBakebook() + from bake.utils.exceptions import ContextNotAvailableError + + with pytest.raises(ContextNotAvailableError, match="Command context not available"): + _ = bakebook.ctx From 89570e976edb8b24ba40771fbeb516f19bea88ef Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Thu, 16 Apr 2026 16:43:14 +0700 Subject: [PATCH 4/9] fix: fix test in python 3.10 --- tests/unit/bake/ui/run/test_run.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/bake/ui/run/test_run.py b/tests/unit/bake/ui/run/test_run.py index 17b2d73..8bcf927 100644 --- a/tests/unit/bake/ui/run/test_run.py +++ b/tests/unit/bake/ui/run/test_run.py @@ -1289,8 +1289,8 @@ def test_sigint_guard_calls_kill_process_tree(self) -> None: mock_proc.poll.return_value = None with ( - mock.patch("bake.ui.run.main._kill_process_tree") as mock_kill, - mock.patch("bake.ui.run.main.signal") as mock_signal, + mock.patch.object(main, "_kill_process_tree") as mock_kill, + mock.patch.object(main, "signal") as mock_signal, ): installed_handler = None @@ -1314,7 +1314,7 @@ def test_sigint_guard_idempotent_on_dead_process(self) -> None: mock_proc = mock.Mock(spec=subprocess.Popen) mock_proc.poll.return_value = 0 - with mock.patch("bake.ui.run.main.signal") as mock_signal: + with mock.patch.object(main, "signal") as mock_signal: installed_handler = None def capture_handler(sig, handler): @@ -1338,7 +1338,7 @@ def mock_signal_fn(sig, handler): call_order.append(("install", sig, handler)) return old_handler - with mock.patch("bake.ui.run.main.signal") as mock_signal: + with mock.patch.object(main, "signal") as mock_signal: mock_signal.SIGINT = signal.SIGINT mock_signal.signal.side_effect = mock_signal_fn @@ -1353,7 +1353,7 @@ def test_sigint_guard_restores_handler_on_exception(self) -> None: mock_proc = mock.Mock(spec=subprocess.Popen) old_handler = mock.Mock() - with mock.patch("bake.ui.run.main.signal") as mock_signal: + with mock.patch.object(main, "signal") as mock_signal: mock_signal.signal.return_value = old_handler with pytest.raises(RuntimeError), main._sigint_guard(mock_proc): From a23294b38eb8eaa6ca7b61dc9c75e1892148cba8 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Thu, 16 Apr 2026 16:46:14 +0700 Subject: [PATCH 5/9] chore: update deps --- examples/python-package/uv.lock | 14 +++++++------- uv.lock | 24 ++++++++++++------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/examples/python-package/uv.lock b/examples/python-package/uv.lock index 48c4d48..8124505 100644 --- a/examples/python-package/uv.lock +++ b/examples/python-package/uv.lock @@ -1291,15 +1291,15 @@ wheels = [ [[package]] name = "zerv-version" -version = "0.8.13" +version = "0.8.14" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/15/2ce4083088ce51a7764ecfa6f92c937391827dd53d603475f5c1fe069020/zerv_version-0.8.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6e4d960d5e08874731ff9154d8d5fb5ead3db21fe4029acd34966ff59aeeaf44", size = 2092587, upload-time = "2026-04-15T04:00:51.593Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/202063d15b5b2311fb268f18492b4ca8a4b6ba9ee056c48242151c017a5e/zerv_version-0.8.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8fbef64fcd9f0f77e0ae671287b4651fe72acd821c13b5064d94a8673a60eedb", size = 1991838, upload-time = "2026-04-15T03:58:41.447Z" }, - { url = "https://files.pythonhosted.org/packages/19/46/66f08ee83ba3d88dddef9421ccc6b5e8b4c28ce560d8d0bd252fd060d6b8/zerv_version-0.8.13-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:b918d6fc814fc785ac160e840c5bb5d29e3888a0f345412193eefa1677bf61d5", size = 2028066, upload-time = "2026-04-15T03:58:48.77Z" }, - { url = "https://files.pythonhosted.org/packages/02/db/6582726c2e2e4cdd083f2cacb90d0cfff86b520513d6e26bdba8447cc623/zerv_version-0.8.13-py3-none-manylinux_2_39_x86_64.whl", hash = "sha256:cb4dabc251c08796bf2737843ab7abb46c9d491dab025a7e75c54c4b8e95b9fc", size = 2180932, upload-time = "2026-04-15T03:58:26.541Z" }, - { url = "https://files.pythonhosted.org/packages/16/6c/ebb5e9737327738eced867f03a06f67b2d26d27eb04febf3656643375996/zerv_version-0.8.13-py3-none-win_amd64.whl", hash = "sha256:af4f4fae52aafab9e93c4298bdc818b27da61ea0d55f856e9e38e75b66e0aa1e", size = 2077771, upload-time = "2026-04-15T04:01:29.058Z" }, - { url = "https://files.pythonhosted.org/packages/4d/62/0c73f76f02ff7c2468f3f56f13030c7c79a85cdb84fbc18f2a1b73c367d0/zerv_version-0.8.13-py3-none-win_arm64.whl", hash = "sha256:e5bb6235f244bf4e29f2f2f110716cb9ec1be27d55d36297390903c10f62893e", size = 1956095, upload-time = "2026-04-15T04:01:55.648Z" }, + { url = "https://files.pythonhosted.org/packages/fe/92/3cc63e00c4d9664b5e9b6849fb7eeb9ba9c196e7d71b63c412f0b690a35e/zerv_version-0.8.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:23d040820ad6c63d766e33cdc7f188c0ab5bb655b0e92c9a6e03a24162029f11", size = 2092893, upload-time = "2026-04-16T01:37:48.394Z" }, + { url = "https://files.pythonhosted.org/packages/5b/73/d7837e431794fe3cd1685f4cd3f917e09092001fa71ee7e936b8dc195083/zerv_version-0.8.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5443a0791dce93e7d4de200e0f8d722939b42001c99bdaec1834992e2b1335ca", size = 1991311, upload-time = "2026-04-16T01:39:20.174Z" }, + { url = "https://files.pythonhosted.org/packages/be/d6/f2ec1eca10eaf121c748387906a8f28219835194e9f2eaa80af8b92f635f/zerv_version-0.8.14-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:341dbee6cf229e60fadb0e73a1b4840b009cde25ac13acee1e3fe954a1c27e9e", size = 2028033, upload-time = "2026-04-16T01:38:25.915Z" }, + { url = "https://files.pythonhosted.org/packages/47/60/06cecc508045feaac6bc6502839eb68520a9798fdee1b83f69bb8d2d8c9b/zerv_version-0.8.14-py3-none-manylinux_2_39_x86_64.whl", hash = "sha256:6a3fffa95db79d8b416c03de664c26e12c86d2c245761506a6ad10d7c2072c27", size = 2181130, upload-time = "2026-04-16T01:39:36.766Z" }, + { url = "https://files.pythonhosted.org/packages/4c/22/3a95963fbc5f18d5717b101be6d38c162379de981b481050aae233c11e4c/zerv_version-0.8.14-py3-none-win_amd64.whl", hash = "sha256:8ba279fd883ccaa28a99ded8eabc7d42f525f1d0e9a93736d64587990fb0f01f", size = 2077733, upload-time = "2026-04-16T01:40:14.345Z" }, + { url = "https://files.pythonhosted.org/packages/af/b1/8d6a28a4a835984f520dfd91f89e2489b1e6542221cc3264a977e26a09d0/zerv_version-0.8.14-py3-none-win_arm64.whl", hash = "sha256:17c9b4e17951936581a9c3429e869b31bd7a3ea1d9b1cfc2e2b21782119ba473", size = 1955308, upload-time = "2026-04-16T01:39:54.733Z" }, ] [[package]] diff --git a/uv.lock b/uv.lock index 42a1253..b1ee14f 100644 --- a/uv.lock +++ b/uv.lock @@ -132,7 +132,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "(python_full_version < '3.11' and implementation_name != 'PyPy' and sys_platform == 'emscripten') or (python_full_version < '3.11' and implementation_name != 'PyPy' and sys_platform == 'win32') or (implementation_name != 'PyPy' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -333,7 +333,7 @@ name = "cryptography" version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "(python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'emscripten') or (python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'win32') or (platform_python_implementation != 'PyPy' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } @@ -460,7 +460,7 @@ name = "importlib-metadata" version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "python_full_version < '3.14'" }, + { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } wheels = [ @@ -1394,8 +1394,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, - { name = "jeepney", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cryptography" }, + { name = "jeepney" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ @@ -1626,15 +1626,15 @@ wheels = [ [[package]] name = "zerv-version" -version = "0.8.13" +version = "0.8.14" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/15/2ce4083088ce51a7764ecfa6f92c937391827dd53d603475f5c1fe069020/zerv_version-0.8.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6e4d960d5e08874731ff9154d8d5fb5ead3db21fe4029acd34966ff59aeeaf44", size = 2092587, upload-time = "2026-04-15T04:00:51.593Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/202063d15b5b2311fb268f18492b4ca8a4b6ba9ee056c48242151c017a5e/zerv_version-0.8.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8fbef64fcd9f0f77e0ae671287b4651fe72acd821c13b5064d94a8673a60eedb", size = 1991838, upload-time = "2026-04-15T03:58:41.447Z" }, - { url = "https://files.pythonhosted.org/packages/19/46/66f08ee83ba3d88dddef9421ccc6b5e8b4c28ce560d8d0bd252fd060d6b8/zerv_version-0.8.13-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:b918d6fc814fc785ac160e840c5bb5d29e3888a0f345412193eefa1677bf61d5", size = 2028066, upload-time = "2026-04-15T03:58:48.77Z" }, - { url = "https://files.pythonhosted.org/packages/02/db/6582726c2e2e4cdd083f2cacb90d0cfff86b520513d6e26bdba8447cc623/zerv_version-0.8.13-py3-none-manylinux_2_39_x86_64.whl", hash = "sha256:cb4dabc251c08796bf2737843ab7abb46c9d491dab025a7e75c54c4b8e95b9fc", size = 2180932, upload-time = "2026-04-15T03:58:26.541Z" }, - { url = "https://files.pythonhosted.org/packages/16/6c/ebb5e9737327738eced867f03a06f67b2d26d27eb04febf3656643375996/zerv_version-0.8.13-py3-none-win_amd64.whl", hash = "sha256:af4f4fae52aafab9e93c4298bdc818b27da61ea0d55f856e9e38e75b66e0aa1e", size = 2077771, upload-time = "2026-04-15T04:01:29.058Z" }, - { url = "https://files.pythonhosted.org/packages/4d/62/0c73f76f02ff7c2468f3f56f13030c7c79a85cdb84fbc18f2a1b73c367d0/zerv_version-0.8.13-py3-none-win_arm64.whl", hash = "sha256:e5bb6235f244bf4e29f2f2f110716cb9ec1be27d55d36297390903c10f62893e", size = 1956095, upload-time = "2026-04-15T04:01:55.648Z" }, + { url = "https://files.pythonhosted.org/packages/fe/92/3cc63e00c4d9664b5e9b6849fb7eeb9ba9c196e7d71b63c412f0b690a35e/zerv_version-0.8.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:23d040820ad6c63d766e33cdc7f188c0ab5bb655b0e92c9a6e03a24162029f11", size = 2092893, upload-time = "2026-04-16T01:37:48.394Z" }, + { url = "https://files.pythonhosted.org/packages/5b/73/d7837e431794fe3cd1685f4cd3f917e09092001fa71ee7e936b8dc195083/zerv_version-0.8.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5443a0791dce93e7d4de200e0f8d722939b42001c99bdaec1834992e2b1335ca", size = 1991311, upload-time = "2026-04-16T01:39:20.174Z" }, + { url = "https://files.pythonhosted.org/packages/be/d6/f2ec1eca10eaf121c748387906a8f28219835194e9f2eaa80af8b92f635f/zerv_version-0.8.14-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:341dbee6cf229e60fadb0e73a1b4840b009cde25ac13acee1e3fe954a1c27e9e", size = 2028033, upload-time = "2026-04-16T01:38:25.915Z" }, + { url = "https://files.pythonhosted.org/packages/47/60/06cecc508045feaac6bc6502839eb68520a9798fdee1b83f69bb8d2d8c9b/zerv_version-0.8.14-py3-none-manylinux_2_39_x86_64.whl", hash = "sha256:6a3fffa95db79d8b416c03de664c26e12c86d2c245761506a6ad10d7c2072c27", size = 2181130, upload-time = "2026-04-16T01:39:36.766Z" }, + { url = "https://files.pythonhosted.org/packages/4c/22/3a95963fbc5f18d5717b101be6d38c162379de981b481050aae233c11e4c/zerv_version-0.8.14-py3-none-win_amd64.whl", hash = "sha256:8ba279fd883ccaa28a99ded8eabc7d42f525f1d0e9a93736d64587990fb0f01f", size = 2077733, upload-time = "2026-04-16T01:40:14.345Z" }, + { url = "https://files.pythonhosted.org/packages/af/b1/8d6a28a4a835984f520dfd91f89e2489b1e6542221cc3264a977e26a09d0/zerv_version-0.8.14-py3-none-win_arm64.whl", hash = "sha256:17c9b4e17951936581a9c3429e869b31bd7a3ea1d9b1cfc2e2b21782119ba473", size = 1955308, upload-time = "2026-04-16T01:39:54.733Z" }, ] [[package]] From 8ef89b7e64ca774a26186d90ff82c382b02ec739 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Fri, 17 Apr 2026 07:55:02 +0700 Subject: [PATCH 6/9] feat: use Mixin in bakelib --- README.md | 30 +++++--- src/bakelib/__init__.py | 12 ++-- src/bakelib/environ/__init__.py | 8 +-- src/bakelib/environ/bakebook.py | 8 +-- src/bakelib/space/service.py | 4 +- tests/unit/bakelib/environ/test_bakebook.py | 17 ++--- tests/unit/bakelib/environ/test_get.py | 79 +++++++++++---------- tests/utils/bakebook.py | 14 ++++ 8 files changed, 97 insertions(+), 75 deletions(-) create mode 100644 tests/utils/bakebook.py diff --git a/README.md b/README.md index f2a0803..5be5766 100644 --- a/README.md +++ b/README.md @@ -416,19 +416,31 @@ BaseSpace provides these tasks (override as needed): #### Multi-Environment Bakebooks -For projects with multiple environments (dev, staging, prod), use environment bakebooks: +For projects with multiple environments (dev, staging, prod), use environment mixins: ```python from bakelib.environ import ( - DevEnvBakebook, - StagingEnvBakebook, - ProdEnvBakebook, + BaseEnv, + DevEnvMixin, + EnvBakebook, + ProdEnvMixin, + StagingEnvMixin, get_bakebook, ) -bakebook_dev = DevEnvBakebook() -bakebook_staging = StagingEnvBakebook() -bakebook_prod = ProdEnvBakebook() +# Compose env mixins with EnvBakebook +class DevBakebook(DevEnvMixin, EnvBakebook[BaseEnv]): + pass + +class StagingBakebook(StagingEnvMixin, EnvBakebook[BaseEnv]): + pass + +class ProdBakebook(ProdEnvMixin, EnvBakebook[BaseEnv]): + pass + +bakebook_dev = DevBakebook() +bakebook_staging = StagingBakebook() +bakebook_prod = ProdBakebook() # Select bakebook based on ENV environment variable bakebook = get_bakebook([bakebook_dev, bakebook_staging, bakebook_prod]) @@ -448,8 +460,8 @@ from bakelib.environ import BaseEnv, EnvBakebook class MyEnv(BaseEnv): ENV_ORDER = ["dev", "sit", "qa", "uat", "prod"] -class MyEnvBakebook(EnvBakebook): - env_: MyEnv = MyEnv("local") +class MyEnvBakebook(EnvBakebook[MyEnv]): + env: MyEnv = MyEnv("local") ``` For more details, see the [bakelib source](https://github.com/wislertt/bakefile/tree/main/src/bakelib). diff --git a/src/bakelib/__init__.py b/src/bakelib/__init__.py index 08fb39c..7b92752 100644 --- a/src/bakelib/__init__.py +++ b/src/bakelib/__init__.py @@ -1,12 +1,12 @@ from bakelib import _params as params from bakelib.environ import ( BaseEnv, - DevEnvBakebook, + DevEnvMixin, EnvBakebook, EnvBakebooks, GcpLandingZoneEnv, - ProdEnvBakebook, - StagingEnvBakebook, + ProdEnvMixin, + StagingEnvMixin, get_bakebook, ) from bakelib.space import SubmodulesUtils @@ -23,17 +23,17 @@ "BaseEnv", "BaseServiceSpace", "BaseSpace", - "DevEnvBakebook", + "DevEnvMixin", "EnvBakebook", "EnvBakebooks", "GcpLandingZoneEnv", "GitHubActionsTools", - "ProdEnvBakebook", + "ProdEnvMixin", "PythonLibSpace", "PythonSpace", "RustLibSpace", "RustSpace", - "StagingEnvBakebook", + "StagingEnvMixin", "SubmodulesUtils", "get_bakebook", "params", diff --git a/src/bakelib/environ/__init__.py b/src/bakelib/environ/__init__.py index 362f389..3d27084 100644 --- a/src/bakelib/environ/__init__.py +++ b/src/bakelib/environ/__init__.py @@ -1,4 +1,4 @@ -from .bakebook import DevEnvBakebook, EnvBakebook, ProdEnvBakebook, StagingEnvBakebook +from .bakebook import DevEnvMixin, EnvBakebook, ProdEnvMixin, StagingEnvMixin from .bakebooks import EnvBakebooks from .base import BaseEnv, BaseSubEnv, EnvPriorityOrderType from .get import get_bakebook @@ -7,13 +7,13 @@ __all__ = [ "BaseEnv", "BaseSubEnv", - "DevEnvBakebook", + "DevEnvMixin", "EnvBakebook", "EnvBakebooks", "EnvPriorityOrderType", "GcpLandingZoneEnv", "GcpLandingZoneSubEnv", - "ProdEnvBakebook", - "StagingEnvBakebook", + "ProdEnvMixin", + "StagingEnvMixin", "get_bakebook", ] diff --git a/src/bakelib/environ/bakebook.py b/src/bakelib/environ/bakebook.py index ea25553..b77eb79 100644 --- a/src/bakelib/environ/bakebook.py +++ b/src/bakelib/environ/bakebook.py @@ -3,7 +3,7 @@ from pydantic.fields import FieldInfo from pydantic_settings import BaseSettings, PydanticBaseSettingsSource -from bake.bakebook.bakebook import Bakebook +from bake.bakebook.bakebook import Bakebook, BakebookMixin from bakelib.environ.base import BaseEnv E = TypeVar("E", bound=BaseEnv) @@ -78,13 +78,13 @@ def settings_customise_sources( ) -class DevEnvBakebook(EnvBakebook[BaseEnv]): +class DevEnvMixin(BakebookMixin): env: BaseEnv = BaseEnv("dev") -class StagingEnvBakebook(EnvBakebook[BaseEnv]): +class StagingEnvMixin(BakebookMixin): env: BaseEnv = BaseEnv("staging") -class ProdEnvBakebook(EnvBakebook[BaseEnv]): +class ProdEnvMixin(BakebookMixin): env: BaseEnv = BaseEnv("prod") diff --git a/src/bakelib/space/service.py b/src/bakelib/space/service.py index e1beef6..ba7d7ce 100644 --- a/src/bakelib/space/service.py +++ b/src/bakelib/space/service.py @@ -1,8 +1,8 @@ -from bake import Bakebook, command, console +from bake import BakebookMixin, command, console from bakelib.space.base import BaseSpace, command_not_available -class ServiceSpaceMixin(Bakebook): +class ServiceSpaceMixin(BakebookMixin): service_name: str | None = None @command(help="Build the service") diff --git a/tests/unit/bakelib/environ/test_bakebook.py b/tests/unit/bakelib/environ/test_bakebook.py index 2981f2b..4f4ce3d 100644 --- a/tests/unit/bakelib/environ/test_bakebook.py +++ b/tests/unit/bakelib/environ/test_bakebook.py @@ -10,12 +10,10 @@ from bake.bakebook.bakebook import Bakebook from bakelib.environ import BaseEnv, EnvBakebook from bakelib.environ.bakebook import ( - DevEnvBakebook, - ProdEnvBakebook, - StagingEnvBakebook, _ExcludeEnvFieldSource, ) from bakelib.environ.presets import GcpLandingZoneEnv +from tests.utils.bakebook import DevEnvBB, ProdEnvBB, StagingEnvBB class TestExcludeEnvFieldSource: @@ -87,12 +85,10 @@ def test_env_bakebook_env_comparison(self): bb_prod = EnvBakebook(env=BaseEnv("prod")) assert bb_dev.env < bb_prod.env - bb_dev = DevEnvBakebook() - bb_prod = ProdEnvBakebook() - assert bb_dev.env < bb_prod.env + assert DevEnvBB().env < ProdEnvBB().env def test_env_included_in_model_dump(self): - bb = DevEnvBakebook() + bb = DevEnvBB() dump = bb.model_dump() assert "env" in dump @@ -104,15 +100,14 @@ class TestEnvSpecificBakebooks: @pytest.mark.parametrize( "bakebook_class,expected_env", [ - (DevEnvBakebook, "dev"), - (StagingEnvBakebook, "staging"), - (ProdEnvBakebook, "prod"), + (DevEnvBB, "dev"), + (StagingEnvBB, "staging"), + (ProdEnvBB, "prod"), ], ) def test_env_bakebook_defaults_to_correct_env(self, bakebook_class, expected_env): bb = bakebook_class() assert str(bb.env) == expected_env - assert isinstance(bb, EnvBakebook) class TestEnvBakebookEnvPrefix: diff --git a/tests/unit/bakelib/environ/test_get.py b/tests/unit/bakelib/environ/test_get.py index 3e6266b..99de53d 100644 --- a/tests/unit/bakelib/environ/test_get.py +++ b/tests/unit/bakelib/environ/test_get.py @@ -2,9 +2,10 @@ import pytest -from bakelib.environ import DevEnvBakebook, EnvBakebook, ProdEnvBakebook, StagingEnvBakebook +from bakelib.environ import EnvBakebook from bakelib.environ.base import BaseEnv from bakelib.environ.get import get_bakebook +from tests.utils.bakebook import DevEnvBB, ProdEnvBB, StagingEnvBB class TestGetBakebook: @@ -15,8 +16,8 @@ def test_raises_error_on_empty_list(self): @pytest.mark.parametrize("env_value", ["dev", "prod"]) def test_matches_exact_env_value(self, monkeypatch: pytest.MonkeyPatch, env_value: str): monkeypatch.setenv("ENV", env_value) - bb_dev = DevEnvBakebook() - bb_prod = ProdEnvBakebook() + bb_dev = DevEnvBB() + bb_prod = ProdEnvBB() assert str(bb_dev.env) == "dev" assert str(bb_prod.env) == "prod" bakebook_map = {"dev": bb_dev, "prod": bb_prod} @@ -27,8 +28,8 @@ def test_matches_exact_env_value(self, monkeypatch: pytest.MonkeyPatch, env_valu assert str(result.env) == env_value def test_falls_to_lowest_priority_when_env_unset(self): - bb_dev = DevEnvBakebook() - bb_prod = ProdEnvBakebook() + bb_dev = DevEnvBB() + bb_prod = ProdEnvBB() assert bb_dev.env < bb_prod.env bbs: list[EnvBakebook[BaseEnv]] = [bb_dev, bb_prod] @@ -37,8 +38,8 @@ def test_falls_to_lowest_priority_when_env_unset(self): def test_raises_error_on_no_match(self, monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("ENV", "staging") - bb_dev = DevEnvBakebook() - bb_prod = ProdEnvBakebook() + bb_dev = DevEnvBB() + bb_prod = ProdEnvBB() bbs: list[EnvBakebook[BaseEnv]] = [bb_dev, bb_prod] with pytest.raises( @@ -49,17 +50,17 @@ def test_raises_error_on_no_match(self, monkeypatch: pytest.MonkeyPatch): def test_uses_custom_env_var_name(self, monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("MY_ENV", "prod") - bb_dev = DevEnvBakebook() - bb_prod = ProdEnvBakebook() + bb_dev = DevEnvBB() + bb_prod = ProdEnvBB() bbs: list[EnvBakebook[BaseEnv]] = [bb_dev, bb_prod] result = get_bakebook(bbs, env_var_name="MY_ENV") assert result is bb_prod def test_handles_all_env_aware_bakebooks(self, monkeypatch: pytest.MonkeyPatch): - bb_dev = DevEnvBakebook() - bb_staging = StagingEnvBakebook() - bb_prod = ProdEnvBakebook() + bb_dev = DevEnvBB() + bb_staging = StagingEnvBB() + bb_prod = ProdEnvBB() bbs: list[EnvBakebook[BaseEnv]] = [bb_dev, bb_staging, bb_prod] # staging has higher priority (index 1) than prod (index 2) @@ -74,8 +75,8 @@ def test_handles_all_env_aware_bakebooks(self, monkeypatch: pytest.MonkeyPatch): assert str(result.env) == "staging" def test_raises_error_on_duplicate_env(self): - bb_dev1 = DevEnvBakebook() - bb_dev2 = DevEnvBakebook() + bb_dev1 = DevEnvBB() + bb_dev2 = DevEnvBB() with pytest.raises(ValueError, match="Duplicate env 'dev' found"): get_bakebook([bb_dev1, bb_dev2]) @@ -85,7 +86,7 @@ class FakeBakebook: pass fake_bb = FakeBakebook() - bb_dev = DevEnvBakebook() + bb_dev = DevEnvBB() with pytest.raises(ValueError, match="All bakebooks must have an 'env' attribute"): get_bakebook([fake_bb, bb_dev]) # ty: ignore[invalid-argument-type] @@ -95,7 +96,7 @@ class TestGetBakebookLazyInit: def test_lazy_init_called_by_default(self): """lazy_init is called by default when selecting bakebook.""" - class CustomBakebook(DevEnvBakebook): + class CustomBakebook(DevEnvBB): lazy_init_called: ClassVar[bool] = False def lazy_init(self) -> None: @@ -109,7 +110,7 @@ def lazy_init(self) -> None: def test_lazy_init_false_skips_call(self): """lazy_init=False skips the lazy_init call.""" - class CustomBakebook(DevEnvBakebook): + class CustomBakebook(DevEnvBB): lazy_init_called: ClassVar[bool] = False def lazy_init(self) -> None: @@ -123,13 +124,13 @@ def lazy_init(self) -> None: def test_lazy_init_called_with_exact_env_match(self, monkeypatch: pytest.MonkeyPatch): """lazy_init is called when exact env match is found.""" - class CustomBakebook(ProdEnvBakebook): + class CustomBakebook(ProdEnvBB): lazy_init_called: ClassVar[bool] = False def lazy_init(self) -> None: CustomBakebook.lazy_init_called = True - bb_dev = DevEnvBakebook() + bb_dev = DevEnvBB() bb_prod = CustomBakebook() monkeypatch.setenv("ENV", "prod") @@ -141,14 +142,14 @@ def lazy_init(self) -> None: def test_lazy_init_called_with_fallback_to_lowest_priority(self): """lazy_init is called when falling back to lowest priority.""" - class CustomBakebook(DevEnvBakebook): + class CustomBakebook(DevEnvBB): lazy_init_called: ClassVar[bool] = False def lazy_init(self) -> None: CustomBakebook.lazy_init_called = True bb_dev = CustomBakebook() - bb_prod = ProdEnvBakebook() + bb_prod = ProdEnvBB() result = get_bakebook([bb_dev, bb_prod]) @@ -160,13 +161,13 @@ def test_multiple_bakebooks_lazy_init_only_called_on_selected( ): """Only the selected bakebook has lazy_init called.""" - class CustomDev(DevEnvBakebook): + class CustomDev(DevEnvBB): lazy_init_called: ClassVar[bool] = False def lazy_init(self) -> None: CustomDev.lazy_init_called = True - class CustomProd(ProdEnvBakebook): + class CustomProd(ProdEnvBB): lazy_init_called: ClassVar[bool] = False def lazy_init(self) -> None: @@ -186,8 +187,8 @@ def lazy_init(self) -> None: class TestGetBakebookFallbackEnv: def test_fallback_env_value_used_when_env_unset(self): """fallback_env_value is used when ENV is not set.""" - bb_dev = DevEnvBakebook() - bb_staging = StagingEnvBakebook() + bb_dev = DevEnvBB() + bb_staging = StagingEnvBB() bbs: list[EnvBakebook[BaseEnv]] = [bb_dev, bb_staging] result = get_bakebook(bbs, fallback_env_value="staging") @@ -196,8 +197,8 @@ def test_fallback_env_value_used_when_env_unset(self): def test_fallback_env_value_used_when_env_empty(self): """fallback_env_value is used when ENV is empty string.""" - bb_dev = DevEnvBakebook() - bb_staging = StagingEnvBakebook() + bb_dev = DevEnvBB() + bb_staging = StagingEnvBB() bbs: list[EnvBakebook[BaseEnv]] = [bb_dev, bb_staging] result = get_bakebook(bbs, env_value="", fallback_env_value="staging") @@ -206,8 +207,8 @@ def test_fallback_env_value_used_when_env_empty(self): def test_env_value_takes_precedence_over_fallback(self): """env_value takes precedence over fallback_env_value.""" - bb_dev = DevEnvBakebook() - bb_staging = StagingEnvBakebook() + bb_dev = DevEnvBB() + bb_staging = StagingEnvBB() bbs: list[EnvBakebook[BaseEnv]] = [bb_dev, bb_staging] result = get_bakebook(bbs, env_value="dev", fallback_env_value="staging") @@ -216,8 +217,8 @@ def test_env_value_takes_precedence_over_fallback(self): def test_env_var_takes_precedence_over_fallback(self, monkeypatch: pytest.MonkeyPatch): """ENV var takes precedence over fallback_env_value.""" - bb_dev = DevEnvBakebook() - bb_staging = StagingEnvBakebook() + bb_dev = DevEnvBB() + bb_staging = StagingEnvBB() bbs: list[EnvBakebook[BaseEnv]] = [bb_dev, bb_staging] monkeypatch.setenv("ENV", "dev") @@ -227,8 +228,8 @@ def test_env_var_takes_precedence_over_fallback(self, monkeypatch: pytest.Monkey def test_both_fallback_and_env_none_uses_min(self): """When both env_value and fallback_env_value are None, uses min.""" - bb_dev = DevEnvBakebook() - bb_staging = StagingEnvBakebook() + bb_dev = DevEnvBB() + bb_staging = StagingEnvBB() bbs: list[EnvBakebook[BaseEnv]] = [bb_dev, bb_staging] result = get_bakebook(bbs) @@ -237,8 +238,8 @@ def test_both_fallback_and_env_none_uses_min(self): def test_invalid_fallback_env_value_raises_error(self): """Invalid fallback_env_value raises ValueError.""" - bb_dev = DevEnvBakebook() - bb_staging = StagingEnvBakebook() + bb_dev = DevEnvBB() + bb_staging = StagingEnvBB() bbs: list[EnvBakebook[BaseEnv]] = [bb_dev, bb_staging] with pytest.raises(ValueError, match="No bakebook found with env='prod'"): @@ -246,8 +247,8 @@ def test_invalid_fallback_env_value_raises_error(self): def test_fallback_chain_env_none_fallback_valid_uses_fallback(self): """env_value=None, valid fallback_env_value → uses fallback.""" - bb_dev = DevEnvBakebook() - bb_staging = StagingEnvBakebook() + bb_dev = DevEnvBB() + bb_staging = StagingEnvBB() bbs: list[EnvBakebook[BaseEnv]] = [bb_dev, bb_staging] result = get_bakebook(bbs, env_value=None, fallback_env_value="staging") @@ -255,8 +256,8 @@ def test_fallback_chain_env_none_fallback_valid_uses_fallback(self): def test_fallback_chain_env_empty_fallback_valid_uses_fallback(self): """env_value='', valid fallback_env_value → uses fallback.""" - bb_dev = DevEnvBakebook() - bb_staging = StagingEnvBakebook() + bb_dev = DevEnvBB() + bb_staging = StagingEnvBB() bbs: list[EnvBakebook[BaseEnv]] = [bb_dev, bb_staging] result = get_bakebook(bbs, env_value="", fallback_env_value="staging") diff --git a/tests/utils/bakebook.py b/tests/utils/bakebook.py new file mode 100644 index 0000000..5fafb4e --- /dev/null +++ b/tests/utils/bakebook.py @@ -0,0 +1,14 @@ +from bakelib.environ import BaseEnv, EnvBakebook +from bakelib.environ.bakebook import DevEnvMixin, ProdEnvMixin, StagingEnvMixin + + +class DevEnvBB(DevEnvMixin, EnvBakebook[BaseEnv]): + pass + + +class StagingEnvBB(StagingEnvMixin, EnvBakebook[BaseEnv]): + pass + + +class ProdEnvBB(ProdEnvMixin, EnvBakebook[BaseEnv]): + pass From 65bd14a67e7387daf07d31cd3f659349d1e1fbbd Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Fri, 17 Apr 2026 08:05:27 +0700 Subject: [PATCH 7/9] feat: dev --- src/bake/bakebook/bakebook.py | 17 ++++++++++------- src/bakelib/utils/env_vars.py | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/bake/bakebook/bakebook.py b/src/bake/bakebook/bakebook.py index 8fb9625..692002f 100644 --- a/src/bake/bakebook/bakebook.py +++ b/src/bake/bakebook/bakebook.py @@ -159,13 +159,16 @@ def __init__(self, **kwargs: Any) -> None: raise TypeError("BakebookMixin can only be used with Bakebook subclasses") super().__init__(**kwargs) - @property - def ctx(self) -> Context: - if not isinstance(self, Bakebook): - raise TypeError( # pragma: no cover - "BakebookMixin can only be used with Bakebook subclasses" - ) - return Bakebook.ctx.fget(self) + # NOTE: ctx is intentionally not defined here — when composed with + # Bakebook, self.ctx resolves to Bakebook.ctx via MRO. Revisit if + # a standalone ctx access pattern is needed outside composition. + # @property + # def ctx(self) -> Context: + # if not isinstance(self, Bakebook): + # raise TypeError( # pragma: no cover + # "BakebookMixin can only be used with Bakebook subclasses" + # ) + # return Bakebook.ctx.fget(self) class Bakebook(BaseSettings): diff --git a/src/bakelib/utils/env_vars.py b/src/bakelib/utils/env_vars.py index 3100715..92b34f4 100644 --- a/src/bakelib/utils/env_vars.py +++ b/src/bakelib/utils/env_vars.py @@ -1,6 +1,6 @@ -from bake import Bakebook +from bake import BakebookMixin -class GitHubActionsEnvVars(Bakebook): +class GitHubActionsEnvVars(BakebookMixin): ci: bool = False github_actions: bool = False From 3d36693cd95fb12a5b304c774fbea771d26af3b4 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Fri, 17 Apr 2026 09:15:41 +0700 Subject: [PATCH 8/9] docs: update docsting --- src/bake/bakebook/bakebook.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bake/bakebook/bakebook.py b/src/bake/bakebook/bakebook.py index 692002f..0880a19 100644 --- a/src/bake/bakebook/bakebook.py +++ b/src/bake/bakebook/bakebook.py @@ -151,7 +151,9 @@ class BakebookMixin(BaseSettings): Multiple BakebookMixin subclasses can be composed with a single Bakebook subclass without MRO conflicts. - Supports ``@command()`` decorator for contributing commands. + Recommended usage: fields only — no methods. This keeps mixins simple + and avoids the need for typed access to base class attributes. + Use ``Bakebook`` subclasses for methods and ``@command()`` definitions. """ def __init__(self, **kwargs: Any) -> None: From ea28b915b2e20a9da589972852a2e0a2cef348a9 Mon Sep 17 00:00:00 2001 From: Wisaroot Lertthaweedech Date: Fri, 17 Apr 2026 12:09:31 +0700 Subject: [PATCH 9/9] chore: update deps --- .claude/hooks/bun.lock | 2 +- .claude/hooks/package.json | 2 +- examples/python-package/uv.lock | 38 ++++++++++++++--------------- examples/simple/bakefile.py.lock | 38 ++++++++++++++--------------- uv.lock | 42 ++++++++++++++++---------------- 5 files changed, 61 insertions(+), 61 deletions(-) diff --git a/.claude/hooks/bun.lock b/.claude/hooks/bun.lock index e9a398c..9f002f7 100644 --- a/.claude/hooks/bun.lock +++ b/.claude/hooks/bun.lock @@ -76,7 +76,7 @@ "tsx": ["tsx@4.21.0", "https://registry.npmmirror.com/tsx/-/tsx-4.21.0.tgz", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": "dist/cli.mjs" }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], - "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], } diff --git a/.claude/hooks/package.json b/.claude/hooks/package.json index fe72979..53c943e 100644 --- a/.claude/hooks/package.json +++ b/.claude/hooks/package.json @@ -11,6 +11,6 @@ "dependencies": { "@types/node": "^25.6.0", "tsx": "^4.21.0", - "typescript": "^6.0.2" + "typescript": "^6.0.3" } } diff --git a/examples/python-package/uv.lock b/examples/python-package/uv.lock index 8124505..aa064b0 100644 --- a/examples/python-package/uv.lock +++ b/examples/python-package/uv.lock @@ -1049,27 +1049,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.10" +version = "0.15.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, - { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, - { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, - { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, - { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, - { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, - { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, - { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, - { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, - { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, - { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, - { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, - { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, - { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, ] [[package]] diff --git a/examples/simple/bakefile.py.lock b/examples/simple/bakefile.py.lock index 2db9c80..b1354f3 100644 --- a/examples/simple/bakefile.py.lock +++ b/examples/simple/bakefile.py.lock @@ -306,27 +306,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.10" +version = "0.15.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, - { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, - { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, - { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, - { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, - { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, - { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, - { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, - { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, - { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, - { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, - { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, - { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, - { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, ] [[package]] diff --git a/uv.lock b/uv.lock index b1ee14f..f432996 100644 --- a/uv.lock +++ b/uv.lock @@ -1366,27 +1366,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, - { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, - { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, - { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, - { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, - { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, - { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, - { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, - { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, - { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, - { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, - { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, - { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, - { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, ] [[package]]