From 0084e94f78742685ddacf2dab28530c2ece3393e Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 28 Nov 2025 16:56:22 +0100 Subject: [PATCH] [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. --- .../resources/mdtest/libraries/numpy.md | 67 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 20 ++++++ 2 files changed, 87 insertions(+) create mode 100644 crates/ty_python_semantic/resources/mdtest/libraries/numpy.md diff --git a/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md b/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md new file mode 100644 index 0000000000..d1afd367ca --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/libraries/numpy.md @@ -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") +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 2b509ce6dc..94ff50f161 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -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[]` 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(