array-api-shape-check 0.1.3__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,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,67 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = [ "setuptools>=77.0.3" ]
4
+
5
+ [project]
6
+ name = "array-api-shape-check"
7
+ version = "1.0.1"
8
+ description = "Check shapes of input arrays easily"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [
12
+ { name = "34j" },
13
+ ]
14
+ requires-python = ">=3.12"
15
+ classifiers = [
16
+ "Development Status :: 2 - Pre-Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Natural Language :: English",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Software Development :: Libraries",
25
+ ]
26
+ dependencies = [
27
+ "attrs>=26.1",
28
+ "more-itertools>=11.0.2",
29
+ "numpy>=2.4.4",
30
+ "types-array-api>=1.1.4",
31
+ ]
32
+ urls."Bug Tracker" = "https://github.com/34j/array-api-shape-check/issues"
33
+ urls.Changelog = "https://github.com/34j/array-api-shape-check/blob/main/CHANGELOG.md"
34
+ urls.documentation = "https://array-api-shape-check.readthedocs.io"
35
+ urls.repository = "https://github.com/34j/array-api-shape-check"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "coverage",
40
+ "packaging-34j[test]",
41
+ "pytest",
42
+ "pytest-cov",
43
+ "ty",
44
+ ]
45
+ docs = [
46
+ "packaging-34j[docs]",
47
+ ]
48
+
49
+ [tool.ruff]
50
+ extend = ".venv/ruff.toml"
51
+
52
+ [tool.pyproject-fmt]
53
+ max_supported_python = "3.14"
54
+
55
+ [tool.ty]
56
+ terminal.error-on-warning = true
57
+
58
+ [tool.pytest]
59
+ addopts = [
60
+ "-v",
61
+ "-Wonce",
62
+ "-p no:doctest",
63
+ "--cov=array_api_shape_check",
64
+ "--cov-report=term",
65
+ "--cov-report=xml",
66
+ ]
67
+ pythonpath = [ "src" ]
@@ -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,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,149 +0,0 @@
1
- [build-system]
2
- build-backend = "setuptools.build_meta"
3
- requires = [ "setuptools>=77.0.3" ]
4
-
5
- [project]
6
- name = "array-api-shape-check"
7
- version = "0.1.3"
8
- description = "Check shapes of input arrays easily"
9
- readme = "README.md"
10
- license = "MIT"
11
- authors = [
12
- { name = "34j", email = "34j.95a2p@simplelogin.com" },
13
- ]
14
- requires-python = ">=3.12"
15
- classifiers = [
16
- "Development Status :: 2 - Pre-Alpha",
17
- "Intended Audience :: Developers",
18
- "Natural Language :: English",
19
- "Operating System :: OS Independent",
20
- "Programming Language :: Python :: 3 :: Only",
21
- "Programming Language :: Python :: 3.12",
22
- "Programming Language :: Python :: 3.13",
23
- "Programming Language :: Python :: 3.14",
24
- "Topic :: Software Development :: Libraries",
25
- ]
26
- dependencies = [
27
- "attrs>=26.1",
28
- "more-itertools>=11.0.2",
29
- "numpy>=2.4.4",
30
- "types-array-api>=1.1.4",
31
- ]
32
- urls."Bug Tracker" = "https://github.com/34j/array-api-shape-check/issues"
33
- urls.Changelog = "https://github.com/34j/array-api-shape-check/blob/main/CHANGELOG.md"
34
- urls.documentation = "https://array-api-shape-check.readthedocs.io"
35
- urls.repository = "https://github.com/34j/array-api-shape-check"
36
-
37
- [dependency-groups]
38
- dev = [
39
- "coverage",
40
- "numpy>=2.4.4",
41
- "pytest==9.*",
42
- "pytest-cov==7.*",
43
- "sybil[pytest]",
44
- "ty",
45
- ]
46
- docs = [
47
- "furo; python_version>='3.12'",
48
- "myst-parser; python_version>='3.12'",
49
- "sphinx==9.*; python_version>='3.12'",
50
- "sphinx-autobuild; python_version>='3.12'",
51
- ]
52
-
53
- [tool.ruff]
54
- line-length = 100
55
- preview = true
56
- format.docstring-code-format = true
57
- lint.select = [
58
- "B", # flake8-bugbear
59
- "C4", # flake8-comprehensions
60
- "D", # flake8-docstrings
61
- "E", # pycodestyle
62
- "F", # pyflake
63
- "I", # isort
64
- "RUF", # ruff specific
65
- "S", # flake8-bandit
66
- "UP", # pyupgrade
67
- "W", # pycodestyle
68
- ]
69
- lint.ignore = [
70
- "D100", # Missing docstring in public module
71
- "D104", # Missing docstring in public package
72
- "D107", # Missing docstring in `__init__`
73
- "D203", # 1 blank line required before class docstring
74
- "D212", # Multi-line docstring summary should start at the first line
75
- "D401", # First line of docstring should be in imperative mood
76
- ]
77
- lint.per-file-ignores."conftest.py" = [ "D100" ]
78
- lint.per-file-ignores."docs/conf.py" = [ "D100" ]
79
- lint.per-file-ignores."setup.py" = [ "D100" ]
80
- lint.per-file-ignores."tests/**/*" = [
81
- "D100",
82
- "D101",
83
- "D102",
84
- "D103",
85
- "D104",
86
- "S101",
87
- ]
88
- lint.isort.known-first-party = [ "array_api_shape_check", "tests" ]
89
-
90
- [tool.pyproject-fmt]
91
- max_supported_python = "3.14"
92
-
93
- [tool.ty]
94
- terminal.error-on-warning = true
95
- src.exclude = [ "docs/" ]
96
-
97
- [tool.pytest]
98
- minversion = "9.0"
99
- addopts = [
100
- "-v",
101
- "-Wonce",
102
- "-p no:doctest",
103
- "--cov=array_api_shape_check",
104
- "--cov-report=term",
105
- "--cov-report=xml",
106
- ]
107
- pythonpath = [ "src" ]
108
-
109
- [tool.coverage]
110
- run.branch = true
111
- paths.source = [
112
- "src",
113
- ".tox/**/site-packages",
114
- ]
115
- report.exclude_lines = [
116
- "@overload",
117
- 'if __name__ == "__main__":',
118
- "if TYPE_CHECKING",
119
- "pragma: no cover",
120
- "raise NotImplementedError",
121
- ]
122
-
123
- [tool.semantic_release]
124
- allow_zero_version = true
125
- version_toml = [ "pyproject.toml:project.version" ]
126
- version_variables = [
127
- "src/array_api_shape_check/__init__.py:__version__",
128
- "docs/conf.py:release",
129
- ]
130
- build_command = """
131
- pip install uv
132
- uv lock
133
- git add uv.lock
134
- uv build
135
- """
136
- branches.main.match = "main"
137
- branches.noop.match = "(?!main$)"
138
- branches.noop.prerelease = true
139
- changelog.exclude_commit_patterns = [
140
- "chore(?:\\([^)]*?\\))?: .+",
141
- "ci(?:\\([^)]*?\\))?: .+",
142
- "refactor(?:\\([^)]*?\\))?: .+",
143
- "style(?:\\([^)]*?\\))?: .+",
144
- "test(?:\\([^)]*?\\))?: .+",
145
- "build\\((?!deps\\): .+)",
146
- "Merged? .*",
147
- "Initial [Cc]ommit.*", # codespell:ignore
148
- ]
149
- changelog.environment.keep_trailing_newline = true
@@ -1,11 +0,0 @@
1
- from array_api_shape_check._main import _get_operand_names
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])