Compare commits
3 Commits
main
...
charlie/dy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
597c35458e | ||
|
|
b88db87755 | ||
|
|
3932ef7c46 |
@@ -20,16 +20,13 @@ class Base: ...
|
||||
class Mixin: ...
|
||||
|
||||
# We synthesize a class type using the name argument
|
||||
Foo = type("Foo", (), {})
|
||||
reveal_type(Foo) # revealed: <class 'Foo'>
|
||||
reveal_type(type("Foo", (), {})) # revealed: <class 'Foo'>
|
||||
|
||||
# With a single base class
|
||||
Foo2 = type("Foo", (Base,), {"attr": 1})
|
||||
reveal_type(Foo2) # revealed: <class 'Foo'>
|
||||
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: <class 'Foo'>
|
||||
|
||||
# With multiple base classes
|
||||
Foo3 = type("Foo", (Base, Mixin), {})
|
||||
reveal_type(Foo3) # revealed: <class 'Foo'>
|
||||
reveal_type(type("Foo", (Base, Mixin), {})) # revealed: <class 'Foo'>
|
||||
|
||||
# The inferred type is assignable to type[Base] since Foo inherits from Base
|
||||
tests: list[type[Base]] = []
|
||||
@@ -415,24 +412,22 @@ reveal_type(type("Bar", (int,), {}, weird_other_arg=42)) # revealed: Unknown
|
||||
reveal_type(type("Baz", (), {}, metaclass=type)) # revealed: Unknown
|
||||
```
|
||||
|
||||
The following calls are also invalid, due to incorrect argument types.
|
||||
|
||||
Inline calls (not assigned to a variable) fall back to regular `type` overload matching, which
|
||||
produces slightly different error messages than assigned dynamic class creation:
|
||||
The following calls are also invalid, due to incorrect argument types:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
# error: 6 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`"
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `Literal[b"Foo"]`"
|
||||
type(b"Foo", (), {})
|
||||
|
||||
# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `<class 'Base'>`"
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter 2 (`bases`) of `type()`: Expected `tuple[type, ...]`, found `<class 'Base'>`"
|
||||
type("Foo", Base, {})
|
||||
|
||||
# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`"
|
||||
# error: 14 [invalid-base] "Invalid class base with type `Literal[1]`"
|
||||
# error: 17 [invalid-base] "Invalid class base with type `Literal[2]`"
|
||||
type("Foo", (1, 2), {})
|
||||
|
||||
# error: 22 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[str | bytes, Any]`"
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter 3 (`namespace`) of `type()`: Expected `dict[str, Any]`, found `dict[Unknown | bytes, Unknown | int]`"
|
||||
type("Foo", (Base,), {b"attr": 1})
|
||||
```
|
||||
|
||||
@@ -656,20 +651,24 @@ def f(*args, **kwargs):
|
||||
|
||||
## Explicit type annotations
|
||||
|
||||
TODO: Annotated assignments with `type()` calls don't currently synthesize the specific class type.
|
||||
This will be fixed when we support all `type()` calls (including inline) via generic handling.
|
||||
When an explicit type annotation is provided, the inferred type is checked against it:
|
||||
|
||||
```py
|
||||
# The annotation `type` is compatible with the inferred class literal type
|
||||
T: type = type("T", (), {})
|
||||
reveal_type(T) # revealed: <class 'T'>
|
||||
|
||||
# The annotation `type[Base]` is compatible with the inferred type
|
||||
class Base: ...
|
||||
|
||||
# TODO: Should infer `<class 'T'>` instead of `type`
|
||||
T: type = type("T", (), {})
|
||||
reveal_type(T) # revealed: type
|
||||
|
||||
# TODO: Should infer `<class 'Derived'>` instead of `type[Base]}
|
||||
# error: [invalid-assignment] "Object of type `type` is not assignable to `type[Base]`"
|
||||
Derived: type[Base] = type("Derived", (Base,), {})
|
||||
reveal_type(Derived) # revealed: type[Base]
|
||||
reveal_type(Derived) # revealed: <class 'Derived'>
|
||||
|
||||
# Incompatible annotation produces an error
|
||||
class Unrelated: ...
|
||||
|
||||
# error: [invalid-assignment]
|
||||
Bad: type[Unrelated] = type("Bad", (Base,), {})
|
||||
```
|
||||
|
||||
## Special base classes
|
||||
|
||||
@@ -169,12 +169,15 @@ Narrowing does not occur in the same way if `type` is used to dynamically create
|
||||
|
||||
```py
|
||||
def _(x: str | int):
|
||||
# Inline type() calls fall back to regular type overload matching.
|
||||
# TODO: Once inline type() calls synthesize class types, this should narrow x to Never.
|
||||
# The following diagnostic is valid, since the three-argument form of `type`
|
||||
# can only be called with `str` as the first argument.
|
||||
#
|
||||
# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `str | int`"
|
||||
# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `str | int`"
|
||||
if type(x, (), {}) is str:
|
||||
reveal_type(x) # revealed: str | int
|
||||
# But we synthesize a new class object as the result of a three-argument call to `type`,
|
||||
# and we know that this synthesized class object is not the same object as the `str` class object,
|
||||
# so here the type is narrowed to `Never`!
|
||||
reveal_type(x) # revealed: Never
|
||||
else:
|
||||
reveal_type(x) # revealed: str | int
|
||||
```
|
||||
|
||||
@@ -67,7 +67,7 @@ where
|
||||
///
|
||||
/// This method may panic or produce unspecified results if the provided module is from a
|
||||
/// different file or Salsa revision than the module to which the node belongs.
|
||||
pub(super) fn new(module_ref: &ParsedModuleRef, node: &T) -> Self {
|
||||
pub(crate) fn new(module_ref: &ParsedModuleRef, node: &T) -> Self {
|
||||
let index = node.node_index().load();
|
||||
debug_assert_eq!(module_ref.get_by_index(index).try_into().ok(), Some(node));
|
||||
|
||||
|
||||
@@ -6529,9 +6529,9 @@ impl<'db> Type<'db> {
|
||||
Some(TypeDefinition::Function(function.definition(db)))
|
||||
}
|
||||
Self::ModuleLiteral(module) => Some(TypeDefinition::Module(module.module(db))),
|
||||
Self::ClassLiteral(class_literal) => Some(class_literal.type_definition(db)),
|
||||
Self::ClassLiteral(class_literal) => class_literal.type_definition(db),
|
||||
Self::GenericAlias(alias) => Some(TypeDefinition::StaticClass(alias.definition(db))),
|
||||
Self::NominalInstance(instance) => Some(instance.class(db).type_definition(db)),
|
||||
Self::NominalInstance(instance) => instance.class(db).type_definition(db),
|
||||
Self::KnownInstance(instance) => match instance {
|
||||
KnownInstanceType::TypeVar(var) => {
|
||||
Some(TypeDefinition::TypeVar(var.definition(db)?))
|
||||
@@ -6545,7 +6545,7 @@ impl<'db> Type<'db> {
|
||||
|
||||
Self::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
|
||||
SubclassOfInner::Dynamic(_) => None,
|
||||
SubclassOfInner::Class(class) => Some(class.type_definition(db)),
|
||||
SubclassOfInner::Class(class) => class.type_definition(db),
|
||||
SubclassOfInner::TypeVar(bound_typevar) => {
|
||||
Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?))
|
||||
}
|
||||
@@ -6575,7 +6575,7 @@ impl<'db> Type<'db> {
|
||||
Self::TypeVar(bound_typevar) => Some(TypeDefinition::TypeVar(bound_typevar.typevar(db).definition(db)?)),
|
||||
|
||||
Self::ProtocolInstance(protocol) => match protocol.inner {
|
||||
Protocol::FromClass(class) => Some(class.type_definition(db)),
|
||||
Protocol::FromClass(class) => class.type_definition(db),
|
||||
Protocol::Synthesized(_) => None,
|
||||
},
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ use crate::types::{
|
||||
};
|
||||
use crate::{
|
||||
Db, FxIndexMap, FxIndexSet, FxOrderSet, Program,
|
||||
ast_node_ref::AstNodeRef,
|
||||
place::{
|
||||
Definedness, LookupError, LookupResult, Place, PlaceAndQualifiers, Widening,
|
||||
known_module_symbol, place_from_bindings, place_from_declarations,
|
||||
@@ -668,10 +669,10 @@ impl<'db> ClassLiteral<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the definition of this class.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
|
||||
/// Returns the definition of this class, if available.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
|
||||
match self {
|
||||
Self::Static(class) => class.definition(db),
|
||||
Self::Static(class) => Some(class.definition(db)),
|
||||
Self::Dynamic(class) => class.definition(db),
|
||||
}
|
||||
}
|
||||
@@ -679,11 +680,11 @@ impl<'db> ClassLiteral<'db> {
|
||||
/// Returns the type definition for this class.
|
||||
///
|
||||
/// For static classes, returns `TypeDefinition::StaticClass`.
|
||||
/// For dynamic classes, returns `TypeDefinition::DynamicClass`.
|
||||
pub(crate) fn type_definition(self, db: &'db dyn Db) -> TypeDefinition<'db> {
|
||||
/// For dynamic classes, returns `TypeDefinition::DynamicClass` if a definition is available.
|
||||
pub(crate) fn type_definition(self, db: &'db dyn Db) -> Option<TypeDefinition<'db>> {
|
||||
match self {
|
||||
Self::Static(class) => TypeDefinition::StaticClass(class.definition(db)),
|
||||
Self::Dynamic(class) => TypeDefinition::DynamicClass(class.definition(db)),
|
||||
Self::Static(class) => Some(TypeDefinition::StaticClass(class.definition(db))),
|
||||
Self::Dynamic(class) => class.definition(db).map(TypeDefinition::DynamicClass),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -941,13 +942,13 @@ impl<'db> ClassType<'db> {
|
||||
self.class_literal(db).known(db)
|
||||
}
|
||||
|
||||
/// Returns the definition for this class.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
|
||||
/// Returns the definition for this class, if available.
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
|
||||
self.class_literal(db).definition(db)
|
||||
}
|
||||
|
||||
/// Returns the type definition for this class.
|
||||
pub(crate) fn type_definition(self, db: &'db dyn Db) -> TypeDefinition<'db> {
|
||||
pub(crate) fn type_definition(self, db: &'db dyn Db) -> Option<TypeDefinition<'db>> {
|
||||
self.class_literal(db).type_definition(db)
|
||||
}
|
||||
|
||||
@@ -4678,14 +4679,10 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
|
||||
/// Supporting namespace dict attributes would require parsing dict literals and tracking
|
||||
/// the attribute types, similar to how TypedDict handles its fields.
|
||||
///
|
||||
/// # Salsa interning
|
||||
/// # Salsa tracking
|
||||
///
|
||||
/// Each `type()` call is uniquely identified by its [`Definition`], which provides
|
||||
/// stable identity without depending on AST node indices that can change when code
|
||||
/// is inserted above the call site.
|
||||
///
|
||||
/// Two different `type()` calls always produce distinct `DynamicClassLiteral`
|
||||
/// instances, even if they have the same name and bases:
|
||||
/// This is a salsa tracked struct. Two different `type()` calls always produce
|
||||
/// distinct `DynamicClassLiteral` instances, even if they have the same name and bases:
|
||||
///
|
||||
/// ```python
|
||||
/// Foo1 = type("Foo", (Base,), {})
|
||||
@@ -4693,9 +4690,11 @@ impl<'db> VarianceInferable<'db> for ClassLiteral<'db> {
|
||||
/// # Foo1 and Foo2 are distinct types
|
||||
/// ```
|
||||
///
|
||||
/// Note: Only assigned `type()` calls are currently supported (e.g., `Foo = type(...)`).
|
||||
/// Inline calls like `process(type(...))` fall back to normal call handling.
|
||||
#[salsa::interned(debug, heap_size = ruff_memory_usage::heap_size)]
|
||||
/// The `_node_ref` field is marked with `#[tracked]` and `#[no_eq]`, which means
|
||||
/// it doesn't participate in struct equality comparisons. Instead, accesses to
|
||||
/// that field are tracked as separate dependencies, providing stable identity
|
||||
/// across AST changes within the same scope.
|
||||
#[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)]
|
||||
#[derive(PartialOrd, Ord)]
|
||||
pub struct DynamicClassLiteral<'db> {
|
||||
/// The name of the class (from the first argument to `type()`).
|
||||
@@ -4706,8 +4705,26 @@ pub struct DynamicClassLiteral<'db> {
|
||||
#[returns(deref)]
|
||||
pub bases: Box<[ClassBase<'db>]>,
|
||||
|
||||
/// The definition where this class is created.
|
||||
pub definition: Definition<'db>,
|
||||
/// The file containing the `type()` call.
|
||||
pub file: File,
|
||||
|
||||
/// The scope containing the `type()` call.
|
||||
pub file_scope: FileScopeId,
|
||||
|
||||
/// The AST node reference for the `type()` call expression.
|
||||
///
|
||||
/// WARNING: Only access this field when doing type inference for the same
|
||||
/// file as where `DynamicClassLiteral` is defined to avoid cross-file query dependencies.
|
||||
#[no_eq]
|
||||
#[tracked]
|
||||
#[returns(ref)]
|
||||
pub(crate) _node_ref: AstNodeRef<ast::ExprCall>,
|
||||
|
||||
/// The definition where this class is created (if in an assignment context).
|
||||
///
|
||||
/// This is `Some` when the `type()` call is part of an assignment,
|
||||
/// allowing go-to-definition to navigate to the creation site.
|
||||
pub definition: Option<Definition<'db>>,
|
||||
|
||||
/// Dataclass parameters if this class has been wrapped with `@dataclass` decorator
|
||||
/// or passed to `dataclass()` as a function.
|
||||
@@ -4727,25 +4744,8 @@ impl<'db> DynamicClassLiteral<'db> {
|
||||
|
||||
/// Returns the range of the `type()` call expression that created this class.
|
||||
pub(super) fn header_range(self, db: &'db dyn Db) -> TextRange {
|
||||
let definition = self.definition(db);
|
||||
let file = definition.file(db);
|
||||
let module = parsed_module(db, file).load(db);
|
||||
|
||||
// Dynamic classes are only created from regular assignments (e.g., `Foo = type(...)`).
|
||||
let DefinitionKind::Assignment(assignment) = definition.kind(db) else {
|
||||
unreachable!("DynamicClassLiteral should only be created from Assignment definitions");
|
||||
};
|
||||
assignment.value(&module).range()
|
||||
}
|
||||
|
||||
/// Returns the file containing the `type()` call.
|
||||
pub(crate) fn file(self, db: &'db dyn Db) -> File {
|
||||
self.definition(db).file(db)
|
||||
}
|
||||
|
||||
/// Returns the scope containing the `type()` call.
|
||||
pub(crate) fn file_scope(self, db: &'db dyn Db) -> FileScopeId {
|
||||
self.definition(db).file_scope(db)
|
||||
let module = parsed_module(db, self.file(db)).load(db);
|
||||
self._node_ref(db).node(&module).range()
|
||||
}
|
||||
|
||||
/// Get the metaclass of this dynamic class.
|
||||
@@ -4918,7 +4918,10 @@ impl<'db> DynamicClassLiteral<'db> {
|
||||
Self::new(
|
||||
db,
|
||||
self.name(db).clone(),
|
||||
self.bases(db),
|
||||
self.bases(db).into(),
|
||||
self.file(db),
|
||||
self.file_scope(db),
|
||||
self._node_ref(db).clone(),
|
||||
self.definition(db),
|
||||
dataclass_params,
|
||||
)
|
||||
|
||||
@@ -96,7 +96,7 @@ pub(crate) fn typing_self<'db>(
|
||||
let identity = TypeVarIdentity::new(
|
||||
db,
|
||||
ast::name::Name::new_static("Self"),
|
||||
Some(class.definition(db)),
|
||||
class.definition(db),
|
||||
TypeVarKind::TypingSelf,
|
||||
);
|
||||
let bounds = TypeVarBoundOrConstraints::UpperBound(Type::instance(
|
||||
|
||||
@@ -169,9 +169,9 @@ pub fn definitions_for_name<'db>(
|
||||
// instead of `int` (hover only shows the docstring of the first definition).
|
||||
.rev()
|
||||
.filter_map(|ty| ty.as_nominal_instance())
|
||||
.map(|instance| {
|
||||
let definition = instance.class_literal(db).definition(db);
|
||||
ResolvedDefinition::Definition(definition)
|
||||
.filter_map(|instance| {
|
||||
let definition = instance.class_literal(db).definition(db)?;
|
||||
Some(ResolvedDefinition::Definition(definition))
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ use super::{
|
||||
infer_definition_types, infer_expression_types, infer_same_file_expression_type,
|
||||
infer_unpack_types,
|
||||
};
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::diagnostic::format_enumeration;
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::place::{
|
||||
@@ -5412,7 +5413,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
// Try to extract the dynamic class with definition.
|
||||
// This returns `None` if it's not a three-arg call to `type()`,
|
||||
// signalling that we must fall back to normal call inference.
|
||||
self.infer_dynamic_type_expression(call_expr, definition)
|
||||
self.infer_dynamic_type_expression(call_expr, Some(definition))
|
||||
.unwrap_or_else(|| {
|
||||
self.infer_call_expression_impl(call_expr, callable_type, tcx)
|
||||
})
|
||||
@@ -6031,7 +6032,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
fn infer_dynamic_type_expression(
|
||||
&mut self,
|
||||
call_expr: &ast::ExprCall,
|
||||
definition: Definition<'db>,
|
||||
definition: Option<Definition<'db>>,
|
||||
) -> Option<Type<'db>> {
|
||||
let db = self.db();
|
||||
|
||||
@@ -6096,7 +6097,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
|
||||
let bases = self.extract_dynamic_type_bases(bases_arg, bases_type, &name);
|
||||
|
||||
let dynamic_class = DynamicClassLiteral::new(db, name, bases, definition, None);
|
||||
let file = self.file();
|
||||
let file_scope = self.scope().file_scope_id(db);
|
||||
let node_ref = AstNodeRef::new(self.module(), call_expr);
|
||||
let dynamic_class = DynamicClassLiteral::new(
|
||||
db, name, bases, file, file_scope, node_ref, definition, None,
|
||||
);
|
||||
|
||||
// Check for MRO errors.
|
||||
if let Err(error) = dynamic_class.try_mro(db) {
|
||||
@@ -9079,6 +9085,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
return Type::TypedDict(typed_dict);
|
||||
}
|
||||
|
||||
// Handle 3-argument `type(name, bases, dict)`.
|
||||
if let Type::ClassLiteral(class) = callable_type
|
||||
&& class.is_known(self.db(), KnownClass::Type)
|
||||
&& let Some(dynamic_type) = self.infer_dynamic_type_expression(call_expression, None)
|
||||
{
|
||||
return dynamic_type;
|
||||
}
|
||||
|
||||
// We don't call `Type::try_call`, because we want to perform type inference on the
|
||||
// arguments after matching them to parameters, but before checking that the argument types
|
||||
// are assignable to any parameter annotations.
|
||||
|
||||
@@ -303,14 +303,14 @@ impl<'db> TypedDictType<'db> {
|
||||
|
||||
pub fn definition(self, db: &'db dyn Db) -> Option<Definition<'db>> {
|
||||
match self {
|
||||
TypedDictType::Class(defining_class) => Some(defining_class.definition(db)),
|
||||
TypedDictType::Class(defining_class) => defining_class.definition(db),
|
||||
TypedDictType::Synthesized(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn type_definition(self, db: &'db dyn Db) -> Option<TypeDefinition<'db>> {
|
||||
match self {
|
||||
TypedDictType::Class(defining_class) => Some(defining_class.type_definition(db)),
|
||||
TypedDictType::Class(defining_class) => defining_class.type_definition(db),
|
||||
TypedDictType::Synthesized(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user