Skip to content

Commit 1918c61

Browse files
authored
[red-knot] class bases are not affected by __future__.annotations (#17456)
## Summary We were over-conflating the conditions for deferred name resolution. `from __future__ import annotations` defers annotations, but not class bases. In stub files, class bases are also deferred. Modeling this correctly also reduces likelihood of cycles in Python files using `from __future__ import annotations` (since deferred resolution is inherently cycle-prone). The same cycles are still possible in `.pyi` files, but much less likely, since typically there isn't anything in a `pyi` file that would cause an early return from a scope, or otherwise cause visibility constraints to persist to end of scope. Usually there is only code at module global scope and class scope, which can't have `return` statements, and `raise` or `assert` statements in a stub file would be very strange. (Technically according to the spec we'd be within our rights to just forbid a whole bunch of syntax outright in a stub file, but I kinda like minimizing unnecessary differences between the handling of Python files and stub files.) ## Test Plan Added mdtests.
1 parent 44ad201 commit 1918c61

File tree

2 files changed

+26
-7
lines changed

2 files changed

+26
-7
lines changed

crates/red_knot_python_semantic/resources/mdtest/annotations/deferred.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,24 @@ def _():
156156
def f(self) -> C:
157157
return self
158158
```
159+
160+
## Base class references
161+
162+
### Not deferred by __future__.annotations
163+
164+
```py
165+
from __future__ import annotations
166+
167+
class A(B): # error: [unresolved-reference]
168+
pass
169+
170+
class B:
171+
pass
172+
```
173+
174+
### Deferred in stub files
175+
176+
```pyi
177+
class A(B): ...
178+
class B: ...
179+
```

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -585,8 +585,8 @@ impl<'db> TypeInferenceBuilder<'db> {
585585

586586
/// Are we currently inferring types in file with deferred types?
587587
/// This is true for stub files and files with `__future__.annotations`
588-
fn are_all_types_deferred(&self) -> bool {
589-
self.index.has_future_annotations() || self.file().is_stub(self.db().upcast())
588+
fn defer_annotations(&self) -> bool {
589+
self.index.has_future_annotations() || self.in_stub()
590590
}
591591

592592
/// Are we currently inferring deferred types?
@@ -1467,7 +1467,7 @@ impl<'db> TypeInferenceBuilder<'db> {
14671467
// If there are type params, parameters and returns are evaluated in that scope, that is, in
14681468
// `infer_function_type_params`, rather than here.
14691469
if type_params.is_none() {
1470-
if self.are_all_types_deferred() {
1470+
if self.defer_annotations() {
14711471
self.types.deferred.insert(definition);
14721472
} else {
14731473
self.infer_optional_annotation_expression(
@@ -1791,9 +1791,7 @@ impl<'db> TypeInferenceBuilder<'db> {
17911791
// TODO: Only defer the references that are actually string literals, instead of
17921792
// deferring the entire class definition if a string literal occurs anywhere in the
17931793
// base class list.
1794-
if self.are_all_types_deferred()
1795-
|| class_node.bases().iter().any(contains_string_literal)
1796-
{
1794+
if self.in_stub() || class_node.bases().iter().any(contains_string_literal) {
17971795
self.types.deferred.insert(definition);
17981796
} else {
17991797
for base in class_node.bases() {
@@ -2919,7 +2917,7 @@ impl<'db> TypeInferenceBuilder<'db> {
29192917

29202918
let mut declared_ty = self.infer_annotation_expression(
29212919
annotation,
2922-
DeferredExpressionState::from(self.are_all_types_deferred()),
2920+
DeferredExpressionState::from(self.defer_annotations()),
29232921
);
29242922

29252923
if target

0 commit comments

Comments
 (0)