Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -1216,6 +1216,69 @@ static_assert(is_subtype_of(TypeOf[F], Callable[[], int]))
static_assert(not is_subtype_of(TypeOf[F], Callable[[], str]))
```

#### Classes with `__init__`

```py
from typing import Callable, overload
from ty_extensions import TypeOf, static_assert, is_subtype_of

class A:
def __init__(self, a: int) -> None: ...

static_assert(is_subtype_of(TypeOf[A], Callable[[int], A]))
static_assert(not is_subtype_of(TypeOf[A], Callable[[], A]))

class B:
@overload
def __init__(self, a: int) -> None: ...
@overload
def __init__(self) -> None: ...
def __init__(self, a: int | None = None) -> None: ...

static_assert(is_subtype_of(TypeOf[B], Callable[[int], B]))
static_assert(is_subtype_of(TypeOf[B], Callable[[], B]))

class C: ...

# TODO: This assertion should be true once we understand `Self`
# error: [static-assert-error] "Static assertion error: argument evaluates to `False`"
static_assert(is_subtype_of(TypeOf[C], Callable[[], C]))
```

#### Classes with `__init__` and `__new__`

```py
from typing import Callable
from ty_extensions import TypeOf, static_assert, is_subtype_of

class A:
def __new__(cls, a: int) -> "A":
return super().__new__(cls)

def __init__(self, a: int) -> None: ...

static_assert(is_subtype_of(TypeOf[A], Callable[[int], A]))
static_assert(not is_subtype_of(TypeOf[A], Callable[[], A]))

class B:
def __new__(cls, a: int) -> int:
return super().__new__(cls)

def __init__(self, a: str) -> None: ...

static_assert(is_subtype_of(TypeOf[B], Callable[[int], int]))
static_assert(not is_subtype_of(TypeOf[B], Callable[[str], int]))

class C:
def __new__(cls, *args, **kwargs) -> "C":
return super().__new__(cls)

def __init__(self, x: int) -> None: ...

static_assert(not is_subtype_of(TypeOf[C], Callable[[int], C]))
Comment on lines +1272 to +1349

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per the spec this "shouldn't" pass, but there's maybe an argument that it should pass

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(As is the test without "not" should pass)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this not pass according to the spec? It seems like the spec would say in this case that the callable type of C would be the union of the __new__ signature, and the __init__ bound-method signature with replaced return type. That union seems like it should be a subtype of Callable[[int], C]?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, so yeah you're right, but what is happening here (after i added ":Any" to args and kwargs)
is we end up calling is_subtype with
self: (*args: Any, **kwargs: Any) -> C
target: (int, /) -> C
which returns false because self isnt fully static

static_assert(not is_subtype_of(TypeOf[C], Callable[[], C]))
```

### Bound methods

```py
Expand Down
10 changes: 2 additions & 8 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1230,10 +1230,7 @@ impl<'db> Type<'db> {
}

(Type::ClassLiteral(class_literal), Type::Callable(_)) => {
if let Some(callable) = class_literal.into_callable(db) {
return callable.is_subtype_of(db, target);
}
false
class_literal.into_callable(db).is_subtype_of(db, target)
}

// `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`.
Expand Down Expand Up @@ -1483,10 +1480,7 @@ impl<'db> Type<'db> {
}

(Type::ClassLiteral(class_literal), Type::Callable(_)) => {
if let Some(callable) = class_literal.into_callable(db) {
return callable.is_assignable_to(db, target);
}
false
class_literal.into_callable(db).is_assignable_to(db, target)
}

(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
Expand Down
119 changes: 105 additions & 14 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::hash::BuildHasherDefault;
use std::sync::{LazyLock, Mutex};

use super::FunctionSignature;
use super::{
class_base::ClassBase, infer_expression_type, infer_unpack_types, IntersectionBuilder,
KnownFunction, MemberLookupPolicy, Mro, MroError, MroIterator, SubclassOfType, Truthiness,
Expand Down Expand Up @@ -872,9 +873,9 @@ impl<'db> ClassLiteral<'db> {
))
}

pub(super) fn into_callable(self, db: &'db dyn Db) -> Option<Type<'db>> {
pub(super) fn into_callable(self, db: &'db dyn Db) -> Type<'db> {
let self_ty = Type::from(self);
let metaclass_call_function_symbol = self_ty
let metaclass_dunder_call_function_symbol = self_ty
.member_lookup_with_policy(
db,
"__call__".into(),
Expand All @@ -883,27 +884,117 @@ impl<'db> ClassLiteral<'db> {
)
.symbol;

if let Symbol::Type(Type::BoundMethod(metaclass_call_function), _) =
metaclass_call_function_symbol
if let Symbol::Type(Type::BoundMethod(metaclass__dunder_call_function), _) =
metaclass_dunder_call_function_symbol
{
// TODO: this intentionally diverges from step 1 in
// https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable
// by always respecting the signature of the metaclass `__call__`, rather than
// using a heuristic which makes unwarranted assumptions to sometimes ignore it.
return Some(metaclass_call_function.into_callable_type(db));
return metaclass__dunder_call_function.into_callable_type(db);
}

let dunder_new_method = self_ty
.find_name_in_mro(db, "__new__")
.expect("find_name_in_mro always succeeds for class literals")
.symbol
.try_call_dunder_get(db, self_ty);
let dunder_new_function_symbol = self_ty
.member_lookup_with_policy(
db,
"__new__".into(),
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
)
.symbol;

let dunder_new_function =
if let Symbol::Type(Type::FunctionLiteral(dunder_new_function), _) =
dunder_new_function_symbol
{
// Step 3: If the return type of the `__new__` evaluates to a type that is not a subclass of this class,
// then we should ignore the `__init__` and just return the `__new__` method.
let new_return_ty = match dunder_new_function.signature(db) {
FunctionSignature::Single(signature)
| FunctionSignature::Overloaded(_, Some(signature)) => signature.return_ty,
FunctionSignature::Overloaded(..) => None,
};

if let Some(new_return_ty) = new_return_ty {
if !new_return_ty.to_meta_type(db).is_subtype_of(db, self_ty) {
Comment thread
MatthewMckee4 marked this conversation as resolved.
Outdated
return dunder_new_function.into_bound_method_type(db, self_ty);
}
}
Some(dunder_new_function.into_bound_method_type(db, self_ty))
} else {
None
};

let dunder_init_function_symbol = self_ty
.member_lookup_with_policy(
db,
"__init__".into(),
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
)
.symbol;

if let Symbol::Type(Type::FunctionLiteral(dunder_new_method), _) = dunder_new_method {
return Some(dunder_new_method.into_bound_method_type(db, self.into()));
// TODO: should be the concrete value of `Self`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not actually sure what the spec means by "the concrete value of Self" here; I don't know what else that could be, other than what you have here (for non-generic classes, anyway).

let correct_return_type = Type::instance(db, ClassType::NonGeneric(self));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about generic classes? Can we add a test for that case? Do we need a TODO?

Should this just be self_ty.to_instance() instead, so we don't have to worry about those distinctions here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think generic cases need some more work, do you think into_callable belongs in ClassLiteral or should it maybe be in ClassType?


// If the class defines an `__init__` method, then we synthesize a `__init__` method
// that has the same parameters as the `__init__` method after it is bound, and with the return type
// of the concrete value of `Self`.
let synthesized_dunder_init_callable =
if let Symbol::Type(Type::FunctionLiteral(dunder_init_function), _) =
dunder_init_function_symbol
{
let synthesized_signature = |signature: Signature<'db>| {
Signature::new(signature.parameters().clone(), Some(correct_return_type))
.bind_self()
};

let synthesized_dunder_init_signature = match dunder_init_function.signature(db) {
FunctionSignature::Single(signature) => {
CallableType::single(db, synthesized_signature(signature.clone()))
}
FunctionSignature::Overloaded(overloads, _) => CallableType::from_overloads(
db,
overloads
.iter()
.map(Clone::clone)
.map(synthesized_signature),
),
};
Some(Type::Callable(synthesized_dunder_init_signature))
} else {
None
};

let concrete_dunder_functions: Vec<_> =
[dunder_new_function, synthesized_dunder_init_callable]
.into_iter()
.flatten()
.collect();

if concrete_dunder_functions.is_empty() {
// If no `__new__` or `__init__` method is found, then we fall back to looking for
// an `object.__new__` method.
let new_function_symbol = self_ty
.member_lookup_with_policy(
db,
"__new__".into(),
MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
)
.symbol;

if let Symbol::Type(Type::FunctionLiteral(new_function), _) = new_function_symbol {
new_function.into_bound_method_type(db, self_ty)
} else {
// Fallback if no `object.__new__` is found.
Type::Callable(CallableType::single(
db,
Signature::new(Parameters::empty(), Some(correct_return_type)),
))
}
} else {
UnionType::from_elements(db, concrete_dunder_functions)
}
// TODO handle `__init__` also
None
}

/// Returns the class member of this class named `name`.
Expand Down