[red-knot] Add `__init__` arguments check when doing `try_call` on a class literal (#16512)

## Summary

* Addresses #16511 for simple cases where only `__init__` method is
bound on class or doesn't exist at all.
* fixes a bug with argument counting in bound method diagnostics

Caveats:
* No handling of `__new__` or modified `__call__` on metaclass.
* This leads to a couple of false positive errors in tests

## Test Plan

- A couple new cases in mdtests
- cargo nextest run -p red_knot_python_semantic --no-fail-fast

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
This commit is contained in:
Mike Perlov 2025-04-08 23:26:20 +02:00 committed by GitHub
parent ed14dbb1a2
commit fab7d820bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 853 additions and 121 deletions

View File

@ -8,7 +8,11 @@ Currently, red-knot doesn't support `typing.NewType` in type annotations.
from typing_extensions import NewType
from types import GenericAlias
X = GenericAlias(type, ())
A = NewType("A", int)
# TODO: typeshed for `typing.GenericAlias` uses `type` for the first argument. `NewType` should be special-cased
# to be compatible with `type`
# error: [invalid-argument-type] "Object of type `NewType` cannot be assigned to parameter 2 (`origin`) of function `__new__`; expected type `type`"
B = GenericAlias(A, ())
def _(

View File

@ -1,7 +1,325 @@
# Constructor
When classes are instantiated, Python calls the meta-class `__call__` method, which can either be
customized by the user or `type.__call__` is used.
The latter calls the `__new__` method of the class, which is responsible for creating the instance
and then calls the `__init__` method on the resulting instance to initialize it with the same
arguments.
Both `__new__` and `__init__` are looked up using full descriptor protocol, but `__new__` is then
called as an implicit static, rather than bound method with `cls` passed as the first argument.
`__init__` has no special handling, it is fetched as bound method and is called just like any other
dunder method.
`type.__call__` does other things too, but this is not yet handled by us.
Since every class has `object` in it's MRO, the default implementations are `object.__new__` and
`object.__init__`. They have some special behavior, namely:
- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for `object`)
\- no arguments are accepted and `TypeError` is raised if any are passed.
- If `__new__` is defined, but `__init__` is not - `object.__init__` will allow arbitrary arguments!
As of today there are a number of behaviors that we do not support:
- `__new__` is assumed to return an instance of the class on which it is called
- User defined `__call__` on metaclass is ignored
## Creating an instance of the `object` class itself
Test the behavior of the `object` class itself. As implementation has to ignore `object` own methods
as defined in typeshed due to behavior not expressible in typeshed (see above how `__init__` behaves
differently depending on whether `__new__` is defined or not), we have to test the behavior of
`object` itself.
```py
reveal_type(object()) # revealed: object
# error: [too-many-positional-arguments] "Too many positional arguments to class `object`: expected 0, got 1"
reveal_type(object(1)) # revealed: object
```
## No init or new
```py
class Foo: ...
reveal_type(Foo()) # revealed: Foo
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 0, got 1"
reveal_type(Foo(1)) # revealed: Foo
```
## `__new__` present on the class itself
```py
class Foo:
def __new__(cls, x: int) -> "Foo":
return object.__new__(cls)
reveal_type(Foo(1)) # revealed: Foo
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
reveal_type(Foo()) # revealed: Foo
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2"
reveal_type(Foo(1, 2)) # revealed: Foo
```
## `__new__` present on a superclass
If the `__new__` method is defined on a superclass, we can still infer the signature of the
constructor from it.
```py
from typing_extensions import Self
class Base:
def __new__(cls, x: int) -> Self: ...
class Foo(Base): ...
reveal_type(Foo(1)) # revealed: Foo
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
reveal_type(Foo()) # revealed: Foo
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2"
reveal_type(Foo(1, 2)) # revealed: Foo
```
## Conditional `__new__`
```py
def _(flag: bool) -> None:
class Foo:
if flag:
def __new__(cls, x: int): ...
else:
def __new__(cls, x: int, y: int = 1): ...
reveal_type(Foo(1)) # revealed: Foo
# error: [invalid-argument-type] "Object of type `Literal["1"]` cannot be assigned to parameter 2 (`x`) of function `__new__`; expected type `int`"
reveal_type(Foo("1")) # revealed: Foo
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
reveal_type(Foo()) # revealed: Foo
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2"
reveal_type(Foo(1, 2)) # revealed: Foo
```
## A descriptor in place of `__new__`
```py
class SomeCallable:
def __call__(self, cls, x: int) -> "Foo":
obj = object.__new__(cls)
obj.x = x
return obj
class Descriptor:
def __get__(self, instance, owner) -> SomeCallable:
return SomeCallable()
class Foo:
__new__: Descriptor = Descriptor()
reveal_type(Foo(1)) # revealed: Foo
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
reveal_type(Foo()) # revealed: Foo
```
## A callable instance in place of `__new__`
### Bound
```py
class Callable:
def __call__(self, cls, x: int) -> "Foo":
return object.__new__(cls)
class Foo:
__new__ = Callable()
reveal_type(Foo(1)) # revealed: Foo
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
reveal_type(Foo()) # revealed: Foo
```
### Possibly Unbound
```py
def _(flag: bool) -> None:
class Callable:
if flag:
def __call__(self, cls, x: int) -> "Foo":
return object.__new__(cls)
class Foo:
__new__ = Callable()
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
reveal_type(Foo(1)) # revealed: Foo
# TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
# but we currently infer the signature of `__call__` as unknown, so it accepts any arguments
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
reveal_type(Foo()) # revealed: Foo
```
## `__init__` present on the class itself
If the class has an `__init__` method, we can infer the signature of the constructor from it.
```py
class Foo:
def __init__(self, x: int): ...
reveal_type(Foo(1)) # revealed: Foo
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
reveal_type(Foo()) # revealed: Foo
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
reveal_type(Foo(1, 2)) # revealed: Foo
```
## `__init__` present on a superclass
If the `__init__` method is defined on a superclass, we can still infer the signature of the
constructor from it.
```py
class Base:
def __init__(self, x: int): ...
class Foo(Base): ...
reveal_type(Foo(1)) # revealed: Foo
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
reveal_type(Foo()) # revealed: Foo
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
reveal_type(Foo(1, 2)) # revealed: Foo
```
## Conditional `__init__`
```py
def _(flag: bool) -> None:
class Foo:
if flag:
def __init__(self, x: int): ...
else:
def __init__(self, x: int, y: int = 1): ...
reveal_type(Foo(1)) # revealed: Foo
# error: [invalid-argument-type] "Object of type `Literal["1"]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `int`"
reveal_type(Foo("1")) # revealed: Foo
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
reveal_type(Foo()) # revealed: Foo
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
reveal_type(Foo(1, 2)) # revealed: Foo
```
## A descriptor in place of `__init__`
```py
class SomeCallable:
# TODO: at runtime `__init__` is checked to return `None` and
# a `TypeError` is raised if it doesn't. However, apparently
# this is not true when the descriptor is used as `__init__`.
# However, we may still want to check this.
def __call__(self, x: int) -> str:
return "a"
class Descriptor:
def __get__(self, instance, owner) -> SomeCallable:
return SomeCallable()
class Foo:
__init__: Descriptor = Descriptor()
reveal_type(Foo(1)) # revealed: Foo
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
reveal_type(Foo()) # revealed: Foo
```
## A callable instance in place of `__init__`
### Bound
```py
class Callable:
def __call__(self, x: int) -> None:
pass
class Foo:
__init__ = Callable()
reveal_type(Foo(1)) # revealed: Foo
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
reveal_type(Foo()) # revealed: Foo
```
### Possibly Unbound
```py
def _(flag: bool) -> None:
class Callable:
if flag:
def __call__(self, x: int) -> None:
pass
class Foo:
__init__ = Callable()
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
reveal_type(Foo(1)) # revealed: Foo
# TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
# but we currently infer the signature of `__call__` as unknown, so it accepts any arguments
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
reveal_type(Foo()) # revealed: Foo
```
## `__new__` and `__init__` both present
### Identical signatures
A common case is to have `__new__` and `__init__` with identical signatures (except for the first
argument). We report errors for both `__new__` and `__init__` if the arguments are incorrect.
At runtime `__new__` is called first and will fail without executing `__init__` if the arguments are
incorrect. However, we decided that it is better to report errors for both methods, since after
fixing the `__new__` method, the user may forget to fix the `__init__` method.
```py
class Foo:
def __new__(cls, x: int) -> "Foo":
return object.__new__(cls)
def __init__(self, x: int): ...
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
reveal_type(Foo()) # revealed: Foo
reveal_type(Foo(1)) # revealed: Foo
```
### Compatible signatures
But they can also be compatible, but not identical. We should correctly report errors only for the
mthod that would fail.
```py
class Foo:
def __new__(cls, *args, **kwargs):
return object.__new__(cls)
def __init__(self, x: int) -> None:
self.x = x
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
reveal_type(Foo()) # revealed: Foo
reveal_type(Foo(1)) # revealed: Foo
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
reveal_type(Foo(1, 2)) # revealed: Foo
```

View File

@ -20,9 +20,11 @@ class C:
def _(subclass_of_c: type[C]):
reveal_type(subclass_of_c(1)) # revealed: C
# TODO: Those should all be errors
# error: [invalid-argument-type] "Object of type `Literal["a"]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `int`"
reveal_type(subclass_of_c("a")) # revealed: C
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
reveal_type(subclass_of_c()) # revealed: C
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
reveal_type(subclass_of_c(1, 2)) # revealed: C
```

View File

@ -111,6 +111,8 @@ class E[T]:
def __init__(self, x: T) -> None: ...
# TODO: revealed: E[int] or E[Literal[1]]
# TODO should not emit an error
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `T`"
reveal_type(E(1)) # revealed: E
```
@ -118,7 +120,8 @@ The types inferred from a type context and from a constructor parameter must be
other:
```py
# TODO: error
# TODO: the error should not leak the `T` typevar and should mention `E[int]`
# error: [invalid-argument-type] "Object of type `Literal["five"]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `T`"
wrong_innards: E[int] = E("five")
```

View File

@ -18,7 +18,7 @@ class Number:
def __invert__(self) -> Literal[True]:
return True
a = Number()
a = Number(0)
reveal_type(+a) # revealed: int
reveal_type(-a) # revealed: int

View File

@ -5,7 +5,7 @@ use std::str::FromStr;
use bitflags::bitflags;
use call::{CallDunderError, CallError, CallErrorKind};
use context::InferContext;
use diagnostic::{INVALID_CONTEXT_MANAGER, NOT_ITERABLE};
use diagnostic::{CALL_POSSIBLY_UNBOUND_METHOD, INVALID_CONTEXT_MANAGER, NOT_ITERABLE};
use itertools::EitherOrBoth;
use ruff_db::files::{File, FileRange};
use ruff_python_ast as ast;
@ -151,20 +151,60 @@ enum InstanceFallbackShadowsNonDataDescriptor {
No,
}
/// Dunder methods are looked up on the meta-type of a type without potentially falling
/// back on attributes on the type itself. For example, when implicitly invoked on an
/// instance, dunder methods are not looked up as instance attributes. And when invoked
/// on a class, dunder methods are only looked up on the metaclass, not the class itself.
///
/// All other attributes use the `WithInstanceFallback` policy.
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
enum MemberLookupPolicy {
bitflags! {
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
pub(crate) struct MemberLookupPolicy: u8 {
/// Dunder methods are looked up on the meta-type of a type without potentially falling
/// back on attributes on the type itself. For example, when implicitly invoked on an
/// instance, dunder methods are not looked up as instance attributes. And when invoked
/// on a class, dunder methods are only looked up on the metaclass, not the class itself.
///
/// All other attributes use the `WithInstanceFallback` policy.
///
/// If this flag is set - look up the attribute on the meta-type only.
const NO_INSTANCE_FALLBACK = 1 << 0;
/// When looking up an attribute on a class, we sometimes need to avoid
/// looking up attributes defined on the `object` class. Usually because
/// typeshed doesn't properly encode runtime behavior (e.g. see how `__new__` & `__init__`
/// are handled during class creation).
///
/// If this flag is set - exclude attributes defined on `object` when looking up attributes.
const MRO_NO_OBJECT_FALLBACK = 1 << 1;
/// When looking up an attribute on a class, we sometimes need to avoid
/// looking up attributes defined on `type` if this is the metaclass of the class.
///
/// This is similar to no object fallback above
const META_CLASS_NO_TYPE_FALLBACK = 1 << 2;
}
}
impl MemberLookupPolicy {
/// Only look up the attribute on the meta-type.
NoInstanceFallback,
/// Look up the attribute on the meta-type, but fall back to attributes on the instance
///
/// If false - Look up the attribute on the meta-type, but fall back to attributes on the instance
/// if the meta-type attribute is not found or if the meta-type attribute is not a data
/// descriptor.
WithInstanceFallback,
pub(crate) const fn no_instance_fallback(self) -> bool {
self.contains(Self::NO_INSTANCE_FALLBACK)
}
/// Exclude attributes defined on `object` when looking up attributes.
pub(crate) const fn mro_no_object_fallback(self) -> bool {
self.contains(Self::MRO_NO_OBJECT_FALLBACK)
}
/// Exclude attributes defined on `type` when looking up meta-class-attributes.
pub(crate) const fn meta_class_no_type_fallback(self) -> bool {
self.contains(Self::META_CLASS_NO_TYPE_FALLBACK)
}
}
impl Default for MemberLookupPolicy {
fn default() -> Self {
Self::empty()
}
}
impl AttributeKind {
@ -444,6 +484,10 @@ impl<'db> Type<'db> {
.expect("Expected a Type::ClassLiteral variant")
}
pub const fn is_subclass_of(&self) -> bool {
matches!(self, Type::SubclassOf(..))
}
pub const fn is_class_literal(&self) -> bool {
matches!(self, Type::ClassLiteral(..))
}
@ -1840,9 +1884,18 @@ impl<'db> Type<'db> {
/// [descriptor guide]: https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance
/// [`_PyType_Lookup`]: https://github.com/python/cpython/blob/e285232c76606e3be7bf216efb1be1e742423e4b/Objects/typeobject.c#L5223
fn find_name_in_mro(&self, db: &'db dyn Db, name: &str) -> Option<SymbolAndQualifiers<'db>> {
self.find_name_in_mro_with_policy(db, name, MemberLookupPolicy::default())
}
fn find_name_in_mro_with_policy(
&self,
db: &'db dyn Db,
name: &str,
policy: MemberLookupPolicy,
) -> Option<SymbolAndQualifiers<'db>> {
match self {
Type::Union(union) => Some(union.map_with_boundness_and_qualifiers(db, |elem| {
elem.find_name_in_mro(db, name)
elem.find_name_in_mro_with_policy(db, name, policy)
// If some elements are classes, and some are not, we simply fall back to `Unbound` for the non-class
// elements instead of short-circuiting the whole result to `None`. We would need a more detailed
// return type otherwise, and since `find_name_in_mro` is usually called via `class_member`, this is
@ -1851,7 +1904,7 @@ impl<'db> Type<'db> {
})),
Type::Intersection(inter) => {
Some(inter.map_with_boundness_and_qualifiers(db, |elem| {
elem.find_name_in_mro(db, name)
elem.find_name_in_mro_with_policy(db, name, policy)
// Fall back to Unbound, similar to the union case (see above).
.unwrap_or_default()
}))
@ -1903,7 +1956,7 @@ impl<'db> Type<'db> {
"__get__" | "__set__" | "__delete__",
) => Some(Symbol::Unbound.into()),
_ => Some(class_literal.class_member(db, name)),
_ => Some(class_literal.class_member(db, name, policy)),
}
}
@ -1926,7 +1979,9 @@ impl<'db> Type<'db> {
{
Some(Symbol::Unbound.into())
}
Type::SubclassOf(subclass_of_ty) => subclass_of_ty.find_name_in_mro(db, name),
Type::SubclassOf(subclass_of_ty) => {
subclass_of_ty.find_name_in_mro_with_policy(db, name, policy)
}
// We eagerly normalize type[object], i.e. Type::SubclassOf(object) to `type`, i.e. Type::Instance(type).
// So looking up a name in the MRO of `Type::Instance(type)` is equivalent to looking up the name in the
@ -1934,7 +1989,7 @@ impl<'db> Type<'db> {
Type::Instance(InstanceType { class }) if class.is_known(db, KnownClass::Type) => {
KnownClass::Object
.to_class_literal(db)
.find_name_in_mro(db, name)
.find_name_in_mro_with_policy(db, name, policy)
}
Type::FunctionLiteral(_)
@ -1964,17 +2019,28 @@ impl<'db> Type<'db> {
///
/// Basically corresponds to `self.to_meta_type().find_name_in_mro(name)`, except for the handling
/// of union and intersection types.
#[salsa::tracked]
fn class_member(self, db: &'db dyn Db, name: Name) -> SymbolAndQualifiers<'db> {
self.class_member_with_policy(db, name, MemberLookupPolicy::default())
}
#[salsa::tracked]
fn class_member_with_policy(
self,
db: &'db dyn Db,
name: Name,
policy: MemberLookupPolicy,
) -> SymbolAndQualifiers<'db> {
tracing::trace!("class_member: {}.{}", self.display(db), name);
match self {
Type::Union(union) => union
.map_with_boundness_and_qualifiers(db, |elem| elem.class_member(db, name.clone())),
Type::Intersection(inter) => inter
.map_with_boundness_and_qualifiers(db, |elem| elem.class_member(db, name.clone())),
Type::Union(union) => union.map_with_boundness_and_qualifiers(db, |elem| {
elem.class_member_with_policy(db, name.clone(), policy)
}),
Type::Intersection(inter) => inter.map_with_boundness_and_qualifiers(db, |elem| {
elem.class_member_with_policy(db, name.clone(), policy)
}),
_ => self
.to_meta_type(db)
.find_name_in_mro(db, name.as_str())
.find_name_in_mro_with_policy(db, name.as_str(), policy)
.expect(
"`Type::find_name_in_mro()` should return `Some()` when called on a meta-type",
),
@ -2235,6 +2301,7 @@ impl<'db> Type<'db> {
name: &str,
fallback: SymbolAndQualifiers<'db>,
policy: InstanceFallbackShadowsNonDataDescriptor,
member_policy: MemberLookupPolicy,
) -> SymbolAndQualifiers<'db> {
let (
SymbolAndQualifiers {
@ -2244,7 +2311,7 @@ impl<'db> Type<'db> {
meta_attr_kind,
) = Self::try_call_dunder_get_on_attribute(
db,
self.class_member(db, name.into()),
self.class_member_with_policy(db, name.into(), member_policy),
self,
self.to_meta_type(db),
);
@ -2323,7 +2390,7 @@ impl<'db> Type<'db> {
/// lookup, like a failed `__get__` call on a descriptor.
#[must_use]
pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
self.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::WithInstanceFallback)
self.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::default())
}
/// Similar to [`Type::member`], but allows the caller to specify what policy should be used
@ -2454,14 +2521,16 @@ impl<'db> Type<'db> {
Type::ModuleLiteral(module) => module.static_member(db, name_str).into(),
Type::AlwaysFalsy | Type::AlwaysTruthy => self.class_member(db, name),
Type::AlwaysFalsy | Type::AlwaysTruthy => {
self.class_member_with_policy(db, name, policy)
}
_ if policy == MemberLookupPolicy::NoInstanceFallback => self
.invoke_descriptor_protocol(
_ if policy.no_instance_fallback() => self.invoke_descriptor_protocol(
db,
name_str,
Symbol::Unbound.into(),
InstanceFallbackShadowsNonDataDescriptor::No,
policy,
),
Type::Instance(..)
@ -2483,15 +2552,21 @@ impl<'db> Type<'db> {
name_str,
fallback,
InstanceFallbackShadowsNonDataDescriptor::No,
policy,
);
let custom_getattr_result = || {
// Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with dynamic imports.
// We explicitly hide it here to prevent arbitrary attributes from being available on modules.
if self
.into_instance()
.is_some_and(|instance| instance.class.is_known(db, KnownClass::ModuleType))
{
// Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with
// dynamic imports. We explicitly hide it here to prevent arbitrary attributes
// from being available on modules. Same for `types.GenericAlias` - its
// `__getattr__` method will delegate to `__origin__` to allow looking up
// attributes on the original type. But in typeshed its return type is `Any`.
// It will need a special handling, so it remember the origin type to properly
// resolve the attribute.
if self.into_instance().is_some_and(|instance| {
instance.class.is_known(db, KnownClass::ModuleType)
|| instance.class.is_known(db, KnownClass::GenericAlias)
}) {
return Symbol::Unbound.into();
}
@ -2525,7 +2600,7 @@ impl<'db> Type<'db> {
}
Type::ClassLiteral(..) | Type::SubclassOf(..) => {
let class_attr_plain = self.find_name_in_mro(db, name_str).expect(
let class_attr_plain = self.find_name_in_mro_with_policy(db, name_str, policy).expect(
"Calling `find_name_in_mro` on class literals and subclass-of types should always return `Some`",
);
@ -2550,6 +2625,7 @@ impl<'db> Type<'db> {
name_str,
class_attr_fallback,
InstanceFallbackShadowsNonDataDescriptor::Yes,
policy,
)
}
}
@ -3066,6 +3142,12 @@ impl<'db> Type<'db> {
},
Type::ClassLiteral(ClassLiteralType { class }) => match class.known(db) {
// TODO: Ideally we'd use `try_call_constructor` for all constructor calls.
// Currently we don't for a few special known types, either because their
// constructors are defined with overloads, or because we want to special case
// their return type beyond what typeshed provides (though this support could
// likely be moved into the `try_call_constructor` path). Once we support
// overloads, re-evaluate the need for these arms.
Some(KnownClass::Bool) => {
// ```py
// class bool(int):
@ -3165,6 +3247,21 @@ impl<'db> Type<'db> {
);
Signatures::single(signature)
}
Some(KnownClass::Object) => {
// ```py
// class object:
// def __init__(self) -> None: ...
// def __new__(cls) -> Self: ...
// ```
let signature = CallableSignature::from_overloads(
self,
[Signature::new(
Parameters::empty(),
Some(KnownClass::Object.to_instance(db)),
)],
);
Signatures::single(signature)
}
Some(KnownClass::Property) => {
let getter_signature = Signature::new(
@ -3234,8 +3331,11 @@ impl<'db> Type<'db> {
Signatures::single(signature)
}
// TODO annotated return type on `__new__` or metaclass `__call__`
// TODO check call vs signatures of `__new__` and/or `__init__`
// Most class literal constructor calls are handled by `try_call_constructor` and
// not via getting the signature here. This signature can still be used in some
// cases (e.g. evaluating callable subtyping). TODO improve this definition
// (intersection of `__new__` and `__init__` signatures? and respect metaclass
// `__call__`).
_ => {
let signature = CallableSignature::single(
self,
@ -3247,6 +3347,10 @@ impl<'db> Type<'db> {
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
ClassBase::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type).signatures(db),
// Most type[] constructor calls are handled by `try_call_constructor` and not via
// getting the signature here. This signature can still be used in some cases (e.g.
// evaluating callable subtyping). TODO improve this definition (intersection of
// `__new__` and `__init__` signatures? and respect metaclass `__call__`).
ClassBase::Class(class) => Type::class_literal(class).signatures(db),
},
@ -3260,7 +3364,7 @@ impl<'db> Type<'db> {
.member_lookup_with_policy(
db,
Name::new_static("__call__"),
MemberLookupPolicy::NoInstanceFallback,
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.symbol
{
@ -3324,7 +3428,7 @@ impl<'db> Type<'db> {
mut argument_types: CallArgumentTypes<'_, 'db>,
) -> Result<Bindings<'db>, CallDunderError<'db>> {
match self
.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::NoInstanceFallback)
.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::NO_INSTANCE_FALLBACK)
.symbol
{
Symbol::Type(dunder_callable, boundness) => {
@ -3485,6 +3589,124 @@ impl<'db> Type<'db> {
}
}
/// Given a class literal or non-dynamic SubclassOf type, try calling it (creating an instance)
/// and return the resulting instance type.
///
/// Models `type.__call__` behavior.
/// TODO: model metaclass `__call__`.
///
/// E.g., for the following code, infer the type of `Foo()`:
/// ```python
/// class Foo:
/// pass
///
/// Foo()
/// ```
fn try_call_constructor(
self,
db: &'db dyn Db,
argument_types: CallArgumentTypes<'_, 'db>,
) -> Result<Type<'db>, ConstructorCallError<'db>> {
debug_assert!(matches!(self, Type::ClassLiteral(_) | Type::SubclassOf(_)));
// As of now we do not model custom `__call__` on meta-classes, so the code below
// only deals with interplay between `__new__` and `__init__` methods.
// The logic is roughly as follows:
// 1. If `__new__` is defined anywhere in the MRO (except for `object`, since it is always
// present), we call it and analyze outcome. We then analyze `__init__` call, but only
// if it is defined somewhere except object. This is because `object.__init__`
// allows arbitrary arguments if and only if `__new__` is defined, but typeshed
// defines `__init__` for `object` with no arguments.
// 2. If `__new__` is not found, we call `__init__`. Here, we allow it to fallback all
// the way to `object` (single `self` argument call). This time it is correct to
// fallback to `object.__init__`, since it will indeed check that no arguments are
// passed.
//
// Note that we currently ignore `__new__` return type, since we do not yet support `Self`
// and most builtin classes use it as return type annotation. We always return the instance
// type.
// Lookup `__new__` method in the MRO up to, but not including, `object`. Also, we must
// avoid `__new__` on `type` since per descriptor protocol, if `__new__` is not defined on
// a class, metaclass attribute would take precedence. But by avoiding `__new__` on
// `object` we would inadvertently unhide `__new__` on `type`, which is not what we want.
// An alternative might be to not skip `object.__new__` but instead mark it such that it's
// easy to check if that's the one we found?
let new_call_outcome: Option<Result<Bindings<'db>, CallDunderError<'db>>> = match self
.member_lookup_with_policy(
db,
"__new__".into(),
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
)
.symbol
{
Symbol::Type(dunder_callable, boundness) => {
let signatures = dunder_callable.signatures(db);
// `__new__` is a static method, so we must inject the `cls` argument.
let mut argument_types = argument_types.prepend_synthetic(self);
Some(
match Bindings::match_parameters(signatures, &mut argument_types)
.check_types(db, &mut argument_types)
{
Ok(bindings) => {
if boundness == Boundness::PossiblyUnbound {
Err(CallDunderError::PossiblyUnbound(Box::new(bindings)))
} else {
Ok(bindings)
}
}
Err(err) => Err(err.into()),
},
)
}
// No explicit `__new__` method found
Symbol::Unbound => None,
};
// TODO: we should use the actual return type of `__new__` to determine the instance type
let instance_ty = self
.to_instance(db)
.expect("Class literal type and subclass-of types should always be convertible to instance type");
let init_call_outcome = if new_call_outcome.is_none()
|| !instance_ty
.member_lookup_with_policy(
db,
"__init__".into(),
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
)
.symbol
.is_unbound()
{
Some(instance_ty.try_call_dunder(db, "__init__", argument_types))
} else {
None
};
match (new_call_outcome, init_call_outcome) {
// All calls are successful or not called at all
(None | Some(Ok(_)), None | Some(Ok(_))) => Ok(instance_ty),
(None | Some(Ok(_)), Some(Err(error))) => {
// no custom `__new__` or it was called and succeeded, but `__init__` failed.
Err(ConstructorCallError::Init(instance_ty, error))
}
(Some(Err(error)), None | Some(Ok(_))) => {
// custom `__new__` was called and failed, but init is ok
Err(ConstructorCallError::New(instance_ty, error))
}
(Some(Err(new_error)), Some(Err(init_error))) => {
// custom `__new__` was called and failed, and `__init__` is also not ok
Err(ConstructorCallError::NewAndInit(
instance_ty,
new_error,
init_error,
))
}
}
}
#[must_use]
pub fn to_instance(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
@ -4718,6 +4940,98 @@ impl<'db> BoolError<'db> {
}
}
/// Error returned if a class instantiation call failed
#[derive(Debug)]
enum ConstructorCallError<'db> {
Init(Type<'db>, CallDunderError<'db>),
New(Type<'db>, CallDunderError<'db>),
NewAndInit(Type<'db>, CallDunderError<'db>, CallDunderError<'db>),
}
impl<'db> ConstructorCallError<'db> {
fn return_type(&self) -> Type<'db> {
match self {
Self::Init(ty, _) => *ty,
Self::New(ty, _) => *ty,
Self::NewAndInit(ty, _, _) => *ty,
}
}
fn report_diagnostic(
&self,
context: &InferContext<'db>,
context_expression_type: Type<'db>,
context_expression_node: ast::AnyNodeRef,
) {
let report_init_error = |call_dunder_error: &CallDunderError<'db>| match call_dunder_error {
CallDunderError::MethodNotAvailable => {
// If we are using vendored typeshed, it should be impossible to have missing
// or unbound `__init__` method on a class, as all classes have `object` in MRO.
// Thus the following may only trigger if a custom typeshed is used.
context.report_lint(
&CALL_POSSIBLY_UNBOUND_METHOD,
context_expression_node,
format_args!(
"`__init__` method is missing on type `{}`. Make sure your `object` in typeshed has its definition.",
context_expression_type.display(context.db()),
),
);
}
CallDunderError::PossiblyUnbound(bindings) => {
context.report_lint(
&CALL_POSSIBLY_UNBOUND_METHOD,
context_expression_node,
format_args!(
"Method `__init__` on type `{}` is possibly unbound.",
context_expression_type.display(context.db()),
),
);
bindings.report_diagnostics(context, context_expression_node);
}
CallDunderError::CallError(_, bindings) => {
bindings.report_diagnostics(context, context_expression_node);
}
};
let report_new_error = |call_dunder_error: &CallDunderError<'db>| match call_dunder_error {
CallDunderError::MethodNotAvailable => {
// We are explicitly checking for `__new__` before attempting to call it,
// so this should never happen.
unreachable!("`__new__` method may not be called if missing");
}
CallDunderError::PossiblyUnbound(bindings) => {
context.report_lint(
&CALL_POSSIBLY_UNBOUND_METHOD,
context_expression_node,
format_args!(
"Method `__new__` on type `{}` is possibly unbound.",
context_expression_type.display(context.db()),
),
);
bindings.report_diagnostics(context, context_expression_node);
}
CallDunderError::CallError(_, bindings) => {
bindings.report_diagnostics(context, context_expression_node);
}
};
match self {
Self::Init(_, call_dunder_error) => {
report_init_error(call_dunder_error);
}
Self::New(_, call_dunder_error) => {
report_new_error(call_dunder_error);
}
Self::NewAndInit(_, new_call_dunder_error, init_call_dunder_error) => {
report_new_error(new_call_dunder_error);
report_init_error(init_call_dunder_error);
}
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Truthiness {
/// For an object `x`, `bool(x)` will always return `True`

View File

@ -109,6 +109,21 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> {
result
}
/// Create a new [`CallArgumentTypes`] by prepending a synthetic argument to the front of this
/// argument list.
pub(crate) fn prepend_synthetic(&self, synthetic: Type<'db>) -> Self {
Self {
arguments: CallArguments(
std::iter::once(Argument::Synthetic)
.chain(self.arguments.iter())
.collect(),
),
types: std::iter::once(synthetic)
.chain(self.types.iter().copied())
.collect(),
}
}
pub(crate) fn iter(&self) -> impl Iterator<Item = (Argument<'a>, Type<'db>)> + '_ {
self.arguments.iter().zip(self.types.iter().copied())
}

View File

@ -924,8 +924,14 @@ impl<'db> Binding<'db> {
first_excess_argument_index,
num_synthetic_args,
),
expected_positional_count: parameters.positional().count(),
provided_positional_count: next_positional,
expected_positional_count: parameters
.positional()
.count()
// using saturating_sub to avoid negative values due to invalid syntax in source code
.saturating_sub(num_synthetic_args),
provided_positional_count: next_positional
// using saturating_sub to avoid negative values due to invalid syntax in source code
.saturating_sub(num_synthetic_args),
});
}
let mut missing = vec![];

View File

@ -2,8 +2,8 @@ use std::sync::{LazyLock, Mutex};
use super::{
class_base::ClassBase, infer_expression_type, infer_unpack_types, IntersectionBuilder,
KnownFunction, Mro, MroError, MroIterator, SubclassOfType, Truthiness, Type, TypeAliasType,
TypeQualifiers, TypeVarInstance,
KnownFunction, MemberLookupPolicy, Mro, MroError, MroIterator, SubclassOfType, Truthiness,
Type, TypeAliasType, TypeQualifiers, TypeVarInstance,
};
use crate::semantic_index::definition::Definition;
use crate::{
@ -323,7 +323,12 @@ impl<'db> Class<'db> {
/// The member resolves to a member on the class itself or any of its proper superclasses.
///
/// TODO: Should this be made private...?
pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
pub(super) fn class_member(
self,
db: &'db dyn Db,
name: &str,
policy: MemberLookupPolicy,
) -> SymbolAndQualifiers<'db> {
if name == "__mro__" {
let tuple_elements = self.iter_mro(db).map(Type::from);
return Symbol::bound(TupleType::from_elements(db, tuple_elements)).into();
@ -354,6 +359,18 @@ impl<'db> Class<'db> {
dynamic_type_to_intersect_with.get_or_insert(Type::from(superclass));
}
ClassBase::Class(class) => {
if class.is_known(db, KnownClass::Object)
// Only exclude `object` members if this is not an `object` class itself
&& (policy.mro_no_object_fallback() && !self.is_known(db, KnownClass::Object))
{
continue;
}
if class.is_known(db, KnownClass::Type) && policy.meta_class_no_type_fallback()
{
continue;
}
lookup_result = lookup_result.or_else(|lookup_error| {
lookup_error.or_fall_back_to(db, class.own_class_member(db, name))
});
@ -776,8 +793,13 @@ impl<'db> ClassLiteralType<'db> {
self.class.body_scope(db)
}
pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
self.class.class_member(db, name)
pub(super) fn class_member(
self,
db: &'db dyn Db,
name: &str,
policy: MemberLookupPolicy,
) -> SymbolAndQualifiers<'db> {
self.class.class_member(db, name, policy)
}
}

View File

@ -100,7 +100,7 @@ use super::slots::check_class_slots;
use super::string_annotation::{
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
};
use super::CallDunderError;
use super::{CallDunderError, ClassLiteralType};
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
@ -2380,7 +2380,8 @@ impl<'db> TypeInferenceBuilder<'db> {
| Type::WrapperDescriptor(_)
| Type::TypeVar(..)
| Type::AlwaysTruthy
| Type::AlwaysFalsy => match object_ty.class_member(db, attribute.into()) {
| Type::AlwaysFalsy => {
match object_ty.class_member(db, attribute.into()) {
meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => {
if emit_diagnostics {
self.context.report_lint(
@ -2404,7 +2405,11 @@ impl<'db> TypeInferenceBuilder<'db> {
let successful_call = meta_dunder_set
.try_call(
db,
CallArgumentTypes::positional([meta_attr_ty, object_ty, value_ty]),
CallArgumentTypes::positional([
meta_attr_ty,
object_ty,
value_ty,
]),
)
.is_ok();
@ -2425,8 +2430,9 @@ impl<'db> TypeInferenceBuilder<'db> {
ensure_assignable_to(meta_attr_ty)
};
let assignable_to_instance_attribute =
if meta_attr_boundness == Boundness::PossiblyUnbound {
let assignable_to_instance_attribute = if meta_attr_boundness
== Boundness::PossiblyUnbound
{
let (assignable, boundness) =
if let Symbol::Type(instance_attr_ty, instance_attr_boundness) =
object_ty.instance_member(db, attribute).symbol
@ -2489,7 +2495,8 @@ impl<'db> TypeInferenceBuilder<'db> {
false
}
}
},
}
}
Type::ClassLiteral(..) | Type::SubclassOf(..) => {
match object_ty.class_member(db, attribute.into()) {
@ -3970,8 +3977,48 @@ impl<'db> TypeInferenceBuilder<'db> {
// arguments after matching them to parameters, but before checking that the argument types
// are assignable to any parameter annotations.
let mut call_arguments = Self::parse_arguments(arguments);
let function_type = self.infer_expression(func);
let signatures = function_type.signatures(self.db());
let callable_type = self.infer_expression(func);
// For class literals we model the entire class instantiation logic, so it is handled
// in a separate function.
let class = match callable_type {
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
ClassBase::Dynamic(_) => None,
ClassBase::Class(class) => Some(class),
},
Type::ClassLiteral(ClassLiteralType { class }) => Some(class),
_ => None,
};
if class.is_some_and(|class| {
// For some known classes we have manual signatures defined and use the `try_call` path
// below. TODO: it should be possible to move these special cases into the
// `try_call_constructor` path instead, or even remove some entirely once we support
// overloads fully.
class.known(self.db()).is_none_or(|class| {
!matches!(
class,
KnownClass::Bool
| KnownClass::Str
| KnownClass::Type
| KnownClass::Object
| KnownClass::Property
)
})
}) {
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
let call_argument_types =
self.infer_argument_types(arguments, call_arguments, &argument_forms);
return callable_type
.try_call_constructor(self.db(), call_argument_types)
.unwrap_or_else(|err| {
err.report_diagnostic(&self.context, callable_type, call_expression.into());
err.return_type()
});
}
let signatures = callable_type.signatures(self.db());
let bindings = Bindings::match_parameters(signatures, &mut call_arguments);
let mut call_argument_types =
self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms);

View File

@ -1,6 +1,6 @@
use crate::symbol::SymbolAndQualifiers;
use super::{ClassBase, ClassLiteralType, Db, KnownClass, Type};
use super::{ClassBase, ClassLiteralType, Db, KnownClass, MemberLookupPolicy, Type};
/// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
@ -66,12 +66,13 @@ impl<'db> SubclassOfType<'db> {
!self.is_dynamic()
}
pub(crate) fn find_name_in_mro(
pub(crate) fn find_name_in_mro_with_policy(
self,
db: &'db dyn Db,
name: &str,
policy: MemberLookupPolicy,
) -> Option<SymbolAndQualifiers<'db>> {
Type::from(self.subclass_of).find_name_in_mro(db, name)
Type::from(self.subclass_of).find_name_in_mro_with_policy(db, name, policy)
}
/// Return `true` if `self` is a subtype of `other`.