python-graphblas 2023.3.0__py3-none-any.whl → 2023.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. graphblas/__init__.py +15 -1
  2. graphblas/agg/__init__.py +4 -4
  3. graphblas/binary/__init__.py +16 -0
  4. graphblas/binary/numpy.py +14 -2
  5. graphblas/binary/ss.py +3 -0
  6. graphblas/core/__init__.py +9 -0
  7. graphblas/core/agg.py +3 -3
  8. graphblas/core/automethods.py +1 -1
  9. graphblas/core/base.py +3 -3
  10. graphblas/core/descriptor.py +1 -0
  11. graphblas/{dtypes.py → core/dtypes.py} +162 -86
  12. graphblas/core/expr.py +1 -1
  13. graphblas/core/formatting.py +1 -0
  14. graphblas/core/mask.py +5 -5
  15. graphblas/core/matrix.py +32 -28
  16. graphblas/core/operator/agg.py +24 -3
  17. graphblas/core/operator/base.py +78 -93
  18. graphblas/core/operator/binary.py +150 -36
  19. graphblas/core/operator/indexunary.py +101 -25
  20. graphblas/core/operator/monoid.py +38 -11
  21. graphblas/core/operator/select.py +109 -7
  22. graphblas/core/operator/semiring.py +41 -21
  23. graphblas/core/operator/unary.py +77 -14
  24. graphblas/core/recorder.py +3 -3
  25. graphblas/core/scalar.py +18 -3
  26. graphblas/core/ss/__init__.py +3 -0
  27. graphblas/core/ss/binary.py +72 -0
  28. graphblas/core/ss/config.py +46 -11
  29. graphblas/core/ss/context.py +146 -0
  30. graphblas/core/ss/descriptor.py +21 -9
  31. graphblas/core/ss/dtypes.py +88 -0
  32. graphblas/core/ss/indexunary.py +77 -0
  33. graphblas/core/ss/matrix.py +116 -105
  34. graphblas/core/ss/select.py +45 -0
  35. graphblas/core/ss/unary.py +62 -0
  36. graphblas/core/ss/vector.py +40 -40
  37. graphblas/core/utils.py +15 -12
  38. graphblas/core/vector.py +69 -22
  39. graphblas/dtypes/__init__.py +43 -0
  40. graphblas/dtypes/ss.py +1 -0
  41. graphblas/indexunary/__init__.py +13 -1
  42. graphblas/indexunary/ss.py +6 -0
  43. graphblas/io/__init__.py +7 -0
  44. graphblas/io/_awkward.py +187 -0
  45. graphblas/io/_matrixmarket.py +140 -0
  46. graphblas/io/_networkx.py +59 -0
  47. graphblas/io/_numpy.py +104 -0
  48. graphblas/io/_scipy.py +118 -0
  49. graphblas/io/_sparse.py +99 -0
  50. graphblas/io/_viz.py +21 -0
  51. graphblas/monoid/__init__.py +13 -1
  52. graphblas/monoid/numpy.py +12 -3
  53. graphblas/monoid/ss.py +5 -0
  54. graphblas/op/__init__.py +9 -1
  55. graphblas/op/numpy.py +5 -1
  56. graphblas/op/ss.py +2 -0
  57. graphblas/select/__init__.py +19 -7
  58. graphblas/select/ss.py +6 -0
  59. graphblas/semiring/__init__.py +27 -0
  60. graphblas/semiring/numpy.py +12 -1
  61. graphblas/semiring/ss.py +2 -0
  62. graphblas/ss/__init__.py +5 -1
  63. graphblas/ss/_core.py +67 -13
  64. graphblas/tests/conftest.py +43 -7
  65. graphblas/tests/test_core.py +5 -2
  66. graphblas/tests/test_dtype.py +23 -9
  67. graphblas/tests/test_formatting.py +70 -3
  68. graphblas/tests/test_infix.py +7 -0
  69. graphblas/tests/test_io.py +28 -8
  70. graphblas/tests/test_matrix.py +100 -47
  71. graphblas/tests/test_numpyops.py +44 -18
  72. graphblas/tests/test_op.py +193 -82
  73. graphblas/tests/test_operator_types.py +6 -0
  74. graphblas/tests/test_pickle.py +6 -0
  75. graphblas/tests/test_scalar.py +12 -10
  76. graphblas/tests/test_ssjit.py +269 -0
  77. graphblas/tests/test_vector.py +58 -16
  78. graphblas/unary/numpy.py +12 -1
  79. graphblas/unary/ss.py +3 -0
  80. graphblas/viz.py +4 -5
  81. {python_graphblas-2023.3.0.dist-info → python_graphblas-2023.7.0.dist-info}/LICENSE +1 -1
  82. {python_graphblas-2023.3.0.dist-info → python_graphblas-2023.7.0.dist-info}/METADATA +72 -32
  83. python_graphblas-2023.7.0.dist-info/RECORD +110 -0
  84. graphblas/io.py +0 -697
  85. python_graphblas-2023.3.0.dist-info/RECORD +0 -91
  86. {python_graphblas-2023.3.0.dist-info → python_graphblas-2023.7.0.dist-info}/WHEEL +0 -0
  87. {python_graphblas-2023.3.0.dist-info → python_graphblas-2023.7.0.dist-info}/top_level.txt +0 -0
graphblas/__init__.py CHANGED
@@ -137,7 +137,21 @@ def _init(backend_arg, blocking, automatic=False):
137
137
 
138
138
  backend = backend_arg
139
139
  if backend in {"suitesparse", "suitesparse-vanilla"}:
140
- from suitesparse_graphblas import ffi, initialize, is_initialized, lib
140
+ try:
141
+ from suitesparse_graphblas import ffi, initialize, is_initialized, lib
142
+ except ImportError: # pragma: no cover (import)
143
+ raise ImportError(
144
+ f"suitesparse_graphblas is required for {backend!r} backend. "
145
+ "It may be installed with pip or conda:\n\n"
146
+ " $ pip install suitesparse-graphblas\n"
147
+ " $ conda install -c conda-forge python-suitesparse-graphblas\n\n"
148
+ "SuiteSparse:GraphBLAS is the primary C implementation and backend of "
149
+ "python-graphblas and is what we recommend to most users. If you are "
150
+ "installing python-graphblas with pip, we recommend installing with one "
151
+ "of the following to automatically include suitespare-graphblas:\n\n"
152
+ " $ pip install python-graphblas[suitesparse]\n"
153
+ " $ pip install python-graphblas[default]"
154
+ ) from None
141
155
 
142
156
  if is_initialized():
143
157
  mode = ffi.new("int32_t*")
graphblas/agg/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- """`graphblas.agg` is an experimental module for exploring Aggregators.
1
+ """``graphblas.agg`` is an experimental module for exploring Aggregators.
2
2
 
3
3
  Aggregators may be used in reduce methods:
4
4
  - Matrix.reduce_rowwise
@@ -59,9 +59,9 @@ Custom recipes (specific to SuiteSparse:GraphBLAS):
59
59
  - ss.argmax
60
60
 
61
61
  .. deprecated:: 2023.1.0
62
- Aggregators `first`, `last`, `first_index`, `last_index`, `argmin`, and `argmax` are
63
- deprecated in the `agg` namespace such as `agg.first`. Use them from `agg.ss` namespace
64
- instead such as `agg.ss.first`. Will be removed in version 2023.9.0 or later.
62
+ Aggregators ``first``, ``last``, ``first_index``, ``last_index``, ``argmin``, and ``argmax``
63
+ are deprecated in the ``agg`` namespace such as ``agg.first``. Use them from ``agg.ss``
64
+ namespace instead such as ``agg.ss.first``. Will be removed in version 2023.9.0 or later.
65
65
 
66
66
  # Possible aggregators:
67
67
  # - absolute_deviation, sum(abs(x - mean(x))), sum_absminus(x, mean(x))
@@ -1,5 +1,7 @@
1
1
  # All items are dynamically added by classes in operator.py
2
2
  # This module acts as a container of BinaryOp instances
3
+ from ..core import _supports_udfs
4
+
3
5
  _delayed = {}
4
6
  _delayed_commutes_to = {
5
7
  "absfirst": "abssecond",
@@ -9,6 +11,15 @@ _delayed_commutes_to = {
9
11
  "rpow": "pow",
10
12
  }
11
13
  _deprecated = {}
14
+ _udfs = {
15
+ "absfirst",
16
+ "abssecond",
17
+ "binom",
18
+ "floordiv",
19
+ "isclose",
20
+ "rfloordiv",
21
+ "rpow",
22
+ }
12
23
 
13
24
 
14
25
  def __dir__():
@@ -50,6 +61,11 @@ def __getattr__(key):
50
61
  ss = import_module(".ss", __name__)
51
62
  globals()["ss"] = ss
52
63
  return ss
64
+ if not _supports_udfs and key in _udfs:
65
+ raise AttributeError(
66
+ f"module {__name__!r} unable to compile UDF for {key!r}; "
67
+ "install numba for UDF support"
68
+ )
53
69
  raise AttributeError(f"module {__name__!r} has no attribute {key!r}")
54
70
 
55
71
 
graphblas/binary/numpy.py CHANGED
@@ -10,6 +10,7 @@ import numpy as _np
10
10
  from .. import _STANDARD_OPERATOR_NAMES
11
11
  from .. import binary as _binary
12
12
  from .. import config as _config
13
+ from ..core import _supports_udfs
13
14
 
14
15
  _delayed = {}
15
16
  _binary_names = {
@@ -130,7 +131,13 @@ _commutes_to = {
130
131
 
131
132
 
132
133
  def __dir__():
133
- return globals().keys() | _delayed.keys() | _binary_names
134
+ if not _supports_udfs and not _config["mapnumpy"]:
135
+ # float_power is special: it's constructed from builtin operators
136
+ return globals().keys() | {"float_power"} # FLAKY COVERAGE
137
+ attrs = _delayed.keys() | _binary_names
138
+ if not _supports_udfs:
139
+ attrs &= _numpy_to_graphblas.keys()
140
+ return attrs | globals().keys()
134
141
 
135
142
 
136
143
  def __getattr__(name):
@@ -141,7 +148,7 @@ def __getattr__(name):
141
148
  return rv
142
149
  if name not in _binary_names:
143
150
  raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
144
- if _config.get("mapnumpy") and name in _numpy_to_graphblas:
151
+ if _config.get("mapnumpy") and name in _numpy_to_graphblas or name == "float_power":
145
152
  if name == "float_power":
146
153
  from ..core.operator import binary
147
154
  from ..dtypes import FP64
@@ -166,6 +173,11 @@ def __getattr__(name):
166
173
  globals()[name] = new_op
167
174
  else:
168
175
  globals()[name] = getattr(_binary, _numpy_to_graphblas[name])
176
+ elif not _supports_udfs:
177
+ raise AttributeError(
178
+ f"module {__name__!r} unable to compile UDF for {name!r}; "
179
+ "install numba for UDF support"
180
+ )
169
181
  else:
170
182
  numpy_func = getattr(_np, name)
171
183
 
graphblas/binary/ss.py CHANGED
@@ -1,3 +1,6 @@
1
1
  from ..core import operator
2
+ from ..core.ss.binary import register_new # noqa: F401
3
+
4
+ _delayed = {}
2
5
 
3
6
  del operator
@@ -1,3 +1,12 @@
1
+ try:
2
+ import numba
3
+ except ImportError:
4
+ _has_numba = _supports_udfs = False
5
+ else:
6
+ _has_numba = _supports_udfs = True
7
+ del numba
8
+
9
+
1
10
  def __getattr__(name):
2
11
  if name in {"ffi", "lib", "NULL"}:
3
12
  from .. import _autoinit
graphblas/core/agg.py CHANGED
@@ -1,14 +1,14 @@
1
1
  """graphblas.core.agg namespace is deprecated; please use graphblas.core.operator.agg instead.
2
2
 
3
3
  .. deprecated:: 2023.3.0
4
- `graphblas.core.agg` will be removed in a future release.
5
- Use `graphblas.core.operator.agg` instead.
4
+ ``graphblas.core.agg`` will be removed in a future release.
5
+ Use ``graphblas.core.operator.agg`` instead.
6
6
  Will be removed in version 2023.11.0 or later.
7
7
 
8
8
  """
9
9
  import warnings
10
10
 
11
- from .operator.agg import *
11
+ from .operator.agg import * # pylint: disable=wildcard-import,unused-wildcard-import
12
12
 
13
13
  warnings.warn(
14
14
  "graphblas.core.agg namespace is deprecated; please use graphblas.core.operator.agg instead.",
@@ -1,6 +1,6 @@
1
1
  """Define functions to use as property methods on expressions.
2
2
 
3
- These will automatically compute the value and avoid the need for `.new()`.
3
+ These will automatically compute the value and avoid the need for ``.new()``.
4
4
 
5
5
  To automatically create the functions, run:
6
6
 
graphblas/core/base.py CHANGED
@@ -348,7 +348,7 @@ class BaseType:
348
348
  return
349
349
  if opts:
350
350
  # Ignore opts for now
351
- descriptor_lookup(**opts)
351
+ desc = descriptor_lookup(**opts)
352
352
  self.value = expr
353
353
  return
354
354
 
@@ -371,7 +371,7 @@ class BaseType:
371
371
  else:
372
372
  if opts:
373
373
  # Ignore opts for now
374
- descriptor_lookup(**opts)
374
+ desc = descriptor_lookup(**opts)
375
375
  self.value = expr
376
376
  return
377
377
  else:
@@ -571,7 +571,7 @@ class BaseExpression:
571
571
  ):
572
572
  if opts:
573
573
  # Ignore opts for now
574
- descriptor_lookup(**opts)
574
+ desc = descriptor_lookup(**opts) # noqa: F841 (keep desc in scope for context)
575
575
  if self._is_scalar and self._value._is_cscalar != is_cscalar:
576
576
  return self._value.dup(is_cscalar=is_cscalar, name=name)
577
577
  rv = self._value
@@ -26,6 +26,7 @@ class Descriptor:
26
26
  self.mask_structure = mask_structure
27
27
  self.transpose_first = transpose_first
28
28
  self.transpose_second = transpose_second
29
+ self._context = None # Used by SuiteSparse:GraphBLAS 8
29
30
 
30
31
  @property
31
32
  def _carg(self):
@@ -1,17 +1,16 @@
1
- import warnings as _warnings
1
+ import warnings
2
2
 
3
- import numba as _numba
4
- import numpy as _np
5
- from numpy import find_common_type as _find_common_type
6
- from numpy import promote_types as _promote_types
3
+ import numpy as np
4
+ from numpy import promote_types, result_type
7
5
 
8
- from . import backend
9
- from .core import NULL as _NULL
10
- from .core import ffi as _ffi
11
- from .core import lib as _lib
6
+ from .. import backend, dtypes
7
+ from ..core import NULL, _has_numba, ffi, lib
8
+
9
+ if _has_numba:
10
+ import numba
12
11
 
13
12
  # Default assumption unless FC32/FC64 are found in lib
14
- _supports_complex = hasattr(_lib, "GrB_FC64") or hasattr(_lib, "GxB_FC64")
13
+ _supports_complex = hasattr(lib, "GrB_FC64") or hasattr(lib, "GxB_FC64")
15
14
 
16
15
 
17
16
  class DataType:
@@ -23,7 +22,7 @@ class DataType:
23
22
  self.gb_name = gb_name
24
23
  self.c_type = c_type
25
24
  self.numba_type = numba_type
26
- self.np_type = _np.dtype(np_type)
25
+ self.np_type = np.dtype(np_type) if np_type is not None else None
27
26
 
28
27
  def __repr__(self):
29
28
  return self.name
@@ -59,7 +58,7 @@ class DataType:
59
58
 
60
59
  @property
61
60
  def _is_anonymous(self):
62
- return globals().get(self.name) is not self
61
+ return getattr(dtypes, self.name, None) is not self
63
62
 
64
63
  @property
65
64
  def _is_udt(self):
@@ -77,27 +76,29 @@ class DataType:
77
76
  def register_new(name, dtype):
78
77
  if not name.isidentifier():
79
78
  raise ValueError(f"`name` argument must be a valid Python identifier; got: {name!r}")
80
- if name in _registry or name in globals():
79
+ if name in _registry or hasattr(dtypes, name):
81
80
  raise ValueError(f"{name!r} name for dtype is unavailable")
82
81
  rv = register_anonymous(dtype, name)
83
82
  _registry[name] = rv
84
- globals()[name] = rv
83
+ setattr(dtypes, name, rv)
85
84
  return rv
86
85
 
87
86
 
88
87
  def register_anonymous(dtype, name=None):
89
88
  try:
90
- dtype = _np.dtype(dtype)
89
+ dtype = np.dtype(dtype)
91
90
  except TypeError:
92
91
  if isinstance(dtype, dict):
93
92
  # Allow dtypes such as `{'x': int, 'y': float}` for convenience
94
- dtype = _np.dtype([(key, lookup_dtype(val).np_type) for key, val in dtype.items()])
93
+ dtype = np.dtype(
94
+ [(key, lookup_dtype(val).np_type) for key, val in dtype.items()], align=True
95
+ )
95
96
  elif isinstance(dtype, str) and "[" in dtype and dtype.endswith("]"):
96
97
  # Allow dtypes such as `"INT64[3, 4]"` for convenience
97
98
  base_dtype, shape = dtype.split("[", 1)
98
99
  base_dtype = lookup_dtype(base_dtype)
99
- shape = _np.lib.format.safe_eval(f"[{shape}")
100
- dtype = _np.dtype((base_dtype.np_type, shape))
100
+ shape = np.lib.format.safe_eval(f"[{shape}")
101
+ dtype = np.dtype((base_dtype.np_type, shape))
101
102
  else:
102
103
  raise
103
104
  if dtype in _registry:
@@ -111,112 +112,194 @@ def register_anonymous(dtype, name=None):
111
112
  if dtype.hasobject:
112
113
  raise ValueError("dtype must not allow Python objects")
113
114
 
114
- from .exceptions import check_status_carg
115
+ from ..exceptions import check_status_carg
115
116
 
116
- gb_obj = _ffi.new("GrB_Type*")
117
+ gb_obj = ffi.new("GrB_Type*")
117
118
  if backend == "suitesparse":
118
119
  # We name this so that we can serialize and deserialize UDTs
119
120
  # We don't yet have C definitions
120
121
  np_repr = _dtype_to_string(dtype).encode()
121
- if len(np_repr) > _lib.GxB_MAX_NAME_LEN:
122
+ if len(np_repr) > lib.GxB_MAX_NAME_LEN:
122
123
  msg = (
123
124
  f"UDT repr is too large to serialize ({len(repr(dtype).encode())} > "
124
- f"{_lib.GxB_MAX_NAME_LEN})."
125
+ f"{lib.GxB_MAX_NAME_LEN})."
125
126
  )
126
127
  if name is not None:
127
- np_repr = name.encode()[: _lib.GxB_MAX_NAME_LEN]
128
+ np_repr = name.encode()[: lib.GxB_MAX_NAME_LEN]
128
129
  else:
129
- np_repr = np_repr[: _lib.GxB_MAX_NAME_LEN]
130
- _warnings.warn(
130
+ np_repr = np_repr[: lib.GxB_MAX_NAME_LEN]
131
+ warnings.warn(
131
132
  f"{msg}. It will use the following name, "
132
133
  f"and the dtype may need to be specified when deserializing: {np_repr}",
133
134
  stacklevel=2,
134
135
  )
135
- status = _lib.GxB_Type_new(gb_obj, dtype.itemsize, np_repr, _NULL)
136
+ status = lib.GxB_Type_new(gb_obj, dtype.itemsize, np_repr, NULL)
136
137
  else:
137
- status = _lib.GrB_Type_new(gb_obj, dtype.itemsize)
138
+ status = lib.GrB_Type_new(gb_obj, dtype.itemsize)
138
139
  check_status_carg(status, "Type", gb_obj[0])
139
140
 
140
141
  # For now, let's use "opaque" unsigned bytes for the c type.
141
142
  if name is None:
142
143
  name = _default_name(dtype)
143
- numba_type = _numba.typeof(dtype).dtype
144
+ numba_type = numba.typeof(dtype).dtype if _has_numba else None
144
145
  rv = DataType(name, gb_obj, None, f"uint8_t[{dtype.itemsize}]", numba_type, dtype)
145
146
  _registry[gb_obj] = rv
146
147
  _registry[dtype] = rv
147
- _registry[numba_type] = rv
148
- _registry[numba_type.name] = rv
148
+ if _has_numba:
149
+ _registry[numba_type] = rv
150
+ _registry[numba_type.name] = rv
149
151
  return rv
150
152
 
151
153
 
152
- BOOL = DataType("BOOL", _lib.GrB_BOOL, "GrB_BOOL", "_Bool", _numba.types.bool_, _np.bool_)
153
- INT8 = DataType("INT8", _lib.GrB_INT8, "GrB_INT8", "int8_t", _numba.types.int8, _np.int8)
154
- UINT8 = DataType("UINT8", _lib.GrB_UINT8, "GrB_UINT8", "uint8_t", _numba.types.uint8, _np.uint8)
155
- INT16 = DataType("INT16", _lib.GrB_INT16, "GrB_INT16", "int16_t", _numba.types.int16, _np.int16)
154
+ BOOL = DataType(
155
+ "BOOL",
156
+ lib.GrB_BOOL,
157
+ "GrB_BOOL",
158
+ "_Bool",
159
+ numba.types.bool_ if _has_numba else None,
160
+ np.bool_,
161
+ )
162
+ INT8 = DataType(
163
+ "INT8", lib.GrB_INT8, "GrB_INT8", "int8_t", numba.types.int8 if _has_numba else None, np.int8
164
+ )
165
+ UINT8 = DataType(
166
+ "UINT8",
167
+ lib.GrB_UINT8,
168
+ "GrB_UINT8",
169
+ "uint8_t",
170
+ numba.types.uint8 if _has_numba else None,
171
+ np.uint8,
172
+ )
173
+ INT16 = DataType(
174
+ "INT16",
175
+ lib.GrB_INT16,
176
+ "GrB_INT16",
177
+ "int16_t",
178
+ numba.types.int16 if _has_numba else None,
179
+ np.int16,
180
+ )
156
181
  UINT16 = DataType(
157
- "UINT16", _lib.GrB_UINT16, "GrB_UINT16", "uint16_t", _numba.types.uint16, _np.uint16
182
+ "UINT16",
183
+ lib.GrB_UINT16,
184
+ "GrB_UINT16",
185
+ "uint16_t",
186
+ numba.types.uint16 if _has_numba else None,
187
+ np.uint16,
188
+ )
189
+ INT32 = DataType(
190
+ "INT32",
191
+ lib.GrB_INT32,
192
+ "GrB_INT32",
193
+ "int32_t",
194
+ numba.types.int32 if _has_numba else None,
195
+ np.int32,
158
196
  )
159
- INT32 = DataType("INT32", _lib.GrB_INT32, "GrB_INT32", "int32_t", _numba.types.int32, _np.int32)
160
197
  UINT32 = DataType(
161
- "UINT32", _lib.GrB_UINT32, "GrB_UINT32", "uint32_t", _numba.types.uint32, _np.uint32
198
+ "UINT32",
199
+ lib.GrB_UINT32,
200
+ "GrB_UINT32",
201
+ "uint32_t",
202
+ numba.types.uint32 if _has_numba else None,
203
+ np.uint32,
204
+ )
205
+ INT64 = DataType(
206
+ "INT64",
207
+ lib.GrB_INT64,
208
+ "GrB_INT64",
209
+ "int64_t",
210
+ numba.types.int64 if _has_numba else None,
211
+ np.int64,
162
212
  )
163
- INT64 = DataType("INT64", _lib.GrB_INT64, "GrB_INT64", "int64_t", _numba.types.int64, _np.int64)
164
213
  # _Index (like UINT64) is for internal use only and shouldn't be exposed to the user
165
214
  _INDEX = DataType(
166
- "UINT64", _lib.GrB_UINT64, "GrB_Index", "GrB_Index", _numba.types.uint64, _np.uint64
215
+ "UINT64",
216
+ lib.GrB_UINT64,
217
+ "GrB_Index",
218
+ "GrB_Index",
219
+ numba.types.uint64 if _has_numba else None,
220
+ np.uint64,
167
221
  )
168
222
  UINT64 = DataType(
169
- "UINT64", _lib.GrB_UINT64, "GrB_UINT64", "uint64_t", _numba.types.uint64, _np.uint64
223
+ "UINT64",
224
+ lib.GrB_UINT64,
225
+ "GrB_UINT64",
226
+ "uint64_t",
227
+ numba.types.uint64 if _has_numba else None,
228
+ np.uint64,
229
+ )
230
+ FP32 = DataType(
231
+ "FP32",
232
+ lib.GrB_FP32,
233
+ "GrB_FP32",
234
+ "float",
235
+ numba.types.float32 if _has_numba else None,
236
+ np.float32,
237
+ )
238
+ FP64 = DataType(
239
+ "FP64",
240
+ lib.GrB_FP64,
241
+ "GrB_FP64",
242
+ "double",
243
+ numba.types.float64 if _has_numba else None,
244
+ np.float64,
170
245
  )
171
- FP32 = DataType("FP32", _lib.GrB_FP32, "GrB_FP32", "float", _numba.types.float32, _np.float32)
172
- FP64 = DataType("FP64", _lib.GrB_FP64, "GrB_FP64", "double", _numba.types.float64, _np.float64)
173
246
 
174
- if _supports_complex and hasattr(_lib, "GxB_FC32"):
247
+ if _supports_complex and hasattr(lib, "GxB_FC32"):
175
248
  FC32 = DataType(
176
- "FC32", _lib.GxB_FC32, "GxB_FC32", "float _Complex", _numba.types.complex64, _np.complex64
249
+ "FC32",
250
+ lib.GxB_FC32,
251
+ "GxB_FC32",
252
+ "float _Complex",
253
+ numba.types.complex64 if _has_numba else None,
254
+ np.complex64,
177
255
  )
178
- if _supports_complex and hasattr(_lib, "GrB_FC32"): # pragma: no cover (unused)
256
+ if _supports_complex and hasattr(lib, "GrB_FC32"): # pragma: no cover (unused)
179
257
  FC32 = DataType(
180
- "FC32", _lib.GrB_FC32, "GrB_FC32", "float _Complex", _numba.types.complex64, _np.complex64
258
+ "FC32",
259
+ lib.GrB_FC32,
260
+ "GrB_FC32",
261
+ "float _Complex",
262
+ numba.types.complex64 if _has_numba else None,
263
+ np.complex64,
181
264
  )
182
- if _supports_complex and hasattr(_lib, "GxB_FC64"):
265
+ if _supports_complex and hasattr(lib, "GxB_FC64"):
183
266
  FC64 = DataType(
184
267
  "FC64",
185
- _lib.GxB_FC64,
268
+ lib.GxB_FC64,
186
269
  "GxB_FC64",
187
270
  "double _Complex",
188
- _numba.types.complex128,
189
- _np.complex128,
271
+ numba.types.complex128 if _has_numba else None,
272
+ np.complex128,
190
273
  )
191
- if _supports_complex and hasattr(_lib, "GrB_FC64"): # pragma: no cover (unused)
274
+ if _supports_complex and hasattr(lib, "GrB_FC64"): # pragma: no cover (unused)
192
275
  FC64 = DataType(
193
276
  "FC64",
194
- _lib.GrB_FC64,
277
+ lib.GrB_FC64,
195
278
  "GrB_FC64",
196
279
  "double _Complex",
197
- _numba.types.complex128,
198
- _np.complex128,
280
+ numba.types.complex128 if _has_numba else None,
281
+ np.complex128,
199
282
  )
200
283
 
201
284
  # Used for testing user-defined functions
202
285
  _sample_values = {
203
- INT8: _np.int8(1),
204
- UINT8: _np.uint8(1),
205
- INT16: _np.int16(1),
206
- UINT16: _np.uint16(1),
207
- INT32: _np.int32(1),
208
- UINT32: _np.uint32(1),
209
- INT64: _np.int64(1),
210
- UINT64: _np.uint64(1),
211
- FP32: _np.float32(0.5),
212
- FP64: _np.float64(0.5),
213
- BOOL: _np.bool_(True),
286
+ INT8: np.int8(1),
287
+ UINT8: np.uint8(1),
288
+ INT16: np.int16(1),
289
+ UINT16: np.uint16(1),
290
+ INT32: np.int32(1),
291
+ UINT32: np.uint32(1),
292
+ INT64: np.int64(1),
293
+ UINT64: np.uint64(1),
294
+ FP32: np.float32(0.5),
295
+ FP64: np.float64(0.5),
296
+ BOOL: np.bool_(True),
214
297
  }
215
298
  if _supports_complex:
216
299
  _sample_values.update(
217
300
  {
218
- FC32: _np.complex64(complex(0, 0.5)),
219
- FC64: _np.complex128(complex(0, 0.5)),
301
+ FC32: np.complex64(complex(0, 0.5)),
302
+ FC64: np.complex128(complex(0, 0.5)),
220
303
  }
221
304
  )
222
305
 
@@ -246,8 +329,9 @@ for dtype in _dtypes_to_register:
246
329
  _registry[dtype.gb_name.lower()] = dtype
247
330
  _registry[dtype.c_type] = dtype
248
331
  _registry[dtype.c_type.upper()] = dtype
249
- _registry[dtype.numba_type] = dtype
250
- _registry[dtype.numba_type.name] = dtype
332
+ if _has_numba:
333
+ _registry[dtype.numba_type] = dtype
334
+ _registry[dtype.numba_type.name] = dtype
251
335
  val = _sample_values[dtype]
252
336
  _registry[val.dtype] = dtype
253
337
  _registry[val.dtype.name] = dtype
@@ -303,19 +387,11 @@ def unify(type1, type2, *, is_left_scalar=False, is_right_scalar=False):
303
387
  if type1 is type2:
304
388
  return type1
305
389
  if is_left_scalar:
306
- scalar_types = [type1.np_type]
307
- array_types = []
308
- elif not is_right_scalar:
309
- # Using `promote_types` is faster than `find_common_type`
310
- return lookup_dtype(_promote_types(type1.np_type, type2.np_type))
311
- else:
312
- scalar_types = []
313
- array_types = [type1.np_type]
314
- if is_right_scalar:
315
- scalar_types.append(type2.np_type)
316
- else:
317
- array_types.append(type2.np_type)
318
- return lookup_dtype(_find_common_type(array_types, scalar_types))
390
+ if not is_right_scalar:
391
+ return lookup_dtype(result_type(np.array(0, type1.np_type), type2.np_type))
392
+ elif is_right_scalar:
393
+ return lookup_dtype(result_type(type1.np_type, np.array(0, type2.np_type)))
394
+ return lookup_dtype(promote_types(type1.np_type, type2.np_type))
319
395
 
320
396
 
321
397
  def _default_name(dtype):
@@ -345,7 +421,7 @@ def _dtype_to_string(dtype):
345
421
  >>> dtype == new_dtype
346
422
  True
347
423
  """
348
- if isinstance(dtype, _np.dtype) and dtype not in _registry:
424
+ if isinstance(dtype, np.dtype) and dtype not in _registry:
349
425
  np_type = dtype
350
426
  else:
351
427
  dtype = lookup_dtype(dtype)
@@ -354,11 +430,11 @@ def _dtype_to_string(dtype):
354
430
  np_type = dtype.np_type
355
431
  s = str(np_type)
356
432
  try:
357
- if _np.dtype(_np.lib.format.safe_eval(s)) == np_type: # pragma: no branch (safety)
433
+ if np.dtype(np.lib.format.safe_eval(s)) == np_type: # pragma: no branch (safety)
358
434
  return s
359
435
  except Exception:
360
436
  pass
361
- if _np.dtype(np_type.str) != np_type: # pragma: no cover (safety)
437
+ if np.dtype(np_type.str) != np_type: # pragma: no cover (safety)
362
438
  raise ValueError(f"Unable to reliably convert dtype to string and back: {dtype}")
363
439
  return repr(np_type.str)
364
440
 
@@ -373,5 +449,5 @@ def _string_to_dtype(s):
373
449
  return lookup_dtype(s)
374
450
  except Exception:
375
451
  pass
376
- np_type = _np.dtype(_np.lib.format.safe_eval(s))
452
+ np_type = np.dtype(np.lib.format.safe_eval(s))
377
453
  return lookup_dtype(np_type)
graphblas/core/expr.py CHANGED
@@ -421,7 +421,7 @@ class Updater:
421
421
  # Fast path using assignElement
422
422
  if self.opts:
423
423
  # Ignore opts for now
424
- descriptor_lookup(**self.opts)
424
+ desc = descriptor_lookup(**self.opts) # noqa: F841 (keep desc in scope for context)
425
425
  self.parent._assign_element(resolved_indexes, obj)
426
426
  else:
427
427
  mask = self.kwargs.get("mask")
@@ -880,6 +880,7 @@ def format_index_expression_html(expr):
880
880
  computed = get_expr_result(expr, html=True)
881
881
  if "__EXPR__" in computed:
882
882
  return computed.replace("<tt>__EXPR__</tt>", topline)
883
+ # BRANCH NOT COVERED
883
884
 
884
885
  keys = []
885
886
  values = []
graphblas/core/mask.py CHANGED
@@ -35,7 +35,7 @@ class Mask:
35
35
  """Return a new object with True values determined by the mask(s).
36
36
 
37
37
  By default, the result is True wherever the mask(s) would have been applied,
38
- and empty otherwise. If `complement` is True, then these are switched:
38
+ and empty otherwise. If ``complement`` is True, then these are switched:
39
39
  the result is empty where the mask(s) would have been applied, and True otherwise.
40
40
 
41
41
  In other words, these are equivalent if complement is False (and mask keyword is None):
@@ -48,14 +48,14 @@ class Mask:
48
48
  >>> C(self) << expr
49
49
  >>> C(~result.S) << expr # equivalent when complement is True
50
50
 
51
- This can also efficiently merge two masks by using the `mask=` argument.
51
+ This can also efficiently merge two masks by using the ``mask=`` argument.
52
52
  This is equivalent to the following (but uses more efficient recipes):
53
53
 
54
54
  >>> val = Matrix(...)
55
55
  >>> val(self) << True
56
56
  >>> val(mask, replace=True) << val
57
57
 
58
- If `complement=` argument is True, then the *complement* will be returned.
58
+ If ``complement=`` argument is True, then the *complement* will be returned.
59
59
  This is equivalent to the following (but uses more efficient recipes):
60
60
 
61
61
  >>> val = Matrix(...)
@@ -83,7 +83,7 @@ class Mask:
83
83
  def __and__(self, other, **opts):
84
84
  """Return the intersection of two masks as a new mask.
85
85
 
86
- `new_mask = mask1 & mask2` is equivalent to the following:
86
+ ``new_mask = mask1 & mask2`` is equivalent to the following:
87
87
 
88
88
  >>> val = Matrix(bool, nrows, ncols)
89
89
  >>> val(mask1) << True
@@ -109,7 +109,7 @@ class Mask:
109
109
  def __or__(self, other, **opts):
110
110
  """Return the union of two masks as a new mask.
111
111
 
112
- `new_mask = mask1 | mask2` is equivalent to the following:
112
+ ``new_mask = mask1 | mask2`` is equivalent to the following:
113
113
 
114
114
  >>> val = Matrix(bool, nrows, ncols)
115
115
  >>> val(mask1) << True