Skip to content

Commit bedb8b0

Browse files
madsbkjakirkham
andauthored
Support of alternative array classes: ndarray-like (#305)
* Implement NDArrayLike * small test * removing memtype * ensure_ndarray and ensure_contiguous_ndarray to force numpy arrays * Adding typing-extensions>=3.7.4 to dependencies * Added release note Co-authored-by: jakirkham <[email protected]>
1 parent f4196a4 commit bedb8b0

8 files changed

+183
-57
lines changed

docs/release.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ Unreleased
1010
By :user:`Haiying Xu <halehawk>`, `John Kirkham <jakirkham>`, `Ryan Abernathey <rabernat>` :
1111
issue:`303`.
1212

13-
.. _release_0.9.1:
13+
* Add support of alternative array classes (other than NumPy arrays)
14+
By :user:`Mads R. B. Kristensen <madsbk>`, :issue:`305`.
1415

1516
* Add ability to find codecs via entrypoints
1617
By :user:`Martin Durant <martindurant>`, :issue:`290`.
1718

19+
.. _release_0.9.1:
20+
1821
0.9.1
1922
-----
2023

numcodecs/compat.py

Lines changed: 104 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,120 @@
11
# flake8: noqa
2+
import functools
23
import sys
34
import codecs
45
import array
56
from functools import reduce
67

78
import numpy as np
89

10+
from .ndarray_like import NDArrayLike, is_ndarray_like
911

10-
def ensure_ndarray(buf):
11-
"""Convenience function to coerce `buf` to a numpy array, if it is not already a
12-
numpy array.
12+
13+
def ensure_ndarray_like(buf) -> NDArrayLike:
14+
"""Convenience function to coerce `buf` to ndarray-like array.
1315
1416
Parameters
1517
----------
16-
buf : array-like or bytes-like
17-
A numpy array or any object exporting a buffer interface.
18+
buf : ndarray-like, array-like, or bytes-like
19+
A numpy array like object such as numpy.ndarray, cupy.ndarray, or
20+
any object exporting a buffer interface.
1821
1922
Returns
2023
-------
21-
arr : ndarray
22-
A numpy array, sharing memory with `buf`.
24+
arr : NDArrayLike
25+
A ndarray-like, sharing memory with `buf`.
2326
2427
Notes
2528
-----
2629
This function will not create a copy under any circumstances, it is guaranteed to
2730
return a view on memory exported by `buf`.
28-
2931
"""
3032

31-
if isinstance(buf, np.ndarray):
32-
# already a numpy array
33-
arr = buf
33+
if not is_ndarray_like(buf):
34+
if isinstance(buf, array.array) and buf.typecode in "cu":
35+
# Guard condition, do not support array.array with unicode type, this is
36+
# problematic because numpy does not support it on all platforms. Also do not
37+
# support char as it was removed in Python 3.
38+
raise TypeError("array.array with char or unicode type is not supported")
39+
else:
40+
# N.B., first take a memoryview to make sure that we subsequently create a
41+
# numpy array from a memory buffer with no copy
42+
mem = memoryview(buf)
43+
# instantiate array from memoryview, ensures no copy
44+
buf = np.array(mem, copy=False)
45+
return buf
3446

35-
elif isinstance(buf, array.array) and buf.typecode in 'cu':
36-
# Guard condition, do not support array.array with unicode type, this is
37-
# problematic because numpy does not support it on all platforms. Also do not
38-
# support char as it was removed in Python 3.
39-
raise TypeError('array.array with char or unicode type is not supported')
4047

41-
else:
48+
def ensure_ndarray(buf) -> np.ndarray:
49+
"""Convenience function to coerce `buf` to a numpy array, if it is not already a
50+
numpy array.
4251
43-
# N.B., first take a memoryview to make sure that we subsequently create a
44-
# numpy array from a memory buffer with no copy
45-
mem = memoryview(buf)
52+
Parameters
53+
----------
54+
buf : array-like or bytes-like
55+
A numpy array or any object exporting a buffer interface.
4656
47-
# instantiate array from memoryview, ensures no copy
48-
arr = np.array(mem, copy=False)
57+
Returns
58+
-------
59+
arr : ndarray
60+
A numpy array, sharing memory with `buf`.
4961
50-
return arr
62+
Notes
63+
-----
64+
This function will not create a copy under any circumstances, it is guaranteed to
65+
return a view on memory exported by `buf`.
66+
"""
67+
return np.array(ensure_ndarray_like(buf), copy=False)
5168

5269

53-
def ensure_contiguous_ndarray(buf, max_buffer_size=None, flatten=True):
54-
"""Convenience function to coerce `buf` to a numpy array, if it is not already a
55-
numpy array. Also ensures that the returned value exports fully contiguous memory,
70+
def ensure_contiguous_ndarray_like(
71+
buf, max_buffer_size=None, flatten=True
72+
) -> NDArrayLike:
73+
"""Convenience function to coerce `buf` to ndarray-like array.
74+
Also ensures that the returned value exports fully contiguous memory,
5675
and supports the new-style buffer interface. If the optional max_buffer_size is
5776
provided, raise a ValueError if the number of bytes consumed by the returned
5877
array exceeds this value.
5978
6079
Parameters
6180
----------
62-
buf : array-like or bytes-like
63-
A numpy array or any object exporting a buffer interface.
81+
buf : ndarray-like, array-like, or bytes-like
82+
A numpy array like object such as numpy.ndarray, cupy.ndarray, or
83+
any object exporting a buffer interface.
6484
max_buffer_size : int
6585
If specified, the largest allowable value of arr.nbytes, where arr
6686
is the returned array.
87+
flatten : bool
88+
If True, the array are flatten.
6789
6890
Returns
6991
-------
70-
arr : ndarray
71-
A numpy array, sharing memory with `buf`.
92+
arr : NDArrayLike
93+
A ndarray-like, sharing memory with `buf`.
7294
7395
Notes
7496
-----
7597
This function will not create a copy under any circumstances, it is guaranteed to
7698
return a view on memory exported by `buf`.
77-
7899
"""
79-
80-
# ensure input is a numpy array
81-
arr = ensure_ndarray(buf)
100+
arr = ensure_ndarray_like(buf)
82101

83102
# check for object arrays, these are just memory pointers, actual memory holding
84103
# item data is scattered elsewhere
85104
if arr.dtype == object:
86-
raise TypeError('object arrays are not supported')
105+
raise TypeError("object arrays are not supported")
87106

88107
# check for datetime or timedelta ndarray, the buffer interface doesn't support those
89-
if arr.dtype.kind in 'Mm':
108+
if arr.dtype.kind in "Mm":
90109
arr = arr.view(np.int64)
91110

92111
# check memory is contiguous, if so flatten
93112
if arr.flags.c_contiguous or arr.flags.f_contiguous:
94-
# check if flatten flag is on or not
95113
if flatten:
96114
# can flatten without copy
97-
arr = arr.reshape(-1, order='A')
98-
115+
arr = arr.reshape(-1, order="A")
99116
else:
100-
raise ValueError('an array with contiguous memory is required')
117+
raise ValueError("an array with contiguous memory is required")
101118

102119
if max_buffer_size is not None and arr.nbytes > max_buffer_size:
103120
msg = "Codec does not support buffers of > {} bytes".format(max_buffer_size)
@@ -106,45 +123,78 @@ def ensure_contiguous_ndarray(buf, max_buffer_size=None, flatten=True):
106123
return arr
107124

108125

109-
def ensure_bytes(buf):
126+
def ensure_contiguous_ndarray(buf, max_buffer_size=None, flatten=True) -> np.array:
127+
"""Convenience function to coerce `buf` to a numpy array, if it is not already a
128+
numpy array. Also ensures that the returned value exports fully contiguous memory,
129+
and supports the new-style buffer interface. If the optional max_buffer_size is
130+
provided, raise a ValueError if the number of bytes consumed by the returned
131+
array exceeds this value.
132+
133+
Parameters
134+
----------
135+
buf : array-like or bytes-like
136+
A numpy array or any object exporting a buffer interface.
137+
max_buffer_size : int
138+
If specified, the largest allowable value of arr.nbytes, where arr
139+
is the returned array.
140+
flatten : bool
141+
If True, the array are flatten.
142+
143+
Returns
144+
-------
145+
arr : ndarray
146+
A numpy array, sharing memory with `buf`.
147+
148+
Notes
149+
-----
150+
This function will not create a copy under any circumstances, it is guaranteed to
151+
return a view on memory exported by `buf`.
152+
"""
153+
154+
return ensure_ndarray(
155+
ensure_contiguous_ndarray_like(
156+
buf, max_buffer_size=max_buffer_size, flatten=flatten
157+
)
158+
)
159+
160+
161+
def ensure_bytes(buf) -> bytes:
110162
"""Obtain a bytes object from memory exposed by `buf`."""
111163

112164
if not isinstance(buf, bytes):
113-
114-
# go via numpy, for convenience
115-
arr = ensure_ndarray(buf)
165+
arr = ensure_ndarray_like(buf)
116166

117167
# check for object arrays, these are just memory pointers,
118168
# actual memory holding item data is scattered elsewhere
119169
if arr.dtype == object:
120-
raise TypeError('object arrays are not supported')
170+
raise TypeError("object arrays are not supported")
121171

122172
# create bytes
123-
buf = arr.tobytes(order='A')
173+
buf = arr.tobytes(order="A")
124174

125175
return buf
126176

127177

128-
def ensure_text(s, encoding='utf-8'):
178+
def ensure_text(s, encoding="utf-8"):
129179
if not isinstance(s, str):
130180
s = ensure_contiguous_ndarray(s)
131181
s = codecs.decode(s, encoding)
132182
return s
133183

134184

135-
def ndarray_copy(src, dst):
185+
def ndarray_copy(src, dst) -> NDArrayLike:
136186
"""Copy the contents of the array from `src` to `dst`."""
137187

138188
if dst is None:
139189
# no-op
140190
return src
141191

142-
# ensure ndarrays
143-
src = ensure_ndarray(src)
144-
dst = ensure_ndarray(dst)
192+
# ensure ndarray like
193+
src = ensure_ndarray_like(src)
194+
dst = ensure_ndarray_like(dst)
145195

146196
# flatten source array
147-
src = src.reshape(-1, order='A')
197+
src = src.reshape(-1, order="A")
148198

149199
# ensure same data type
150200
if dst.dtype != object:
@@ -153,9 +203,9 @@ def ndarray_copy(src, dst):
153203
# reshape source to match destination
154204
if src.shape != dst.shape:
155205
if dst.flags.f_contiguous:
156-
order = 'F'
206+
order = "F"
157207
else:
158-
order = 'C'
208+
order = "C"
159209
src = src.reshape(dst.shape, order=order)
160210

161211
# copy via numpy

numcodecs/ndarray_like.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import sys
2+
from typing import Any, Optional, Tuple
3+
4+
if sys.version_info >= (3, 8):
5+
from typing import Protocol, runtime_checkable
6+
else:
7+
from typing_extensions import Protocol, runtime_checkable
8+
9+
10+
@runtime_checkable
11+
class DType(Protocol):
12+
itemsize: int
13+
name: str
14+
kind: str
15+
16+
17+
@runtime_checkable
18+
class FlagsObj(Protocol):
19+
c_contiguous: bool
20+
f_contiguous: bool
21+
owndata: bool
22+
23+
24+
@runtime_checkable
25+
class NDArrayLike(Protocol):
26+
dtype: DType
27+
shape: Tuple[int, ...]
28+
strides: Tuple[int, ...]
29+
ndim: int
30+
size: int
31+
itemsize: int
32+
nbytes: int
33+
flags: FlagsObj
34+
35+
def __len__(self) -> int:
36+
...
37+
38+
def __getitem__(self, key) -> Any:
39+
...
40+
41+
def __setitem__(self, key, value):
42+
...
43+
44+
def tobytes(self, order: Optional[str] = ...) -> bytes:
45+
...
46+
47+
def reshape(self, *shape: int, order: str = ...) -> "NDArrayLike":
48+
...
49+
50+
def view(self, dtype: DType = ...) -> "NDArrayLike":
51+
...
52+
53+
54+
def is_ndarray_like(obj: object) -> bool:
55+
"""Return True when `obj` is ndarray-like"""
56+
return isinstance(obj, NDArrayLike)

numcodecs/tests/test_ndarray_like.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import pytest
2+
3+
from numcodecs.ndarray_like import NDArrayLike
4+
5+
6+
@pytest.mark.parametrize("module", ["numpy", "cupy"])
7+
def test_is_ndarray_like(module):
8+
m = pytest.importorskip(module)
9+
a = m.arange(10)
10+
assert isinstance(a, NDArrayLike)
11+
12+
13+
def test_is_not_ndarray_like():
14+
assert not isinstance([1, 2, 3], NDArrayLike)
15+
assert not isinstance(b"1,2,3", NDArrayLike)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ numpy
33
msgpack
44
pytest
55
zfpy
6+
typing-extensions

requirements_dev.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@ Cython==0.29.21
22
msgpack==1.0.2
33
numpy==1.21.0
44
zfpy==0.5.5
5-
6-
5+
typing-extensions>=3.7.4

requirements_rtfd.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ mock
77
numpy
88
cython
99
zfpy==0.5.5
10+
typing-extensions

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ def run_setup(with_extensions):
338338
],
339339
install_requires=[
340340
'numpy>=1.7',
341+
'typing-extensions>=3.7.4',
341342
],
342343
extras_require={
343344
'msgpack': ["msgpack"],

0 commit comments

Comments
 (0)