From 40d25f84ab2a280fc9114fe752aacdfc906c6bf4 Mon Sep 17 00:00:00 2001 From: Ramesh Padmanabhaiah <22363102+codeforester@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:37:56 +0530 Subject: [PATCH] release: prepare base-cli 0.4.3 --- .github/workflows/compatibility.yml | 2 +- CHANGELOG.md | 12 +++++++++++- README.md | 2 +- VERSION | 2 +- docs/compatibility-dashboard.md | 2 +- docs/dependency-support.md | 2 +- docs/typer-adapter.md | 4 ++-- lib/python/base_cli/_attach.py | 3 ++- lib/python/base_cli/_click_compat.py | 20 +++++++++++++++++++- lib/python/base_cli/_lifecycle.py | 3 ++- scripts/record_compatibility_evidence.py | 2 +- tests/test_platform_edge_paths.py | 7 +++++++ 12 files changed, 49 insertions(+), 12 deletions(-) diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 83c53be..0a15288 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -20,7 +20,7 @@ jobs: fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] - typer-version: ["0.25.1", "0.26.0", "0.27.1"] + typer-version: ["0.25.1", "0.26.0", "0.27.1", "0.27.2"] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d4d4cb..dc84684 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ and versions are tracked in the repo-root `VERSION` file. - Continue compatibility hardening and adoption work for the next release. +## [0.4.3] - 2026-08-29 + +This is a compatible pre-1.0 patch release. It contains lifecycle hardening, +security fixes, versioned machine-readable contracts, and adoption tooling +after 0.4.2; it does not introduce a new API or JSON-contract compatibility +boundary. Existing Click and Typer command trees remain supported. + ### Changed - Add branch-aware coverage reporting with documented floors for lifecycle, @@ -39,6 +46,8 @@ and versions are tracked in the repo-root `VERSION` file. ### Fixed +- Keep the Typer adapter compatible with Typer 0.27.2's vendored exit + exception layout and validate that release in the compatibility matrix. - Apply the documented count-only 20-bundle retention default to implicit JSON mode while preserving explicit retention policies, human-mode safe defaults, and the `max_log_files` compatibility path. @@ -296,7 +305,8 @@ the API stability policy and migration guide before upgrading from `0.3.x`. - Added the guarded package build, artifact validation, and protected TestPyPI/PyPI publication workflow. -[Unreleased]: https://github.com/basefoundry/base-cli/compare/v0.4.2...HEAD +[Unreleased]: https://github.com/basefoundry/base-cli/compare/v0.4.3...HEAD +[0.4.3]: https://github.com/basefoundry/base-cli/compare/v0.4.2...v0.4.3 [0.4.2]: https://github.com/basefoundry/base-cli/compare/v0.4.1...v0.4.2 [0.4.1]: https://github.com/basefoundry/base-cli/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/basefoundry/base-cli/compare/v0.3.0...v0.4.0 diff --git a/README.md b/README.md index efbdace..e2f408e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ | Version | License | Install | Release notes | | --- | --- | --- | --- | -| `0.4.2` | [Apache-2.0](LICENSE) | `python -m pip install base-cli` | [v0.4.2](https://github.com/basefoundry/base-cli/releases/tag/v0.4.2) | +| `0.4.3` | [Apache-2.0](LICENSE) | `python -m pip install base-cli` | [v0.4.3](https://github.com/basefoundry/base-cli/releases/tag/v0.4.3) | `base-cli` is the production lifecycle layer for Click and Typer Python CLIs. It standardizes context, logging, configuration, cleanup, and machine-readable diff --git a/VERSION b/VERSION index 2b7c5ae..17b2ccd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.2 +0.4.3 diff --git a/docs/compatibility-dashboard.md b/docs/compatibility-dashboard.md index 5f1c0dc..7dfa0c3 100644 --- a/docs/compatibility-dashboard.md +++ b/docs/compatibility-dashboard.md @@ -17,7 +17,7 @@ red badge means that evidence is pending or needs investigation. | Python | `>=3.10,<4` | Python 3.10, Python 3.11, Python 3.12, Python 3.13, Python 3.14 | Supported and tested | | Click | `click>=8.1,<8.5` | 8.1, 8.2, 8.3, 8.4 | Supported and tested on Python 3.10 and 3.14 | | PyYAML extra | `PyYAML>=6.0,<7` | 6.0 | Supported when `base-cli[yaml]` is installed | -| Typer extra | `typer>=0.12,<0.28` | 0.25.1, 0.26.0, 0.27.1 | Supported through `attach_typer()` | +| Typer extra | `typer>=0.12,<0.28` | 0.25.1, 0.26.0, 0.27.1, 0.27.2 | Supported through `attach_typer()` | | Platforms | Pure-Python core | macos-latest, ubuntu-latest, windows-latest, Debian 12, Fedora latest, WSL2 | Supported tiers documented below | ## What the labels mean diff --git a/docs/dependency-support.md b/docs/dependency-support.md index 7a95fe1..b72fbf5 100644 --- a/docs/dependency-support.md +++ b/docs/dependency-support.md @@ -23,7 +23,7 @@ Optional integrations are intentionally independent of the core window: | Extra | Declared window | Compatibility evidence | | --- | --- | --- | -| `typer` | `>=0.12,<0.28` | Typer 0.25--0.27 across Python 3.10--3.14 | +| `typer` | `>=0.12,<0.28` | Typer 0.25.1, 0.26.0, 0.27.1, and 0.27.2 across Python 3.10--3.14 | | `rich` | `>=13.7,<15` | Integration tests and graceful-degradation checks | | `telemetry` | `>=1.24,<2` | Telemetry integration tests | diff --git a/docs/typer-adapter.md b/docs/typer-adapter.md index 42b487a..b9bb8e3 100644 --- a/docs/typer-adapter.md +++ b/docs/typer-adapter.md @@ -74,5 +74,5 @@ Typer is an optional extra and is imported lazily. Importing `base_cli` and using the Click integration never imports or requires Typer. The compatibility workflow exercises the adapter and a typed consumer against -Typer 0.25.1, 0.26.0, and 0.27.1 on Python 3.10 through 3.14. Keep this matrix -green before widening the supported Typer range again. +Typer 0.25.1, 0.26.0, 0.27.1, and 0.27.2 on Python 3.10 through 3.14. Keep +this matrix green before widening the supported Typer range again. diff --git a/lib/python/base_cli/_attach.py b/lib/python/base_cli/_attach.py index ae70513..80a1c56 100644 --- a/lib/python/base_cli/_attach.py +++ b/lib/python/base_cli/_attach.py @@ -36,6 +36,7 @@ _validate_standard_options, _warn_lifecycle_failure, ) +from ._click_compat import exit_exception_type from ._lifecycle import RunRecorder, outcome_from_exception, outcome_from_exit_code from ._lifecycle_install import _resolve_lifecycle_values, _standard_options_from_values from ._run import _reject_async_callback, _reject_async_result @@ -109,7 +110,7 @@ def lifecycle_aware_exit(code: int = 0) -> Any: # is called from a close hook, that recursive close can unwind # this resource with no exception information, so capture the # terminal outcome before delegating. - self.record_exception(self.click.exceptions.Exit(code)) + self.record_exception(exit_exception_type(self.click)(code)) return original_click_exit(code) self.original_click_exit = original_click_exit diff --git a/lib/python/base_cli/_click_compat.py b/lib/python/base_cli/_click_compat.py index 91e681c..96619a3 100644 --- a/lib/python/base_cli/_click_compat.py +++ b/lib/python/base_cli/_click_compat.py @@ -125,6 +125,24 @@ def dialect_for_typer(typer: Any) -> Any: return dialect if dialect is not None else click +def exit_exception_type(click: Any) -> type[BaseException]: + """Return the owning dialect's exit exception across Click variants. + + Typer 0.27.2 keeps ``Exit`` on its core module (and maps it to + ``typer.exceptions.Exit``) instead of exporting it from + ``click.exceptions``. Older Click and Typer releases use the latter. + """ + exceptions = getattr(click, "exceptions", None) + candidate = getattr(exceptions, "Exit", None) + if isinstance(candidate, type): + return candidate + core = getattr(click, "core", None) + candidate = getattr(core, "Exit", None) + if isinstance(candidate, type): + return candidate + raise AttributeError("Click dialect does not expose an Exit exception type") + + def mark_command_dialect(command: Any, dialect: Any) -> Any: """Remember the owning dialect on a generated command object.""" @@ -171,4 +189,4 @@ def is_command(command: Any) -> bool: return dialect is not click and isinstance(command, dialect.Command) -__all__ = ["dialect_for_command", "dialect_for_typer", "is_command", "mark_command_dialect"] +__all__ = ["dialect_for_command", "dialect_for_typer", "exit_exception_type", "is_command", "mark_command_dialect"] diff --git a/lib/python/base_cli/_lifecycle.py b/lib/python/base_cli/_lifecycle.py index f495eaa..7816491 100644 --- a/lib/python/base_cli/_lifecycle.py +++ b/lib/python/base_cli/_lifecycle.py @@ -5,6 +5,7 @@ from datetime import datetime from typing import Any +from ._click_compat import exit_exception_type from ._private_files import write_private_json from ._runtime import refresh_run_bundle_index from .context import Context @@ -128,7 +129,7 @@ def outcome_from_exception(click: Any, exc: BaseException) -> InvocationOutcome: if isinstance(exc.__cause__, KeyboardInterrupt): return InvocationOutcome("interrupted", "aborted", ExitCode.INTERRUPTED) return InvocationOutcome("aborted", "error", ExitCode.FAILURE) - if isinstance(exc, click.exceptions.Exit): + if isinstance(exc, exit_exception_type(click)): exit_code = _click_exception_exit_code(exc) if exit_code is None: return InvocationOutcome("unexpected_error", "error", ExitCode.FAILURE) diff --git a/scripts/record_compatibility_evidence.py b/scripts/record_compatibility_evidence.py index f772744..83ab9db 100644 --- a/scripts/record_compatibility_evidence.py +++ b/scripts/record_compatibility_evidence.py @@ -13,7 +13,7 @@ ROOT = Path(__file__).resolve().parents[1] FIXTURES = ("atlas_click", "beacon_typer", "cinder_automation") PYTHON_VERSIONS = ("3.10", "3.11", "3.12", "3.13", "3.14") -TYPER_VERSIONS = ("0.25.1", "0.26.0", "0.27.1") +TYPER_VERSIONS = ("0.25.1", "0.26.0", "0.27.1", "0.27.2") def _revision() -> str: diff --git a/tests/test_platform_edge_paths.py b/tests/test_platform_edge_paths.py index 0fc1347..926bfd4 100644 --- a/tests/test_platform_edge_paths.py +++ b/tests/test_platform_edge_paths.py @@ -60,6 +60,13 @@ def test_parent_directory_open_is_disabled_on_windows(self) -> None: class ClickCompatibilityEdgeTests(unittest.TestCase): + def test_exit_exception_type_supports_core_fallback(self) -> None: + fallback = types.SimpleNamespace( + exceptions=types.SimpleNamespace(), + core=types.SimpleNamespace(Exit=RuntimeError), + ) + self.assertIs(click_compat.exit_exception_type(fallback), RuntimeError) + def test_dialect_falls_back_to_public_click_without_typer(self) -> None: with mock.patch.dict("sys.modules", {"typer": None}): command = object()