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 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

View file

@ -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())
);

View file

@ -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 {