diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index 9185e1722e..91f3412e11 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -586,5 +586,157 @@ reveal_type(C.f2(1)) # revealed: str reveal_type(C().f2(1)) # revealed: str ``` +## Builtin functions and methods + +Some builtin functions and methods are heavily special-cased by ty. This mdtest checks that various +properties are understood correctly for these functions and methods. + +```py +import types +from typing import Callable +from ty_extensions import static_assert, CallableTypeOf, is_assignable_to, TypeOf + +def f(obj: type) -> None: ... + +class MyClass: + @property + def my_property(self) -> int: + return 42 + + @my_property.setter + def my_property(self, value: int | str) -> None: ... + +static_assert(is_assignable_to(types.FunctionType, Callable)) + +# revealed: +reveal_type(types.FunctionType.__get__) + +# TODO: should pass +# error: [static-assert-error] +static_assert(is_assignable_to(TypeOf[types.FunctionType.__get__], Callable)) + +# revealed: def f(obj: type) -> None +reveal_type(f) +static_assert(is_assignable_to(TypeOf[f], Callable)) + +# revealed: +reveal_type(f.__get__) + +# TODO: should pass +# error: [static-assert-error] +static_assert(is_assignable_to(TypeOf[f.__get__], Callable)) + +# revealed: def __call__(self, *args: Any, **kwargs: Any) -> Any +reveal_type(types.FunctionType.__call__) +static_assert(is_assignable_to(TypeOf[types.FunctionType.__call__], Callable)) + +# revealed: +reveal_type(f.__call__) + +# TODO: should pass +# error: [static-assert-error] +static_assert(is_assignable_to(TypeOf[f.__call__], Callable)) + +# revealed: +reveal_type(property.__get__) + +# TODO: should pass +# error: [static-assert-error] +static_assert(is_assignable_to(TypeOf[property.__get__], Callable)) + +# revealed: property +reveal_type(MyClass.my_property) +static_assert(is_assignable_to(TypeOf[property], Callable)) +static_assert(not is_assignable_to(TypeOf[MyClass.my_property], Callable)) + +# revealed: +reveal_type(MyClass.my_property.__get__) + +# TODO: should pass +# error: [static-assert-error] +static_assert(is_assignable_to(TypeOf[MyClass.my_property.__get__], Callable)) + +# revealed: +reveal_type(property.__set__) + +# TODO: should pass +# error: [static-assert-error] +static_assert(is_assignable_to(TypeOf[property.__set__], Callable)) + +# revealed: +reveal_type(MyClass.my_property.__set__) + +# TODO: should pass +# error: [static-assert-error] +static_assert(is_assignable_to(TypeOf[MyClass.my_property.__set__], Callable)) + +# revealed: def startswith(self, prefix: str | tuple[str, ...], start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool +reveal_type(str.startswith) +static_assert(is_assignable_to(TypeOf[str.startswith], Callable)) + +# revealed: +reveal_type("foo".startswith) + +# TODO: should pass +# error: [static-assert-error] +static_assert(is_assignable_to(TypeOf["foo".startswith], Callable)) + +def _( + a: CallableTypeOf[types.FunctionType.__get__], + b: CallableTypeOf[f], + c: CallableTypeOf[f.__get__], + d: CallableTypeOf[types.FunctionType.__call__], + # TODO: false-positive diagnostic + e: CallableTypeOf[f.__call__], # error: [invalid-type-form] + f: CallableTypeOf[property], + g: CallableTypeOf[property.__get__], + h: CallableTypeOf[MyClass.my_property.__get__], + i: CallableTypeOf[property.__set__], + j: CallableTypeOf[MyClass.my_property.__set__], + k: CallableTypeOf[str.startswith], + l: CallableTypeOf["foo".startswith], +): + # revealed: Overload[(self: FunctionType, instance: None, owner: type, /) -> Unknown, (self: FunctionType, instance: object, owner: type | None = None, /) -> Unknown] + reveal_type(a) + + # revealed: (obj: type) -> None + reveal_type(b) + + # TODO: ideally this would have precise return types rather than `Unknown` + # revealed: Overload[(instance: None, owner: type, /) -> Unknown, (instance: object, owner: type | None = None, /) -> Unknown] + reveal_type(c) + + # revealed: (self, *args: Any, **kwargs: Any) -> Any + reveal_type(d) + + # TODO: this should be `(obj: type) -> None` + # revealed: Unknown + reveal_type(e) + + # revealed: (fget: ((Any, /) -> Any) | None = None, fset: ((Any, Any, /) -> None) | None = None, fdel: ((Any, /) -> Any) | None = None, doc: str | None = None) -> Unknown + reveal_type(f) + + # revealed: Overload[(self: property, instance: None, owner: type, /) -> Unknown, (self: property, instance: object, owner: type | None = None, /) -> Unknown] + reveal_type(g) + + # TODO: ideally this would have precise return types rather than `Unknown` + # revealed: Overload[(instance: None, owner: type, /) -> Unknown, (instance: object, owner: type | None = None, /) -> Unknown] + reveal_type(h) + + # TODO: ideally this would have `-> None` rather than `-> Unknown` + # revealed: (self: property, instance: object, value: object, /) -> Unknown + reveal_type(i) + + # TODO: ideally this would have a more precise input type and `-> None` rather than `-> Unknown` + # revealed: (instance: object, value: object, /) -> Unknown + reveal_type(j) + + # revealed: (self, prefix: str | tuple[str, ...], start: SupportsIndex | None = ellipsis, end: SupportsIndex | None = ellipsis, /) -> bool + reveal_type(k) + + # revealed: (prefix: str | tuple[str, ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> bool + reveal_type(l) +``` + [functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods [`__init_subclass__`]: https://docs.python.org/3/reference/datamodel.html#object.__init_subclass__ diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 42ad5d2fd8..ccf4a9f2f1 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -534,6 +534,49 @@ impl<'db> PropertyInstanceType<'db> { ty.find_legacy_typevars_impl(db, binding_context, typevars, visitor); } } + + fn when_equivalent_to>(self, db: &'db dyn Db, other: Self) -> C { + self.is_equivalent_to_impl( + db, + other, + &IsEquivalentVisitor::new(C::always_satisfiable(db)), + ) + } + + fn is_equivalent_to_impl>( + self, + db: &'db dyn Db, + other: Self, + visitor: &IsEquivalentVisitor<'db, C>, + ) -> C { + let getter_equivalence = if let Some(getter) = self.getter(db) { + let Some(other_getter) = other.getter(db) else { + return C::unsatisfiable(db); + }; + getter.is_equivalent_to_impl(db, other_getter, visitor) + } else { + if other.getter(db).is_some() { + return C::unsatisfiable(db); + } + C::always_satisfiable(db) + }; + + let setter_equivalence = || { + if let Some(setter) = self.setter(db) { + let Some(other_setter) = other.setter(db) else { + return C::unsatisfiable(db); + }; + setter.is_equivalent_to_impl(db, other_setter, visitor) + } else { + if other.setter(db).is_some() { + return C::unsatisfiable(db); + } + C::always_satisfiable(db) + } + }; + + getter_equivalence.and(db, setter_equivalence) + } } bitflags! { @@ -1931,6 +1974,11 @@ impl<'db> Type<'db> { let class_literal = instance.class(db).class_literal(db).0; C::from_bool(db, is_single_member_enum(db, class_literal)) } + + (Type::PropertyInstance(left), Type::PropertyInstance(right)) => { + left.is_equivalent_to_impl(db, right, visitor) + } + _ => C::unsatisfiable(db), } } @@ -9251,14 +9299,15 @@ impl<'db> KnownBoundMethodType<'db> { ) => self_function.has_relation_to_impl(db, other_function, relation, visitor), ( - KnownBoundMethodType::PropertyDunderGet(_), - KnownBoundMethodType::PropertyDunderGet(_), + KnownBoundMethodType::PropertyDunderGet(self_property), + KnownBoundMethodType::PropertyDunderGet(other_property), ) | ( - KnownBoundMethodType::PropertyDunderSet(_), - KnownBoundMethodType::PropertyDunderSet(_), - ) - | (KnownBoundMethodType::StrStartswith(_), KnownBoundMethodType::StrStartswith(_)) => { + KnownBoundMethodType::PropertyDunderSet(self_property), + KnownBoundMethodType::PropertyDunderSet(other_property), + ) => self_property.when_equivalent_to(db, other_property), + + (KnownBoundMethodType::StrStartswith(_), KnownBoundMethodType::StrStartswith(_)) => { C::from_bool(db, self == other) } @@ -9295,14 +9344,15 @@ impl<'db> KnownBoundMethodType<'db> { ) => self_function.is_equivalent_to_impl(db, other_function, visitor), ( - KnownBoundMethodType::PropertyDunderGet(_), - KnownBoundMethodType::PropertyDunderGet(_), + KnownBoundMethodType::PropertyDunderGet(self_property), + KnownBoundMethodType::PropertyDunderGet(other_property), ) | ( - KnownBoundMethodType::PropertyDunderSet(_), - KnownBoundMethodType::PropertyDunderSet(_), - ) - | (KnownBoundMethodType::StrStartswith(_), KnownBoundMethodType::StrStartswith(_)) => { + KnownBoundMethodType::PropertyDunderSet(self_property), + KnownBoundMethodType::PropertyDunderSet(other_property), + ) => self_property.is_equivalent_to_impl(db, other_property, visitor), + + (KnownBoundMethodType::StrStartswith(_), KnownBoundMethodType::StrStartswith(_)) => { C::from_bool(db, self == other) }