From f3d2bcb7f3af4c9cb13cb14e6c6a002ec79f038a Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 22 Nov 2023 00:23:10 -0700 Subject: [PATCH 01/11] Allow constructing indices with ndindex[idx] --- ndindex/ndindex.py | 100 +++++++++++++++++++--------------- ndindex/tests/test_ndindex.py | 2 + 2 files changed, 58 insertions(+), 44 deletions(-) diff --git a/ndindex/ndindex.py b/ndindex/ndindex.py index c9e04e03..5338006a 100644 --- a/ndindex/ndindex.py +++ b/ndindex/ndindex.py @@ -4,7 +4,7 @@ newaxis = None -def ndindex(obj): +class NdindexConstructor: """ Convert an object into an ndindex type @@ -16,65 +16,77 @@ def ndindex(obj): >>> ndindex(slice(0, 10)) Slice(0, 10, None) + You can also index the `ndindex` object directly. This is useful for + creating slices, as the `a:b` slice syntax is only valid in an index. + + >>> ndindex[0:10, :] + Tuple(slice(0, 10, None), slice(None, None, None)) + """ - if isinstance(obj, NDIndex): - return obj + def __call__(self, obj): + if isinstance(obj, NDIndex): + return obj + + if 'numpy' in sys.modules: + from numpy import ndarray, bool_ + else: # pragma: no cover + bool_ = bool + ndarray = () + + if isinstance(obj, (bool, bool_)): + from . import BooleanArray + return BooleanArray(obj) - if 'numpy' in sys.modules: - from numpy import ndarray, bool_ - else: # pragma: no cover - bool_ = bool - ndarray = () + if isinstance(obj, (list, ndarray)): + from . import IntegerArray, BooleanArray - if isinstance(obj, (bool, bool_)): - from . import BooleanArray - return BooleanArray(obj) + try: + return IntegerArray(obj) + except TypeError: + pass + try: + return BooleanArray(obj) + except TypeError: + pass - if isinstance(obj, (list, ndarray)): - from . import IntegerArray, BooleanArray + # Match the NumPy exceptions + if isinstance(obj, ndarray): + raise IndexError("arrays used as indices must be of integer (or boolean) type") + else: + raise IndexError("only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and integer or boolean arrays are valid indices") try: - return IntegerArray(obj) - except TypeError: - pass - try: - return BooleanArray(obj) + from . import Integer + # If operator.index() works, use that + return Integer(obj) except TypeError: pass - # Match the NumPy exceptions - if isinstance(obj, ndarray): - raise IndexError("arrays used as indices must be of integer (or boolean) type") - else: - raise IndexError("only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and integer or boolean arrays are valid indices") + if isinstance(obj, slice): + from . import Slice + return Slice(obj) - try: - from . import Integer - # If operator.index() works, use that - return Integer(obj) - except TypeError: - pass + if isinstance(obj, tuple): + from . import Tuple + return Tuple(*obj) - if isinstance(obj, slice): - from . import Slice - return Slice(obj) + from . import ellipsis - if isinstance(obj, tuple): - from . import Tuple - return Tuple(*obj) + if obj == ellipsis: + raise IndexError("Got ellipsis class. Did you mean to use the instance, ellipsis()?") + if obj is Ellipsis: + return ellipsis() - from . import ellipsis + if obj == newaxis: + from . import Newaxis + return Newaxis() - if obj == ellipsis: - raise IndexError("Got ellipsis class. Did you mean to use the instance, ellipsis()?") - if obj is Ellipsis: - return ellipsis() + raise IndexError("only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and integer or boolean arrays are valid indices") - if obj == newaxis: - from . import Newaxis - return Newaxis() + def __getitem__(self, idx): + return self(idx) - raise IndexError("only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and integer or boolean arrays are valid indices") +ndindex = NdindexConstructor() class classproperty(object): def __init__(self, f): diff --git a/ndindex/tests/test_ndindex.py b/ndindex/tests/test_ndindex.py index 8b537a99..b13cccec 100644 --- a/ndindex/tests/test_ndindex.py +++ b/ndindex/tests/test_ndindex.py @@ -78,6 +78,8 @@ def test_eq_explicit(): def test_ndindex(idx): index = ndindex(idx) assert index == idx + assert ndindex[idx] == index + def test_raw_eq(idx, index): if isinstance(idx, np.ndarray): assert_equal(index.raw, idx) From a866e44229a5e6596674c9c4524dd366b62f35f1 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 22 Nov 2023 00:34:47 -0700 Subject: [PATCH 02/11] Document that ndindex[idx] should be preferred over ndindex(idx) --- ndindex/ndindex.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/ndindex/ndindex.py b/ndindex/ndindex.py index 5338006a..5b325ab3 100644 --- a/ndindex/ndindex.py +++ b/ndindex/ndindex.py @@ -6,24 +6,27 @@ class NdindexConstructor: """ - Convert an object into an ndindex type + Convert an object into an ndindex type using indexing. Invalid indices will raise `IndexError`. >>> from ndindex import ndindex - >>> ndindex(1) + >>> ndindex[1] Integer(1) + >>> ndindex[0:10, :] + Tuple(slice(0, 10, None), slice(None, None, None)) + + You can also create indices by calling `ndindex(idx)`. However, if you do + this, you cannot use the `a:b` slice syntax, as it is not syntatically valid. + >>> ndindex(slice(0, 10)) Slice(0, 10, None) - You can also index the `ndindex` object directly. This is useful for - creating slices, as the `a:b` slice syntax is only valid in an index. - - >>> ndindex[0:10, :] - Tuple(slice(0, 10, None), slice(None, None, None)) + `ndindex[idx]` should generally be preferred. `ndindex(idx)` is provided + for backwards compatibility. """ - def __call__(self, obj): + def __getitem__(self, obj): if isinstance(obj, NDIndex): return obj @@ -83,8 +86,8 @@ def __call__(self, obj): raise IndexError("only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and integer or boolean arrays are valid indices") - def __getitem__(self, idx): - return self(idx) + def __call__(self, obj): + return self[obj] ndindex = NdindexConstructor() From a32d4cfc41c80520e1ed8ad70d53ff626889a4b7 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 22 Nov 2023 00:38:39 -0700 Subject: [PATCH 03/11] Use more consistent class name for ndindex --- ndindex/ndindex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ndindex/ndindex.py b/ndindex/ndindex.py index 5b325ab3..dc475d78 100644 --- a/ndindex/ndindex.py +++ b/ndindex/ndindex.py @@ -4,7 +4,7 @@ newaxis = None -class NdindexConstructor: +class NDIndexConstructor: """ Convert an object into an ndindex type using indexing. From e7fee14795eafba0ea4208499a9854e9d29ed7ed Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 22 Nov 2023 00:38:51 -0700 Subject: [PATCH 04/11] Be more tolerant about ndindex[idx] vs. ndindex(idx) I think the call syntax is just fine if you are converting a variable. It's a little clearer it's a constructor. The getitem syntax is really only preferable when it reduces typing for tuple and slice literals. --- ndindex/ndindex.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/ndindex/ndindex.py b/ndindex/ndindex.py index dc475d78..604d46c6 100644 --- a/ndindex/ndindex.py +++ b/ndindex/ndindex.py @@ -17,13 +17,32 @@ class NDIndexConstructor: Tuple(slice(0, 10, None), slice(None, None, None)) You can also create indices by calling `ndindex(idx)`. However, if you do - this, you cannot use the `a:b` slice syntax, as it is not syntatically valid. + this, you cannot use the `a:b` slice syntax, as it is not syntactically + valid. >>> ndindex(slice(0, 10)) Slice(0, 10, None) + >>> ndindex(0:10) + Traceback (most recent call last): + ... + ndindex(0:10) + ^ + SyntaxError: invalid syntax + + Additionally, the `ndindex[idx]` syntax does not require parentheses when + creating a tuple index. + + >>> ndindex[0, 1] + Tuple(0, 1) + >>> ndindex(0, 1) + Traceback (most recent call last): + ... + TypeError: NdindexConstructor.__call__() takes 2 positional arguments but 3 were given + >>> ndindex((0, 1)) + Tuple(0, 1) - `ndindex[idx]` should generally be preferred. `ndindex(idx)` is provided - for backwards compatibility. + Therefore `ndindex[idx]` should generally be preferred when creating an + index from a tuple or slice literal. """ def __getitem__(self, obj): From 53ba85e0bfcfa820077d99b5d0f722676c090d58 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 22 Nov 2023 00:42:22 -0700 Subject: [PATCH 05/11] Fix undefined variable --- ndindex/ndindex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ndindex/ndindex.py b/ndindex/ndindex.py index 604d46c6..e3c9ad55 100644 --- a/ndindex/ndindex.py +++ b/ndindex/ndindex.py @@ -108,7 +108,7 @@ def __getitem__(self, obj): def __call__(self, obj): return self[obj] -ndindex = NdindexConstructor() +ndindex = NDIndexConstructor() class classproperty(object): def __init__(self, f): From 1252f1c91b09a77409168c42e28ccf00ed55cecd Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 22 Nov 2023 00:42:52 -0700 Subject: [PATCH 06/11] Write more in the ndindex docstring --- ndindex/ndindex.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ndindex/ndindex.py b/ndindex/ndindex.py index e3c9ad55..556823a7 100644 --- a/ndindex/ndindex.py +++ b/ndindex/ndindex.py @@ -6,10 +6,12 @@ class NDIndexConstructor: """ - Convert an object into an ndindex type using indexing. + Convert an object into an ndindex type. Invalid indices will raise `IndexError`. + Indices are created by calling `ndindex` with getitem syntax: + >>> from ndindex import ndindex >>> ndindex[1] Integer(1) @@ -18,9 +20,9 @@ class NDIndexConstructor: You can also create indices by calling `ndindex(idx)`. However, if you do this, you cannot use the `a:b` slice syntax, as it is not syntactically - valid. + valid: - >>> ndindex(slice(0, 10)) + >>> ndindex[0:10] Slice(0, 10, None) >>> ndindex(0:10) Traceback (most recent call last): @@ -28,16 +30,18 @@ class NDIndexConstructor: ndindex(0:10) ^ SyntaxError: invalid syntax + >>> ndindex(slice(0, 10)) + Slice(0, 10, None) Additionally, the `ndindex[idx]` syntax does not require parentheses when - creating a tuple index. + creating a tuple index: >>> ndindex[0, 1] Tuple(0, 1) >>> ndindex(0, 1) Traceback (most recent call last): ... - TypeError: NdindexConstructor.__call__() takes 2 positional arguments but 3 were given + TypeError: NDIndexConstructor.__call__() takes 2 positional arguments but 3 were given >>> ndindex((0, 1)) Tuple(0, 1) From c99982fee4aa1d9a1ae1bd7ea2d7a691e6f7e17c Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 22 Nov 2023 00:49:48 -0700 Subject: [PATCH 07/11] Update places that use ndindex() to use ndindex[] I've primarily only done this in places where the index is a literal (particularly a slice or tuple literal, which is the only place where it matters). In other places, especially in the code, a generic ndindex(idx) is clearer as a constructor than ndindex[idx], so I've left it alone. --- benchmarks/ndindex.py | 2 +- docs/type-confusion.md | 39 ++++++++++++++------------- ndindex/slice.py | 7 +++++ ndindex/tests/test_no_dependencies.py | 4 +-- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/benchmarks/ndindex.py b/benchmarks/ndindex.py index d940db11..fd7600b9 100644 --- a/benchmarks/ndindex.py +++ b/benchmarks/ndindex.py @@ -47,7 +47,7 @@ def setup(self): self.tuple = (slice(0, 4, 2), ..., 1) def time_ndindex_slice(self): - ndindex(slice(0, 4, 2)) + ndindex[0:4:2] def time_ndindex_int(self): ndindex(1) diff --git a/docs/type-confusion.md b/docs/type-confusion.md index b05bf3f5..7ff3a6ba 100644 --- a/docs/type-confusion.md +++ b/docs/type-confusion.md @@ -64,8 +64,8 @@ Some general types to help avoid type confusion: ```py indices = { - ndindex(slice(0, 10)): 0, - ndindex(slice(10, 20)): 1 + ndindex[0:10]: 0, + ndindex[10:20]: 1 } ``` @@ -85,7 +85,7 @@ Some general types to help avoid type confusion: ```py # Typo would be caught right away - idx = ndindex(slice(1, 2.)) + idx = ndindex[1:2.] # OR idx = Slice(1, 2.) ``` @@ -106,7 +106,8 @@ Some general types to help avoid type confusion: **Right** ```py - if ndindex(idx) == ndindex((slice(0, 2), np.array([0, 0])): + # Note only one side of the == needs to be an ndindex type + if ndindex(idx) == (slice(0, 2), np.array([0, 0]): ... ``` @@ -211,7 +212,7 @@ Additionally, some advice for specific types: **Right:** ```py - idx = ndindex((0, slice(0, 1)) + idx = ndindex[0, 0:1] idx.raw[0] # Gives int(0) idx.args[0] # Gives Integer(0) ``` @@ -219,7 +220,7 @@ Additionally, some advice for specific types: **Wrong:** ```py - idx = ndindex((0, slice(0, 1)) + idx = ndindex[0, 0:1] idx[0] # Produces an error ``` @@ -233,7 +234,7 @@ Additionally, some advice for specific types: **Better:** ```py - ndindex((0, slice(0, 1))) + ndindex[0, 0:1] ``` **Wrong:** @@ -246,7 +247,7 @@ Additionally, some advice for specific types: ## ellipsis - You should almost never use the ndindex {class}`~.ellipsis` class directly. - Instead, **use `...` or `ndindex(...)`**. As noted above, all ndindex + Instead, **use `...` or `ndindex[...]`**. As noted above, all ndindex methods and `Tuple` will automatically convert `...` into the ndindex type. **Right:** @@ -270,13 +271,13 @@ Additionally, some advice for specific types: **Right:** ```py - idx = ndindex((0, ..., 1)) + idx = ndindex[0, ..., 1] ``` **Wrong:** ```py - idx = ndindex((0, Ellipsis, 1)) # Less readable + idx = ndindex[0, Ellipsis, 1] # Less readable ``` @@ -287,25 +288,25 @@ Additionally, some advice for specific types: **Right:** ```py - idx = ndindex((0, ..., 1)) + idx = ndindex[0, ..., 1] ``` **Wrong:** ```py - idx = ndindex((0, ellipsis, 1)) # Gives an error + idx = ndindex[0, ellipsis, 1] # Gives an error ``` The below do not give errors, but it is easy to confuse them with the above. It is best to just use `...`, which is more concise and easier to read. ```py - idx = ndindex((0, ellipsis(), 1)) # Easy to confuse, less readable + idx = ndindex[0, ellipsis(), 1] # Easy to confuse, less readable idx.reduce() ``` ```py - idx = ndindex((0, Ellipsis, 1)) # Easy to confuse, less readable + idx = ndindex[0, Ellipsis, 1] # Easy to confuse, less readable idx.reduce() ``` @@ -347,7 +348,7 @@ for `None`. **Right:** ```py - idx = ndindex(np.newaxis) + idx = ndindex[np.newaxis] idx.reduce() ``` @@ -365,17 +366,17 @@ for `None`. **Right:** ```py - idx = ndindex((0, np.newaxis, 1)) + idx = ndindex[0, np.newaxis, 1] ``` ```py - idx = ndindex((0, None, 1)) + idx = ndindex[0, None, 1] ``` **Wrong:** ```py - idx = ndindex((0, Newaxis, 1)) # Gives an error + idx = ndindex[0, Newaxis, 1] # Gives an error ``` The below does not give an error, but it is easy to confuse it with the @@ -383,7 +384,7 @@ for `None`. and easier to read. ```py - idx = ndindex((0, Newaxis(), 1)) # Easy to confuse + idx = ndindex[0, Newaxis(), 1] # Easy to confuse idx.reduce() ``` diff --git a/ndindex/slice.py b/ndindex/slice.py index c73aca43..de330cac 100644 --- a/ndindex/slice.py +++ b/ndindex/slice.py @@ -49,6 +49,13 @@ class Slice(NDIndex): >>> s.raw slice(None, 10, None) + For most use-cases, it's more convenient to create Slice objects using + `ndindex[slice]`, which allows using `a:b` slicing syntax: + + >>> from ndindex import ndindex + >>> ndindex[0: 10] + Slice(0, 10, None + """ __slots__ = () diff --git a/ndindex/tests/test_no_dependencies.py b/ndindex/tests/test_no_dependencies.py index 9cf715f7..f43e595f 100644 --- a/ndindex/tests/test_no_dependencies.py +++ b/ndindex/tests/test_no_dependencies.py @@ -27,7 +27,7 @@ def _test_dependency_ndindex(mod): ndindex(1) assert mod not in sys.modules - ndindex(slice(0, 1)) + ndindex[0:1] assert mod not in sys.modules ndindex(ndindex(1)) @@ -36,7 +36,7 @@ def _test_dependency_ndindex(mod): ndindex(None) assert mod not in sys.modules - ndindex((1, slice(0, 1))) + ndindex[1, 0:1] assert mod not in sys.modules ndindex(...) From b2bc4e6005bc49c97459a43723e7c6315e57088e Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Fri, 24 Nov 2023 01:21:46 -0700 Subject: [PATCH 08/11] Fix typo in doctest --- ndindex/slice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ndindex/slice.py b/ndindex/slice.py index de330cac..ab18c860 100644 --- a/ndindex/slice.py +++ b/ndindex/slice.py @@ -53,8 +53,8 @@ class Slice(NDIndex): `ndindex[slice]`, which allows using `a:b` slicing syntax: >>> from ndindex import ndindex - >>> ndindex[0: 10] - Slice(0, 10, None + >>> ndindex[0:10] + Slice(0, 10, None) """ __slots__ = () From 91291fa1ef7a3028be7756c7323d0352d27a2932 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Fri, 24 Nov 2023 01:26:54 -0700 Subject: [PATCH 09/11] Fix some wording --- ndindex/ndindex.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ndindex/ndindex.py b/ndindex/ndindex.py index 556823a7..a688b0ba 100644 --- a/ndindex/ndindex.py +++ b/ndindex/ndindex.py @@ -18,9 +18,9 @@ class NDIndexConstructor: >>> ndindex[0:10, :] Tuple(slice(0, 10, None), slice(None, None, None)) - You can also create indices by calling `ndindex(idx)`. However, if you do - this, you cannot use the `a:b` slice syntax, as it is not syntactically - valid: + You can also create indices by calling `ndindex(idx)` like a function. + However, if you do this, you cannot use the `a:b` slice syntax, as it is + not syntactically valid: >>> ndindex[0:10] Slice(0, 10, None) From 2c00367fd7f2e87fb2d5c0bc6557fe32a304dde6 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Fri, 24 Nov 2023 01:28:16 -0700 Subject: [PATCH 10/11] Correct text about what exceptions ndindex raises --- ndindex/ndindex.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ndindex/ndindex.py b/ndindex/ndindex.py index a688b0ba..1e437427 100644 --- a/ndindex/ndindex.py +++ b/ndindex/ndindex.py @@ -8,7 +8,9 @@ class NDIndexConstructor: """ Convert an object into an ndindex type. - Invalid indices will raise `IndexError`. + Invalid indices will raise `IndexError`, `TypeError`, or `ValueError` + (generally, the same error NumPy would raise if the index were used on an + array). Indices are created by calling `ndindex` with getitem syntax: From 40e34a459559e4c547a783c0cec1915d87f20b18 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 3 Jan 2024 16:56:00 -0700 Subject: [PATCH 11/11] Fix failing doctest in older Python versions --- ndindex/ndindex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ndindex/ndindex.py b/ndindex/ndindex.py index 1e437427..823ca445 100644 --- a/ndindex/ndindex.py +++ b/ndindex/ndindex.py @@ -40,7 +40,7 @@ class NDIndexConstructor: >>> ndindex[0, 1] Tuple(0, 1) - >>> ndindex(0, 1) + >>> ndindex(0, 1) # doctest:+IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... TypeError: NDIndexConstructor.__call__() takes 2 positional arguments but 3 were given