Skip to content

Commit a95c73d

Browse files
authored
Implement deferred annotations for Python 3.14 (astral-sh#17658)
This PR updates the semantic model for Python 3.14 by essentially equating "run using Python 3.14" with "uses `from __future__ import annotations`". While this is not technically correct under the hood, it appears to be correct for the purposes of our semantic model. That is: from the point of view of deciding when to parse, bind, etc. annotations, these two contexts behave the same. More generally these contexts behave the same unless you are performing some kind of introspection like the following: Without future import: ```pycon >>> from annotationlib import get_annotations,Format >>> def foo()->Bar:... ... >>> get_annotations(foo,format=Format.FORWARDREF) {'return': ForwardRef('Bar')} >>> get_annotations(foo,format=Format.STRING) {'return': 'Bar'} >>> get_annotations(foo,format=Format.VALUE) Traceback (most recent call last): [...] NameError: name 'Bar' is not defined >>> get_annotations(foo) Traceback (most recent call last): [...] NameError: name 'Bar' is not defined ``` With future import: ``` >>> from __future__ import annotations >>> from annotationlib import get_annotations,Format >>> def foo()->Bar:... ... >>> get_annotations(foo,format=Format.FORWARDREF) {'return': 'Bar'} >>> get_annotations(foo,format=Format.STRING) {'return': 'Bar'} >>> get_annotations(foo,format=Format.VALUE) {'return': 'Bar'} >>> get_annotations(foo) {'return': 'Bar'} ``` (Note: the result of the last call to `get_annotations` in these examples relies on the fact that, as of this writing, the default value for `format` is `Format.VALUE`). If one day we support lint rules targeting code that introspects using the new `annotationlib`, then it is possible we will need to revisit our approximation. Closes astral-sh#15100
1 parent 78b4c3c commit a95c73d

File tree

3 files changed

+28
-10
lines changed

3 files changed

+28
-10
lines changed

crates/ruff_linter/src/checkers/ast/annotation.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use ruff_python_ast::StmtFunctionDef;
1+
use ruff_python_ast::{PythonVersion, StmtFunctionDef};
22
use ruff_python_semantic::{ScopeKind, SemanticModel};
33

44
use crate::rules::flake8_type_checking;
@@ -29,7 +29,11 @@ pub(super) enum AnnotationContext {
2929
impl AnnotationContext {
3030
/// Determine the [`AnnotationContext`] for an annotation based on the current scope of the
3131
/// semantic model.
32-
pub(super) fn from_model(semantic: &SemanticModel, settings: &LinterSettings) -> Self {
32+
pub(super) fn from_model(
33+
semantic: &SemanticModel,
34+
settings: &LinterSettings,
35+
version: PythonVersion,
36+
) -> Self {
3337
// If the annotation is in a class scope (e.g., an annotated assignment for a
3438
// class field) or a function scope, and that class or function is marked as
3539
// runtime-required, treat the annotation as runtime-required.
@@ -59,7 +63,7 @@ impl AnnotationContext {
5963
// If `__future__` annotations are enabled or it's a stub file,
6064
// then annotations are never evaluated at runtime,
6165
// so we can treat them as typing-only.
62-
if semantic.future_annotations_or_stub() {
66+
if semantic.future_annotations_or_stub() || version.defers_annotations() {
6367
return Self::TypingOnly;
6468
}
6569

@@ -81,14 +85,15 @@ impl AnnotationContext {
8185
function_def: &StmtFunctionDef,
8286
semantic: &SemanticModel,
8387
settings: &LinterSettings,
88+
version: PythonVersion,
8489
) -> Self {
8590
if flake8_type_checking::helpers::runtime_required_function(
8691
function_def,
8792
&settings.flake8_type_checking.runtime_required_decorators,
8893
semantic,
8994
) {
9095
Self::RuntimeRequired
91-
} else if semantic.future_annotations_or_stub() {
96+
} else if semantic.future_annotations_or_stub() || version.defers_annotations() {
9297
Self::TypingOnly
9398
} else {
9499
Self::RuntimeEvaluated

crates/ruff_linter/src/checkers/ast/mod.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,9 +1004,13 @@ impl<'a> Visitor<'a> for Checker<'a> {
10041004
}
10051005

10061006
// Function annotations are always evaluated at runtime, unless future annotations
1007-
// are enabled.
1008-
let annotation =
1009-
AnnotationContext::from_function(function_def, &self.semantic, self.settings);
1007+
// are enabled or the Python version is at least 3.14.
1008+
let annotation = AnnotationContext::from_function(
1009+
function_def,
1010+
&self.semantic,
1011+
self.settings,
1012+
self.target_version(),
1013+
);
10101014

10111015
// The first parameter may be a single dispatch.
10121016
let singledispatch =
@@ -1203,7 +1207,11 @@ impl<'a> Visitor<'a> for Checker<'a> {
12031207
value,
12041208
..
12051209
}) => {
1206-
match AnnotationContext::from_model(&self.semantic, self.settings) {
1210+
match AnnotationContext::from_model(
1211+
&self.semantic,
1212+
self.settings,
1213+
self.target_version(),
1214+
) {
12071215
AnnotationContext::RuntimeRequired => {
12081216
self.visit_runtime_required_annotation(annotation);
12091217
}
@@ -1358,7 +1366,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
13581366
// we can't defer again, or we'll infinitely recurse!
13591367
&& !self.semantic.in_deferred_type_definition()
13601368
&& self.semantic.in_type_definition()
1361-
&& self.semantic.future_annotations_or_stub()
1369+
&& (self.semantic.future_annotations_or_stub()||self.target_version.defers_annotations())
13621370
&& (self.semantic.in_annotation() || self.source_type.is_stub())
13631371
{
13641372
if let Expr::StringLiteral(string_literal) = expr {
@@ -2585,7 +2593,8 @@ impl<'a> Checker<'a> {
25852593
// if they are annotations in a module where `from __future__ import
25862594
// annotations` is active, or they are type definitions in a stub file.
25872595
debug_assert!(
2588-
self.semantic.future_annotations_or_stub()
2596+
(self.semantic.future_annotations_or_stub()
2597+
|| self.target_version.defers_annotations())
25892598
&& (self.source_type.is_stub() || self.semantic.in_annotation())
25902599
);
25912600

crates/ruff_python_ast/src/python_version.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ impl PythonVersion {
7373
pub fn supports_pep_701(self) -> bool {
7474
self >= Self::PY312
7575
}
76+
77+
pub fn defers_annotations(self) -> bool {
78+
self >= Self::PY314
79+
}
7680
}
7781

7882
impl Default for PythonVersion {

0 commit comments

Comments
 (0)