Skip to content

Commit

Permalink
Merge pull request #168 from asmeurer/ndindex-getitem
Browse files Browse the repository at this point in the history
Allow constructing indices with ndindex[idx]
  • Loading branch information
asmeurer authored Feb 7, 2024
2 parents 80c57df + 0e5d3df commit e5629f5
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 69 deletions.
2 changes: 1 addition & 1 deletion benchmarks/ndindex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 20 additions & 19 deletions docs/type-confusion.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```

Expand All @@ -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.)
```
Expand All @@ -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]):
...
```

Expand Down Expand Up @@ -211,15 +212,15 @@ 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)
```

**Wrong:**

```py
idx = ndindex((0, slice(0, 1))
idx = ndindex[0, 0:1]
idx[0] # Produces an error
```

Expand All @@ -233,7 +234,7 @@ Additionally, some advice for specific types:

**Better:**
```py
ndindex((0, slice(0, 1)))
ndindex[0, 0:1]
```

**Wrong:**
Expand All @@ -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:**
Expand All @@ -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
```


Expand All @@ -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()
```

Expand Down Expand Up @@ -347,7 +348,7 @@ for `None`.
**Right:**

```py
idx = ndindex(np.newaxis)
idx = ndindex[np.newaxis]
idx.reduce()
```

Expand All @@ -365,25 +366,25 @@ 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
above. It is best to just use `np.newaxis` or `None`, which is more concise
and easier to read.

```py
idx = ndindex((0, Newaxis(), 1)) # Easy to confuse
idx = ndindex[0, Newaxis(), 1] # Easy to confuse
idx.reduce()
```

Expand Down
134 changes: 87 additions & 47 deletions ndindex/ndindex.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,77 +4,117 @@

newaxis = None

def ndindex(obj):
class NDIndexConstructor:
"""
Convert an object into an ndindex type
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 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)` 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)
>>> ndindex(0:10)
Traceback (most recent call last):
...
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:
>>> ndindex[0, 1]
Tuple(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
>>> ndindex((0, 1))
Tuple(0, 1)
Therefore `ndindex[idx]` should generally be preferred when creating an
index from a tuple or slice literal.
"""
if isinstance(obj, NDIndex):
return obj
def __getitem__(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 __call__(self, obj):
return self[obj]

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):
Expand Down
7 changes: 7 additions & 0 deletions ndindex/slice.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = ()

Expand Down
2 changes: 2 additions & 0 deletions ndindex/tests/test_ndindex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions ndindex/tests/test_no_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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(...)
Expand Down

0 comments on commit e5629f5

Please sign in to comment.