Allow flake8-type-checking rules to automatically quote runtime-evaluated references (#6001)

## Summary

This allows us to fix usages like:

```python
from pandas import DataFrame

def baz() -> DataFrame:
    ...
```

By quoting the `DataFrame` in `-> DataFrame`. Without quotes, moving
`from pandas import DataFrame` into an `if TYPE_CHECKING:` block will
fail at runtime, since Python tries to evaluate the annotation to add it
to the function's `__annotations__`.

Unfortunately, this does require us to split our "annotation kind" flags
into three categories, rather than two:

- `typing-only`: The annotation is only evaluated at type-checking-time.
- `runtime-evaluated`: Python will evaluate the annotation at runtime
(like above) -- but we're willing to quote it.
- `runtime-required`: Python will evaluate the annotation at runtime
(like above), and some library (like Pydantic) needs it to be available
at runtime, so we _can't_ quote it.

This functionality is gated behind a setting
(`flake8-type-checking.quote-annotations`).

Closes https://github.com/astral-sh/ruff/issues/5559.
This commit is contained in:
Charlie Marsh 2023-12-12 22:12:38 -05:00 committed by GitHub
parent 4d2ee5bf98
commit 1a65e544c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1034 additions and 208 deletions

View file

@ -1642,6 +1642,57 @@ pub struct Flake8TypeCheckingOptions {
"#
)]
pub runtime_evaluated_decorators: Option<Vec<String>>,
/// Whether to add quotes around type annotations, if doing so would allow
/// the corresponding import to be moved into a type-checking block.
///
/// For example, in the following, Python requires that `Sequence` be
/// available at runtime, despite the fact that it's only used in a type
/// annotation:
///
/// ```python
/// from collections.abc import Sequence
///
///
/// def func(value: Sequence[int]) -> None:
/// ...
/// ```
///
/// In other words, moving `from collections.abc import Sequence` into an
/// `if TYPE_CHECKING:` block above would cause a runtime error, as the
/// type would no longer be available at runtime.
///
/// By default, Ruff will respect such runtime semantics and avoid moving
/// the import to prevent such runtime errors.
///
/// Setting `quote-annotations` to `true` will instruct Ruff to add quotes
/// around the annotation (e.g., `"Sequence[int]"`), which in turn enables
/// Ruff to move the import into an `if TYPE_CHECKING:` block, like so:
///
/// ```python
/// from typing import TYPE_CHECKING
///
/// if TYPE_CHECKING:
/// from collections.abc import Sequence
///
///
/// def func(value: "Sequence[int]") -> None:
/// ...
/// ```
///
/// Note that this setting has no effect when `from __future__ import annotations`
/// is present, as `__future__` annotations are always treated equivalently
/// to quoted annotations.
#[option(
default = "false",
value_type = "bool",
example = r#"
# Add quotes around type annotations, if doing so would allow
# an import to be moved into a type-checking block.
quote-annotations = true
"#
)]
pub quote_annotations: Option<bool>,
}
impl Flake8TypeCheckingOptions {
@ -1651,8 +1702,9 @@ impl Flake8TypeCheckingOptions {
exempt_modules: self
.exempt_modules
.unwrap_or_else(|| vec!["typing".to_string()]),
runtime_evaluated_base_classes: self.runtime_evaluated_base_classes.unwrap_or_default(),
runtime_evaluated_decorators: self.runtime_evaluated_decorators.unwrap_or_default(),
runtime_required_base_classes: self.runtime_evaluated_base_classes.unwrap_or_default(),
runtime_required_decorators: self.runtime_evaluated_decorators.unwrap_or_default(),
quote_annotations: self.quote_annotations.unwrap_or_default(),
}
}
}