Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.4.2
0.4.3
2 changes: 1 addition & 1 deletion docs/compatibility-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/dependency-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
4 changes: 2 additions & 2 deletions docs/typer-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 2 additions & 1 deletion lib/python/base_cli/_attach.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion lib/python/base_cli/_click_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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"]
3 changes: 2 additions & 1 deletion lib/python/base_cli/_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion scripts/record_compatibility_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions tests/test_platform_edge_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading