Extend PEP 604 rewrites to support some quoted annotations (#5725)

## Summary

Python doesn't allow `"Foo" | None` if the annotation will be evaluated
at runtime (see the comments in the PR, or the semantic model
documentation for more on what this means and when it is true), but it
_does_ allow it if the annotation is typing-only.

This, for example, is invalid, as Python will evaluate `"Foo" | None` at
runtime in order to
populate the function's `__annotations__`:

```python
def f(x: "Foo" | None): ...
```

This, however, is valid:

```python
def f():
    x: "Foo" | None
```

As is this:

```python
from __future__ import annotations

def f(x: "Foo" | None): ...
```

Closes #5706.
This commit is contained in:
Charlie Marsh 2023-07-13 07:34:04 -04:00 committed by GitHub
parent 549173b395
commit 932c9a4789
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 38 additions and 7 deletions

View file

@ -127,22 +127,36 @@ pub fn to_pep604_operator(
slice: &Expr,
semantic: &SemanticModel,
) -> Option<Pep604Operator> {
/// Returns `true` if any argument in the slice is a string.
fn any_arg_is_str(slice: &Expr) -> bool {
/// Returns `true` if any argument in the slice is a quoted annotation).
fn quoted_annotation(slice: &Expr) -> bool {
match slice {
Expr::Constant(ast::ExprConstant {
value: Constant::Str(_),
..
}) => true,
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().any(any_arg_is_str),
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().any(quoted_annotation),
_ => false,
}
}
// If any of the _arguments_ are forward references, we can't use PEP 604.
// Ex) `Union["str", "int"]` can't be converted to `"str" | "int"`.
if any_arg_is_str(slice) {
return None;
// If the slice is a forward reference (e.g., `Optional["Foo"]`), it can only be rewritten
// if we're in a typing-only context.
//
// This, for example, is invalid, as Python will evaluate `"Foo" | None` at runtime in order to
// populate the function's `__annotations__`:
// ```python
// def f(x: "Foo" | None): ...
// ```
//
// This, however, is valid:
// ```python
// def f():
// x: "Foo" | None
// ```
if quoted_annotation(slice) {
if semantic.execution_context().is_runtime() {
return None;
}
}
semantic