Summary
Other than object, all nominal instance types are disjoint from None. But... this is not true for instances of structural (Protocol) types -- and it turns out that this causes issues for our current implementation of the descriptor protocol.
For example, compare and contrast the different behaviour of these two protocols. SupportsFoo is disjoint from None; SupportsStr is not:
from typing import Protocol
class SupportsFoo(Protocol):
def foo(self) -> str: ...
class SupportsStr(Protocol):
def __str__(self) -> str: ...
def f(f: SupportsFoo, s: SupportsStr):
reveal_type(f.foo) # revealed: `bound method SupportsFoo.foo() -> str`
f.foo() # no diagnostic
reveal_type(s.__str__) # revealed: `def __str__(self) -> str`
s.__str__() # error: No argument provided for required parameter `self` of function `__str__` (missing-argument)
https://play.ty.dev/09505473-95b5-4b51-b992-7784595bcf55
Whether or not a type is disjoint from None impacts our understanding of the way the descriptor protocol is invoked when a method is accessed on that type. The type that a method is accessed on is passed to FunctionType.__get__ when we invoke the descriptor protocol; if that type is not disjoint from None, we end up having to pick the first overload here, which means that the attribute access is (incorrectly!) evaluated as resolving to the original function object rather than a bound method:
https://github.com/astral-sh/ruff/blob/c6fd11fe3694646c7b5667e37dfc67b114e2f50a/crates/ty_python_semantic/src/types.rs#L3526-L3575
Many thanks to @sharkdp who helped me track this bug down over the last couple of days!
Version
c6fd11f
Summary
Other than
object, all nominal instance types are disjoint fromNone. But... this is not true for instances of structural (Protocol) types -- and it turns out that this causes issues for our current implementation of the descriptor protocol.For example, compare and contrast the different behaviour of these two protocols.
SupportsFoois disjoint fromNone;SupportsStris not:https://play.ty.dev/09505473-95b5-4b51-b992-7784595bcf55
Whether or not a type is disjoint from
Noneimpacts our understanding of the way the descriptor protocol is invoked when a method is accessed on that type. The type that a method is accessed on is passed toFunctionType.__get__when we invoke the descriptor protocol; if that type is not disjoint fromNone, we end up having to pick the first overload here, which means that the attribute access is (incorrectly!) evaluated as resolving to the original function object rather than a bound method:https://github.com/astral-sh/ruff/blob/c6fd11fe3694646c7b5667e37dfc67b114e2f50a/crates/ty_python_semantic/src/types.rs#L3526-L3575
Many thanks to @sharkdp who helped me track this bug down over the last couple of days!
Version
c6fd11f