mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-24 05:25:17 +00:00
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:
parent
78b4c3ccf1
commit
a95c73d5d0
3 changed files with 28 additions and 10 deletions
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue