Skip to content

Commit 9f03471

Browse files
committed
TYP: Correct PEP 688 semantics in numpy.dtype.__new__
1 parent 1f8c545 commit 9f03471

File tree

3 files changed

+114
-37
lines changed

3 files changed

+114
-37
lines changed

numpy/__init__.pyi

Lines changed: 110 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -783,9 +783,8 @@ _GenericCType: TypeAlias = _NumberCType | type[ct.c_bool | ct.c_char | ct.py_obj
783783
# `dtype[object_]`, when their *type* is passed to the `dtype` constructor
784784
# NOTE: `builtins.object` should not be included here
785785
_BuiltinObjectLike: TypeAlias = (
786-
type | bytearray | slice | BaseException
786+
slice | Decimal | Fraction | UUID
787787
| dt.date | dt.time | dt.timedelta | dt.tzinfo
788-
| Decimal | Fraction | UUID
789788
| tuple[Any, ...] | list[Any] | set[Any] | frozenset[Any] | dict[Any, Any]
790789
) # fmt: skip
791790

@@ -794,64 +793,140 @@ class dtype(Generic[_DTypeScalar_co]):
794793
names: None | tuple[builtins.str, ...]
795794
def __hash__(self) -> int: ...
796795

797-
# Overload for `dtype` instances, scalar types, and instances that have
798-
# with a `dtype: dtype[_SCT]` attribute
796+
# `None` results in the default dtype
799797
@overload
800798
def __new__(
801799
cls,
802-
dtype: _DTypeLike[_SCT],
800+
dtype: None | type[float64],
803801
align: builtins.bool = ...,
804802
copy: builtins.bool = ...,
805-
metadata: dict[builtins.str, Any] = ...,
806-
) -> dtype[_SCT]: ...
803+
metadata: dict[builtins.str, Any] = ...
804+
) -> dtype[float64]: ...
807805

808-
# `None` results in the default dtype
806+
# Overload for `dtype` instances, scalar types, and instances that have a
807+
# `dtype: dtype[_SCT]` attribute
809808
@overload
810809
def __new__(
811810
cls,
812-
dtype: None,
811+
dtype: _DTypeLike[_SCT],
813812
align: builtins.bool = ...,
814813
copy: builtins.bool = ...,
815-
metadata: dict[builtins.str, Any] = ...
816-
) -> dtype[float64]: ...
814+
metadata: dict[builtins.str, Any] = ...,
815+
) -> dtype[_SCT]: ...
817816

818817
# Builtin types
819-
# NOTE: Type-checkers act as if `bool <: int <: float <: complex <: object`,
820-
# even though at runtime, `int`, `float` and `complex` are not subtypes of
821-
# each other.
818+
#
819+
# NOTE: Typecheckers act as if `bool <: int <: float <: complex <: object`,
820+
# even though at runtime `int`, `float`, and `complex` aren't subtypes..
822821
# This makes it impossible to express e.g. "a float that isn't an int",
823-
# since type checkers treat `_: float` as if it's `_: float | int`.
824-
# https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex
822+
# since type checkers treat `_: float` like `_: float | int`.
823+
#
824+
# For more details, see:
825+
# - https://github.com/numpy/numpy/issues/27032#issuecomment-2278958251
826+
# - https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex
825827
@overload
826-
def __new__(cls, dtype: type[builtins.bool], align: builtins.bool = ..., copy: builtins.bool = ..., metadata: dict[builtins.str, Any] = ...) -> dtype[np.bool]: ...
827-
# NOTE: this also accepts `dtype: type[int | bool]`
828+
def __new__(
829+
cls,
830+
dtype: type[builtins.bool | np.bool],
831+
align: builtins.bool = ...,
832+
copy: builtins.bool = ...,
833+
metadata: dict[str, Any] = ...,
834+
) -> dtype[np.bool]: ...
835+
# NOTE: `_: type[int]` also accepts `type[int | bool]`
828836
@overload
829-
def __new__(cls, dtype: type[int], align: builtins.bool = ..., copy: builtins.bool = ..., metadata: dict[builtins.str, Any] = ...) -> dtype[int_ | np.bool]: ...
830-
# NOTE: This also accepts `dtype: type[float | int | bool]`
837+
def __new__(
838+
cls,
839+
dtype: type[int | int_ | np.bool],
840+
align: builtins.bool = ...,
841+
copy: builtins.bool = ...,
842+
metadata: dict[str, Any] = ...,
843+
) -> dtype[int_ | np.bool]: ...
844+
# NOTE: `_: type[float]` also accepts `type[float | int | bool]`
845+
# NOTE: `float64` inheritcs from `float` at runtime; but this isn't
846+
# reflected in these stubs. So an explicit `float64` is required here.
831847
@overload
832-
def __new__(cls, dtype: type[float], align: builtins.bool = ..., copy: builtins.bool = ..., metadata: dict[builtins.str, Any] = ...) -> dtype[float64 | int_ | np.bool]: ...
833-
# NOTE: This also accepts `dtype: type[complex | float | int | bool]`
848+
def __new__(
849+
cls,
850+
dtype: None | type[float | float64 | int_ | np.bool],
851+
align: builtins.bool = ...,
852+
copy: builtins.bool = ...,
853+
metadata: dict[str, Any] = ...,
854+
) -> dtype[float64 | int_ | np.bool]: ...
855+
# NOTE: `_: type[complex]` also accepts `type[complex | float | int | bool]`
834856
@overload
835-
def __new__(cls, dtype: type[complex], align: builtins.bool = ..., copy: builtins.bool = ..., metadata: dict[builtins.str, Any] = ...) -> dtype[complex128 | float64 | int_ | np.bool]: ...
836-
# TODO: This weird `memoryview` order is needed to work around a bug in
837-
# typeshed, which causes typecheckers to treat `memoryview` as a subtype
838-
# of `bytes`, even though there's no mention of that in the typing docs.
857+
def __new__(
858+
cls,
859+
dtype: type[complex | complex128 | float64 | int_ | np.bool],
860+
align: builtins.bool = ...,
861+
copy: builtins.bool = ...,
862+
metadata: dict[str, Any] = ...,
863+
) -> dtype[complex128 | float64 | int_ | np.bool]: ...
839864
@overload
840-
def __new__(cls, dtype: type[memoryview], align: builtins.bool = ..., copy: builtins.bool = ..., metadata: dict[builtins.str, Any] = ...) -> dtype[void]: ...
865+
def __new__(
866+
cls,
867+
dtype: type[bytes], # also includes `type[bytes_]`
868+
align: builtins.bool = ...,
869+
copy: builtins.bool = ...,
870+
metadata: dict[str, Any] = ...,
871+
) -> dtype[bytes_]: ...
841872
@overload
842-
def __new__(cls, dtype: type[builtins.str], align: builtins.bool = ..., copy: builtins.bool = ..., metadata: dict[builtins.str, Any] = ...) -> dtype[str_]: ...
843-
# TODO: remove this overload once the typeshed bug is fixed
873+
def __new__(
874+
cls,
875+
dtype: type[str], # also includes `type[str_]`
876+
align: builtins.bool = ...,
877+
copy: builtins.bool = ...,
878+
metadata: dict[str, Any] = ...,
879+
) -> dtype[str_]: ...
880+
# NOTE: These `memoryview` overloads assume PEP 688, which requires mypy to
881+
# be run with the (undocumented) `--disable-memoryview-promotion` flag,
882+
# This will be the default in a future mypy release, see:
883+
# https://github.com/python/mypy/issues/15313
884+
# Pyright / Pylance requires setting `disableBytesTypePromotions=true`,
885+
# which is the default in strict mode
844886
@overload
845-
def __new__(cls, dtype: type[memoryview | builtins.str], align: builtins.bool = ..., copy: builtins.bool = ..., metadata: dict[builtins.str, Any] = ...) -> dtype[flexible]: ...
887+
def __new__(
888+
cls,
889+
dtype: type[memoryview | void],
890+
align: builtins.bool = ...,
891+
copy: builtins.bool = ...,
892+
metadata: dict[str, Any] = ...,
893+
) -> dtype[void]: ...
894+
# NOTE: `_: type[object]` would also accept e.g. `type[object | complex]`,
895+
# and is therefore not included here
846896
@overload
847-
def __new__(cls, dtype: type[bytes], align: builtins.bool = ..., copy: builtins.bool = ..., metadata: dict[builtins.str, Any] = ...) -> dtype[bytes_]: ...
897+
def __new__(
898+
cls,
899+
dtype: type[_BuiltinObjectLike | object_],
900+
align: builtins.bool = ...,
901+
copy: builtins.bool = ...,
902+
metadata: dict[str, Any] = ...,
903+
) -> dtype[object_]: ...
904+
905+
# Unions of builtins.
848906
@overload
849-
def __new__(cls, dtype: type[builtins.str | bytes], align: builtins.bool = ..., copy: builtins.bool = ..., metadata: dict[builtins.str, Any] = ...) -> dtype[character]: ...
907+
def __new__(
908+
cls,
909+
dtype: type[bytes | str],
910+
align: builtins.bool = ...,
911+
copy: builtins.bool = ...,
912+
metadata: dict[str, Any] = ...,
913+
) -> dtype[character]: ...
850914
@overload
851-
def __new__(cls, dtype: type[memoryview | builtins.str | bytes], align: builtins.bool = ..., copy: builtins.bool = ..., metadata: dict[builtins.str, Any] = ...) -> dtype[flexible]: ...
852-
# NOTE: `dtype: type[object]` also accepts e.g. `type[object | complex | ...]`
915+
def __new__(
916+
cls,
917+
dtype: type[bytes | str | memoryview],
918+
align: builtins.bool = ...,
919+
copy: builtins.bool = ...,
920+
metadata: dict[str, Any] = ...,
921+
) -> dtype[flexible]: ...
853922
@overload
854-
def __new__(cls, dtype: type[_BuiltinObjectLike], align: builtins.bool = ..., copy: builtins.bool = ..., metadata: dict[builtins.str, Any] = ...) -> dtype[object_]: ...
923+
def __new__(
924+
cls,
925+
dtype: type[complex | bytes | str | memoryview | _BuiltinObjectLike],
926+
align: builtins.bool = ...,
927+
copy: builtins.bool = ...,
928+
metadata: dict[str, Any] = ...,
929+
) -> dtype[np.bool | int_ | float64 | complex128 | flexible | object_]: ...
855930

856931
# `unsignedinteger` string-based representations and ctypes
857932
@overload

numpy/typing/tests/data/mypy.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ implicit_reexport = False
55
pretty = True
66
disallow_any_unimported = True
77
disallow_any_generics = True
8+
; https://github.com/python/mypy/issues/15313
9+
disable_bytearray_promotion = true
10+
disable_memoryview_promotion = true

numpy/typing/tests/data/reveal/dtype.pyi

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ py_float_co: type[float | int | bool]
2525
py_complex_co: type[complex | float | int | bool]
2626
py_object: type[_PyObjectLike]
2727
py_character: type[str | bytes]
28-
# TODO: also include `bytes` here once mypy has been upgraded to >=1.11
29-
py_flexible: type[memoryview] | type[str] # | type[bytes]
28+
py_flexible: type[str | bytes | memoryview]
3029

3130
ct_floating: type[ct.c_float | ct.c_double | ct.c_longdouble]
3231
ct_number: type[ct.c_uint8 | ct.c_float]

0 commit comments

Comments
 (0)