diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md index c9bec98216..688dcb321a 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md @@ -343,7 +343,7 @@ def _(c: Callable[[int, Unpack[Ts]], int]): from typing import Callable def _(c: Callable[[int], int]): - reveal_type(c.__init__) # revealed: def __init__(self) -> None + reveal_type(c.__init__) # revealed: bound method object.__init__() -> None reveal_type(c.__class__) # revealed: type reveal_type(c.__call__) # revealed: (int, /) -> int ``` diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index c80b1fb992..3c5ebbeb4d 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -201,6 +201,36 @@ type IntOrStr = int | str reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any) -> _SpecialForm ``` +## Method calls on types not disjoint from `None` + +Very few methods are defined on `object`, `None`, and other types not disjoint from `None`. However, +descriptor-binding behaviour works on these types in exactly the same way as descriptor binding on +other types. This is despite the fact that `None` is used as a sentinel internally by the descriptor +protocol to indicate that a method was accessed on the class itself rather than an instance of the +class: + +```py +from typing import Protocol, Literal +from ty_extensions import AlwaysFalsy + +class Foo: ... + +class SupportsStr(Protocol): + def __str__(self) -> str: ... + +class Falsy(Protocol): + def __bool__(self) -> Literal[False]: ... + +def _(a: object, b: SupportsStr, c: Falsy, d: AlwaysFalsy, e: None, f: Foo | None): + a.__str__() + b.__str__() + c.__str__() + d.__str__() + # TODO: these should not error + e.__str__() # error: [missing-argument] + f.__str__() # error: [missing-argument] +``` + ## Error cases: Calling `__get__` for methods The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed: @@ -234,16 +264,18 @@ method_wrapper(C()) method_wrapper(C(), None) method_wrapper(None, C) -# Passing `None` without an `owner` argument is an -# error: [invalid-argument-type] "Argument to method wrapper `__get__` of function `f` is incorrect: Expected `~None`, found `None`" +reveal_type(object.__str__.__get__(object(), None)()) # revealed: str + +# TODO: passing `None` without an `owner` argument fails at runtime. +# Ideally we would emit a diagnostic here: method_wrapper(None) # Passing something that is not assignable to `type` as the `owner` argument is an # error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" method_wrapper(None, 1) -# Passing `None` as the `owner` argument when `instance` is `None` is an -# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" +# TODO: passing `None` as the `owner` argument when `instance` is `None` fails at runtime. +# Ideally we would emit a diagnostic here. method_wrapper(None, None) # Calling `__get__` without any arguments is an diff --git a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md index 9210847ab9..6d413f7100 100644 --- a/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/ty_python_semantic/resources/mdtest/descriptor_protocol.md @@ -619,8 +619,9 @@ wrapper_descriptor() # error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments" wrapper_descriptor(f) -# Calling it without the `owner` argument if `instance` is not `None` is an -# error: [invalid-argument-type] "Argument to wrapper descriptor `FunctionType.__get__` is incorrect: Expected `~None`, found `None`" +# TODO: Calling it without the `owner` argument if `instance` is not `None` fails at runtime. +# Ideally we would emit a diagnostic here, +# but this is hard to model without introducing false positives elsewhere wrapper_descriptor(f, None) # But calling it with an instance is fine (in this case, the `owner` argument is optional): diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 978608211e..dd7bbd1c65 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -3069,10 +3069,6 @@ impl<'db> Type<'db> { Type::ModuleLiteral(module) => module.static_member(db, name_str).into(), - Type::AlwaysFalsy | Type::AlwaysTruthy => { - self.class_member_with_policy(db, name, policy) - } - _ if policy.no_instance_fallback() => self.invoke_descriptor_protocol( db, name_str, @@ -3094,6 +3090,8 @@ impl<'db> Type<'db> { | Type::KnownInstance(..) | Type::PropertyInstance(..) | Type::FunctionLiteral(..) + | Type::AlwaysTruthy + | Type::AlwaysFalsy | Type::TypeIs(..) => { let fallback = self.instance_member(db, name_str); @@ -3533,7 +3531,6 @@ impl<'db> Type<'db> { // For `builtins.property.__get__`, we use the same signature. The return types are not // specified yet, they will be dynamically added in `Bindings::evaluate_known_cases`. - let not_none = Type::none(db).negate(db); CallableBinding::from_overloads( self, [ @@ -3549,7 +3546,7 @@ impl<'db> Type<'db> { Signature::new( Parameters::new([ Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(not_none), + .with_annotated_type(Type::object(db)), Parameter::positional_only(Some(Name::new_static("owner"))) .with_annotated_type(UnionType::from_elements( db, @@ -3575,7 +3572,6 @@ impl<'db> Type<'db> { // TODO: Consider merging this signature with the one in the previous match clause, // since the previous one is just this signature with the `self` parameters // removed. - let not_none = Type::none(db).negate(db); let descriptor = match kind { WrapperDescriptorKind::FunctionTypeDunderGet => { KnownClass::FunctionType.to_instance(db) @@ -3606,7 +3602,7 @@ impl<'db> Type<'db> { Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(descriptor), Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(not_none), + .with_annotated_type(Type::object(db)), Parameter::positional_only(Some(Name::new_static("owner"))) .with_annotated_type(UnionType::from_elements( db,