[pyflakes] Fix false positives for __annotate__ (Py3.14+) and __warningregistry__ (F821) (#20154)

## Summary

Fixes #19970

---------

Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
This commit is contained in:
Dan Parizher 2025-09-23 08:16:00 -04:00 committed by GitHub
parent 742f8a4ee6
commit 346842f003
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 153 additions and 28 deletions

View file

@ -56,7 +56,7 @@ use ruff_python_semantic::{
Import, Module, ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel, Import, Module, ModuleKind, ModuleSource, NodeId, ScopeId, ScopeKind, SemanticModel,
SemanticModelFlags, StarImport, SubmoduleImport, SemanticModelFlags, StarImport, SubmoduleImport,
}; };
use ruff_python_stdlib::builtins::{MAGIC_GLOBALS, python_builtins}; use ruff_python_stdlib::builtins::{python_builtins, python_magic_globals};
use ruff_python_trivia::CommentRanges; use ruff_python_trivia::CommentRanges;
use ruff_source_file::{OneIndexed, SourceFile, SourceFileBuilder, SourceRow}; use ruff_source_file::{OneIndexed, SourceFile, SourceFileBuilder, SourceRow};
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange, TextSize};
@ -2550,7 +2550,7 @@ impl<'a> Checker<'a> {
for builtin in standard_builtins { for builtin in standard_builtins {
bind_builtin(builtin); bind_builtin(builtin);
} }
for builtin in MAGIC_GLOBALS { for builtin in python_magic_globals(target_version.minor) {
bind_builtin(builtin); bind_builtin(builtin);
} }
for builtin in &settings.builtins { for builtin in &settings.builtins {

View file

@ -920,6 +920,42 @@ mod tests {
flakes("__annotations__", &[]); flakes("__annotations__", &[]);
} }
#[test]
fn module_warningregistry() {
// Using __warningregistry__ should not be considered undefined.
flakes("__warningregistry__", &[]);
}
#[test]
fn module_annotate_py314_available() {
// __annotate__ is available starting in Python 3.14.
let diagnostics = crate::test::test_snippet(
"__annotate__",
&crate::settings::LinterSettings {
unresolved_target_version: ruff_python_ast::PythonVersion::PY314.into(),
..crate::settings::LinterSettings::for_rules(vec![
crate::codes::Rule::UndefinedName,
])
},
);
assert!(diagnostics.is_empty());
}
#[test]
fn module_annotate_pre_py314_undefined() {
// __annotate__ is not available before Python 3.14.
let diagnostics = crate::test::test_snippet(
"__annotate__",
&crate::settings::LinterSettings {
unresolved_target_version: ruff_python_ast::PythonVersion::PY313.into(),
..crate::settings::LinterSettings::for_rules(vec![
crate::codes::Rule::UndefinedName,
])
},
);
assert_eq!(diagnostics.len(), 1);
}
#[test] #[test]
fn magic_globals_file() { fn magic_globals_file() {
// Use of the C{__file__} magic global should not emit an undefined name // Use of the C{__file__} magic global should not emit an undefined name

View file

@ -20,9 +20,15 @@ pub const MAGIC_GLOBALS: &[&str] = &[
"__annotations__", "__annotations__",
"__builtins__", "__builtins__",
"__cached__", "__cached__",
"__warningregistry__",
"__file__", "__file__",
]; ];
/// Magic globals that are only available starting in specific Python versions.
///
/// `__annotate__` was introduced in Python 3.14.
static PY314_PLUS_MAGIC_GLOBALS: &[&str] = &["__annotate__"];
static ALWAYS_AVAILABLE_BUILTINS: &[&str] = &[ static ALWAYS_AVAILABLE_BUILTINS: &[&str] = &[
"ArithmeticError", "ArithmeticError",
"AssertionError", "AssertionError",
@ -216,6 +222,21 @@ pub fn python_builtins(minor_version: u8, is_notebook: bool) -> impl Iterator<It
.copied() .copied()
} }
/// Return the list of magic globals for the given Python minor version.
pub fn python_magic_globals(minor_version: u8) -> impl Iterator<Item = &'static str> {
let py314_magic_globals = if minor_version >= 14 {
Some(PY314_PLUS_MAGIC_GLOBALS)
} else {
None
};
py314_magic_globals
.into_iter()
.flatten()
.chain(MAGIC_GLOBALS)
.copied()
}
/// Returns `true` if the given name is that of a Python builtin. /// Returns `true` if the given name is that of a Python builtin.
/// ///
/// Intended to be kept in sync with [`python_builtins`]. /// Intended to be kept in sync with [`python_builtins`].

View file

@ -0,0 +1,31 @@
# `__annotate__` as an implicit global is version-gated (Py3.14+)
## Absent before 3.14
`__annotate__` is never present in the global namespace on Python \<3.14.
```toml
[environment]
python-version = "3.13"
```
```py
# error: [unresolved-reference]
reveal_type(__annotate__) # revealed: Unknown
```
## Present in 3.14+
The `__annotate__` global may be present in Python 3.14, but only if at least one global symbol in
the module is annotated (e.g. `x: int` or `x: int = 42`). Currently we model `__annotate__` as
always being possibly unbound on Python 3.14+.
```toml
[environment]
python-version = "3.14"
```
```py
# error: [possibly-unresolved-reference]
reveal_type(__annotate__) # revealed: (format: int, /) -> dict[str, Any]
```

View file

@ -16,6 +16,8 @@ reveal_type(__doc__) # revealed: str | None
reveal_type(__spec__) # revealed: ModuleSpec | None reveal_type(__spec__) # revealed: ModuleSpec | None
reveal_type(__path__) # revealed: MutableSequence[str] reveal_type(__path__) # revealed: MutableSequence[str]
reveal_type(__builtins__) # revealed: Any reveal_type(__builtins__) # revealed: Any
# error: [possibly-unresolved-reference] "Name `__warningregistry__` used when possibly not defined"
reveal_type(__warningregistry__) # revealed: dict[Any, int]
import sys import sys
@ -75,6 +77,8 @@ reveal_type(module.__file__) # revealed: Unknown | None
reveal_type(module.__path__) # revealed: list[str] reveal_type(module.__path__) # revealed: list[str]
reveal_type(module.__doc__) # revealed: Unknown reveal_type(module.__doc__) # revealed: Unknown
reveal_type(module.__spec__) # revealed: Unknown | ModuleSpec | None reveal_type(module.__spec__) # revealed: Unknown | ModuleSpec | None
# error: [unresolved-attribute]
reveal_type(module.__warningregistry__) # revealed: Unknown
def nested_scope(): def nested_scope():
global __loader__ global __loader__

View file

@ -1339,12 +1339,15 @@ fn is_reexported(db: &dyn Db, definition: Definition<'_>) -> bool {
mod implicit_globals { mod implicit_globals {
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use crate::Program;
use crate::db::Db; use crate::db::Db;
use crate::place::PlaceAndQualifiers; use crate::place::{Boundness, PlaceAndQualifiers};
use crate::semantic_index::symbol::Symbol; use crate::semantic_index::symbol::Symbol;
use crate::semantic_index::{place_table, use_def_map}; use crate::semantic_index::{place_table, use_def_map};
use crate::types::{KnownClass, Type}; use crate::types::{CallableType, KnownClass, Parameter, Parameters, Signature, Type};
use ruff_python_ast::PythonVersion;
use super::{Place, place_from_declarations}; use super::{Place, place_from_declarations};
@ -1392,28 +1395,58 @@ mod implicit_globals {
db: &'db dyn Db, db: &'db dyn Db,
name: &str, name: &str,
) -> PlaceAndQualifiers<'db> { ) -> PlaceAndQualifiers<'db> {
// We special-case `__file__` here because we know that for an internal implicit global match name {
// lookup in a Python module, it is always a string, even though typeshed says `str | // We special-case `__file__` here because we know that for an internal implicit global
// None`. // lookup in a Python module, it is always a string, even though typeshed says `str |
if name == "__file__" { // None`.
Place::bound(KnownClass::Str.to_instance(db)).into() "__file__" => Place::bound(KnownClass::Str.to_instance(db)).into(),
} else if name == "__builtins__" {
Place::bound(Type::any()).into() "__builtins__" => Place::bound(Type::any()).into(),
} else if name == "__debug__" {
Place::bound(KnownClass::Bool.to_instance(db)).into() "__debug__" => Place::bound(KnownClass::Bool.to_instance(db)).into(),
}
// In general we wouldn't check to see whether a symbol exists on a class before doing the // Created lazily by the warnings machinery; may be absent.
// `.member()` call on the instance type -- we'd just do the `.member`() call on the instance // Model as possibly-unbound to avoid false negatives.
// type, since it has the same end result. The reason to only call `.member()` on `ModuleType` "__warningregistry__" => Place::Type(
// when absolutely necessary is that this function is used in a very hot path (name resolution KnownClass::Dict
// in `infer.rs`). We use less idiomatic (and much more verbose) code here as a micro-optimisation. .to_specialized_instance(db, [Type::any(), KnownClass::Int.to_instance(db)]),
else if module_type_symbols(db) Boundness::PossiblyUnbound,
.iter() )
.any(|module_type_member| &**module_type_member == name) .into(),
{
KnownClass::ModuleType.to_instance(db).member(db, name) // Marked as possibly-unbound as it is only present in the module namespace
} else { // if at least one global symbol is annotated in the module.
Place::Unbound.into() "__annotate__" if Program::get(db).python_version(db) >= PythonVersion::PY314 => {
let signature = Signature::new(
Parameters::new(
[Parameter::positional_only(Some(Name::new_static("format")))
.with_annotated_type(KnownClass::Int.to_instance(db))],
),
Some(KnownClass::Dict.to_specialized_instance(
db,
[KnownClass::Str.to_instance(db), Type::any()],
)),
);
Place::Type(
CallableType::function_like(db, signature),
Boundness::PossiblyUnbound,
)
.into()
}
// In general we wouldn't check to see whether a symbol exists on a class before doing the
// `.member()` call on the instance type -- we'd just do the `.member`() call on the instance
// type, since it has the same end result. The reason to only call `.member()` on `ModuleType`
// when absolutely necessary is that this function is used in a very hot path (name resolution
// in `infer.rs`). We use less idiomatic (and much more verbose) code here as a micro-optimisation.
_ if module_type_symbols(db)
.iter()
.any(|module_type_member| &**module_type_member == name) =>
{
KnownClass::ModuleType.to_instance(db).member(db, name)
}
_ => Place::Unbound.into(),
} }
} }

View file

@ -28,7 +28,7 @@ pub(crate) use self::infer::{
infer_expression_types, infer_isolated_expression, infer_scope_types, infer_expression_types, infer_isolated_expression, infer_scope_types,
static_expression_truthiness, static_expression_truthiness,
}; };
pub(crate) use self::signatures::{CallableSignature, Signature}; pub(crate) use self::signatures::{CallableSignature, Parameter, Parameters, Signature};
pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType};
use crate::module_name::ModuleName; use crate::module_name::ModuleName;
use crate::module_resolver::{KnownModule, resolve_module}; use crate::module_resolver::{KnownModule, resolve_module};
@ -64,7 +64,7 @@ pub use crate::types::ide_support::{
use crate::types::infer::infer_unpack_types; use crate::types::infer::infer_unpack_types;
use crate::types::mro::{Mro, MroError, MroIterator}; use crate::types::mro::{Mro, MroError, MroIterator};
pub(crate) use crate::types::narrow::infer_narrowing_constraint; pub(crate) use crate::types::narrow::infer_narrowing_constraint;
use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signature}; use crate::types::signatures::{ParameterForm, walk_signature};
use crate::types::tuple::TupleSpec; use crate::types::tuple::TupleSpec;
pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type}; pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
use crate::types::variance::{TypeVarVariance, VarianceInferable}; use crate::types::variance::{TypeVarVariance, VarianceInferable};