diff --git a/src/zarr/core/dtype/wrapper.py b/src/zarr/core/dtype/wrapper.py index 8068045ebb..deee8a8e70 100644 --- a/src/zarr/core/dtype/wrapper.py +++ b/src/zarr/core/dtype/wrapper.py @@ -84,7 +84,27 @@ def _check_native_dtype(cls: type[Self], dtype: TBaseDType) -> TypeGuard[DType]: Bool True if the dtype matches, False otherwise. """ - return type(dtype) is cls.dtype_cls + # Keep the exact class check in a variable: narrowing on ``type(dtype) is ...`` + # makes mypy treat the layout fallback below as unreachable. + exact_match = type(dtype) is cls.dtype_cls + if exact_match: + return True + # NumPy's C type spellings do not always map to the canonical dtype class: + # ``np.dtype("q")`` is a ``LongLongDType`` instance rather than an + # ``Int64DType`` instance, even though the two describe the same memory + # layout. The width of ``long long`` depends on the platform NumPy was + # built for, so the dtype class alone is not a reliable test. Fall back to + # comparing the layout (kind and item size), which is what a Zarr data type + # actually encodes. + try: + # numpy's stubs only type ``dtype(...)`` with an argument; the concrete + # dtype classes (``np.dtypes.Int64DType`` etc.) construct without one. + expected: TBaseDType = cls.dtype_cls() # type: ignore[call-overload] + except TypeError: + # Flexible/parametric dtypes (e.g. ``VoidDType``) have no fixed layout + # to compare against, so the exact dtype class check above stands. + return False + return bool(dtype.kind == expected.kind and dtype.itemsize == expected.itemsize) @classmethod @abstractmethod diff --git a/tests/test_dtype/test_npy/test_int.py b/tests/test_dtype/test_npy/test_int.py index f25fa1a564..13f1f7c024 100644 --- a/tests/test_dtype/test_npy/test_int.py +++ b/tests/test_dtype/test_npy/test_int.py @@ -117,7 +117,10 @@ class TestInt32(BaseTestZDType): class TestInt64(BaseTestZDType): test_cls = Int64 scalar_type = np.int64 - valid_dtype = (np.dtype(">i8"), np.dtype("i8"), np.dtype("u8"), np.dtype("u8"), np.dtype(" None: + """ + Test that the C type-name spellings resolve through the registry. + + ``np.dtype("q")`` and ``np.dtype("Q")`` are ``LongLongDType`` and + ``ULongLongDType`` instances rather than ``Int64DType`` and ``UInt64DType`` + instances, even though on this platform they describe the same layout. They + must still resolve to exactly one data type. See issue #3282. + """ + dtype = np.dtype(dtype_str) + assert isinstance(data_type_registry.match_dtype(dtype), expected_cls) + for zarr_format in (2, 3): + assert isinstance(parse_dtype(dtype, zarr_format=zarr_format), expected_cls) + @staticmethod def test_unregistered_dtype(data_type_registry_fixture: DataTypeRegistry) -> None: """