[ty] Allow PEP-604 unions in stubs and `TYPE_CHECKING` blocks prior to 3.10 (#21379)

This commit is contained in:
Alex Waygood 2025-11-11 14:33:43 +00:00 committed by GitHub
parent 7b237d316f
commit 44b0c9ebac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 65 additions and 13 deletions

View File

@ -272,6 +272,54 @@ def g(
): ... ): ...
``` ```
## `|` unions in stubs and `TYPE_CHECKING` blocks
In runtime contexts, `|` unions are only permitted on Python 3.10+. But in suites of code that are
never executed at runtime (stub files, `if TYPE_CHECKING` blocks, and stringified annotations), they
are permitted even if the target version is set to Python 3.9 or earlier.
```toml
[environment]
python-version = "3.9"
```
`bar.pyi`:
```pyi
Z = int | str
GLOBAL_CONSTANT: Z
```
`foo.py`:
```py
from typing import TYPE_CHECKING
from bar import GLOBAL_CONSTANT
reveal_type(GLOBAL_CONSTANT) # revealed: int | str
if TYPE_CHECKING:
class ItsQuiteCloudyInManchester:
X = int | str
def f(obj: X):
reveal_type(obj) # revealed: int | str
# TODO: we currently only understand code as being inside a `TYPE_CHECKING` block
# if a whole *scope* is inside the `if TYPE_CHECKING` block
# (like the `ItsQuiteCloudyInManchester` class above); this is a false-positive
Y = int | str # error: [unsupported-operator]
def g(obj: Y):
# TODO: should be `int | str`
reveal_type(obj) # revealed: Unknown
Y = list["int | str"]
def g(obj: Y):
reveal_type(obj) # revealed: list[int | str]
```
## Generic types ## Generic types
Implicit type aliases can also refer to generic types: Implicit type aliases can also refer to generic types:

View File

@ -8899,6 +8899,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
emitted_division_by_zero_diagnostic = self.check_division_by_zero(node, op, left_ty); emitted_division_by_zero_diagnostic = self.check_division_by_zero(node, op, left_ty);
} }
let pep_604_unions_allowed = || {
Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310
|| self.file().is_stub(self.db())
|| self.scope().scope(self.db()).in_type_checking_block()
};
match (left_ty, right_ty, op) { match (left_ty, right_ty, op) {
(Type::Union(lhs_union), rhs, _) => lhs_union.try_map(self.db(), |lhs_element| { (Type::Union(lhs_union), rhs, _) => lhs_union.try_map(self.db(), |lhs_element| {
self.infer_binary_expression_type( self.infer_binary_expression_type(
@ -9160,7 +9166,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| KnownInstanceType::Annotated(_), | KnownInstanceType::Annotated(_),
), ),
ast::Operator::BitOr, ast::Operator::BitOr,
) if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310 => { ) if pep_604_unions_allowed() => {
if left_ty.is_equivalent_to(self.db(), right_ty) { if left_ty.is_equivalent_to(self.db(), right_ty) {
Some(left_ty) Some(left_ty)
} else { } else {
@ -9186,7 +9192,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| Type::KnownInstance(..) | Type::KnownInstance(..)
| Type::SpecialForm(..), | Type::SpecialForm(..),
ast::Operator::BitOr, ast::Operator::BitOr,
) if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310 ) if pep_604_unions_allowed()
&& instance.has_known_class(self.db(), KnownClass::NoneType) => && instance.has_known_class(self.db(), KnownClass::NoneType) =>
{ {
Some(Type::KnownInstance(KnownInstanceType::UnionType( Some(Type::KnownInstance(KnownInstanceType::UnionType(
@ -9210,17 +9216,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
_, _,
Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..), Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..),
ast::Operator::BitOr, ast::Operator::BitOr,
) if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310 => { ) if pep_604_unions_allowed() => Type::try_call_bin_op_with_policy(
Type::try_call_bin_op_with_policy( self.db(),
self.db(), left_ty,
left_ty, ast::Operator::BitOr,
ast::Operator::BitOr, right_ty,
right_ty, MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, )
) .ok()
.ok() .map(|binding| binding.return_type(self.db())),
.map(|binding| binding.return_type(self.db()))
}
// We've handled all of the special cases that we support for literals, so we need to // We've handled all of the special cases that we support for literals, so we need to
// fall back on looking for dunder methods on one of the operand types. // fall back on looking for dunder methods on one of the operand types.