[ty] Fix descriptor lookups for most types that overlap with None (#19120)
Some checks failed
CI / Determine changes (push) Has been cancelled
CI / cargo fmt (push) Has been cancelled
CI / cargo build (release) (push) Has been cancelled
CI / python package (push) Has been cancelled
CI / pre-commit (push) Has been cancelled
CI / mkdocs (push) Has been cancelled
[ty Playground] Release / publish (push) Has been cancelled
CI / cargo clippy (push) Has been cancelled
CI / cargo test (linux) (push) Has been cancelled
CI / cargo test (linux, release) (push) Has been cancelled
CI / cargo test (windows) (push) Has been cancelled
CI / cargo test (wasm) (push) Has been cancelled
CI / cargo build (msrv) (push) Has been cancelled
CI / cargo fuzz build (push) Has been cancelled
CI / fuzz parser (push) Has been cancelled
CI / test scripts (push) Has been cancelled
CI / ecosystem (push) Has been cancelled
CI / Fuzz for new ty panics (push) Has been cancelled
CI / cargo shear (push) Has been cancelled
CI / formatter instabilities and black similarity (push) Has been cancelled
CI / test ruff-lsp (push) Has been cancelled
CI / check playground (push) Has been cancelled
CI / benchmarks-instrumented (push) Has been cancelled
CI / benchmarks-walltime (push) Has been cancelled

This commit is contained in:
Alex Waygood 2025-07-05 19:34:23 +01:00 committed by GitHub
parent 44f2f77748
commit 08d8819c8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 44 additions and 15 deletions

View file

@ -343,7 +343,7 @@ def _(c: Callable[[int, Unpack[Ts]], int]):
from typing import Callable from typing import Callable
def _(c: Callable[[int], int]): 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.__class__) # revealed: type
reveal_type(c.__call__) # revealed: (int, /) -> int reveal_type(c.__call__) # revealed: (int, /) -> int
``` ```

View file

@ -201,6 +201,36 @@ type IntOrStr = int | str
reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any) -> _SpecialForm 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 ## Error cases: Calling `__get__` for methods
The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed: 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(C(), None)
method_wrapper(None, C) method_wrapper(None, C)
# Passing `None` without an `owner` argument is an reveal_type(object.__str__.__get__(object(), None)()) # revealed: str
# error: [invalid-argument-type] "Argument to method wrapper `__get__` of function `f` is incorrect: Expected `~None`, found `None`"
# TODO: passing `None` without an `owner` argument fails at runtime.
# Ideally we would emit a diagnostic here:
method_wrapper(None) method_wrapper(None)
# Passing something that is not assignable to `type` as the `owner` argument is an # 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" # error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
method_wrapper(None, 1) method_wrapper(None, 1)
# Passing `None` as the `owner` argument when `instance` is `None` is an # TODO: passing `None` as the `owner` argument when `instance` is `None` fails at runtime.
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments" # Ideally we would emit a diagnostic here.
method_wrapper(None, None) method_wrapper(None, None)
# Calling `__get__` without any arguments is an # Calling `__get__` without any arguments is an

View file

@ -619,8 +619,9 @@ wrapper_descriptor()
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments" # error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
wrapper_descriptor(f) wrapper_descriptor(f)
# Calling it without the `owner` argument if `instance` is not `None` is an # TODO: Calling it without the `owner` argument if `instance` is not `None` fails at runtime.
# error: [invalid-argument-type] "Argument to wrapper descriptor `FunctionType.__get__` is incorrect: Expected `~None`, found `None`" # Ideally we would emit a diagnostic here,
# but this is hard to model without introducing false positives elsewhere
wrapper_descriptor(f, None) wrapper_descriptor(f, None)
# But calling it with an instance is fine (in this case, the `owner` argument is optional): # But calling it with an instance is fine (in this case, the `owner` argument is optional):

View file

@ -3069,10 +3069,6 @@ impl<'db> Type<'db> {
Type::ModuleLiteral(module) => module.static_member(db, name_str).into(), 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( _ if policy.no_instance_fallback() => self.invoke_descriptor_protocol(
db, db,
name_str, name_str,
@ -3094,6 +3090,8 @@ impl<'db> Type<'db> {
| Type::KnownInstance(..) | Type::KnownInstance(..)
| Type::PropertyInstance(..) | Type::PropertyInstance(..)
| Type::FunctionLiteral(..) | Type::FunctionLiteral(..)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
| Type::TypeIs(..) => { | Type::TypeIs(..) => {
let fallback = self.instance_member(db, name_str); 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 // 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`. // specified yet, they will be dynamically added in `Bindings::evaluate_known_cases`.
let not_none = Type::none(db).negate(db);
CallableBinding::from_overloads( CallableBinding::from_overloads(
self, self,
[ [
@ -3549,7 +3546,7 @@ impl<'db> Type<'db> {
Signature::new( Signature::new(
Parameters::new([ Parameters::new([
Parameter::positional_only(Some(Name::new_static("instance"))) 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"))) Parameter::positional_only(Some(Name::new_static("owner")))
.with_annotated_type(UnionType::from_elements( .with_annotated_type(UnionType::from_elements(
db, db,
@ -3575,7 +3572,6 @@ impl<'db> Type<'db> {
// TODO: Consider merging this signature with the one in the previous match clause, // 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 // since the previous one is just this signature with the `self` parameters
// removed. // removed.
let not_none = Type::none(db).negate(db);
let descriptor = match kind { let descriptor = match kind {
WrapperDescriptorKind::FunctionTypeDunderGet => { WrapperDescriptorKind::FunctionTypeDunderGet => {
KnownClass::FunctionType.to_instance(db) KnownClass::FunctionType.to_instance(db)
@ -3606,7 +3602,7 @@ impl<'db> Type<'db> {
Parameter::positional_only(Some(Name::new_static("self"))) Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(descriptor), .with_annotated_type(descriptor),
Parameter::positional_only(Some(Name::new_static("instance"))) 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"))) Parameter::positional_only(Some(Name::new_static("owner")))
.with_annotated_type(UnionType::from_elements( .with_annotated_type(UnionType::from_elements(
db, db,