Skip to content

Commit d1e9046

Browse files
committed
[red-knot] Lookup of __new__
1 parent 8a6787b commit d1e9046

File tree

5 files changed

+129
-50
lines changed

5 files changed

+129
-50
lines changed

crates/red_knot_python_semantic/resources/mdtest/call/constructor.md

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
# Constructor
22

3-
When classes are instantiated, Python calls the meta-class `__call__` method, which can either be
3+
When classes are instantiated, Python calls the metaclass `__call__` method, which can either be
44
customized by the user or `type.__call__` is used.
55

66
The latter calls the `__new__` method of the class, which is responsible for creating the instance
77
and then calls the `__init__` method on the resulting instance to initialize it with the same
88
arguments.
99

10-
Both `__new__` and `__init__` are looked up using full descriptor protocol, but `__new__` is then
11-
called as an implicit static, rather than bound method with `cls` passed as the first argument.
12-
`__init__` has no special handling, it is fetched as bound method and is called just like any other
13-
dunder method.
10+
Both `__new__` and `__init__` are looked up using the descriptor protocol, i.e. `__get__` is called
11+
if these attributes are descriptors. `__new__` is always treated as a static method, i.e. `cls` is
12+
passed as the first argument. `__init__` has no special handling, it is fetched as bound method and
13+
is called just like any other dunder method.
1414

1515
`type.__call__` does other things too, but this is not yet handled by us.
1616

1717
Since every class has `object` in it's MRO, the default implementations are `object.__new__` and
1818
`object.__init__`. They have some special behavior, namely:
1919

20-
- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for `object`)
21-
\- no arguments are accepted and `TypeError` is raised if any are passed.
22-
- If `__new__` is defined, but `__init__` is not - `object.__init__` will allow arbitrary arguments!
20+
- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for
21+
`object`), no arguments are accepted and `TypeError` is raised if any are passed.
22+
- If `__new__` is defined, but `__init__` is not, `object.__init__` will allow arbitrary arguments!
2323

2424
As of today there are a number of behaviors that we do not support:
2525

@@ -146,6 +146,25 @@ reveal_type(Foo()) # revealed: Foo
146146

147147
### Possibly Unbound
148148

149+
### Possibly unbound `__new__` method
150+
151+
```py
152+
def _(flag: bool) -> None:
153+
class Foo:
154+
if flag:
155+
def __new__(cls):
156+
return object.__new__(cls)
157+
158+
# error: [call-possibly-unbound-method]
159+
reveal_type(Foo()) # revealed: Foo
160+
161+
# error: [call-possibly-unbound-method]
162+
# error: [too-many-positional-arguments]
163+
reveal_type(Foo(1)) # revealed: Foo
164+
```
165+
166+
### Possibly unbound `__call__` on `__new__` callable
167+
149168
```py
150169
def _(flag: bool) -> None:
151170
class Callable:
@@ -323,3 +342,28 @@ reveal_type(Foo(1)) # revealed: Foo
323342
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
324343
reveal_type(Foo(1, 2)) # revealed: Foo
325344
```
345+
346+
### Lookup of `__new__`
347+
348+
The `__new__` method is always invoked on the class itself, never on the metaclass. This is
349+
different from how other dunder methods like `__lt__` are implicitly called (always on the
350+
meta-type, never on the type itself).
351+
352+
```py
353+
from typing_extensions import Literal
354+
355+
class Meta(type):
356+
def __new__(mcls, name, bases, namespace, /, **kwargs):
357+
return super().__new__(mcls, name, bases, namespace)
358+
359+
def __lt__(cls, other) -> Literal[True]:
360+
return True
361+
362+
class C(metaclass=Meta): ...
363+
364+
# No error is raised here, since we don't implicitly call `Meta.__new__`
365+
reveal_type(C()) # revealed: C
366+
367+
# Meta.__lt__ is implicitly called here:
368+
reveal_type(C < C) # revealed: Literal[True]
369+
```

crates/red_knot_python_semantic/src/symbol.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,33 @@ impl<'db> Symbol<'db> {
107107
qualifiers,
108108
}
109109
}
110+
111+
/// Try to call `__get__(None, owner)` on the type of this symbol (not on the meta type).
112+
/// If it succeeds, return the `__get__` return type. Otherwise, returns the original symbol.
113+
pub(crate) fn try_call_dunder_get(self, db: &'db dyn Db, owner: Type<'db>) -> Symbol<'db> {
114+
match self {
115+
Symbol::Type(Type::Union(union), boundness) => union.map_with_boundness(db, |elem| {
116+
Symbol::Type(*elem, boundness).try_call_dunder_get(db, owner)
117+
}),
118+
119+
Symbol::Type(Type::Intersection(intersection), boundness) => intersection
120+
.map_with_boundness(db, |elem| {
121+
Symbol::Type(*elem, boundness).try_call_dunder_get(db, owner)
122+
}),
123+
124+
Symbol::Type(self_ty, boundness) => {
125+
if let Some((dunder_get_return_ty, _)) =
126+
self_ty.try_call_dunder_get(db, Type::none(db), owner)
127+
{
128+
Symbol::Type(dunder_get_return_ty, boundness)
129+
} else {
130+
self
131+
}
132+
}
133+
134+
Symbol::Unbound => Symbol::Unbound,
135+
}
136+
}
110137
}
111138

112139
impl<'db> From<LookupResult<'db>> for SymbolAndQualifiers<'db> {

crates/red_knot_python_semantic/src/types.rs

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ fn definition_expression_type<'db>(
154154
/// method or a `__delete__` method. This enum is used to categorize attributes into two
155155
/// groups: (1) data descriptors and (2) normal attributes or non-data descriptors.
156156
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, salsa::Update)]
157-
enum AttributeKind {
157+
pub(crate) enum AttributeKind {
158158
DataDescriptor,
159159
NormalOrNonDataDescriptor,
160160
}
@@ -2512,7 +2512,7 @@ impl<'db> Type<'db> {
25122512
///
25132513
/// If `__get__` is not defined on the meta-type, this method returns `None`.
25142514
#[salsa::tracked]
2515-
fn try_call_dunder_get(
2515+
pub(crate) fn try_call_dunder_get(
25162516
self,
25172517
db: &'db dyn Db,
25182518
instance: Type<'db>,
@@ -4348,16 +4348,27 @@ impl<'db> Type<'db> {
43484348
// easy to check if that's the one we found?
43494349
// Note that `__new__` is a static method, so we must inject the `cls` argument.
43504350
let new_call_outcome = argument_types.with_self(Some(self_type), |argument_types| {
4351-
let result = self_type.try_call_dunder_with_policy(
4352-
db,
4353-
"__new__",
4354-
argument_types,
4355-
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK
4356-
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
4357-
);
4358-
match result {
4359-
Err(CallDunderError::MethodNotAvailable) => None,
4360-
_ => Some(result),
4351+
let new_method = self_type
4352+
.find_name_in_mro_with_policy(
4353+
db,
4354+
"__new__",
4355+
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK
4356+
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
4357+
)?
4358+
.symbol
4359+
.try_call_dunder_get(db, self_type);
4360+
4361+
match new_method {
4362+
Symbol::Type(new_method, boundness) => {
4363+
let result = new_method.try_call(db, CallArgumentTypes::clone(argument_types));
4364+
4365+
if boundness == Boundness::PossiblyUnbound {
4366+
return Some(Err(DunderNewCallError::PossiblyUnbound(result.err())));
4367+
}
4368+
4369+
Some(result.map_err(DunderNewCallError::CallError))
4370+
}
4371+
Symbol::Unbound => None,
43614372
}
43624373
});
43634374

@@ -6133,12 +6144,18 @@ impl<'db> BoolError<'db> {
61336144
}
61346145
}
61356146

6147+
#[derive(Debug)]
6148+
enum DunderNewCallError<'db> {
6149+
CallError(CallError<'db>),
6150+
PossiblyUnbound(Option<CallError<'db>>),
6151+
}
6152+
61366153
/// Error returned if a class instantiation call failed
61376154
#[derive(Debug)]
61386155
enum ConstructorCallError<'db> {
61396156
Init(Type<'db>, CallDunderError<'db>),
6140-
New(Type<'db>, CallDunderError<'db>),
6141-
NewAndInit(Type<'db>, CallDunderError<'db>, CallDunderError<'db>),
6157+
New(Type<'db>, DunderNewCallError<'db>),
6158+
NewAndInit(Type<'db>, DunderNewCallError<'db>, CallDunderError<'db>),
61426159
}
61436160

61446161
impl<'db> ConstructorCallError<'db> {
@@ -6188,13 +6205,8 @@ impl<'db> ConstructorCallError<'db> {
61886205
}
61896206
};
61906207

6191-
let report_new_error = |call_dunder_error: &CallDunderError<'db>| match call_dunder_error {
6192-
CallDunderError::MethodNotAvailable => {
6193-
// We are explicitly checking for `__new__` before attempting to call it,
6194-
// so this should never happen.
6195-
unreachable!("`__new__` method may not be called if missing");
6196-
}
6197-
CallDunderError::PossiblyUnbound(bindings) => {
6208+
let report_new_error = |error: &DunderNewCallError<'db>| match error {
6209+
DunderNewCallError::PossiblyUnbound(call_error) => {
61986210
if let Some(builder) =
61996211
context.report_lint(&CALL_POSSIBLY_UNBOUND_METHOD, context_expression_node)
62006212
{
@@ -6204,22 +6216,24 @@ impl<'db> ConstructorCallError<'db> {
62046216
));
62056217
}
62066218

6207-
bindings.report_diagnostics(context, context_expression_node);
6219+
if let Some(CallError(_kind, bindings)) = call_error {
6220+
bindings.report_diagnostics(context, context_expression_node);
6221+
}
62086222
}
6209-
CallDunderError::CallError(_, bindings) => {
6223+
DunderNewCallError::CallError(CallError(_kind, bindings)) => {
62106224
bindings.report_diagnostics(context, context_expression_node);
62116225
}
62126226
};
62136227

62146228
match self {
6215-
Self::Init(_, call_dunder_error) => {
6216-
report_init_error(call_dunder_error);
6229+
Self::Init(_, init_call_dunder_error) => {
6230+
report_init_error(init_call_dunder_error);
62176231
}
6218-
Self::New(_, call_dunder_error) => {
6219-
report_new_error(call_dunder_error);
6232+
Self::New(_, new_call_error) => {
6233+
report_new_error(new_call_error);
62206234
}
6221-
Self::NewAndInit(_, new_call_dunder_error, init_call_dunder_error) => {
6222-
report_new_error(new_call_dunder_error);
6235+
Self::NewAndInit(_, new_call_error, init_call_dunder_error) => {
6236+
report_new_error(new_call_error);
62236237
report_init_error(init_call_dunder_error);
62246238
}
62256239
}

crates/red_knot_python_semantic/src/types/call/arguments.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub(crate) enum Argument<'a> {
5555
}
5656

5757
/// Arguments for a single call, in source order, along with inferred types for each argument.
58+
#[derive(Clone, Debug)]
5859
pub(crate) struct CallArgumentTypes<'a, 'db> {
5960
arguments: CallArguments<'a>,
6061
types: VecDeque<Type<'db>>,

crates/red_knot_python_semantic/src/types/class.rs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -792,13 +792,10 @@ impl<'db> ClassLiteral<'db> {
792792
}
793793

794794
let new_function_symbol = self_ty
795-
.member_lookup_with_policy(
796-
db,
797-
"__new__".into(),
798-
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK
799-
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
800-
)
801-
.symbol;
795+
.find_name_in_mro(db, "__new__".into())
796+
.expect("find_name_in_mro always succeeds for class literals")
797+
.symbol
798+
.try_call_dunder_get(db, self_ty);
802799

803800
if let Symbol::Type(Type::FunctionLiteral(new_function), _) = new_function_symbol {
804801
return Some(new_function.into_bound_method_type(db, self.into()));
@@ -879,12 +876,8 @@ impl<'db> ClassLiteral<'db> {
879876
continue;
880877
}
881878

882-
// HACK: we should implement some more general logic here that supports arbitrary custom
883-
// metaclasses, not just `type` and `ABCMeta`.
884-
if matches!(
885-
class.known(db),
886-
Some(KnownClass::Type | KnownClass::ABCMeta)
887-
) && policy.meta_class_no_type_fallback()
879+
if matches!(class.known(db), Some(KnownClass::Type))
880+
&& policy.meta_class_no_type_fallback()
888881
{
889882
continue;
890883
}

0 commit comments

Comments
 (0)