array-api-shape-check 1.0.0__tar.gz → 1.0.1__tar.gz

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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: array-api-shape-check
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: Check shapes of input arrays easily
5
5
  Author: 34j
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ requires = [ "setuptools>=77.0.3" ]
4
4
 
5
5
  [project]
6
6
  name = "array-api-shape-check"
7
- version = "1.0.0"
7
+ version = "1.0.1"
8
8
  description = "Check shapes of input arrays easily"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -39,36 +39,46 @@ class SubscriptInfoFromShapeItemUnchecked(SubscriptInfoFromSubcriptItem):
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
- 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]}"
50
54
 
51
55
 
52
56
  @attrs.frozen(kw_only=True)
53
- class SubscriptInfoFromShapeItem(SubscriptInfoFromShapeItemUnchecked):
54
- shape_broadcasted: tuple[int, ...]
55
- """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."""
56
62
 
57
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"
58
67
  if self.is_variable:
59
68
  if self.shape_current != self.shape_broadcasted:
60
69
  return f"*{self.name}:{self.shape_current}->{self.shape_broadcasted}"
61
70
  return f"*{self.name}:{self.shape_current}"
62
71
  if self.shape_current != self.shape_broadcasted:
63
- return f"{self.name}:{self.shape_current[0]}->{self.shape_broadcasted[0]}"
72
+ broadcasted = cast("tuple[int, ...]", self.shape_broadcasted)
73
+ return f"{self.name}:{self.shape_current[0]}->{broadcasted[0]}"
64
74
  return f"{self.name}:{self.shape_current[0]}"
65
75
 
66
76
 
67
77
  @attrs.frozen(kw_only=True)
68
- class SubscriptInfoFromShape:
69
- all: tuple[tuple[SubscriptInfoFromShapeItem, ...], ...]
78
+ class SubscriptInfoFromShape[BroadcastShape: tuple[int, ...] | None]:
79
+ all: tuple[tuple[SubscriptInfoFromShapeItem[BroadcastShape], ...], ...]
70
80
  """The subscript info grouped by order, then by operand"""
71
- unique: dict[str, SubscriptInfoFromShapeItemUnique]
81
+ unique: dict[str, SubscriptInfoFromShapeItemUnique[BroadcastShape]]
72
82
  """The unique subscripts,
73
83
  ignoring order and shapes not broadcasted,
74
84
  sorted lexicographically by name"""
@@ -111,9 +121,14 @@ class InconsistentNdimErrorNoSolutions(InconsistentNdimError): # noqa: N818
111
121
 
112
122
 
113
123
  class InconsistentShapeError(ValueError):
114
- subscript_info: SubscriptInfoFromShape
124
+ subscript_info: SubscriptInfoFromShape[tuple[int, ...] | None]
115
125
 
116
- 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:
117
132
  super().__init__("Inconsistent shapes: " + str(subscript_info.all) + f"\n {reason}", *args)
118
133
  self.subscript_info = subscript_info
119
134
 
@@ -344,7 +359,7 @@ def _parse_shapes(
344
359
 
345
360
  def check_shapes(
346
361
  subscripts: str, /, *operands: Array | tuple[int | None, ...], names: str | None = None
347
- ) -> SubscriptInfoFromShape:
362
+ ) -> SubscriptInfoFromShape[tuple[int, ...]]:
348
363
  """
349
364
  Parse variable subscript ndims by solving linear equations.
350
365
 
@@ -435,12 +450,15 @@ def check_shapes(
435
450
  info_flatten_keyed = [(i, item) for i, info_array in enumerate(info_all) for item in info_array]
436
451
  errors = []
437
452
  shape_broadcasted = {}
438
- 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
+ ):
439
456
  group_list = list(group)
440
457
  shapes = [item.shape_current for _, item in group_list]
441
458
  try:
442
459
  shape_broadcasted[key] = np.broadcast_shapes(*shapes)
443
460
  except ValueError:
461
+ shape_broadcasted[key] = None
444
462
  inner_text = ", ".join([f"{shape} ({names_[i]})" for i, shape in group_list])
445
463
  errors.append(f"Key {key}: shapes {inner_text} are not broadcastable")
446
464
 
@@ -475,4 +493,5 @@ def check_shapes(
475
493
  if errors:
476
494
  raise InconsistentShapeError(result, "\n".join(errors))
477
495
 
478
- return result
496
+ # No broadcast shape can be None unless an inconsistency was recorded.
497
+ return cast("SubscriptInfoFromShape[tuple[int, ...]]", result)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: array-api-shape-check
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: Check shapes of input arrays easily
5
5
  Author: 34j
6
6
  License-Expression: MIT
@@ -0,0 +1,43 @@
1
+ import pytest
2
+
3
+ from array_api_shape_check import InconsistentShapeError, check_shapes
4
+ from array_api_shape_check._main import _get_operand_names # noqa: PLC2701
5
+
6
+
7
+ def test_get_operand_names():
8
+ def inner(a, b) -> list[str | None]:
9
+ return _get_operand_names(a, b)
10
+
11
+ c = 1
12
+ assert inner(c, 4 * 4)[0] == "c"
13
+ # assert inner(c, 4 * 4)[1] is not None
14
+ # assert "@py_assert" in repr(inner(c, 4 * 4)[1])
15
+
16
+
17
+ @pytest.mark.parametrize(
18
+ ("subscripts", "shapes", "key"),
19
+ [
20
+ ("i,i", ((3,), (4,)), "i"),
21
+ ("ij,ji", ((3, 4), (4, 9)), "i"),
22
+ ("*BM,*BN", ((2, 3), (4, 3)), "B"),
23
+ ],
24
+ )
25
+ def test_incompatible_shapes(
26
+ subscripts: str, shapes: tuple[tuple[int, ...], ...], key: str
27
+ ) -> None:
28
+ with pytest.raises(InconsistentShapeError, match=f"Key {key}") as exc:
29
+ check_shapes(subscripts, *shapes, names="first,second")
30
+ assert exc.value.subscript_info.unique[key].shape_broadcasted is None
31
+
32
+
33
+ def test_broadcasts_nonconsecutive_subscripts() -> None:
34
+ info = check_shapes("ij,ji", (1, 4), (4, 9), names="first,second")
35
+ assert info.unique["i"].shape_broadcasted == (9,)
36
+ assert info.unique["j"].shape_broadcasted == (4,)
37
+
38
+
39
+ def test_reports_all_inconsistent_subscripts() -> None:
40
+ with pytest.raises(InconsistentShapeError) as exc:
41
+ check_shapes("ij,ij", (2, 3), (4, 5), names="first,second")
42
+ assert "Key i" in str(exc.value)
43
+ assert "Key j" in str(exc.value)
@@ -1,11 +0,0 @@
1
- from array_api_shape_check._main import _get_operand_names # noqa: PLC2701
2
-
3
-
4
- def test_get_operand_names():
5
- def inner(a, b) -> list[str | None]:
6
- return _get_operand_names(a, b)
7
-
8
- c = 1
9
- assert inner(c, 4 * 4)[0] == "c"
10
- # assert inner(c, 4 * 4)[1] is not None
11
- # assert "@py_assert" in repr(inner(c, 4 * 4)[1])