From a95c73d5d0d26d16736dca6e94c2e648d9d94e3c Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 5 May 2025 06:40:36 -0500 Subject: [PATCH] Implement deferred annotations for Python 3.14 (#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 #15100 --- .../src/checkers/ast/annotation.rs | 13 ++++++++---- crates/ruff_linter/src/checkers/ast/mod.rs | 21 +++++++++++++------ crates/ruff_python_ast/src/python_version.rs | 4 ++++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/crates/ruff_linter/src/checkers/ast/annotation.rs b/crates/ruff_linter/src/checkers/ast/annotation.rs index 86d1ba50f8..f545ba27eb 100644 --- a/crates/ruff_linter/src/checkers/ast/annotation.rs +++ b/crates/ruff_linter/src/checkers/ast/annotation.rs @@ -1,4 +1,4 @@ -use ruff_python_ast::StmtFunctionDef; +use ruff_python_ast::{PythonVersion, StmtFunctionDef}; use ruff_python_semantic::{ScopeKind, SemanticModel}; use crate::rules::flake8_type_checking; @@ -29,7 +29,11 @@ pub(super) enum AnnotationContext { impl AnnotationContext { /// Determine the [`AnnotationContext`] for an annotation based on the current scope of the /// semantic model. - pub(super) fn from_model(semantic: &SemanticModel, settings: &LinterSettings) -> Self { + pub(super) fn from_model( + semantic: &SemanticModel, + settings: &LinterSettings, + version: PythonVersion, + ) -> Self { // If the annotation is in a class scope (e.g., an annotated assignment for a // class field) or a function scope, and that class or function is marked as // runtime-required, treat the annotation as runtime-required. @@ -59,7 +63,7 @@ impl AnnotationContext { // If `__future__` annotations are enabled or it's a stub file, // then annotations are never evaluated at runtime, // so we can treat them as typing-only. - if semantic.future_annotations_or_stub() { + if semantic.future_annotations_or_stub() || version.defers_annotations() { return Self::TypingOnly; } @@ -81,6 +85,7 @@ impl AnnotationContext { function_def: &StmtFunctionDef, semantic: &SemanticModel, settings: &LinterSettings, + version: PythonVersion, ) -> Self { if flake8_type_checking::helpers::runtime_required_function( function_def, @@ -88,7 +93,7 @@ impl AnnotationContext { semantic, ) { Self::RuntimeRequired - } else if semantic.future_annotations_or_stub() { + } else if semantic.future_annotations_or_stub() || version.defers_annotations() { Self::TypingOnly } else { Self::RuntimeEvaluated diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 2a5a8f3a2b..819e28fdc0 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -1004,9 +1004,13 @@ impl<'a> Visitor<'a> for Checker<'a> { } // Function annotations are always evaluated at runtime, unless future annotations - // are enabled. - let annotation = - AnnotationContext::from_function(function_def, &self.semantic, self.settings); + // are enabled or the Python version is at least 3.14. + let annotation = AnnotationContext::from_function( + function_def, + &self.semantic, + self.settings, + self.target_version(), + ); // The first parameter may be a single dispatch. let singledispatch = @@ -1203,7 +1207,11 @@ impl<'a> Visitor<'a> for Checker<'a> { value, .. }) => { - match AnnotationContext::from_model(&self.semantic, self.settings) { + match AnnotationContext::from_model( + &self.semantic, + self.settings, + self.target_version(), + ) { AnnotationContext::RuntimeRequired => { self.visit_runtime_required_annotation(annotation); } @@ -1358,7 +1366,7 @@ impl<'a> Visitor<'a> for Checker<'a> { // we can't defer again, or we'll infinitely recurse! && !self.semantic.in_deferred_type_definition() && self.semantic.in_type_definition() - && self.semantic.future_annotations_or_stub() + && (self.semantic.future_annotations_or_stub()||self.target_version.defers_annotations()) && (self.semantic.in_annotation() || self.source_type.is_stub()) { if let Expr::StringLiteral(string_literal) = expr { @@ -2585,7 +2593,8 @@ impl<'a> Checker<'a> { // if they are annotations in a module where `from __future__ import // annotations` is active, or they are type definitions in a stub file. debug_assert!( - self.semantic.future_annotations_or_stub() + (self.semantic.future_annotations_or_stub() + || self.target_version.defers_annotations()) && (self.source_type.is_stub() || self.semantic.in_annotation()) ); diff --git a/crates/ruff_python_ast/src/python_version.rs b/crates/ruff_python_ast/src/python_version.rs index 20906a2510..a9add9d5a4 100644 --- a/crates/ruff_python_ast/src/python_version.rs +++ b/crates/ruff_python_ast/src/python_version.rs @@ -73,6 +73,10 @@ impl PythonVersion { pub fn supports_pep_701(self) -> bool { self >= Self::PY312 } + + pub fn defers_annotations(self) -> bool { + self >= Self::PY314 + } } impl Default for PythonVersion {