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
This commit is contained in:
Dylan 2025-05-05 06:40:36 -05:00 committed by GitHub
parent 78b4c3ccf1
commit a95c73d5d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 28 additions and 10 deletions

View file

@ -1,4 +1,4 @@
use ruff_python_ast::StmtFunctionDef; use ruff_python_ast::{PythonVersion, StmtFunctionDef};
use ruff_python_semantic::{ScopeKind, SemanticModel}; use ruff_python_semantic::{ScopeKind, SemanticModel};
use crate::rules::flake8_type_checking; use crate::rules::flake8_type_checking;
@ -29,7 +29,11 @@ pub(super) enum AnnotationContext {
impl AnnotationContext { impl AnnotationContext {
/// Determine the [`AnnotationContext`] for an annotation based on the current scope of the /// Determine the [`AnnotationContext`] for an annotation based on the current scope of the
/// semantic model. /// 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 // 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 // class field) or a function scope, and that class or function is marked as
// runtime-required, treat the annotation as runtime-required. // 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, // If `__future__` annotations are enabled or it's a stub file,
// then annotations are never evaluated at runtime, // then annotations are never evaluated at runtime,
// so we can treat them as typing-only. // 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; return Self::TypingOnly;
} }
@ -81,6 +85,7 @@ impl AnnotationContext {
function_def: &StmtFunctionDef, function_def: &StmtFunctionDef,
semantic: &SemanticModel, semantic: &SemanticModel,
settings: &LinterSettings, settings: &LinterSettings,
version: PythonVersion,
) -> Self { ) -> Self {
if flake8_type_checking::helpers::runtime_required_function( if flake8_type_checking::helpers::runtime_required_function(
function_def, function_def,
@ -88,7 +93,7 @@ impl AnnotationContext {
semantic, semantic,
) { ) {
Self::RuntimeRequired Self::RuntimeRequired
} else if semantic.future_annotations_or_stub() { } else if semantic.future_annotations_or_stub() || version.defers_annotations() {
Self::TypingOnly Self::TypingOnly
} else { } else {
Self::RuntimeEvaluated Self::RuntimeEvaluated

View file

@ -1004,9 +1004,13 @@ impl<'a> Visitor<'a> for Checker<'a> {
} }
// Function annotations are always evaluated at runtime, unless future annotations // Function annotations are always evaluated at runtime, unless future annotations
// are enabled. // are enabled or the Python version is at least 3.14.
let annotation = let annotation = AnnotationContext::from_function(
AnnotationContext::from_function(function_def, &self.semantic, self.settings); function_def,
&self.semantic,
self.settings,
self.target_version(),
);
// The first parameter may be a single dispatch. // The first parameter may be a single dispatch.
let singledispatch = let singledispatch =
@ -1203,7 +1207,11 @@ impl<'a> Visitor<'a> for Checker<'a> {
value, value,
.. ..
}) => { }) => {
match AnnotationContext::from_model(&self.semantic, self.settings) { match AnnotationContext::from_model(
&self.semantic,
self.settings,
self.target_version(),
) {
AnnotationContext::RuntimeRequired => { AnnotationContext::RuntimeRequired => {
self.visit_runtime_required_annotation(annotation); 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! // we can't defer again, or we'll infinitely recurse!
&& !self.semantic.in_deferred_type_definition() && !self.semantic.in_deferred_type_definition()
&& self.semantic.in_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()) && (self.semantic.in_annotation() || self.source_type.is_stub())
{ {
if let Expr::StringLiteral(string_literal) = expr { 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 // if they are annotations in a module where `from __future__ import
// annotations` is active, or they are type definitions in a stub file. // annotations` is active, or they are type definitions in a stub file.
debug_assert!( 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()) && (self.source_type.is_stub() || self.semantic.in_annotation())
); );

View file

@ -73,6 +73,10 @@ impl PythonVersion {
pub fn supports_pep_701(self) -> bool { pub fn supports_pep_701(self) -> bool {
self >= Self::PY312 self >= Self::PY312
} }
pub fn defers_annotations(self) -> bool {
self >= Self::PY314
}
} }
impl Default for PythonVersion { impl Default for PythonVersion {