mirror of https://github.com/astral-sh/ruff
[ty] Fix subtyping of `type[Any]` / `type[T]` and protocols (#21678)
## Summary This is a bugfix for subtyping of `type[Any]` / `type[T]` and protocols. ## Test Plan Regression test that will only be really meaningful once https://github.com/astral-sh/ruff/pull/21553 lands.
This commit is contained in:
parent
566c959add
commit
0084e94f78
|
|
@ -0,0 +1,67 @@
|
|||
# numpy
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.14"
|
||||
```
|
||||
|
||||
## numpy's `dtype`
|
||||
|
||||
numpy functions often accept a `dtype` parameter. For example, one of `np.array`'s overloads accepts
|
||||
a `dtype` parameter of type `DTypeLike | None`. Here, we build up something that resembles numpy's
|
||||
internals in order to model the type `DTypeLike`. Many details have been left out.
|
||||
|
||||
`mini_numpy.py`:
|
||||
|
||||
```py
|
||||
from typing import TypeVar, Generic, Any, Protocol, TypeAlias, runtime_checkable, final
|
||||
import builtins
|
||||
|
||||
_ItemT_co = TypeVar("_ItemT_co", default=Any, covariant=True)
|
||||
|
||||
class generic(Generic[_ItemT_co]):
|
||||
@property
|
||||
def dtype(self) -> _DTypeT_co:
|
||||
raise NotImplementedError
|
||||
|
||||
_BoolItemT_co = TypeVar("_BoolItemT_co", bound=builtins.bool, default=builtins.bool, covariant=True)
|
||||
|
||||
class bool(generic[_BoolItemT_co], Generic[_BoolItemT_co]): ...
|
||||
|
||||
@final
|
||||
class object_(generic): ...
|
||||
|
||||
_ScalarT = TypeVar("_ScalarT", bound=generic)
|
||||
_ScalarT_co = TypeVar("_ScalarT_co", bound=generic, default=Any, covariant=True)
|
||||
|
||||
@final
|
||||
class dtype(Generic[_ScalarT_co]): ...
|
||||
|
||||
_DTypeT_co = TypeVar("_DTypeT_co", bound=dtype, default=dtype, covariant=True)
|
||||
|
||||
@runtime_checkable
|
||||
class _SupportsDType(Protocol[_DTypeT_co]):
|
||||
@property
|
||||
def dtype(self) -> _DTypeT_co: ...
|
||||
|
||||
# TODO: no errors here
|
||||
# error: [invalid-type-arguments] "Type `typing.TypeVar` is not assignable to upper bound `generic[Any]` of type variable `_ScalarT_co@dtype`"
|
||||
# error: [invalid-type-arguments] "Type `typing.TypeVar` is not assignable to upper bound `generic[Any]` of type variable `_ScalarT_co@dtype`"
|
||||
_DTypeLike: TypeAlias = type[_ScalarT] | dtype[_ScalarT] | _SupportsDType[dtype[_ScalarT]]
|
||||
|
||||
DTypeLike: TypeAlias = _DTypeLike[Any] | str | None
|
||||
```
|
||||
|
||||
Now we can make sure that a function which accepts `DTypeLike | None` works as expected:
|
||||
|
||||
```py
|
||||
import mini_numpy as np
|
||||
|
||||
def accepts_dtype(dtype: np.DTypeLike | None) -> None: ...
|
||||
|
||||
accepts_dtype(dtype=np.bool)
|
||||
accepts_dtype(dtype=np.dtype[np.bool])
|
||||
accepts_dtype(dtype=object)
|
||||
accepts_dtype(dtype=np.object_)
|
||||
accepts_dtype(dtype="U")
|
||||
```
|
||||
|
|
@ -2468,6 +2468,26 @@ impl<'db> Type<'db> {
|
|||
})
|
||||
}
|
||||
|
||||
// `type[Any]` is assignable to arbitrary protocols as it has arbitrary attributes
|
||||
// (this is handled by a lower-down branch), but it is only a subtype of a given
|
||||
// protocol if `type` is a subtype of that protocol. Similarly, `type[T]` will
|
||||
// always be assignable to any protocol if `type[<upper bound of T>]` is assignable
|
||||
// to that protocol (handled lower down), but it is only a subtype of that protocol
|
||||
// if `type` is a subtype of that protocol.
|
||||
(Type::SubclassOf(self_subclass_ty), Type::ProtocolInstance(_))
|
||||
if (self_subclass_ty.is_dynamic() || self_subclass_ty.is_type_var())
|
||||
&& !relation.is_assignability() =>
|
||||
{
|
||||
KnownClass::Type.to_instance(db).has_relation_to_impl(
|
||||
db,
|
||||
target,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
}
|
||||
|
||||
(_, Type::ProtocolInstance(protocol)) => {
|
||||
relation_visitor.visit((self, target, relation), || {
|
||||
self.satisfies_protocol(
|
||||
|
|
|
|||
Loading…
Reference in New Issue