-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathdtypes.py
More file actions
667 lines (600 loc) · 22.5 KB
/
Copy pathdtypes.py
File metadata and controls
667 lines (600 loc) · 22.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
import dataclasses
import warnings
from ast import literal_eval
import numpy as np
from numpy import promote_types, result_type
from .. import backend, dtypes
from ..core import NULL, _has_numba, ffi, lib
if _has_numba:
import numba
# Default assumption unless FC32/FC64 are found in lib
_supports_complex = hasattr(lib, "GrB_FC64") or hasattr(lib, "GxB_FC64")
class DataType:
__slots__ = (
"name",
"gb_obj",
"gb_name",
"c_type",
"numba_type",
"np_type",
# ``(c_name, c_definition)`` actually registered with SuiteSparse,
# or ``None``. Captured at first registration. ``GxB_JIT_C_DEFINITION``
# is one-shot in SS, so this is the authoritative record of what SS
# will use for JIT, regardless of any later Python-side rename.
"_jit_c_info",
"__weakref__",
)
def __init__(self, name, gb_obj, gb_name, c_type, numba_type, np_type):
self.name = name
self.gb_obj = gb_obj
self.gb_name = gb_name
self.c_type = c_type
self.numba_type = numba_type
self.np_type = np.dtype(np_type) if np_type is not None else None
self._jit_c_info = None
def __repr__(self):
return self.name
def __eq__(self, other):
try:
return self is lookup_dtype(other)
except ValueError:
raise TypeError(f"Invalid or unknown datatype: {other}") from None
def __hash__(self):
return hash(self.np_type)
def __lt__(self, other):
# Let us sort for prettier error reporting
try:
t1 = self.np_type
t2 = lookup_dtype(other).np_type
except ValueError:
raise TypeError(f"Invalid or unknown datatype: {other}") from None
return (t1.kind, t1.itemsize, t1.name) < (t2.kind, t2.itemsize, t2.name)
def __reduce__(self):
if self._is_udt:
return (self._deserialize, (self.name, self.np_type, self._is_anonymous))
if self.gb_name == "GrB_Index":
return "_INDEX"
return self.name
@property
def _carg(self):
return self.gb_obj[0] if self.gb_name is None else self.gb_obj
@property
def _is_anonymous(self):
return getattr(dtypes, self.name, None) is not self
@property
def _is_udt(self):
return self.gb_name is None
@property
def jit_c_name(self):
"""The C type name SuiteSparse uses for this UDT, or ``None``.
Returns the name SuiteSparse actually registered (not the latest
Python-side ``self.name``, which can diverge after a re-register). Is
``None`` for built-in types and for UDTs not expressible in C.
"""
return self._jit_c_info[0] if self._jit_c_info is not None else None
@property
def jit_c_definition(self):
"""The C struct typedef SuiteSparse uses for this UDT, or ``None``."""
return self._jit_c_info[1] if self._jit_c_info is not None else None
@staticmethod
def _deserialize(name, dtype, is_anonymous):
if is_anonymous:
return register_anonymous(dtype, name)
if name in _registry:
return _registry[name]
return register_new(name, dtype)
def _get_udt_c_type_info(datatype):
"""Return ``(c_name, c_typedef)`` for a UDT, or ``None`` if not expressible in C.
The returned ``c_name`` may differ from ``datatype.name``: if the user
didn't supply a name, or supplied one that isn't a valid C identifier,
``_udt_c_typedef`` synthesizes a fresh ``_gbudt_NNN`` name so the UDT
can still take the JIT path. Returns ``None`` only for the cases that
remain unrepresentable in C: an unsupported field type (object,
datetime, string), or a field name that collides with a C reserved
word or isn't an identifier. Supports record UDTs (including nested
records) and array UDTs (e.g., ``FP64[3]``).
"""
from .operator.udt_utils import _udt_c_typedef
if datatype.np_type is None:
return None
return _udt_c_typedef(datatype.name, datatype.np_type)
def _set_udt_jit_c_definition(datatype):
"""Set JIT C name and typedef on a UDT so SuiteSparse can JIT-compile kernels.
Without these, SuiteSparse treats UDTs as opaque byte arrays and cannot
JIT-compile optimized kernels for operations on them. We generate a C
struct definition from the numpy dtype and set both ``GxB_JIT_C_NAME``
and ``GxB_JIT_C_DEFINITION`` on the ``GrB_Type``. Both are needed for
SuiteSparse's eWise and reduce JIT templates to expand correctly.
Setting ``GxB_JIT_C_NAME`` causes ``GxB_deserialize_type_name`` to
return this short name rather than the numpy repr stored in
``GrB_NAME``, so the deserialize path in ``Matrix.ss.deserialize`` and
``Vector.ss.deserialize`` falls back to ``GrB_NAME`` when the short
name doesn't resolve to a known dtype. This preserves round-tripping
for anonymous-by-name UDTs.
Supports both record UDTs (``{'x': float, 'y': float}``) and array UDTs
(``np.dtype((np.float64, (3,)))``).
"""
from .operator.udt_utils import _has_jit_set
if not _has_jit_set or not datatype._is_udt:
return
info = _get_udt_c_type_info(datatype)
if info is None:
return
c_name, typedef = info
lib.GrB_Type_set_String(datatype._carg, ffi.new("char[]", c_name.encode()), lib.GxB_JIT_C_NAME)
lib.GrB_Type_set_String(
datatype._carg, ffi.new("char[]", typedef.encode()), lib.GxB_JIT_C_DEFINITION
)
# Remember what SS actually has. ``GxB_JIT_C_DEFINITION`` is one-shot
# (returns ``GrB_ALREADY_SET`` on a second call), so a later rename of
# the Python DataType does not change what SS uses for JIT, and the
# introspection properties must reflect what SS has rather than the
# renamed value.
datatype._jit_c_info = info
def register_new(name, dtype=None):
"""Register a user-defined type and put it on the ``gb.dtypes`` namespace.
Symmetric with :func:`register_anonymous`. Accepts the same dtype forms
(numpy dtype, dict, string, dataclass class/instance). When ``dtype`` is a
dataclass and no explicit ``name`` is given, the dataclass class name is
used; passing ``register_new(MyDataclass)`` is shorthand for
``register_new(MyDataclass.__name__, MyDataclass)``.
"""
if dtype is None:
# Sole-argument call. Accept a dataclass (use its class name) or a
# ``DataType``; otherwise complain with a useful message.
if dataclasses.is_dataclass(name):
dtype = name
name = name.__name__ if isinstance(name, type) else type(name).__name__
else:
raise TypeError(
"register_new() requires both `name` and `dtype`, or a single "
"@dataclass class/instance whose class name becomes the dtype name"
)
if not isinstance(name, str) or not name.isidentifier():
raise ValueError(f"`name` argument must be a valid Python identifier; got: {name!r}")
if name in _registry or hasattr(dtypes, name):
raise ValueError(f"{name!r} name for dtype is unavailable")
rv = register_anonymous(dtype, name)
_registry[name] = rv
setattr(dtypes, name, rv)
return rv
def register_anonymous(dtype, name=None):
"""Register a user-defined type without adding it to the ``gb.dtypes`` namespace.
Returns the existing ``DataType`` if one is already registered for this
``np.dtype``. The new ``name`` (if given) replaces the old Python-side
name; the SuiteSparse-side C name set at first registration does not
change. See ``DataType.jit_c_name``.
Parameters
----------
dtype : np.dtype | dict | str | @dataclass
The dtype to register. Supported forms:
- A ``np.dtype`` directly (record or array, including multi-dim).
- A dict like ``{"x": int, "y": float}``: keys become field names,
and values resolve through ``lookup_dtype``.
- A string like ``"INT64[3, 4]"`` for array UDTs.
- A ``@dataclass`` class or instance: fields become record fields,
and the class name is used as the default UDT name. Field type
annotations may be either real types (``int``) or strings
(``"int"``), e.g., with ``from __future__ import annotations`` or
PEP 649. Compound annotations like ``Optional[int]`` raise from
``lookup_dtype``.
Field types must resolve through ``lookup_dtype``. Python built-ins
(``int``, ``float``, ``bool``, ``complex``), the corresponding numpy
scalar types, and graphblas dtype names are supported. Object,
string, and nested-UDT fields are not.
name : str, optional
A human-readable name for this UDT. Used in error messages and
``repr``. Must be a valid C identifier (and not a C reserved word)
to be usable on the SuiteSparse JIT path; otherwise JIT is skipped
silently and ops fall back to the Numba cfunc path.
Returns
-------
DataType
The registered UDT.
"""
# Convert a ``@dataclass`` (class or instance) directly to a numpy
# record dtype. ``lookup_dtype`` handles both type-object and
# string-annotation forms of field types.
if dataclasses.is_dataclass(dtype):
fields = dataclasses.fields(dtype)
if not fields:
raise ValueError("dataclass must have at least one field to convert to a UDT")
if name is None:
# Default the UDT name to the dataclass class name for both
# class form (``MyDC``) and instance form (``MyDC(...)``).
name = dtype.__name__ if isinstance(dtype, type) else type(dtype).__name__
dtype = np.dtype([(f.name, lookup_dtype(f.type).np_type) for f in fields], align=True)
try:
dtype = np.dtype(dtype)
except TypeError:
if isinstance(dtype, dict):
# Allow dtypes such as `{'x': int, 'y': float}` for convenience
dtype = np.dtype(
[(key, lookup_dtype(val).np_type) for key, val in dtype.items()], align=True
)
elif isinstance(dtype, str) and "[" in dtype and dtype.endswith("]"):
# Allow dtypes such as `"INT64[3, 4]"` for convenience
base_dtype, shape = dtype.split("[", 1)
base_dtype = lookup_dtype(base_dtype)
shape = literal_eval(f"[{shape}")
dtype = np.dtype((base_dtype.np_type, shape))
else:
raise
if dtype in _registry:
# Always use the same object, but use the latest name. The
# Python-side ``rv.name`` updates here, but the SuiteSparse-side
# ``GxB_JIT_C_NAME`` and ``GxB_JIT_C_DEFINITION`` are one-shot
# (return ``GrB_ALREADY_SET``) and keep the first name.
# ``rv._jit_c_info`` was pinned at first registration; that's what
# ``jit_c_name`` and ``jit_c_definition`` return, so introspection
# reflects what SS actually has regardless of any later Python-side
# rename. JIT codegen for ops on this UDT also uses the pinned name
# (see ``_make_jit_c_definition``).
rv = _registry[dtype]
if name is not None:
if rv.gb_name is not None and name != rv.gb_name:
raise ValueError("dtype must not be a builtin type")
rv.name = name
return rv
if dtype.hasobject:
raise ValueError("dtype must not allow Python objects")
from ..exceptions import check_status_carg
gb_obj = ffi.new("GrB_Type*")
if hasattr(lib, "GrB_Type_set_String"):
# We name this so that we can serialize and deserialize UDTs
# We don't yet have C definitions
np_repr = _dtype_to_string(dtype)
status = lib.GrB_Type_new(gb_obj, dtype.itemsize)
check_status_carg(status, "Type", gb_obj[0])
val_obj = ffi.new("char[]", np_repr.encode())
status = lib.GrB_Type_set_String(gb_obj[0], val_obj, lib.GrB_NAME)
elif backend == "suitesparse":
# For SuiteSparse < 9
# We name this so that we can serialize and deserialize UDTs
# We don't yet have C definitions
np_repr = _dtype_to_string(dtype).encode()
if len(np_repr) > lib.GxB_MAX_NAME_LEN:
msg = f"UDT repr is too large to serialize ({len(np_repr)} > {lib.GxB_MAX_NAME_LEN})."
if name is not None:
np_repr = name.encode()[: lib.GxB_MAX_NAME_LEN]
else:
np_repr = np_repr[: lib.GxB_MAX_NAME_LEN]
warnings.warn(
f"{msg}. It will use the following name, "
f"and the dtype may need to be specified when deserializing: {np_repr}",
stacklevel=2,
)
status = lib.GxB_Type_new(gb_obj, dtype.itemsize, np_repr, NULL)
else:
status = lib.GrB_Type_new(gb_obj, dtype.itemsize)
check_status_carg(status, "Type", gb_obj[0])
# For now, let's use "opaque" unsigned bytes for the c type.
if name is None:
name = _default_name(dtype)
numba_type = numba.typeof(dtype).dtype if _has_numba else None
rv = DataType(name, gb_obj, None, f"uint8_t[{dtype.itemsize}]", numba_type, dtype)
_registry[gb_obj] = rv
_registry[dtype] = rv
if _has_numba:
_registry[numba_type] = rv
_registry[numba_type.name] = rv
# Set JIT C type definition so SuiteSparse can JIT-compile kernels for this UDT.
_set_udt_jit_c_definition(rv)
return rv
BOOL = DataType(
"BOOL",
lib.GrB_BOOL,
"GrB_BOOL",
"_Bool",
numba.types.bool_ if _has_numba else None,
np.bool_,
)
INT8 = DataType(
"INT8", lib.GrB_INT8, "GrB_INT8", "int8_t", numba.types.int8 if _has_numba else None, np.int8
)
UINT8 = DataType(
"UINT8",
lib.GrB_UINT8,
"GrB_UINT8",
"uint8_t",
numba.types.uint8 if _has_numba else None,
np.uint8,
)
INT16 = DataType(
"INT16",
lib.GrB_INT16,
"GrB_INT16",
"int16_t",
numba.types.int16 if _has_numba else None,
np.int16,
)
UINT16 = DataType(
"UINT16",
lib.GrB_UINT16,
"GrB_UINT16",
"uint16_t",
numba.types.uint16 if _has_numba else None,
np.uint16,
)
INT32 = DataType(
"INT32",
lib.GrB_INT32,
"GrB_INT32",
"int32_t",
numba.types.int32 if _has_numba else None,
np.int32,
)
UINT32 = DataType(
"UINT32",
lib.GrB_UINT32,
"GrB_UINT32",
"uint32_t",
numba.types.uint32 if _has_numba else None,
np.uint32,
)
INT64 = DataType(
"INT64",
lib.GrB_INT64,
"GrB_INT64",
"int64_t",
numba.types.int64 if _has_numba else None,
np.int64,
)
# _Index (like UINT64) is for internal use only and shouldn't be exposed to the user
_INDEX = DataType(
"UINT64",
lib.GrB_UINT64,
"GrB_Index",
"GrB_Index",
numba.types.uint64 if _has_numba else None,
np.uint64,
)
UINT64 = DataType(
"UINT64",
lib.GrB_UINT64,
"GrB_UINT64",
"uint64_t",
numba.types.uint64 if _has_numba else None,
np.uint64,
)
FP32 = DataType(
"FP32",
lib.GrB_FP32,
"GrB_FP32",
"float",
numba.types.float32 if _has_numba else None,
np.float32,
)
FP64 = DataType(
"FP64",
lib.GrB_FP64,
"GrB_FP64",
"double",
numba.types.float64 if _has_numba else None,
np.float64,
)
if _supports_complex and hasattr(lib, "GxB_FC32"):
FC32 = DataType(
"FC32",
lib.GxB_FC32,
"GxB_FC32",
"float _Complex",
numba.types.complex64 if _has_numba else None,
np.complex64,
)
if _supports_complex and hasattr(lib, "GrB_FC32"): # pragma: no cover (unused)
FC32 = DataType(
"FC32",
lib.GrB_FC32,
"GrB_FC32",
"float _Complex",
numba.types.complex64 if _has_numba else None,
np.complex64,
)
if _supports_complex and hasattr(lib, "GxB_FC64"):
FC64 = DataType(
"FC64",
lib.GxB_FC64,
"GxB_FC64",
"double _Complex",
numba.types.complex128 if _has_numba else None,
np.complex128,
)
if _supports_complex and hasattr(lib, "GrB_FC64"): # pragma: no cover (unused)
FC64 = DataType(
"FC64",
lib.GrB_FC64,
"GrB_FC64",
"double _Complex",
numba.types.complex128 if _has_numba else None,
np.complex128,
)
# Used for testing user-defined functions
_sample_values = {
INT8: np.int8(1),
UINT8: np.uint8(1),
INT16: np.int16(1),
UINT16: np.uint16(1),
INT32: np.int32(1),
UINT32: np.uint32(1),
INT64: np.int64(1),
UINT64: np.uint64(1),
FP32: np.float32(0.5),
FP64: np.float64(0.5),
BOOL: np.bool_(True),
}
if _supports_complex:
_sample_values.update(
{
FC32: np.complex64(complex(0, 0.5)),
FC64: np.complex128(complex(0, 0.5)),
}
)
# Create register to easily lookup types by name, gb_obj, or c_type
_registry = {}
_dtypes_to_register = [
BOOL,
INT8,
UINT8,
INT16,
UINT16,
INT32,
UINT32,
INT64,
UINT64,
FP32,
FP64,
]
if _supports_complex:
_dtypes_to_register.extend([FC32, FC64])
for dtype in _dtypes_to_register:
_registry[dtype.name] = dtype
_registry[dtype.name.lower()] = dtype
_registry[dtype.gb_obj] = dtype
_registry[dtype.gb_name] = dtype
_registry[dtype.gb_name.lower()] = dtype
_registry[dtype.c_type] = dtype
_registry[dtype.c_type.upper()] = dtype
if _has_numba:
_registry[dtype.numba_type] = dtype
_registry[dtype.numba_type.name] = dtype
val = _sample_values[dtype]
_registry[val.dtype] = dtype
_registry[val.dtype.name] = dtype
del dtype, val
# Add some common Python types as lookup keys
_registry[bool] = BOOL
_registry[int] = INT64
_registry[float] = FP64
_registry["bool"] = BOOL
_registry["int"] = INT64
_registry["float"] = FP64 # Choose 'float' to match numpy/Python; c_type 'float' would be FP32
if _supports_complex:
_registry[complex] = FC64
_registry["complex"] = FC64
def lookup_dtype(key, value=None):
# Check for silly lookup where key is already a DataType
if type(key) is DataType:
return key
try:
return _registry[key]
except (KeyError, TypeError):
pass
if value is not None and hasattr(value, "dtype") and value.dtype in _registry:
return _registry[value.dtype]
# np.dtype(x) accepts some weird values; we may want to guard against some
if key is None:
raise TypeError("Bad dtype: None. A valid dtype must be provided.")
try:
# Auto-register!
return register_anonymous(key)
except Exception:
pass
try:
return lookup_dtype(key.literal_type) # For numba dtype inference
except Exception:
pass
raise ValueError(f"Unknown dtype: {key} of type {type(key)}")
def unify(type1, type2, *, is_left_scalar=False, is_right_scalar=False):
"""Returns a type that can hold both type1 and type2.
For example:
unify(INT32, INT64) -> INT64
unify(INT8, UINT16) -> INT32
unify(BOOL, UINT16) -> UINT16
unify(FP32, INT32) -> FP64
"""
if type1 is type2:
return type1
if is_left_scalar:
if not is_right_scalar:
return lookup_dtype(result_type(np.array(0, type1.np_type), type2.np_type))
elif is_right_scalar:
return lookup_dtype(result_type(type1.np_type, np.array(0, type2.np_type)))
return lookup_dtype(promote_types(type1.np_type, type2.np_type))
def _default_name(dtype):
if dtype in _registry:
dt = _registry[dtype]
if not dt._is_udt:
return dt.name
if dtype.subdtype is not None:
subdtype = _default_name(dtype.subdtype[0])
shape = ", ".join(map(str, dtype.subdtype[1]))
return f"{subdtype}[{shape}]"
if dtype.names:
args = ", ".join(
f"{name!r}: {_default_name(dtype.fields[name][0])}" for name in dtype.names
)
return f"{{{args}}}"
return repr(dtype)
def _dtype_to_string(dtype):
"""Convert a dtype to a string that can be safely evaluated to recreate the dtype.
This is useful when serializing UDT. To recreate the dtype, do:
>>> s = _dtype_to_string(dtype)
>>> new_dtype = _string_to_dtype(s)
>>> dtype == new_dtype
True
"""
if isinstance(dtype, np.dtype) and dtype not in _registry:
np_type = dtype
else:
dtype = lookup_dtype(dtype)
if not dtype._is_udt:
return dtype.name
np_type = dtype.np_type
s = str(np_type)
try:
if np.dtype(literal_eval(s)) == np_type:
return s
except Exception:
pass
if np.dtype(np_type.str) == np_type:
return repr(np_type.str)
# Some nested dtypes (e.g. ``align=True`` outer + ``packed`` inner) don't
# round-trip via ``str(np_type)`` because numpy's reconstruction enforces
# the outer's alignment on the inner. Fall back to an explicit per-field
# dict using literal offsets/itemsize, which encodes the exact layout
# without invoking ``align``. ``literal_eval`` accepts the result, so
# ``_string_to_dtype`` doesn't need a special case for it.
return repr(_dtype_to_explicit_dict(np_type))
def _dtype_to_explicit_dict(np_type):
"""Encode ``np_type`` as a plain dict/tuple/str tree of literals.
Layout-preserving fallback for :func:`_dtype_to_string`. Records become a
``{names, formats, offsets, itemsize}`` dict whose nested ``formats`` may
be further nested dicts; array dtypes become a ``(base_str, shape)``
tuple. The result round-trips through ``np.dtype(literal_eval(s))``
without ever using ``aligned=True``, so the original byte layout is
preserved verbatim even for the aligned-outer / packed-inner cases.
"""
if np_type.names is not None:
formats = []
for name in np_type.names:
sub = np_type.fields[name][0]
if sub.names is not None or sub.subdtype is not None:
formats.append(_dtype_to_explicit_dict(sub))
else:
formats.append(sub.str)
return {
"names": list(np_type.names),
"formats": formats,
"offsets": [np_type.fields[name][1] for name in np_type.names],
"itemsize": np_type.itemsize,
}
if np_type.subdtype is not None:
base, shape = np_type.subdtype
if base.names is not None:
base_repr = _dtype_to_explicit_dict(base)
else:
base_repr = base.str
return (base_repr, shape)
return np_type.str
def _string_to_dtype(s):
"""Convert a string back to a dtype.
>>> _string_to_dtype(_dtype_to_string(dtype)) == dtype
True
"""
try:
return lookup_dtype(s)
except Exception:
pass
np_type = np.dtype(literal_eval(s))
return lookup_dtype(np_type)