mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-23 13:36:52 +00:00
Some checks are pending
CI / cargo fmt (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}) (push) Blocked by required conditions
CI / cargo test (macos-latest) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / ty completion evaluation (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks walltime (medium|multithreaded) (push) Blocked by required conditions
CI / benchmarks walltime (small|large) (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
## Summary
Not a high-priority task... but it _is_ a weekend :P
This PR improves our diagnostics for invalid exceptions. Specifically:
- We now give a special-cased ``help: Did you mean
`NotImplementedError`` subdiagnostic for `except NotImplemented`, `raise
NotImplemented` and `raise <EXCEPTION> from NotImplemented`
- If the user catches a tuple of exceptions (`except (foo, bar, baz):`)
and multiple elements in the tuple are invalid, we now collect these
into a single diagnostic rather than emitting a separate diagnostic for
each tuple element
- The explanation of why the `except`/`raise` was invalid ("must be a
`BaseException` instance or `BaseException` subclass", etc.) is
relegated to a subdiagnostic. This makes the top-level diagnostic
summary much more concise.
## Test Plan
Lots of snapshots. And here's some screenshots:
<details>
<summary>Screenshots</summary>
<img width="1770" height="1520" alt="image"
src="https://github.com/user-attachments/assets/7f27fd61-c74d-4ddf-ad97-ea4fd24d06fd"
/>
<img width="1916" height="1392" alt="image"
src="https://github.com/user-attachments/assets/83e5027c-8798-48a6-a0ec-1babfc134000"
/>
<img width="1696" height="588" alt="image"
src="https://github.com/user-attachments/assets/1bc16048-6eb4-4dfa-9ace-dd271074530f"
/>
</details>
5.9 KiB
5.9 KiB
Exception Handling
Single Exception
import re
try:
help()
except NameError as e:
reveal_type(e) # revealed: NameError
except re.error as f:
reveal_type(f) # revealed: error
Unknown type in except handler does not cause spurious diagnostic
from nonexistent_module import foo # error: [unresolved-import]
try:
help()
except foo as e:
reveal_type(foo) # revealed: Unknown
reveal_type(e) # revealed: Unknown
Multiple Exceptions in a Tuple
EXCEPTIONS = (AttributeError, TypeError)
try:
help()
except (RuntimeError, OSError) as e:
reveal_type(e) # revealed: RuntimeError | OSError
except EXCEPTIONS as f:
reveal_type(f) # revealed: AttributeError | TypeError
Dynamic exception types
def foo(
x: type[AttributeError],
y: tuple[type[OSError], type[RuntimeError]],
z: tuple[type[BaseException], ...],
zz: tuple[type[TypeError | RuntimeError], ...],
zzz: type[BaseException] | tuple[type[BaseException], ...],
):
try:
help()
except x as e:
reveal_type(e) # revealed: AttributeError
except y as f:
reveal_type(f) # revealed: OSError | RuntimeError
except z as g:
reveal_type(g) # revealed: BaseException
except zz as h:
reveal_type(h) # revealed: TypeError | RuntimeError
except zzz as i:
reveal_type(i) # revealed: BaseException
We do not emit an invalid-exception-caught if a class is caught that has Any or Unknown in its
MRO, as the dynamic element in the MRO could materialize to some subclass of BaseException:
from compat import BASE_EXCEPTION_CLASS # error: [unresolved-import] "Cannot resolve imported module `compat`"
class Error(BASE_EXCEPTION_CLASS): ...
try:
...
except Error as err:
...
Exception with no captured type
try:
{}.get("foo")
except TypeError:
pass
Exception which catches typevar
[environment]
python-version = "3.12"
from typing import Callable
def silence[T: type[BaseException]](
func: Callable[[], None],
exception_type: T,
):
try:
func()
except exception_type as e:
reveal_type(e) # revealed: T'instance@silence
def silence2[T: (
type[ValueError],
type[TypeError],
)](func: Callable[[], None], exception_type: T,):
try:
func()
except exception_type as e:
reveal_type(e) # revealed: T'instance@silence2
Invalid exception handlers
try:
pass
# error: [invalid-exception-caught]
except 3 as e:
reveal_type(e) # revealed: Unknown
try:
pass
# error: [invalid-exception-caught]
except (ValueError, OSError, "foo", b"bar") as e:
reveal_type(e) # revealed: ValueError | OSError | Unknown
def foo(
x: type[str],
y: tuple[type[OSError], type[RuntimeError], int],
z: tuple[type[str], ...],
):
try:
help()
# error: [invalid-exception-caught]
except x as e:
reveal_type(e) # revealed: Unknown
# error: [invalid-exception-caught]
except y as f:
reveal_type(f) # revealed: OSError | RuntimeError | Unknown
# error: [invalid-exception-caught]
except z as g:
reveal_type(g) # revealed: Unknown
try:
{}.get("foo")
# error: [invalid-exception-caught]
except int:
pass
Object raised is not an exception
try:
raise AttributeError() # fine
except:
...
try:
raise FloatingPointError # fine
except:
...
try:
raise 1 # error: [invalid-raise]
except:
...
try:
raise int # error: [invalid-raise]
except:
...
def _(e: Exception | type[Exception]):
raise e # fine
def _(e: Exception | type[Exception] | None):
raise e # error: [invalid-raise]
Exception cause is not an exception
def _():
try:
raise EOFError() from GeneratorExit # fine
except:
...
def _():
try:
raise StopIteration from MemoryError() # fine
except:
...
def _():
try:
raise BufferError() from None # fine
except:
...
def _():
try:
raise ZeroDivisionError from False # error: [invalid-raise]
except:
...
def _():
try:
raise SystemExit from bool() # error: [invalid-raise]
except:
...
def _():
try:
raise
except KeyboardInterrupt as e: # fine
reveal_type(e) # revealed: KeyboardInterrupt
raise LookupError from e # fine
def _():
try:
raise
except int as e: # error: [invalid-exception-caught]
reveal_type(e) # revealed: Unknown
raise KeyError from e
def _(e: Exception | type[Exception]):
raise ModuleNotFoundError from e # fine
def _(e: Exception | type[Exception] | None):
raise IndexError from e # fine
def _(e: int | None):
raise IndexError from e # error: [invalid-raise]
The caught exception is cleared at the end of the except clause
e = None
reveal_type(e) # revealed: None
try:
raise ValueError()
except ValueError as e:
reveal_type(e) # revealed: ValueError
# error: [unresolved-reference]
reveal_type(e) # revealed: Unknown
e = None
def cond() -> bool:
return True
try:
if cond():
raise ValueError()
except ValueError as e:
reveal_type(e) # revealed: ValueError
# error: [possibly-unresolved-reference]
reveal_type(e) # revealed: None
def f(x: type[Exception]):
e = None
try:
raise x
except ValueError as e:
pass
except:
pass
# error: [possibly-unresolved-reference]
reveal_type(e) # revealed: None
Special-cased diagnostics for NotImplemented
try:
# error: [invalid-raise]
# error: [invalid-raise]
raise NotImplemented from NotImplemented
# error: [invalid-exception-caught]
except NotImplemented:
pass
# error: [invalid-exception-caught]
except (TypeError, NotImplemented):
pass