array-api-shape-check 0.1.3__py3-none-any.whl → 1.0.1__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.
@@ -1,8 +1,9 @@
1
+ import inspect
1
2
  import re
2
3
  from collections import defaultdict
3
4
  from collections.abc import Sequence
4
5
  from itertools import groupby
5
- from typing import Any
6
+ from typing import Any, cast
6
7
 
7
8
  import attrs
8
9
  import numpy as np
@@ -34,43 +35,50 @@ class SubscriptInfoFromShapeItemUnchecked(SubscriptInfoFromSubcriptItem):
34
35
  def __repr__(self) -> str:
35
36
  if self.is_variable:
36
37
  return f"*{self.name}:{self.shape_current}"
37
- else:
38
- return f"{self.name}:{self.shape_current[0]}"
38
+ return f"{self.name}:{self.shape_current[0]}"
39
39
 
40
40
 
41
41
  @attrs.frozen(kw_only=True)
42
- class SubscriptInfoFromShapeItemUnique(SubscriptInfoFromSubcriptItem):
43
- shape_broadcasted: tuple[int, ...]
44
- """The shape after broadcasting with other subscripts with the same name"""
42
+ class SubscriptInfoFromShapeItemUnique[BroadcastShape: tuple[int, ...] | None](
43
+ SubscriptInfoFromSubcriptItem
44
+ ):
45
+ shape_broadcasted: BroadcastShape
46
+ """The broadcast shape, or None for an inconsistent subscript."""
45
47
 
46
48
  def __repr__(self) -> str:
47
49
  if self.is_variable:
48
50
  return f"*{self.name}:{self.shape_broadcasted}"
49
- else:
50
- return f"{self.name}:{self.shape_broadcasted[0]}"
51
+ if self.shape_broadcasted is None:
52
+ return f"{self.name}:None"
53
+ return f"{self.name}:{cast('tuple[int, ...]', self.shape_broadcasted)[0]}"
51
54
 
52
55
 
53
56
  @attrs.frozen(kw_only=True)
54
- class SubscriptInfoFromShapeItem(SubscriptInfoFromShapeItemUnchecked):
55
- shape_broadcasted: tuple[int, ...]
56
- """The shape after broadcasting with other subscripts with the same name"""
57
+ class SubscriptInfoFromShapeItem[BroadcastShape: tuple[int, ...] | None](
58
+ SubscriptInfoFromShapeItemUnchecked
59
+ ):
60
+ shape_broadcasted: BroadcastShape
61
+ """The broadcast shape, or None for an inconsistent subscript."""
57
62
 
58
63
  def __repr__(self) -> str:
64
+ if self.shape_broadcasted is None:
65
+ current = self.shape_current if self.is_variable else self.shape_current[0]
66
+ return f"{'*' if self.is_variable else ''}{self.name}:{current}->None"
59
67
  if self.is_variable:
60
68
  if self.shape_current != self.shape_broadcasted:
61
69
  return f"*{self.name}:{self.shape_current}->{self.shape_broadcasted}"
62
70
  return f"*{self.name}:{self.shape_current}"
63
- else:
64
- if self.shape_current != self.shape_broadcasted:
65
- return f"{self.name}:{self.shape_current[0]}->{self.shape_broadcasted[0]}"
66
- return f"{self.name}:{self.shape_current[0]}"
71
+ if self.shape_current != self.shape_broadcasted:
72
+ broadcasted = cast("tuple[int, ...]", self.shape_broadcasted)
73
+ return f"{self.name}:{self.shape_current[0]}->{broadcasted[0]}"
74
+ return f"{self.name}:{self.shape_current[0]}"
67
75
 
68
76
 
69
77
  @attrs.frozen(kw_only=True)
70
- class SubscriptInfoFromShape:
71
- all: tuple[tuple[SubscriptInfoFromShapeItem, ...], ...]
78
+ class SubscriptInfoFromShape[BroadcastShape: tuple[int, ...] | None]:
79
+ all: tuple[tuple[SubscriptInfoFromShapeItem[BroadcastShape], ...], ...]
72
80
  """The subscript info grouped by order, then by operand"""
73
- unique: dict[str, SubscriptInfoFromShapeItemUnique]
81
+ unique: dict[str, SubscriptInfoFromShapeItemUnique[BroadcastShape]]
74
82
  """The unique subscripts,
75
83
  ignoring order and shapes not broadcasted,
76
84
  sorted lexicographically by name"""
@@ -82,7 +90,7 @@ class InconsistentNdimError(ValueError):
82
90
  reason: str
83
91
 
84
92
 
85
- class InconsistentNdimErrorMultipleSolutions(InconsistentNdimError):
93
+ class InconsistentNdimErrorMultipleSolutions(InconsistentNdimError): # noqa: N818
86
94
  def __init__(
87
95
  self, ndims: Sequence[int], reason: str, shapes: Sequence[int] | None = None
88
96
  ) -> None:
@@ -97,7 +105,7 @@ class InconsistentNdimErrorMultipleSolutions(InconsistentNdimError):
97
105
  )
98
106
 
99
107
 
100
- class InconsistentNdimErrorNoSolutions(InconsistentNdimError):
108
+ class InconsistentNdimErrorNoSolutions(InconsistentNdimError): # noqa: N818
101
109
  def __init__(
102
110
  self, ndims: Sequence[int], reason: str, shapes: Sequence[int] | None = None
103
111
  ) -> None:
@@ -113,16 +121,19 @@ class InconsistentNdimErrorNoSolutions(InconsistentNdimError):
113
121
 
114
122
 
115
123
  class InconsistentShapeError(ValueError):
116
- subscript_info: SubscriptInfoFromShape
124
+ subscript_info: SubscriptInfoFromShape[tuple[int, ...] | None]
117
125
 
118
- def __init__(self, subscript_info: SubscriptInfoFromShape, reason: str, *args: object) -> None:
126
+ def __init__(
127
+ self,
128
+ subscript_info: SubscriptInfoFromShape[tuple[int, ...] | None],
129
+ reason: str,
130
+ *args: object,
131
+ ) -> None:
119
132
  super().__init__("Inconsistent shapes: " + str(subscript_info.all) + f"\n {reason}", *args)
120
133
  self.subscript_info = subscript_info
121
134
 
122
135
 
123
136
  def _get_operand_names(*operands: Any) -> list[str | None]:
124
- import inspect
125
-
126
137
  frame = inspect.currentframe()
127
138
  if frame is None:
128
139
  return [None] * len(operands)
@@ -236,6 +247,10 @@ def parse_variable_ndim(subscripts: str, ndims: Sequence[int], /) -> dict[str, i
236
247
  ------
237
248
  ValueError
238
249
  If the subscript is invalid.
250
+ InconsistentNdimErrorMultipleSolutions
251
+ If the variable dimensions cannot be uniquely determined.
252
+ InconsistentNdimErrorNoSolutions
253
+ If the variable dimensions have no valid solution.
239
254
 
240
255
  Examples
241
256
  --------
@@ -306,18 +321,19 @@ def _parse_shapes(
306
321
  )
307
322
  shapes: tuple[tuple[int, ...], ...] = ()
308
323
  for operand in operands:
309
- shape: tuple[int, ...] # type: ignore
310
324
  if isinstance(operand, tuple):
311
- shape = operand
325
+ if None in operand:
326
+ raise ValueError("Operand shape cannot contain None")
327
+ shape = cast("tuple[int, ...]", operand)
312
328
  elif hasattr(operand, "shape"):
313
329
  if None in operand.shape:
314
330
  raise ValueError("Operand shape cannot contain None")
315
- shape = operand.shape
331
+ shape = cast("tuple[int, ...]", operand.shape)
316
332
  else:
317
333
  raise TypeError(
318
334
  f"Invalid operand: expected an array or a shape tuple, but got {type(operand)}"
319
335
  )
320
- shapes += (shape,) # type: ignore
336
+ shapes += (shape,)
321
337
  del operands
322
338
  ndims = [len(shape) for shape in shapes]
323
339
  try:
@@ -336,14 +352,14 @@ def _parse_shapes(
336
352
  shape_current=shape[: name_to_ndim[item.name]],
337
353
  ),
338
354
  )
339
- shape = shape[name_to_ndim[item.name] :]
355
+ shape = shape[name_to_ndim[item.name] :] # noqa: PLW2901
340
356
  info_all += (info_array_new,)
341
357
  return info_all
342
358
 
343
359
 
344
360
  def check_shapes(
345
361
  subscripts: str, /, *operands: Array | tuple[int | None, ...], names: str | None = None
346
- ) -> SubscriptInfoFromShape:
362
+ ) -> SubscriptInfoFromShape[tuple[int, ...]]:
347
363
  """
348
364
  Parse variable subscript ndims by solving linear equations.
349
365
 
@@ -373,6 +389,8 @@ def check_shapes(
373
389
  ------
374
390
  ValueError
375
391
  If the subscript is invalid.
392
+ InconsistentShapeError
393
+ If the operand shapes are inconsistent.
376
394
 
377
395
  Examples
378
396
  --------
@@ -432,12 +450,15 @@ def check_shapes(
432
450
  info_flatten_keyed = [(i, item) for i, info_array in enumerate(info_all) for item in info_array]
433
451
  errors = []
434
452
  shape_broadcasted = {}
435
- for key, group in groupby(info_flatten_keyed, key=lambda x: x[1].name):
453
+ for key, group in groupby(
454
+ sorted(info_flatten_keyed, key=lambda x: x[1].name), key=lambda x: x[1].name
455
+ ):
436
456
  group_list = list(group)
437
457
  shapes = [item.shape_current for _, item in group_list]
438
458
  try:
439
459
  shape_broadcasted[key] = np.broadcast_shapes(*shapes)
440
460
  except ValueError:
461
+ shape_broadcasted[key] = None
441
462
  inner_text = ", ".join([f"{shape} ({names_[i]})" for i, shape in group_list])
442
463
  errors.append(f"Key {key}: shapes {inner_text} are not broadcastable")
443
464
 
@@ -450,7 +471,7 @@ def check_shapes(
450
471
  name=item.name,
451
472
  is_variable=item.is_variable,
452
473
  shape_current=item.shape_current,
453
- shape_broadcasted=shape_broadcasted.get(item.name), # type: ignore
474
+ shape_broadcasted=shape_broadcasted[item.name],
454
475
  ),
455
476
  )
456
477
  info_all_new += (info_array_new,)
@@ -459,7 +480,7 @@ def check_shapes(
459
480
  SubscriptInfoFromShapeItemUnique(
460
481
  name=item.name,
461
482
  is_variable=item.is_variable,
462
- shape_broadcasted=shape_broadcasted.get(item.name), # type: ignore
483
+ shape_broadcasted=shape_broadcasted[item.name],
463
484
  )
464
485
  for info_array in info_all_new
465
486
  for item in info_array
@@ -472,4 +493,5 @@ def check_shapes(
472
493
  if errors:
473
494
  raise InconsistentShapeError(result, "\n".join(errors))
474
495
 
475
- return result
496
+ # No broadcast shape can be None unless an inconsistency was recorded.
497
+ return cast("SubscriptInfoFromShape[tuple[int, ...]]", result)
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: array-api-shape-check
3
- Version: 0.1.3
3
+ Version: 1.0.1
4
4
  Summary: Check shapes of input arrays easily
5
- Author-email: 34j <34j.95a2p@simplelogin.com>
5
+ Author: 34j
6
6
  License-Expression: MIT
7
7
  Project-URL: Bug Tracker, https://github.com/34j/array-api-shape-check/issues
8
8
  Project-URL: Changelog, https://github.com/34j/array-api-shape-check/blob/main/CHANGELOG.md
@@ -0,0 +1,8 @@
1
+ array_api_shape_check/__init__.py,sha256=8i9ZV_x6HXtVd0g7N3ec190lQ1LUC2s93A9p-J3C9WI,940
2
+ array_api_shape_check/_main.py,sha256=jKI3eDWql14v2oXesGXeZE9_114hfp5D8TUaqKbMAY4,17808
3
+ array_api_shape_check/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ array_api_shape_check-1.0.1.dist-info/licenses/LICENSE,sha256=Otwb96xcK4QBqXXVdBKpLZVnc4mJuFTa8rsMUEpphMM,1061
5
+ array_api_shape_check-1.0.1.dist-info/METADATA,sha256=0DwyMEHAVpY908iaOjNLGR8uFSxDLLRfebEFnm0I89I,6179
6
+ array_api_shape_check-1.0.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
7
+ array_api_shape_check-1.0.1.dist-info/top_level.txt,sha256=pirw2DeaWnDU_rSg3CZ1MsE-tJWEvndYSQG0illDMkI,22
8
+ array_api_shape_check-1.0.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (84.2.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,8 +0,0 @@
1
- array_api_shape_check/__init__.py,sha256=8i9ZV_x6HXtVd0g7N3ec190lQ1LUC2s93A9p-J3C9WI,940
2
- array_api_shape_check/_main.py,sha256=LqwDK6OD9ldcUaXmmbG5aBXzOW0gEJuu1iAe9rs2BiY,16652
3
- array_api_shape_check/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- array_api_shape_check-0.1.3.dist-info/licenses/LICENSE,sha256=Otwb96xcK4QBqXXVdBKpLZVnc4mJuFTa8rsMUEpphMM,1061
5
- array_api_shape_check-0.1.3.dist-info/METADATA,sha256=of_gXs9JcFp-b0KQwa9Sbf8GIXPvChmU5VGfvBWyYOQ,6213
6
- array_api_shape_check-0.1.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
- array_api_shape_check-0.1.3.dist-info/top_level.txt,sha256=pirw2DeaWnDU_rSg3CZ1MsE-tJWEvndYSQG0illDMkI,22
8
- array_api_shape_check-0.1.3.dist-info/RECORD,,