Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,39 @@ reveal_type(__dict__)
reveal_type(__init__)
```

## `ModuleType` globals combined with explicit assignments and declarations

A `ModuleType` attribute can be overridden in the global scope with a different type, but it must be
a type assignable to the declaration on `ModuleType` unless it is accompanied by an explicit
redeclaration:

`module.py`:

```py
__file__ = None
__path__: list[str] = []
__doc__: int # error: [invalid-declaration] "Cannot declare type `int` for inferred type `str | None`"
# error: [invalid-declaration] "Cannot shadow implicit global attribute `__package__` with declaration of type `int`"
__package__: int = 42
__spec__ = 42 # error: [invalid-assignment] "Object of type `Literal[42]` is not assignable to `ModuleSpec | None`"
```

`main.py`:

```py
import module

reveal_type(module.__file__) # revealed: Unknown | None
reveal_type(module.__path__) # revealed: list[str]
reveal_type(module.__doc__) # revealed: Unknown
reveal_type(module.__spec__) # revealed: Unknown | ModuleSpec | None

def nested_scope():
global __loader__
reveal_type(__loader__) # revealed: LoaderProtocol | None
__loader__ = 56 # error: [invalid-assignment] "Object of type `Literal[56]` is not assignable to `LoaderProtocol | None`"
```

## Accessed as attributes

`ModuleType` attributes can also be accessed as attributes on module-literal types. The special
Expand Down Expand Up @@ -105,29 +138,31 @@ defined as a global, however, a name lookup should union the `ModuleType` type w
conditionally defined type:

```py
__file__ = 42
__file__ = "foo"

def returns_bool() -> bool:
return True

if returns_bool():
__name__ = 1
__name__ = 1 # error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `str`"

reveal_type(__file__) # revealed: Literal[42]
reveal_type(__name__) # revealed: Literal[1] | str
reveal_type(__file__) # revealed: Literal["foo"]
reveal_type(__name__) # revealed: str
```

## Conditionally global or `ModuleType` attribute, with annotation

The same is true if the name is annotated:

```py
# error: [invalid-declaration] "Cannot shadow implicit global attribute `__file__` with declaration of type `int`"
__file__: int = 42

def returns_bool() -> bool:
return True

if returns_bool():
# error: [invalid-declaration] "Cannot shadow implicit global attribute `__name__` with declaration of type `int`"
__name__: int = 1

reveal_type(__file__) # revealed: Literal[42]
Expand Down
36 changes: 31 additions & 5 deletions crates/ty_python_semantic/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ use crate::types::{
};
use crate::{resolve_module, Db, KnownModule, Program};

pub(crate) use implicit_globals::module_type_implicit_global_symbol;
pub(crate) use implicit_globals::{
module_type_implicit_global_declaration, module_type_implicit_global_symbol,
};

#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub(crate) enum Boundness {
Expand Down Expand Up @@ -275,7 +277,6 @@ pub(crate) fn explicit_global_symbol<'db>(
/// rather than being looked up as symbols explicitly defined/declared in the global scope.
///
/// Use [`imported_symbol`] to perform the lookup as seen from outside the file (e.g. via imports).
#[cfg(test)]
pub(crate) fn global_symbol<'db>(
db: &'db dyn Db,
file: File,
Expand Down Expand Up @@ -958,11 +959,36 @@ mod implicit_globals {
use ruff_python_ast as ast;

use crate::db::Db;
use crate::semantic_index::{self, symbol_table};
use crate::semantic_index::{self, symbol_table, use_def_map};
use crate::symbol::SymbolAndQualifiers;
use crate::types::KnownClass;
use crate::types::{KnownClass, Type};

use super::Symbol;
use super::{symbol_from_declarations, Symbol, SymbolFromDeclarationsResult};

pub(crate) fn module_type_implicit_global_declaration<'db>(
db: &'db dyn Db,
name: &str,
) -> SymbolFromDeclarationsResult<'db> {
if !module_type_symbols(db)
.iter()
.any(|module_type_member| &**module_type_member == name)
{
return Ok(Symbol::Unbound.into());
}
let Type::ClassLiteral(module_type_class) = KnownClass::ModuleType.to_class_literal(db)
else {
return Ok(Symbol::Unbound.into());
};
let module_type_scope = module_type_class.body_scope(db);
let symbol_table = symbol_table(db, module_type_scope);
let Some(symbol_id) = symbol_table.symbol_id_by_name(name) else {
return Ok(Symbol::Unbound.into());
};
symbol_from_declarations(
db,
use_def_map(db, module_type_scope).public_declarations(symbol_id),
)
}

/// Looks up the type of an "implicit global symbol". Returns [`Symbol::Unbound`] if
/// `name` is not present as an implicit symbol in module-global namespaces.
Expand Down
77 changes: 67 additions & 10 deletions crates/ty_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ use crate::semantic_index::symbol::{
};
use crate::semantic_index::{semantic_index, EagerSnapshotResult, SemanticIndex};
use crate::symbol::{
builtins_module_scope, builtins_symbol, explicit_global_symbol,
module_type_implicit_global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
typing_extensions_symbol, Boundness, LookupError,
builtins_module_scope, builtins_symbol, explicit_global_symbol, global_symbol,
module_type_implicit_global_declaration, module_type_implicit_global_symbol, symbol,
symbol_from_bindings, symbol_from_declarations, typing_extensions_symbol, Boundness,
LookupError,
};
use crate::types::call::{Argument, Bindings, CallArgumentTypes, CallArguments, CallError};
use crate::types::class::{MetaclassErrorKind, SliceLiteral};
Expand Down Expand Up @@ -1420,8 +1421,9 @@ impl<'db> TypeInferenceBuilder<'db> {
let symbol_id = binding.symbol(self.db());

let global_use_def_map = self.index.use_def_map(FileScopeId::global());
let declarations = if self.skip_non_global_scopes(file_scope_id, symbol_id) {
let symbol_name = symbol_table.symbol(symbol_id).name();
let symbol_name = symbol_table.symbol(symbol_id).name();
let skip_non_global_scopes = self.skip_non_global_scopes(file_scope_id, symbol_id);
let declarations = if skip_non_global_scopes {
match self
.index
.symbol_table(FileScopeId::global())
Expand All @@ -1436,6 +1438,20 @@ impl<'db> TypeInferenceBuilder<'db> {
};

let declared_ty = symbol_from_declarations(self.db(), declarations)
.and_then(|symbol| {
let symbol = if matches!(symbol.symbol, Symbol::Type(_, Boundness::Bound)) {
symbol
} else if skip_non_global_scopes
|| self.scope().file_scope_id(self.db()).is_global()
{
let module_type_declarations =
module_type_implicit_global_declaration(self.db(), symbol_name)?;
symbol.or_fall_back_to(self.db(), || module_type_declarations)
} else {
symbol
};
Ok(symbol)
})
.map(|SymbolAndQualifiers { symbol, .. }| {
symbol.ignore_possibly_unbound().unwrap_or(Type::unknown())
})
Expand Down Expand Up @@ -1487,6 +1503,23 @@ impl<'db> TypeInferenceBuilder<'db> {
let prior_bindings = use_def.bindings_at_declaration(declaration);
// unbound_ty is Never because for this check we don't care about unbound
let inferred_ty = symbol_from_bindings(self.db(), prior_bindings)
.with_qualifiers(TypeQualifiers::empty())
.or_fall_back_to(self.db(), || {
// Fallback to bindings declared on `types.ModuleType` if it's a global symbol
let scope = self.scope().file_scope_id(self.db());
if scope.is_global() {
module_type_implicit_global_symbol(
self.db(),
self.index
.symbol_table(scope)
.symbol(declaration.symbol(self.db()))
.name(),
)
} else {
Symbol::Unbound.into()
}
})
.symbol
.ignore_possibly_unbound()
.unwrap_or(Type::Never);
let ty = if inferred_ty.is_assignable_to(self.db(), ty.inner_type()) {
Expand Down Expand Up @@ -1525,6 +1558,34 @@ impl<'db> TypeInferenceBuilder<'db> {
declared_ty,
inferred_ty,
} => {
let file_scope_id = self.scope().file_scope_id(self.db());
if file_scope_id.is_global() {
let symbol_table = self.index.symbol_table(file_scope_id);
let symbol_name = symbol_table.symbol(definition.symbol(self.db())).name();
if let Some(module_type_implicit_declaration) =
module_type_implicit_global_declaration(self.db(), symbol_name)
.ok()
.and_then(|sym| sym.symbol.ignore_possibly_unbound())
{
let declared_type = declared_ty.inner_type();
if !declared_type
.is_assignable_to(self.db(), module_type_implicit_declaration)
{
if let Some(builder) =
self.context.report_lint(&INVALID_DECLARATION, node)
{
let mut diagnostic = builder.into_diagnostic(format_args!(
"Cannot shadow implicit global attribute `{symbol_name}` with declaration of type `{}`",
declared_type.display(self.db())
));
diagnostic.info(format_args!("The global symbol `{}` must always have a type assignable to `{}`",
symbol_name,
module_type_implicit_declaration.display(self.db())
));
}
}
}
}
if inferred_ty.is_assignable_to(self.db(), declared_ty.inner_type()) {
(declared_ty, inferred_ty)
} else {
Expand Down Expand Up @@ -5386,11 +5447,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.is_some_and(|symbol_id| self.skip_non_global_scopes(file_scope_id, symbol_id));

if skip_non_global_scopes {
return symbol(
db,
FileScopeId::global().to_scope_id(db, current_file),
symbol_name,
);
return global_symbol(self.db(), self.file(), symbol_name);
}

// If it's a function-like scope and there is one or more binding in this scope (but
Expand Down
Loading