chore: finish compatibility cleanup

This commit is contained in:
lda
2026-06-29 17:15:06 +07:00 Verified
parent 99f733c5ea
commit 664beec837
4 changed files with 256 additions and 313 deletions
@@ -1,97 +0,0 @@
### Description
After Typer vendored Click in 0.26.0, exceptions imported from the external
`click` package are no longer handled as normal Typer CLI errors.
A real invocation renders a Rich traceback ending in
`ClickException: broken`. Under `typer.testing.CliRunner`, both stdout and
stderr are empty and the raw `click.ClickException` remains in
`result.exception`.
Typer 0.25.0 renders the expected concise error panel and returns `SystemExit`.
### Root Cause
Typer commit `1829d73` vendored Click as `typer._click`. Typer's `_main()`
catches `typer._click.exceptions.ClickException`, but an application that
raises `click.exceptions.ClickException` raises a different class:
```python
import click
import typer._click
assert click.ClickException is not typer._click.ClickException
```
The exception therefore bypasses Typer's Click-exception handler. This affects
the external Click variants of `ClickException`, `UsageError`, `BadParameter`,
and other subclasses.
Typer publicly re-exports some vendored exception types, including
`typer.BadParameter` and `typer.Exit`, but it does not currently expose a public
`typer.ClickException` or `typer.UsageError` equivalent.
### Minimal Reproduction
```python
import click
import typer
from typer.testing import CliRunner
app = typer.Typer()
@app.command()
def fail() -> None:
raise click.ClickException("broken")
result = CliRunner().invoke(app)
print("exit:", result.exit_code)
print("stdout:", repr(result.stdout))
print("stderr:", repr(result.stderr))
print("exception:", type(result.exception).__name__)
```
The same behavior occurs in nested Typer command groups.
### Version Matrix
Tested with Click 8.4.2 unless noted otherwise:
| Typer | Result |
| --- | --- |
| 0.24.2 | Concise error output; `SystemExit` |
| 0.25.0 | Concise error output; `SystemExit` |
| 0.26.0 | Empty runner output; raw `ClickException` |
| 0.26.8 with Click 8.1.7 | Empty runner output; raw `ClickException` |
| 0.26.8 with Click 8.4.2 | Empty runner output; raw `ClickException` |
| `master` at `b210c0e2` | Empty runner output; raw `ClickException` |
Test environment:
- Python 3.14.3
- Windows 11
- Click 8.1.7 and 8.4.2
### Expected Behavior
Typer should provide a public way to raise its concise general-purpose CLI
exception after vendoring Click. Possible resolutions include:
- re-exporting the vendored `ClickException` and `UsageError` classes from the
`typer` namespace;
- preserving compatibility with exceptions imported from external Click; or
- documenting that external Click exceptions are no longer compatible and
identifying their supported Typer replacements.
### Workaround
Applications can write the message explicitly and raise `typer.Exit(1)`:
```python
typer.echo("Error: something went wrong", err=True)
raise typer.Exit(code=1)
```
Importing from `typer._click` also works, but relies on a private API.
@@ -0,0 +1,74 @@
### Feature Request
Expose a public, general-purpose Typer exception for concise user-facing CLI
errors after Click vendoring.
Typer 0.26.0 intentionally vendored Click and no longer supports using Click
directly. This is documented in the 0.26.0 breaking changes and the Vendored
Click guide. This request is not asking to restore compatibility with exception
classes imported from the external `click` package.
### Current API Gap
Typer publicly re-exports several vendored exception types, including
`typer.BadParameter`, `typer.Abort`, and `typer.Exit`, but it does not expose a
general-purpose `typer.ClickException` or `typer.UsageError` equivalent.
Before 0.26.0, an application could raise `click.ClickException("broken")` to
produce Typer's concise error panel and exit with code 1. After vendoring, the
supported public workaround is to write and terminate separately:
```python
typer.echo("Error: broken", err=True)
raise typer.Exit(code=1)
```
This works, but each application must reproduce the error prefix, output
stream, formatting policy, and exit behavior instead of expressing one typed
CLI error.
`typer.Abort` is not an equivalent because it represents user cancellation and
adds `Aborted!` output.
### Proposed API
Expose the vendored general-purpose exception through Typer's public namespace,
or provide a Typer-native equivalent:
```python
import typer
app = typer.Typer()
@app.command()
def fail() -> None:
raise typer.ClickException("broken")
```
Expected output should use Typer's standard concise error formatting and be
captured by `typer.testing.CliRunner` on stderr.
The exact public name is not important. A Typer-native `UsageError` or another
documented general-purpose CLI error would satisfy the same need.
### Why This Is Distinct From Existing Exports
- `typer.BadParameter` describes parameter validation and requires parameter
context for its best output.
- `typer.Abort` describes cancellation and prints `Aborted!`.
- `typer.Exit` controls termination but carries no error message or formatting.
A general-purpose error is useful when adapting failures from HTTP clients,
RPC calls, configuration loading, or other application services at the CLI
boundary.
### Version Context
- Typer 0.25.0 with external Click: `click.ClickException` produced concise
output.
- Typer 0.26.0 and newer: direct Click use is intentionally unsupported.
- Typer 0.26.8 and `master` at `b210c0e2`: no public general-purpose Typer
exception is exported.
Test environment: Python 3.14.3 on Windows 11.