Skip to content

Commit 7cadc6f

Browse files
committed
[ty] Workaround for problem with numpy's dtype
1 parent c534bfa commit 7cadc6f

File tree

3 files changed

+97
-1
lines changed

3 files changed

+97
-1
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# numpy
2+
3+
```toml
4+
[environment]
5+
python-version = "3.14"
6+
```
7+
8+
## numpy's `dtype`
9+
10+
numpy functions often accept a `dtype` parameter. For example, one of `np.array`'s overloads accepts
11+
a `dtype` parameter of type `DTypeLike | None`. Here, we build up something that resembles numpy's
12+
internals in order to model the type `DTypeLike`. Many details have been left out.
13+
14+
`mini_numpy.py`:
15+
16+
```py
17+
from typing import TypeVar, Generic, Any, Protocol, TypeAlias, runtime_checkable, final
18+
import builtins
19+
20+
_ItemT_co = TypeVar("_ItemT_co", default=Any, covariant=True)
21+
22+
class generic(Generic[_ItemT_co]):
23+
@property
24+
def dtype(self) -> _DTypeT_co:
25+
raise NotImplementedError
26+
27+
_BoolItemT_co = TypeVar("_BoolItemT_co", bound=builtins.bool, default=builtins.bool, covariant=True)
28+
29+
class bool(generic[_BoolItemT_co], Generic[_BoolItemT_co]): ...
30+
31+
@final
32+
class object_(generic): ...
33+
34+
_ScalarT = TypeVar("_ScalarT", bound=generic)
35+
_ScalarT_co = TypeVar("_ScalarT_co", bound=generic, default=Any, covariant=True)
36+
37+
@final
38+
class dtype(Generic[_ScalarT_co]): ...
39+
40+
_DTypeT_co = TypeVar("_DTypeT_co", bound=dtype, default=dtype, covariant=True)
41+
42+
@runtime_checkable
43+
class _SupportsDType(Protocol[_DTypeT_co]):
44+
@property
45+
def dtype(self) -> _DTypeT_co: ...
46+
47+
# TODO: no errors here
48+
# error: [invalid-type-arguments] "Type `typing.TypeVar` is not assignable to upper bound `generic[Any]` of type variable `_ScalarT_co@dtype`"
49+
# error: [invalid-type-arguments] "Type `typing.TypeVar` is not assignable to upper bound `generic[Any]` of type variable `_ScalarT_co@dtype`"
50+
_DTypeLike: TypeAlias = type[_ScalarT] | dtype[_ScalarT] | _SupportsDType[dtype[_ScalarT]]
51+
52+
DTypeLike: TypeAlias = _DTypeLike[Any] | str | None
53+
```
54+
55+
Now we can make sure that a function which accepts `DTypeLike | None` works as expected:
56+
57+
```py
58+
import mini_numpy as np
59+
60+
def accepts_dtype(dtype: np.DTypeLike | None) -> None: ...
61+
62+
accepts_dtype(dtype=np.bool)
63+
accepts_dtype(dtype=np.dtype[np.bool])
64+
accepts_dtype(dtype=object)
65+
accepts_dtype(dtype=np.object_)
66+
accepts_dtype(dtype="U")
67+
```

crates/ty_python_semantic/resources/mdtest/type_of/generics.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ def _[T: int](_: T):
163163
static_assert(is_assignable_to(type[T], Callable[..., T]))
164164
static_assert(not is_disjoint_from(type[T], Callable[..., T]))
165165

166-
static_assert(is_assignable_to(type[T], IntCallback))
166+
# TODO: No error here. See https://github.com/astral-sh/ty/issues/1666 for details
167+
static_assert(is_assignable_to(type[T], IntCallback)) # error: [static-assert-error]
167168
static_assert(not is_disjoint_from(type[T], IntCallback))
168169

169170
static_assert(is_subtype_of(type[T], type[T] | None))

crates/ty_python_semantic/src/types.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2468,6 +2468,34 @@ impl<'db> Type<'db> {
24682468
})
24692469
}
24702470

2471+
// TODO: The following two branches are a temporary workaround to prevent an important union type
2472+
// in numpy's internals from collapsing to a single type, which causes thousands of downstream
2473+
// false positive diagnostics across the ecosytem. See https://github.com/astral-sh/ty/issues/1666
2474+
// for details.
2475+
(Type::SubclassOf(self_subclass_ty), Type::ProtocolInstance(_))
2476+
if self_subclass_ty.is_type_var() =>
2477+
{
2478+
ConstraintSet::from(false)
2479+
}
2480+
(Type::SubclassOf(self_subclass_ty), Type::ProtocolInstance(_))
2481+
if self_subclass_ty.is_dynamic() =>
2482+
{
2483+
match relation {
2484+
TypeRelation::Assignability => {
2485+
// type[Dynamic] has arbitrary attributes, so it satisfies all protocols.
2486+
ConstraintSet::from(true)
2487+
}
2488+
_ => KnownClass::Type.to_instance(db).has_relation_to_impl(
2489+
db,
2490+
target,
2491+
inferable,
2492+
relation,
2493+
relation_visitor,
2494+
disjointness_visitor,
2495+
),
2496+
}
2497+
}
2498+
24712499
(_, Type::ProtocolInstance(protocol)) => {
24722500
relation_visitor.visit((self, target, relation), || {
24732501
self.satisfies_protocol(

0 commit comments

Comments
 (0)