$$
\require{enclose}
\begin{aligned}
@@ -855,38 +856,38 @@ reasons why this way of thinking creates more confusion than it removes.
\begin{array}{r r r r r r r r r r r r r r r r r r}
a = & [&\phantom{|}&\mathtt{\textsf{'}a\textsf{'}}, &\phantom{|}& \mathtt{\textsf{'}b\textsf{'}}, &\phantom{|}& \mathtt{\textsf{'}c\textsf{'}}, &\phantom{|}& \mathtt{\textsf{'}d\textsf{'}}, &\phantom{|}& \mathtt{\textsf{'}e\textsf{'}}, &\phantom{|}& \mathtt{\textsf{'}f\textsf{'}}, &\phantom{|}& \mathtt{\textsf{'}g\textsf{'}}&\phantom{|}&]&\\
&
- & \color{red}{|}
+ & \color{#EE0000}{|}
&
- & \color{red}{|}
+ & \color{#EE0000}{|}
&
- & \color{red}{|}
+ & \color{#EE0000}{|}
&
- & \color{red}{|}
+ & \color{#EE0000}{|}
&
- & \color{blue}{|}
+ & \color{#5E5EFF}{|}
&
- & \color{blue}{|}
+ & \color{#5E5EFF}{|}
&
- & \color{blue}{|}
+ & \color{#5E5EFF}{|}
&
- & \color{red}{|}\\
- \color{red}{\text{index}}
+ & \color{#EE0000}{|}\\
+ \color{#EE0000}{\text{index}}
&
- & \color{red}{-8}
+ & \color{#EE0000}{-8}
&
- & \color{red}{-7}
+ & \color{#EE0000}{-7}
&
- & \color{red}{-6}
+ & \color{#EE0000}{-6}
&
- & \color{red}{-5}
+ & \color{#EE0000}{-5}
&
- & \color{blue}{-4}
+ & \color{#5E5EFF}{-4}
&
- & \color{blue}{-3}
+ & \color{#5E5EFF}{-3}
&
- & \color{blue}{-2}
+ & \color{#5E5EFF}{-2}
&
- & \color{red}{-1}\\
+ & \color{#EE0000}{-1}\\
\end{array}\\
\small{\text{(not a great way of thinking about negative indices)}}
\end{array}
@@ -896,7 +897,7 @@ reasons why this way of thinking creates more confusion than it removes.
a[-2:-4:-1] "==" ['e', 'd']
-
(WRONG)
+
(WRONG)
$$
\require{enclose}
\begin{aligned}
@@ -904,40 +905,40 @@ reasons why this way of thinking creates more confusion than it removes.
\begin{array}{r r r r r r r r r r r r r r r r r r}
a = & [&\phantom{|}&\mathtt{\textsf{'}a\textsf{'}}, &\phantom{|}& \mathtt{\textsf{'}b\textsf{'}}, &\phantom{|}& \mathtt{\textsf{'}c\textsf{'}}, &\phantom{|}& \mathtt{\textsf{'}d\textsf{'}}, &\phantom{|}& \mathtt{\textsf{'}e\textsf{'}}, &\phantom{|}& \mathtt{\textsf{'}f\textsf{'}}, &\phantom{|}& \mathtt{\textsf{'}g\textsf{'}}&\phantom{|}&]&\\
&
- & \color{red}{|}
+ & \color{#EE0000}{|}
&
- & \color{red}{|}
+ & \color{#EE0000}{|}
&
- & \color{red}{|}
+ & \color{#EE0000}{|}
&
- & \color{blue}{|}
+ & \color{#5E5EFF}{|}
&
- & \color{blue}{|}
+ & \color{#5E5EFF}{|}
&
- & \color{blue}{|}
+ & \color{#5E5EFF}{|}
&
- & \color{red}{|}
+ & \color{#EE0000}{|}
&
- & \color{red}{|}\\
- \color{red}{\text{index}}
+ & \color{#EE0000}{|}\\
+ \color{#EE0000}{\text{index}}
&
- & \color{red}{-7}
+ & \color{#EE0000}{-7}
&
- & \color{red}{-6}
+ & \color{#EE0000}{-6}
&
- & \color{red}{-5}
+ & \color{#EE0000}{-5}
&
- & \color{blue}{-4}
+ & \color{#5E5EFF}{-4}
&
- & \color{blue}{-3}
+ & \color{#5E5EFF}{-3}
&
- & \color{blue}{-2}
+ & \color{#5E5EFF}{-2}
&
- & \color{red}{-1}
+ & \color{#EE0000}{-1}
&
- & \color{red}{0}\\
+ & \color{#EE0000}{0}\\
\end{array}\\
- \small{\color{red}{\textbf{THIS IS WRONG!}}}
+ \small{\color{#EE0000}{\textbf{THIS IS WRONG!}}}
\end{array}
\end{aligned}
$$
@@ -1006,12 +1007,16 @@ here. It isn't worth it.
### Negative Indices
Negative indices in slices work the same way they do with [integer
-indices](integer-indices). **For `a[start:stop:step]`, negative `start` or
-`stop` use −1-based indexing from the end of `a`.** However, negative `start`
-or `stop` does *not* change the order of the slicing---only the `step` does
-that. The other [rules](rules) of slicing do not change when the `start` or
-`stop` is negative. [The `stop` is still not included](half-open), values less
-than `-len(a)` still [clip](clipping), and so on.
+indices](integer-indices).
+
+> **For `a[start:stop:step]`, negative `start` or `stop` use −1-based indexing
+ from the end of `a`.**
+
+However, negative `start` or `stop` does *not* change the order of the
+slicing---only the [`step`](steps) does that. The other [rules](rules) of
+slicing do not change when the `start` or `stop` is negative. [The `stop` is
+still not included](half-open), values less than `-len(a)` still
+[clip](clipping), and so on.
Note that positive and negative indices can be mixed. The following slices of
`a` all produce `['d', 'e']`:
@@ -1023,22 +1028,22 @@ $$
\begin{aligned}
\begin{array}{r c c c c c c c}
a = & [\mathtt{\textsf{'}a\textsf{'}}, & \mathtt{\textsf{'}b\textsf{'}}, & \mathtt{\textsf{'}c\textsf{'}}, & \mathtt{\textsf{'}d\textsf{'}}, & \mathtt{\textsf{'}e\textsf{'}}, & \mathtt{\textsf{'}f\textsf{'}}, & \mathtt{\textsf{'}g\textsf{'}}]\\
-\color{red}{\text{nonnegative index}}
- & \color{red}{0\phantom{,}}
- & \color{red}{1\phantom{,}}
- & \color{red}{\enclose{circle}{2}\phantom{,}}
- & \color{blue}{3\phantom{,}}
- & \color{blue}{4\phantom{,}}
- & \color{red}{\enclose{circle}{5}\phantom{,}}
- & \color{red}{6\phantom{,}}\\
-\color{red}{\text{negative index}}
- & \color{red}{-7\phantom{,}}
- & \color{red}{-6\phantom{,}}
- & \color{red}{\enclose{circle}{-5}\phantom{,}}
- & \color{blue}{-4\phantom{,}}
- & \color{blue}{-3\phantom{,}}
- & \color{red}{\enclose{circle}{-2}\phantom{,}}
- & \color{red}{-1\phantom{,}}\\
+\color{#EE0000}{\text{nonnegative index}}
+ & \color{#EE0000}{0\phantom{,}}
+ & \color{#EE0000}{1\phantom{,}}
+ & \color{#EE0000}{\enclose{circle}{2}\phantom{,}}
+ & \color{#5E5EFF}{3\phantom{,}}
+ & \color{#5E5EFF}{4\phantom{,}}
+ & \color{#EE0000}{\enclose{circle}{5}\phantom{,}}
+ & \color{#EE0000}{6\phantom{,}}\\
+\color{#EE0000}{\text{negative index}}
+ & \color{#EE0000}{-7\phantom{,}}
+ & \color{#EE0000}{-6\phantom{,}}
+ & \color{#EE0000}{\enclose{circle}{-5}\phantom{,}}
+ & \color{#5E5EFF}{-4\phantom{,}}
+ & \color{#5E5EFF}{-3\phantom{,}}
+ & \color{#EE0000}{\enclose{circle}{-2}\phantom{,}}
+ & \color{#EE0000}{-1\phantom{,}}\\
\end{array}
\end{aligned}
$$
@@ -1141,7 +1146,8 @@ example, instead of using `mid - n//2`, we could use `max(mid - n//2, 0)`.
Slices can never give an out-of-bounds `IndexError`. This is different from
[integer indices](integer-indices) which require the index to be in bounds.
-**If `start` or `stop` index before the beginning or after the end of the
+
+> **If `start` or `stop` index before the beginning or after the end of the
`a`, they will clip to the bounds of `a`**:
```py
@@ -1198,9 +1204,9 @@ Thus far, we have only considered slices with the default step size of 1. When
the step is greater than 1, the slice picks every `step` element contained in
the bounds of `start` and `stop`.
-**The proper way to think about `step` is that the slice starts at `start` and
-successively adds `step` until it reaches an index that is at or past the
-`stop`, and then stops without including that index.**
+> **The proper way to think about `step` is that the slice starts at `start`
+ and successively adds `step` until it reaches an index that is at or past
+ the `stop`, and then stops without including that index.**
The important thing to remember about the `step` is that it being non-1 does
not change the fundamental [rules](rules) of slices that we have learned so
@@ -1218,21 +1224,21 @@ $$
\begin{aligned}
\begin{array}{r c c c c c c l}
a = & [\mathtt{\textsf{'}a\textsf{'}}, & \mathtt{\textsf{'}b\textsf{'}}, & \mathtt{\textsf{'}c\textsf{'}}, & \mathtt{\textsf{'}d\textsf{'}}, & \mathtt{\textsf{'}e\textsf{'}}, & \mathtt{\textsf{'}f\textsf{'}}, & \mathtt{\textsf{'}g\textsf{'}}]\\
-\color{red}{\text{index}}
- & \color{blue}{\enclose{circle}{0}}
- & \color{red}{1\phantom{,}}
- & \color{red}{2\phantom{,}}
- & \color{blue}{\enclose{circle}{3}}
- & \color{red}{4\phantom{,}}
- & \color{red}{5\phantom{,}}
- & \color{red}{\enclose{circle}{6}}\\
- & \color{blue}{\text{start}}
+\color{#EE0000}{\text{index}}
+ & \color{#5E5EFF}{\enclose{circle}{0}}
+ & \color{#EE0000}{1\phantom{,}}
+ & \color{#EE0000}{2\phantom{,}}
+ & \color{#5E5EFF}{\enclose{circle}{3}}
+ & \color{#EE0000}{4\phantom{,}}
+ & \color{#EE0000}{5\phantom{,}}
+ & \color{#EE0000}{\enclose{circle}{6}}\\
+ & \color{#5E5EFF}{\text{start}}
&
& \rightarrow
- & \color{blue}{+3}
+ & \color{#5E5EFF}{+3}
&
& \rightarrow
- & \color{red}{+3\ (\geq \text{stop})}
+ & \color{#EE0000}{+3\ (\geq \text{stop})}
\end{array}
\end{aligned}
$$
@@ -1265,23 +1271,23 @@ $$
\begin{aligned}
\begin{array}{r c c c c c c c l}
a = & [\mathtt{\textsf{'}a\textsf{'}}, & \mathtt{\textsf{'}b\textsf{'}}, & \mathtt{\textsf{'}c\textsf{'}}, & \mathtt{\textsf{'}d\textsf{'}}, & \mathtt{\textsf{'}e\textsf{'}}, & \mathtt{\textsf{'}f\textsf{'}}, & \mathtt{\textsf{'}g\textsf{'}}]\\
-\color{red}{\text{index}}
- & \color{red}{0\phantom{,}}
- & \color{blue}{\enclose{circle}{1}}
- & \color{red}{2\phantom{,}}
- & \color{red}{3\phantom{,}}
- & \color{blue}{\enclose{circle}{4}}
- & \color{red}{5\phantom{,}}
- & \color{red}{\underline{6}\phantom{,}}
- & \color{red}{\enclose{circle}{\phantom{7}}}\\
+\color{#EE0000}{\text{index}}
+ & \color{#EE0000}{0\phantom{,}}
+ & \color{#5E5EFF}{\enclose{circle}{1}}
+ & \color{#EE0000}{2\phantom{,}}
+ & \color{#EE0000}{3\phantom{,}}
+ & \color{#5E5EFF}{\enclose{circle}{4}}
+ & \color{#EE0000}{5\phantom{,}}
+ & \color{#EE0000}{\underline{6}\phantom{,}}
+ & \color{#EE0000}{\enclose{circle}{\phantom{7}}}\\
&
- & \color{blue}{\text{start}}
+ & \color{#5E5EFF}{\text{start}}
&
& \rightarrow
- & \color{blue}{+3}
+ & \color{#5E5EFF}{+3}
&
& \rightarrow
- & \color{red}{+3\ (\geq \text{stop})}
+ & \color{#EE0000}{+3\ (\geq \text{stop})}
\end{array}
\end{aligned}
$$
@@ -1334,9 +1340,9 @@ slices will necessarily have many piecewise conditions.
Recall what we said [above](steps):
-**The proper way to think about `step` is that the slice starts at `start` and
-successively adds `step` until it reaches an index that is at or past the
-`stop`, and then stops without including that index.**
+> **The proper way to think about `step` is that the slice starts at `start`
+ and successively adds `step` until it reaches an index that is at or past
+ the `stop`, and then stops without including that index.**
The key thing to remember with negative `step` is that this rule still
applies. That is, the index starts at `start` then adds the `step` (which
@@ -1415,22 +1421,22 @@ $$
\begin{aligned}
\begin{array}{r r c c c c c c l}
a = & [\mathtt{\textsf{'}a\textsf{'}}, & \mathtt{\textsf{'}b\textsf{'}}, & \mathtt{\textsf{'}c\textsf{'}}, & \mathtt{\textsf{'}d\textsf{'}}, & \mathtt{\textsf{'}e\textsf{'}}, & \mathtt{\textsf{'}f\textsf{'}}, & \mathtt{\textsf{'}g\textsf{'}}]\\
-\color{red}{\text{index}}
- & \color{red}{\enclose{circle}{0}\phantom{,}}
- & \color{red}{1\phantom{,}}
- & \color{red}{2\phantom{,}}
- & \color{blue}{\enclose{circle}{3}}
- & \color{red}{4\phantom{,}}
- & \color{red}{5\phantom{,}}
- & \color{blue}{\enclose{circle}{6}}\\
- & \color{red}{-3}\phantom{\mathtt{\textsf{'},}}
+\color{#EE0000}{\text{index}}
+ & \color{#EE0000}{\enclose{circle}{0}\phantom{,}}
+ & \color{#EE0000}{1\phantom{,}}
+ & \color{#EE0000}{2\phantom{,}}
+ & \color{#5E5EFF}{\enclose{circle}{3}}
+ & \color{#EE0000}{4\phantom{,}}
+ & \color{#EE0000}{5\phantom{,}}
+ & \color{#5E5EFF}{\enclose{circle}{6}}\\
+ & \color{#EE0000}{-3}\phantom{\mathtt{\textsf{'},}}
& \leftarrow
&
- & \color{blue}{-3}\phantom{\mathtt{\textsf{'},}}
+ & \color{#5E5EFF}{-3}\phantom{\mathtt{\textsf{'},}}
& \leftarrow
&
- & \color{blue}{\text{start}}\\
- & \color{red}{(\leq \text{stop})}
+ & \color{#5E5EFF}{\text{start}}\\
+ & \color{#EE0000}{(\leq \text{stop})}
\end{array}
\end{aligned}
$$
@@ -1468,9 +1474,11 @@ trying to remember some rule based on where a colon is. But the colons in a
slice are not indicators, they are separators.
As to the semantic meaning of omitted entries, the easiest one is the `step`.
-**If the `step` is omitted, it always defaults to `1`.** If the `step` is
-omitted the second colon before the `step` can also be omitted. That is to
-say, the following are completely equivalent:
+
+> **If the `step` is omitted, it always defaults to `1`.**
+
+If the `step` is omitted the second colon before the `step` can also be
+omitted. That is to say, the following are completely equivalent:
```py
a[i:j:1]
@@ -1480,8 +1488,11 @@ a[i:j]
-**For the `start` and `stop`, the rule is that being omitted extends the slice
-all the way to the beginning or end of `a` in the direction being sliced.** If
+> **For the `start` and `stop`, the rule is that being omitted extends the
+ slice all the way to the beginning or end of `a` in the direction being
+ sliced.**
+
+If
the `step` is positive, this means `start` extends to the beginning of `a` and
`stop` extends to the end. If `step` is negative, it is reversed: `start`
extends to the end of `a` and `stop` extends to the beginning.
@@ -1493,28 +1504,28 @@ $$
\begin{aligned}
\begin{array}{r r c c c c c c c c c c c c l}
a = & [\mathtt{\textsf{'}a\textsf{'}}, && \mathtt{\textsf{'}b\textsf{'}}, && \mathtt{\textsf{'}c\textsf{'}}, && \mathtt{\textsf{'}d\textsf{'}}, && \mathtt{\textsf{'}e\textsf{'}}, && \mathtt{\textsf{'}f\textsf{'}}, && \mathtt{\textsf{'}g\textsf{'}}]\\
-\color{red}{\text{index}}
- & \color{blue}{\enclose{circle}{0}}
+\color{#EE0000}{\text{index}}
+ & \color{#5E5EFF}{\enclose{circle}{0}}
&
- & \color{blue}{\enclose{circle}{1}}
+ & \color{#5E5EFF}{\enclose{circle}{1}}
&
- & \color{blue}{\enclose{circle}{2}}
+ & \color{#5E5EFF}{\enclose{circle}{2}}
&
- & \color{red}{\enclose{circle}{3}}
+ & \color{#EE0000}{\enclose{circle}{3}}
&
- & \color{red}{4\phantom{,}}
+ & \color{#EE0000}{4\phantom{,}}
&
- & \color{red}{5\phantom{,}}
+ & \color{#EE0000}{5\phantom{,}}
&
- & \color{red}{6\phantom{,}}\\
- \color{blue}{\text{start}}
- & \color{blue}{\text{(beginning)}}
+ & \color{#EE0000}{6\phantom{,}}\\
+ \color{#5E5EFF}{\text{start}}
+ & \color{#5E5EFF}{\text{(beginning)}}
& \rightarrow
- & \color{blue}{+1}
+ & \color{#5E5EFF}{+1}
& \rightarrow
- & \color{blue}{+1}
+ & \color{#5E5EFF}{+1}
& \rightarrow
- & \color{red}{\text{stop}}
+ & \color{#EE0000}{\text{stop}}
&
& \phantom{\rightarrow}
&
@@ -1533,34 +1544,34 @@ $$
\begin{aligned}
\begin{array}{r r c c c c c c c c c c c c l}
a = & [\mathtt{\textsf{'}a\textsf{'}}, && \mathtt{\textsf{'}b\textsf{'}}, && \mathtt{\textsf{'}c\textsf{'}}, && \mathtt{\textsf{'}d\textsf{'}}, && \mathtt{\textsf{'}e\textsf{'}}, && \mathtt{\textsf{'}f\textsf{'}}, && \mathtt{\textsf{'}g\textsf{'}}]\\
-\color{red}{\text{index}}
- & \color{red}{0\phantom{,}}
+\color{#EE0000}{\text{index}}
+ & \color{#EE0000}{0\phantom{,}}
&
- & \color{red}{1\phantom{,}}
+ & \color{#EE0000}{1\phantom{,}}
&
- & \color{red}{2\phantom{,}}
+ & \color{#EE0000}{2\phantom{,}}
&
- & \color{blue}{\enclose{circle}{3}}
+ & \color{#5E5EFF}{\enclose{circle}{3}}
&
- & \color{blue}{\enclose{circle}{4}}
+ & \color{#5E5EFF}{\enclose{circle}{4}}
&
- & \color{blue}{\enclose{circle}{5}}
+ & \color{#5E5EFF}{\enclose{circle}{5}}
&
- & \color{blue}{\enclose{circle}{6}\phantom{,}}\\
+ & \color{#5E5EFF}{\enclose{circle}{6}\phantom{,}}\\
&
& \phantom{\rightarrow}
&
& \phantom{\rightarrow}
&
& \phantom{\rightarrow}
- & \color{blue}{\text{start}}
+ & \color{#5E5EFF}{\text{start}}
& \rightarrow
- & \color{blue}{+1}
+ & \color{#5E5EFF}{+1}
& \rightarrow
- & \color{blue}{+1}
+ & \color{#5E5EFF}{+1}
& \rightarrow
- & \color{blue}{\text{stop}}
- & \color{blue}{\text{(end)}}
+ & \color{#5E5EFF}{\text{stop}}
+ & \color{#5E5EFF}{\text{(end)}}
\end{array}
\end{aligned}
$$
@@ -1573,34 +1584,34 @@ $$
\begin{aligned}
\begin{array}{r r c c c c c c c c c c c c l}
a = & [\mathtt{\textsf{'}a\textsf{'}}, && \mathtt{\textsf{'}b\textsf{'}}, && \mathtt{\textsf{'}c\textsf{'}}, && \mathtt{\textsf{'}d\textsf{'}}, && \mathtt{\textsf{'}e\textsf{'}}, && \mathtt{\textsf{'}f\textsf{'}}, && \mathtt{\textsf{'}g\textsf{'}}]\\
-\color{red}{\text{index}}
- & \color{red}{0\phantom{,}}
+\color{#EE0000}{\text{index}}
+ & \color{#EE0000}{0\phantom{,}}
&
- & \color{red}{1\phantom{,}}
+ & \color{#EE0000}{1\phantom{,}}
&
- & \color{red}{2\phantom{,}}
+ & \color{#EE0000}{2\phantom{,}}
&
- & \color{red}{\enclose{circle}{3}}
+ & \color{#EE0000}{\enclose{circle}{3}}
&
- & \color{blue}{\enclose{circle}{4}}
+ & \color{#5E5EFF}{\enclose{circle}{4}}
&
- & \color{blue}{\enclose{circle}{5}}
+ & \color{#5E5EFF}{\enclose{circle}{5}}
&
- & \color{blue}{\enclose{circle}{6}\phantom{,}}\\
+ & \color{#5E5EFF}{\enclose{circle}{6}\phantom{,}}\\
&
& \phantom{\leftarrow}
&
& \phantom{\leftarrow}
&
& \phantom{\leftarrow}
- & \color{red}{\text{stop}}
+ & \color{#EE0000}{\text{stop}}
& \leftarrow
- & \color{blue}{-1}
+ & \color{#5E5EFF}{-1}
& \leftarrow
- & \color{blue}{-1}
+ & \color{#5E5EFF}{-1}
& \leftarrow
- & \color{blue}{\text{start}}
- & \color{blue}{\text{(end)}}
+ & \color{#5E5EFF}{\text{start}}
+ & \color{#5E5EFF}{\text{(end)}}
\end{array}
\end{aligned}
$$
@@ -1613,28 +1624,28 @@ $$
\begin{aligned}
\begin{array}{r r c c c c c c c c c c c c l}
a = & [\mathtt{\textsf{'}a\textsf{'}}, && \mathtt{\textsf{'}b\textsf{'}}, && \mathtt{\textsf{'}c\textsf{'}}, && \mathtt{\textsf{'}d\textsf{'}}, && \mathtt{\textsf{'}e\textsf{'}}, && \mathtt{\textsf{'}f\textsf{'}}, && \mathtt{\textsf{'}g\textsf{'}}]\\
-\color{red}{\text{index}}
- & \color{blue}{\enclose{circle}{0}}
+\color{#EE0000}{\text{index}}
+ & \color{#5E5EFF}{\enclose{circle}{0}}
&
- & \color{blue}{\enclose{circle}{1}}
+ & \color{#5E5EFF}{\enclose{circle}{1}}
&
- & \color{blue}{\enclose{circle}{2}}
+ & \color{#5E5EFF}{\enclose{circle}{2}}
&
- & \color{blue}{\enclose{circle}{3}}
+ & \color{#5E5EFF}{\enclose{circle}{3}}
&
- & \color{red}{4\phantom{,}}
+ & \color{#EE0000}{4\phantom{,}}
&
- & \color{red}{5\phantom{,}}
+ & \color{#EE0000}{5\phantom{,}}
&
- & \color{red}{6\phantom{,}}\\
- \color{blue}{\text{stop}}
- & \color{blue}{\text{(beginning)}}
+ & \color{#EE0000}{6\phantom{,}}\\
+ \color{#5E5EFF}{\text{stop}}
+ & \color{#5E5EFF}{\text{(beginning)}}
& \leftarrow
- & \color{blue}{-1}
+ & \color{#5E5EFF}{-1}
& \leftarrow
- & \color{blue}{-1}
+ & \color{#5E5EFF}{-1}
& \leftarrow
- & \color{blue}{\text{start}}
+ & \color{#5E5EFF}{\text{start}}
&
& \phantom{\leftarrow}
&
@@ -1682,7 +1693,7 @@ hard to write slice arithmetic. The arithmetic is already hard enough due to
the modular nature of `step`, but the discontinuous aspect of `start` and
`stop` increases this tenfold. If you are unconvinced of this, take a look at
the [source
-code](https://github.com/Quansight-labs/ndindex/blob/master/ndindex/slice.py) for
+code](https://github.com/Quansight-labs/ndindex/blob/main/ndindex/slice.py) for
`ndindex.Slice()`. You will see lots of nested `if` blocks.[^source-footnote]
This is because slices have *fundamentally* different definitions if the
`start` or `stop` are `None`, negative, or nonnegative. Furthermore, `None` is
diff --git a/github_deploy_key_quansight_labs_ndindex.enc b/github_deploy_key_quansight_labs_ndindex.enc
deleted file mode 100644
index 78d11c5e..00000000
--- a/github_deploy_key_quansight_labs_ndindex.enc
+++ /dev/null
@@ -1 +0,0 @@
-gAAAAABgeI3n7QXDzdcsx7Px8UM3QsoRMz8gUpAHctdQWzrdxKqBPgAkC_iF9RWdFESBjpxWHRhnldS3iwSqPhvHttDjcg4lZVJOfTROwe0SepRlCCf2IrsJng4aXxDms4UAXc8XtwqaZCXAuEMl017D_C7ce9VoIuIfJqer5cXKU662I-56dtfYWGvpcYgjmTrehSnrlBh0b_ZDOEhqgWauqQo0o23eaglszV9434o2TkKj2w-sESc9Fc92goA5WO8G6Y4EgMAlJNpByHIJ6ryFVN4-NL-ZO1YQ0PN6NVzlN1oNt5djoRXSvd9Yraz5L73myzHNZJVWAlKHMbOHuXcLdSY8crn4uJ7QTeDtoaesDU3qywkTEDC-CV82o5GJ9WNLrrKjPlOjdgUWVxUdpGGLh3nhWad_z2fTIkMerRxirUyHO0jL32tJttmSYLi49kmGJ-2GPLtgynm1hxQ66rGMrha7FseSHVRoUStnwEBECBtzVClkY6PQK_5aywGXywOSIwB9sgD0golfCawZkP4uAfyjBSkEdwRb4_f3chJv7Wh5WFguCFgVp78NilyEEyUSf20_lsWwgEfQRdXO_GJFyJqVTUQpekc2Iv_D2LQA01W7FbrXhg6gfARG_1uiCzacU8YUyrGfqCCAbeR9YgCoDbca6KM63vNyCMW328nGz_NvA8Vz0iKOCf10EsUjbr8lVufVav1125mvNvNCQzpAWpcjJDUw242Ztv0kiKVd0QiUmolDnXnY_2nNsY2VCXpSD6AottoaZkXaVBQXjQm8yW1cuSWoOSdw4HvyK8ibdV4Y8IAVgA8U7l4a_fMfrzawjd3KIkgCOYS_SwXT8o-Vc6D3zoWvUWDTtlPDl5V6PwggAdHvYThmDyScDacyIQ0HeZC93jrmtr71JrZXzo41zzGojQrgUx8Q7xqKIN2tlOEpeQJd9IaSGQvsJ9wslxOqQzrUCiWpm10bdwZB73cQDyNzX1gkVPUPUBco7A6cBIaa6iUwhdkUoE549DicLYNbpXvCPQhfpEzw9AP6APhGGH03z4sy2PbwDsi-Js3EVu78ahE34H_GuMQ94KjMPLD9_x3qQpZQKE8dmKJeakCOlmXUhtunfWzkUnroP1RPbuk7B_WDTsOCKPaMv32tSNQ06J4mGIGRQchPKQvptUHRZMQP6NANqarUUbrvzzKKFowRL3ZDfxjdUaeq0cKyCAq5fOy4IZgchGXvVKDcZ8ij2m0Lxeg5PiKJPm-nyLROJ2O3HWivLT_Id-rfddivP8yaCaB72DUI1MsUT6dlmN6jrDPKN92H3bYCx1FbnoaXBQ3j6BCt6dp2mydBZjh2vdDs2oeaL4Nlk3QQXbCF6dLPwoT058Ws2V1VPVvgVDFxPJe5ZUILf85BN_zVHkL89DGqbBRwsftrwILHaBrJBBSHB8wNjG3meAY0DEPwKki4ya4N1Av3Z_rCdbtAaMmUbuQNayZZBExTfp5qw2eJ5QRo0KGkPKjmiGpnno8u-Poy2Ybn3wFDkIdd7Vk_7xohALRXAG2SSKpQDOsvAC3aheXMzqxpxpKN4_j6YBhF-TGmZfAVHYa11TVsDv5nQ9hKMfnqcuEtPt7fI78kWLebwRWNiAm1kaFoG_Mv8e6DOo2se7aqTtwT7vDwmzir4R25CvyrlqboVER_oapeBi18j0sCMBCJMTn54F05PFb2KnnrRlluxH_sZj4prvIRNGz706rCMewLe2hjtxx7PkxMguXozbSWWpmgnPk5XZIcfT46jIuDMVQH5WNbpJEgkQcBWJxD8CXwdmGF38JTVIAu4x_jkZ8fHBKUHmn1qYQwX1gAkKQcqm0SluRSYkHnPLavf26hqdbjFSL-G2oColkiT2kELB1ZYBzwxeGKKCaBqFMEY0Rmjd7Ob7Vcd7DUCnK-b_h60F6bKnvLIupIhP5rbN0EmWf3SCeUh0t1RbRD_LGfNY3yLS_pz1qLkmS3qAijg6v5upOKpvciCx43U9mew3jn6zvAO_SfKKh4OmfrkKDG3geaV86qzGK3Rg93IEqt_M8jKAIF0boFnLWtrQjTCil3W6-9kk3SWLJtUJoMZ_kggMDTkkqg_oIE1IgYbCBoXvHXoaHvV4q5n7Uso9Ds5ag4b4uv8x9N710OtkNFEzjtvHALGEyYT9xfQ5x47bniYSz4WbJ0O8EwukH66NeXJHQ1KUc2S8A9iV0wJSoeZfVa8IkNt04hxxRNrXfBvp66R_ndPCFRulS56AgF4iYczgE5O2QvdhXZu8fdVErD91Vq0ck8moKCIVkrri1kWpSVx2vfjK0EJtixTHprpKpVBRKJXsKnVpRqaFnDAK9SILVUAHWRM5Jmw1zcyR36ozV2YoqQubDkQ0UGUBXKbbXHzIUndqCw0EMmQGPcY_iZ0WTAt-rlX5nYMy-GlHhab3Vwb9NBbUMhJ6hjSwwQqpSb_rspOKQU8k2C5sr-nKDk5UEsqqG_QNZcOkioN0Wu3zJ95RcirfBOWVuOymW5dW2VUBP4qvA8m5gnPQiEk7_hAoOPEIncSRnqVSg47JboRLIt6LcedOzMeOiF9WY7mKqxwK_XRgxDCC3Jxv4RtDkECY78qePSb1WiS1kKcozxPtK96FZFu3CGzb4lz5iouG0yXXQY0H6Vnu7hVD-U5fL5qv1g_4v4aEluZKI5p2IS5_Nz1Rc9AFTQ_9EFnEhL0bPgXUAoxDhBgkRgkg7zL-4PkxnLTpMwIsP6l8Rb7tcqD5vj-FZd2v3ty4iPdiFPj9tlMw0d79npiaMSCb4F7F1D37OeCaIGvAvrHHl-G7HgcnDgcYlTp9EGdJXYd8iyc59Mip7-3DdQazRukK7g7gNN0ZiGyXfElUR3VPIQKCyZ45y3rLEwt-4oyHLyLSZwI-xUsPMXJ1yRopR9VgZCMXUGkgCJ9DvkFdO7PaKW-8qo3aJA0dACSNnfiuiMrwaRNm8Jnyzun34uOAVwTLuYB3XEd0hrIu6_brwdvU6JKpYFu9pLVwuzBNLmE5ftiiD8yZjn5WFqvlBtX4SUJG9zY-aJeKHwkt8XzllTwGdiN0Zhm5rlhvnWP18m7NMGNJzYnkaWOSuUyZYryYzJaTTcq1a-E-ikEtWxf_PMxwi5QLjhTjSx5SBXElb46OdHfS3gjAmap3qKF6qUK8b0DCRlBExIfNumBRKqO8K_ivNOCwGDk8620dJ33jSaeX4uU38EVXBxPO2znkXtOX0K31u4tq4H7cYMRSqsaiojOrBoD4tOJvmdHV0gmhV1oWcedA7MIpkToRVUM-mjY5-0GFFYWzTVq8H_ZP2gSWT0tEE_2sr_IrTZcQaMq5SGd6aLrLdedv-WLlA1WQ-_aYx5Zeme8jEM9z6hlI5Y__5EqO6jHpKVm4uLRJZCSMobD0zL8EHevkMtJxjgdEmnpHAumKIpRhSYOnPBp_fSZM7mIIclSvRfD6pb2fs9dMVtKZUlPSyoNlDszYbDFR_RlXD47tSd8MOnYLBHo6sG-hBd-vz-yUIDyGGcKGXuDNJ8kk0jdOwmx8BKvQiAqfEw8AjY5TL06XETSheNPKAj7rTdAbrBb70e_9wAx056wZUnwez-N4IIT8xB_8OT3NYu00d3AN4SK2a7NJX1tSYef4ofkYSXOPcb9eF8vyU-3IE1b2ySTMQeedvLiyngPp0E0_RWv_P4p0LHdYhP_d68DbHHYpcmBpTBlc3n0mGS4uCW9-cf3PKQJqkkJ6ubCi_cBX5kX5Qppq2ZlYI_QXn23g4uHQjZc-YGSVXeijFNH1rn06TnHz8tucz9OdTQN1eCtspUjC863K3VN9ChdnfNWSom6knYqN0PfagkgoyxpStDxcadyWZp0mmf2PKc6V5ofj_i6NNXgqoBeWiz5be9xnpm0JOS4ANDhxEIWEIhXmJsTo0EeWxXerABbDH6wSIBPyMPqoyZRNWO9mFAioBnQQYaZa4iHBdusSr1E3XDZli6uUoBkFAU_bidUF0o-ed36r7efx1EzUOq1JMbnItVVHz6ztMiO3A3XoD1up8F6f_BGOK3NQ_spa-khBHxMQFAPgOX9oY5jHziDhMu47sjNB9MYLkzFrhJFsXPES9rtLACrHlAy21IHV5Zqn1f1LAq2abbUTQK6Z0YZFrwWigfeWoYKwB7IcnhlttZ76LgaDwpwfsO0ks1x8_UfzDyCsd-z6B4Gsmo4GtDcw2Xdr_fs8Km61yfAxbohDHerEaMByz382AT36OGkesqEndoW1V2bIbaHuveBmLOssjZ5-cflKZ38dc_Dj_JlCIwXjUJxrNdtxjWZMmW9CQviWXJkI8eW8ngpTztrEl14eH4XtSI5J66bgVbxAzr69AEj81SE6mbb8S3du4uHTWJmtP_RkgC1o4BiYAcP0Uk3aH9F9GYlRKort7YIq7qvy8CSQ-A_whPhFdbg9G4q5cFEI9zifTr3akE6g==
\ No newline at end of file
diff --git a/ndindex/__init__.py b/ndindex/__init__.py
index 8a5e60cf..debe4015 100644
--- a/ndindex/__init__.py
+++ b/ndindex/__init__.py
@@ -1,8 +1,12 @@
__all__ = []
-from .ndindex import ndindex, iter_indices, AxisError, BroadcastError
+from .ndindex import ndindex
-__all__ += ['ndindex', 'iter_indices', 'AxisError', 'BroadcastError']
+__all__ += ['ndindex']
+
+from .shapetools import broadcast_shapes, iter_indices, AxisError, BroadcastError
+
+__all__ += ['broadcast_shapes', 'iter_indices', 'AxisError', 'BroadcastError']
from .slice import Slice
diff --git a/ndindex/array.py b/ndindex/array.py
index 618e5ff4..4eafd990 100644
--- a/ndindex/array.py
+++ b/ndindex/array.py
@@ -1,6 +1,7 @@
import warnings
-from .ndindex import NDIndex, asshape
+from .ndindex import NDIndex
+from .shapetools import asshape
class ArrayIndex(NDIndex):
"""
@@ -22,6 +23,10 @@ def _typecheck(self, idx, shape=None, _copy=True):
from numpy import ndarray, asarray, integer, bool_, empty, intp
except ImportError: # pragma: no cover
raise ImportError("NumPy must be installed to create array indices")
+ try:
+ from numpy import VisibleDeprecationWarning
+ except ImportError: # pragma: no cover
+ from numpy.exceptions import VisibleDeprecationWarning
if self.dtype is None:
raise TypeError("Do not instantiate the superclass ArrayIndex directly")
@@ -37,7 +42,10 @@ def _typecheck(self, idx, shape=None, _copy=True):
if isinstance(idx, (list, ndarray, bool, integer, int, bool_)):
# Ignore deprecation warnings for things like [1, []]. These will be
# filtered out anyway since they produce object arrays.
- with warnings.catch_warnings(record=True):
+ with warnings.catch_warnings():
+ warnings.filterwarnings('ignore',
+ category=VisibleDeprecationWarning,
+ message='Creating an ndarray from ragged nested sequences')
a = asarray(idx)
if a is idx and _copy:
a = a.copy()
@@ -159,3 +167,14 @@ def __str__(self):
def __hash__(self):
return hash(self.array.tobytes())
+
+ def isvalid(self, shape, _axis=0):
+ shape = asshape(shape)
+ try:
+ # The logic is in _raise_indexerror because the error message uses
+ # the additional information that is computed when checking if the
+ # array is valid.
+ self._raise_indexerror(shape, _axis)
+ except IndexError:
+ return False
+ return True
diff --git a/ndindex/booleanarray.py b/ndindex/booleanarray.py
index 262b2b5b..a4620a9c 100644
--- a/ndindex/booleanarray.py
+++ b/ndindex/booleanarray.py
@@ -1,5 +1,5 @@
from .array import ArrayIndex
-from .ndindex import asshape
+from .shapetools import asshape
class BooleanArray(ArrayIndex):
"""
@@ -102,7 +102,15 @@ def count_nonzero(self):
from numpy import count_nonzero
return count_nonzero(self.array)
- def reduce(self, shape=None, axis=0):
+ def _raise_indexerror(self, shape, axis=0):
+ if len(shape) < self.ndim + axis:
+ raise IndexError(f"too many indices for array: array is {len(shape)}-dimensional, but {self.ndim + axis} were indexed")
+
+ for i in range(axis, axis+self.ndim):
+ if self.shape[i-axis] != 0 and shape[i] != self.shape[i-axis]:
+ raise IndexError(f'boolean index did not match indexed array along axis {i}; size of axis is {shape[i]} but size of corresponding boolean axis is {self.shape[i-axis]}')
+
+ def reduce(self, shape=None, *, axis=0, negative_int=False):
"""
Reduce a `BooleanArray` index on an array of shape `shape`.
@@ -116,7 +124,7 @@ def reduce(self, shape=None, axis=0):
>>> idx.reduce((3,))
Traceback (most recent call last):
...
- IndexError: boolean index did not match indexed array along dimension 0; dimension is 3 but corresponding boolean dimension is 2
+ IndexError: boolean index did not match indexed array along axis 0; size of axis is 3 but size of corresponding boolean axis is 2
>>> idx.reduce((2,))
BooleanArray([True, False])
@@ -137,22 +145,14 @@ def reduce(self, shape=None, axis=0):
shape = asshape(shape)
- if len(shape) < self.ndim + axis:
- raise IndexError(f"too many indices for array: array is {len(shape)}-dimensional, but {self.ndim + axis} were indexed")
-
- for i in range(axis, axis+self.ndim):
- if self.shape[i-axis] != 0 and shape[i] != self.shape[i-axis]:
-
- raise IndexError(f"boolean index did not match indexed array along dimension {i}; dimension is {shape[i]} but corresponding boolean dimension is {self.shape[i-axis]}")
-
+ self._raise_indexerror(shape, axis)
return self
def newshape(self, shape):
# The docstring for this method is on the NDIndex base class
shape = asshape(shape)
- # reduce will raise IndexError if it should be raised
- self.reduce(shape)
+ self._raise_indexerror(shape)
return (self.count_nonzero,) + shape[self.ndim:]
def isempty(self, shape=None):
diff --git a/ndindex/chunking.py b/ndindex/chunking.py
index 019fc47c..77b619cf 100644
--- a/ndindex/chunking.py
+++ b/ndindex/chunking.py
@@ -1,12 +1,13 @@
from collections.abc import Sequence
from itertools import product
-from .ndindex import ImmutableObject, operator_index, asshape, ndindex
+from .ndindex import ImmutableObject, operator_index, ndindex
from .tuple import Tuple
from .slice import Slice
from .integer import Integer
from .integerarray import IntegerArray
from .newaxis import Newaxis
+from .shapetools import asshape
from .subindex_helpers import ceiling
from ._crt import prod
diff --git a/ndindex/ellipsis.py b/ndindex/ellipsis.py
index 6caa1cbc..81cf2291 100644
--- a/ndindex/ellipsis.py
+++ b/ndindex/ellipsis.py
@@ -1,5 +1,6 @@
-from .ndindex import NDIndex, asshape
+from .ndindex import NDIndex
from .tuple import Tuple
+from .shapetools import asshape
class ellipsis(NDIndex):
"""
@@ -45,7 +46,7 @@ class ellipsis(NDIndex):
def _typecheck(self):
return ()
- def reduce(self, shape=None):
+ def reduce(self, shape=None, *, negative_int=False):
"""
Reduce an ellipsis index
@@ -77,6 +78,10 @@ def reduce(self, shape=None):
def raw(self):
return ...
+ def isvalid(self, shape):
+ shape = asshape(shape)
+ return True
+
def newshape(self, shape):
# The docstring for this method is on the NDIndex base class
shape = asshape(shape)
diff --git a/ndindex/integer.py b/ndindex/integer.py
index 84ec7a87..0ba7d89f 100644
--- a/ndindex/integer.py
+++ b/ndindex/integer.py
@@ -1,4 +1,5 @@
-from .ndindex import NDIndex, asshape, operator_index
+from .ndindex import NDIndex, operator_index
+from .shapetools import AxisError, asshape
class Integer(NDIndex):
"""
@@ -48,13 +49,29 @@ def __len__(self):
"""
return 1
- def reduce(self, shape=None, axis=0):
+ def isvalid(self, shape, _axis=0):
+ # The docstring for this method is on the NDIndex base class
+ shape = asshape(shape)
+ if not shape:
+ return False
+ size = shape[_axis]
+ return -size <= self.raw < size
+
+ def _raise_indexerror(self, shape, axis=0):
+ if not self.isvalid(shape, axis):
+ size = shape[axis]
+ raise IndexError(f"index {self.raw} is out of bounds for axis {axis} with size {size}")
+
+ def reduce(self, shape=None, *, axis=0, negative_int=False, axiserror=False):
"""
Reduce an Integer index on an array of shape `shape`.
The result will either be `IndexError` if the index is invalid for the
given shape, or an Integer index where the value is nonnegative.
+ If `negative_int` is `True` and a `shape` is provided, then the result
+ will be an Integer index where the value is negative.
+
>>> from ndindex import Integer
>>> idx = Integer(-5)
>>> idx.reduce((3,))
@@ -79,13 +96,22 @@ def reduce(self, shape=None, axis=0):
if shape is None:
return self
+ if axiserror:
+ if not isinstance(shape, int): # pragma: no cover
+ raise TypeError("axiserror=True requires shape to be an integer")
+ if not self.isvalid(shape):
+ raise AxisError(self.raw, shape)
+
shape = asshape(shape, axis=axis)
- size = shape[axis]
- if self.raw >= size or -size > self.raw < 0:
- raise IndexError(f"index {self.raw} is out of bounds for axis {axis} with size {size}")
- if self.raw < 0:
+ self._raise_indexerror(shape, axis)
+
+ if self.raw < 0 and not negative_int:
+ size = shape[axis]
return self.__class__(size + self.raw)
+ elif self.raw >= 0 and negative_int:
+ size = shape[axis]
+ return self.__class__(self.raw - size)
return self
@@ -93,8 +119,7 @@ def newshape(self, shape):
# The docstring for this method is on the NDIndex base class
shape = asshape(shape)
- # reduce will raise IndexError if it should be raised
- self.reduce(shape)
+ self._raise_indexerror(shape)
return shape[1:]
def as_subindex(self, index):
diff --git a/ndindex/integerarray.py b/ndindex/integerarray.py
index 13938ed3..67a6094e 100644
--- a/ndindex/integerarray.py
+++ b/ndindex/integerarray.py
@@ -1,5 +1,5 @@
from .array import ArrayIndex
-from .ndindex import asshape
+from .shapetools import asshape
from .subindex_helpers import subindex_slice
class IntegerArray(ArrayIndex):
@@ -51,7 +51,13 @@ def dtype(self):
from numpy import intp
return intp
- def reduce(self, shape=None, axis=0):
+ def _raise_indexerror(self, shape, axis=0):
+ size = shape[axis]
+ out_of_bounds = (self.array >= size) | ((-size > self.array) & (self.array < 0))
+ if out_of_bounds.any():
+ raise IndexError(f"index {self.array[out_of_bounds].flat[0]} is out of bounds for axis {axis} with size {size}")
+
+ def reduce(self, shape=None, *, axis=0, negative_int=False):
"""
Reduce an `IntegerArray` index on an array of shape `shape`.
@@ -60,6 +66,10 @@ def reduce(self, shape=None, axis=0):
nonnegative, or, if `self` is a scalar array index (`self.shape ==
()`), an `Integer` whose value is nonnegative.
+ If `negative_int` is `True` and a `shape` is provided, the result will
+ be an `IntegerArray` with negative entries instead of positive
+ entries.
+
>>> from ndindex import IntegerArray
>>> idx = IntegerArray([-5, 2])
>>> idx.reduce((3,))
@@ -68,6 +78,8 @@ def reduce(self, shape=None, axis=0):
IndexError: index -5 is out of bounds for axis 0 with size 3
>>> idx.reduce((9,))
IntegerArray([4, 2])
+ >>> idx.reduce((9,), negative_int=True)
+ IntegerArray([-5, -7])
See Also
========
@@ -82,28 +94,28 @@ def reduce(self, shape=None, axis=0):
"""
if self.shape == ():
- return Integer(self.array).reduce(shape, axis=axis)
+ return Integer(self.array).reduce(shape, axis=axis, negative_int=negative_int)
if shape is None:
return self
shape = asshape(shape, axis=axis)
+ self._raise_indexerror(shape, axis)
+
size = shape[axis]
new_array = self.array.copy()
- out_of_bounds = (new_array >= size) | ((-size > new_array) & (new_array < 0))
- if out_of_bounds.any():
- raise IndexError(f"index {new_array[out_of_bounds].flat[0]} is out of bounds for axis {axis} with size {size}")
-
- new_array[new_array < 0] += size
+ if negative_int:
+ new_array[new_array >= 0] -= size
+ else:
+ new_array[new_array < 0] += size
return IntegerArray(new_array)
def newshape(self, shape):
# The docstring for this method is on the NDIndex base class
shape = asshape(shape)
- # reduce will raise IndexError if it should be raised
- self.reduce(shape)
+ self._raise_indexerror(shape)
return self.shape + shape[1:]
def isempty(self, shape=None):
diff --git a/ndindex/ndindex.py b/ndindex/ndindex.py
index f9120d19..c9e04e03 100644
--- a/ndindex/ndindex.py
+++ b/ndindex/ndindex.py
@@ -1,9 +1,6 @@
import sys
import inspect
-import itertools
-import numbers
import operator
-import functools
newaxis = None
@@ -258,7 +255,7 @@ def __hash__(self):
# as hash(self.args)
return hash(self.raw)
- def reduce(self, shape=None):
+ def reduce(self, shape=None, *, negative_int=False):
"""
Simplify an index given that it will be applied to an array of a given shape.
@@ -297,6 +294,46 @@ def reduce(self, shape=None):
# XXX: Should the default be raise NotImplementedError or return self?
raise NotImplementedError
+ def isvalid(self, shape):
+ """
+ Check whether a given index is valid on an array of a given shape.
+
+ Returns `True` if an array of shape `shape` can be indexed by `self`
+ and `False` if it would raise `IndexError`.
+
+ >>> from ndindex import ndindex
+ >>> ndindex(3).isvalid((4,))
+ True
+ >>> ndindex(3).isvalid((2,))
+ False
+
+ Note that some indices can never be valid and will raise a
+ `IndexError` or `TypeError` if you attempt to construct them.
+
+ >>> ndindex((..., 0, ...))
+ Traceback (most recent call last):
+ ...
+ IndexError: an index can only have a single ellipsis ('...')
+ >>> ndindex(slice(True))
+ Traceback (most recent call last):
+ ...
+ TypeError: 'bool' object cannot be interpreted as an integer
+
+ See Also
+ ========
+ .NDIndex.newshape
+
+ """
+ # Every class except for Tuple has a more direct efficient
+ # implementation. The logic for checking if a Tuple index is valid is
+ # basically the same as the logic in reduce/expand, so there's no
+ # point in duplicating it.
+ try:
+ self.reduce(shape)
+ return True
+ except IndexError:
+ return False
+
def expand(self, shape):
r"""
Expand a Tuple index on an array of shape `shape`
@@ -377,8 +414,8 @@ def newshape(self, shape):
`shape` should be a tuple of ints, or an int, which is equivalent to a
1-D shape.
- Raises `IndexError` if `self` would be out of shape for an array of
- shape `shape`.
+ Raises `IndexError` if `self` would be invalid for an array of shape
+ `shape`.
>>> from ndindex import Slice, Integer, Tuple
>>> shape = (6, 7, 8)
@@ -393,6 +430,10 @@ def newshape(self, shape):
>>> Tuple(0, ..., Slice(1, 3)).newshape(shape)
(7, 2)
+ See Also
+ ========
+ .NDIndex.isvalid
+
"""
raise NotImplementedError
@@ -557,307 +598,6 @@ def broadcast_arrays(self):
"""
return self
-
-# TODO: Use this in other places in the code that check broadcast compatibility.
-class BroadcastError(ValueError):
- """
- Exception raised by :func:`iter_indices()` when the input shapes are not
- broadcast compatible.
-
- This is used instead of the NumPy exception of the same name so that
- `iter_indices` does not need to depend on NumPy.
- """
-
-class AxisError(ValueError, IndexError):
- """
- Exception raised by :func:`iter_indices()` when the `skip_axes` argument
- is out of bounds.
-
- This is used instead of the NumPy exception of the same name so that
- `iter_indices` does not need to depend on NumPy.
-
- """
- __slots__ = ("axis", "ndim")
-
- def __init__(self, axis, ndim):
- self.axis = axis
- self.ndim = ndim
-
- def __str__(self):
- return f"axis {self.axis} is out of bounds for array of dimension {self.ndim}"
-
-def broadcast_shapes(*shapes):
- """
- Broadcast the input shapes `shapes` to a single shape.
-
- This is the same as :py:func:`np.broadcast_shapes()
-
`. It is included as a separate helper function
- because `np.broadcast_shapes()` is on available in NumPy 1.20 or newer, and
- so that ndindex functions that use this function can do without requiring
- NumPy to be installed.
-
- """
-
- def _broadcast_shapes(shape1, shape2):
- """Broadcasts `shape1` and `shape2`"""
- N1 = len(shape1)
- N2 = len(shape2)
- N = max(N1, N2)
- shape = [None for _ in range(N)]
- i = N - 1
- while i >= 0:
- n1 = N1 - N + i
- if N1 - N + i >= 0:
- d1 = shape1[n1]
- else:
- d1 = 1
- n2 = N2 - N + i
- if N2 - N + i >= 0:
- d2 = shape2[n2]
- else:
- d2 = 1
-
- if d1 == 1:
- shape[i] = d2
- elif d2 == 1:
- shape[i] = d1
- elif d1 == d2:
- shape[i] = d1
- else:
- # TODO: Build an error message that matches NumPy
- raise BroadcastError("shape mismatch: objects cannot be broadcast to a single shape.")
-
- i = i - 1
-
- return tuple(shape)
-
- return functools.reduce(_broadcast_shapes, shapes, ())
-
-def iter_indices(*shapes, skip_axes=(), _debug=False):
- """
- Iterate indices for every element of an arrays of shape `shapes`.
-
- Each shape in `shapes` should be a shape tuple, which are broadcast
- compatible. Each iteration step will produce a tuple of indices, one for
- each shape, which would correspond to the same elements if the arrays of
- the given shapes were first broadcast together.
-
- This is a generalization of the NumPy :py:class:`np.ndindex()
- ` function (which otherwise has no relation).
- `np.ndindex()` only iterates indices for a single shape, whereas
- `iter_indices()` supports generating indices for multiple broadcast
- compatible shapes at once. This is equivalent to first broadcasting the
- arrays then generating indices for the single broadcasted shape.
-
- Additionally, this function supports the ability to skip axes of the
- shapes using `skip_axes`. These axes will be fully sliced in each index.
- The remaining axes will be indexed one element at a time with integer
- indices.
-
- `skip_axes` should be a tuple of axes to skip. It can use negative
- integers, e.g., `skip_axes=(-1,)` will skip the last axis. The order of
- the axes in `skip_axes` does not matter, but it should not contain
- duplicate axes. The axes in `skip_axes` refer to the final broadcasted
- shape of `shapes`. For example, `iter_indices((3,), (1, 2, 3),
- skip_axes=(0,))` will skip the first axis, and only applies to the second
- shape, since the first shape corresponds to axis `2` of the final
- broadcasted shape `(1, 2, 3)`
-
- For example, suppose `a` is an array with shape `(3, 2, 4, 4)`, which we
- wish to think of as a `(3, 2)` stack of 4 x 4 matrices. We can generate an
- iterator for each matrix in the "stack" with `iter_indices((3, 2, 4, 4),
- skip_axes=(-1, -2))`:
-
- >>> from ndindex import iter_indices
- >>> for idx in iter_indices((3, 2, 4, 4), skip_axes=(-1, -2)):
- ... print(idx)
- (Tuple(0, 0, slice(None, None, None), slice(None, None, None)),)
- (Tuple(0, 1, slice(None, None, None), slice(None, None, None)),)
- (Tuple(1, 0, slice(None, None, None), slice(None, None, None)),)
- (Tuple(1, 1, slice(None, None, None), slice(None, None, None)),)
- (Tuple(2, 0, slice(None, None, None), slice(None, None, None)),)
- (Tuple(2, 1, slice(None, None, None), slice(None, None, None)),)
-
- Note that the iterates of `iter_indices` are always a tuple, even if only
- a single shape is provided (one could instead use `for idx, in
- iter_indices(...)` above).
-
- As another example, say `a` is shape `(1, 3)` and `b` is shape `(2, 1)`,
- and we want to generate indices for every value of the broadcasted
- operation `a + b`. We can do this by using `a[idx1.raw] + b[idx2.raw]` for every
- `idx1` and `idx2` as below:
-
- >>> import numpy as np
- >>> a = np.arange(3).reshape((1, 3))
- >>> b = np.arange(100, 111, 10).reshape((2, 1))
- >>> a
- array([[0, 1, 2]])
- >>> b
- array([[100],
- [110]])
- >>> for idx1, idx2 in iter_indices((1, 3), (2, 1)): # doctest: +SKIP37
- ... print(f"{idx1 = }; {idx2 = }; {(a[idx1.raw], b[idx2.raw]) = }")
- idx1 = Tuple(0, 0); idx2 = Tuple(0, 0); (a[idx1.raw], b[idx2.raw]) = (0, 100)
- idx1 = Tuple(0, 1); idx2 = Tuple(0, 0); (a[idx1.raw], b[idx2.raw]) = (1, 100)
- idx1 = Tuple(0, 2); idx2 = Tuple(0, 0); (a[idx1.raw], b[idx2.raw]) = (2, 100)
- idx1 = Tuple(0, 0); idx2 = Tuple(1, 0); (a[idx1.raw], b[idx2.raw]) = (0, 110)
- idx1 = Tuple(0, 1); idx2 = Tuple(1, 0); (a[idx1.raw], b[idx2.raw]) = (1, 110)
- idx1 = Tuple(0, 2); idx2 = Tuple(1, 0); (a[idx1.raw], b[idx2.raw]) = (2, 110)
- >>> a + b
- array([[100, 101, 102],
- [110, 111, 112]])
-
- To include an index into the final broadcasted array, you can simply
- include the final broadcasted shape as one of the shapes (the NumPy
- function :func:`np.broadcast_shapes() ` is
- useful here).
-
- >>> np.broadcast_shapes((1, 3), (2, 1))
- (2, 3)
- >>> for idx1, idx2, broadcasted_idx in iter_indices((1, 3), (2, 1), (2, 3)):
- ... print(broadcasted_idx)
- Tuple(0, 0)
- Tuple(0, 1)
- Tuple(0, 2)
- Tuple(1, 0)
- Tuple(1, 1)
- Tuple(1, 2)
-
- """
- if not shapes:
- yield ()
- return
-
- shapes = [asshape(shape) for shape in shapes]
- ndim = len(max(shapes, key=len))
-
- if isinstance(skip_axes, int):
- skip_axes = (skip_axes,)
- _skip_axes = []
- for a in skip_axes:
- try:
- a = ndindex(a).reduce(ndim).args[0]
- except IndexError:
- raise AxisError(a, ndim)
- if a in _skip_axes:
- raise ValueError("skip_axes should not contain duplicate axes")
- _skip_axes.append(a)
-
- _shapes = [(1,)*(ndim - len(shape)) + shape for shape in shapes]
- iters = [[] for i in range(len(shapes))]
- broadcasted_shape = broadcast_shapes(*shapes)
-
- for i in range(-1, -ndim-1, -1):
- for it, shape, _shape in zip(iters, shapes, _shapes):
- if -i > len(shape):
- for j in range(len(it)):
- if broadcasted_shape[i] not in [0, 1]:
- it[j] = ncycles(it[j], broadcasted_shape[i])
- break
- elif ndim + i in _skip_axes:
- it.insert(0, [slice(None)])
- else:
- if broadcasted_shape[i] != 1 and shape[i] == 1:
- it.insert(0, ncycles(range(shape[i]), broadcasted_shape[i]))
- else:
- it.insert(0, range(shape[i]))
-
- if _debug: # pragma: no cover
- print(iters)
- # Use this instead when we drop Python 3.7 support
- # print(f"{iters = }")
- for idxes in itertools.zip_longest(*[itertools.product(*i) for i in
- iters], fillvalue=()):
- yield tuple(ndindex(idx) for idx in idxes)
-
-class ncycles:
- """
- Iterate `iterable` repeated `n` times.
-
- This is based on a recipe from the `Python itertools docs
- `_,
- but improved to give a repr, and to denest when it can. This makes
- debugging :func:`~.iter_indices` easier.
-
- >>> from ndindex.ndindex import ncycles
- >>> ncycles(range(3), 2)
- ncycles(range(0, 3), 2)
- >>> list(_)
- [0, 1, 2, 0, 1, 2]
- >>> ncycles(ncycles(range(3), 3), 2)
- ncycles(range(0, 3), 6)
-
- """
- def __new__(cls, iterable, n):
- if n == 1:
- return iterable
- return object.__new__(cls)
-
- def __init__(self, iterable, n):
- if isinstance(iterable, ncycles):
- self.iterable = iterable.iterable
- self.n = iterable.n*n
- else:
- self.iterable = iterable
- self.n = n
-
- def __repr__(self):
- return f"ncycles({self.iterable!r}, {self.n!r})"
-
- def __iter__(self):
- return itertools.chain.from_iterable(itertools.repeat(tuple(self.iterable), self.n))
-
-def asshape(shape, axis=None):
- """
- Cast `shape` as a valid NumPy shape.
-
- The input can be an integer `n`, which is equivalent to `(n,)`, or a tuple
- of integers.
-
- If the `axis` argument is provided, an `IndexError` is raised if it is out
- of bounds for the shape.
-
- The resulting shape is always a tuple of nonnegative integers.
-
- All ndindex functions that take a shape input should use::
-
- shape = asshape(shape)
-
- or::
-
- shape = asshape(shape, axis=axis)
-
- """
- from .integer import Integer
- from .tuple import Tuple
- if isinstance(shape, (Tuple, Integer)):
- raise TypeError("ndindex types are not meant to be used as a shape - "
- "did you mean to use the built-in tuple type?")
-
- if isinstance(shape, numbers.Number):
- shape = (operator_index(shape),)
-
- try:
- l = len(shape)
- except TypeError:
- raise TypeError("expected sequence object with len >= 0 or a single integer")
-
- newshape = []
- # numpy uses __getitem__ rather than __iter__ to index into shape, so we
- # match that
- for i in range(l):
- # Raise TypeError if invalid
- newshape.append(operator_index(shape[i]))
-
- if shape[i] < 0:
- raise ValueError("unknown (negative) dimensions are not supported")
-
- if axis is not None:
- if len(newshape) <= axis:
- raise IndexError(f"too many indices for array: array is {len(shape)}-dimensional, but {axis + 1} were indexed")
-
- return tuple(newshape)
-
def operator_index(idx):
"""
Convert `idx` into an integer index using `__index__()` or raise
diff --git a/ndindex/newaxis.py b/ndindex/newaxis.py
index 28df499b..120c0ef2 100644
--- a/ndindex/newaxis.py
+++ b/ndindex/newaxis.py
@@ -1,4 +1,5 @@
-from .ndindex import NDIndex, asshape
+from .ndindex import NDIndex
+from .shapetools import asshape
class Newaxis(NDIndex):
"""
@@ -42,7 +43,7 @@ def _typecheck(self):
def raw(self):
return None
- def reduce(self, shape=None, axis=0):
+ def reduce(self, shape=None, *, axis=0, negative_int=False):
"""
Reduce a `Newaxis` index
@@ -69,6 +70,10 @@ def reduce(self, shape=None, axis=0):
shape = asshape(shape)
return self
+ def isvalid(self, shape):
+ shape = asshape(shape)
+ return True
+
def newshape(self, shape):
# The docstring for this method is on the NDIndex base class
shape = asshape(shape)
diff --git a/ndindex/shapetools.py b/ndindex/shapetools.py
new file mode 100644
index 00000000..567c9d91
--- /dev/null
+++ b/ndindex/shapetools.py
@@ -0,0 +1,490 @@
+import numbers
+import itertools
+from collections.abc import Sequence
+from ._crt import prod
+
+from .ndindex import ndindex, operator_index
+
+class BroadcastError(ValueError):
+ """
+ Exception raised by :func:`iter_indices()` and
+ :func:`broadcast_shapes()` when the input shapes are not broadcast
+ compatible.
+
+ """
+ __slots__ = ("arg1", "shape1", "arg2", "shape2")
+
+ def __init__(self, arg1, shape1, arg2, shape2):
+ self.arg1 = arg1
+ self.shape1 = shape1
+ self.arg2 = arg2
+ self.shape2 = shape2
+
+ def __str__(self):
+ arg1, shape1, arg2, shape2 = self.args
+ return f"shape mismatch: objects cannot be broadcast to a single shape. Mismatch is between arg {arg1} with shape {shape1} and arg {arg2} with shape {shape2}."
+
+class AxisError(ValueError, IndexError):
+ """
+ Exception raised by :func:`iter_indices()` and
+ :func:`broadcast_shapes()` when the `skip_axes` argument is out-of-bounds.
+
+ This is used instead of the NumPy exception of the same name so that
+ `iter_indices` does not need to depend on NumPy.
+
+ """
+ __slots__ = ("axis", "ndim")
+
+ def __init__(self, axis, ndim):
+ # NumPy allows axis=-1 for 0-d arrays
+ if (ndim < 0 or -ndim <= axis < ndim) and not (ndim == 0 and axis == -1):
+ raise ValueError(f"Invalid AxisError ({axis}, {ndim})")
+ self.axis = axis
+ self.ndim = ndim
+
+ def __str__(self):
+ return f"axis {self.axis} is out of bounds for array of dimension {self.ndim}"
+
+def broadcast_shapes(*shapes, skip_axes=()):
+ """
+ Broadcast the input shapes `shapes` to a single shape.
+
+ This is the same as :py:func:`np.broadcast_shapes()
+ `, except is also supports skipping axes in the
+ shape with `skip_axes`.
+
+ If the shapes are not broadcast compatible (excluding `skip_axes`),
+ :class:`BroadcastError` is raised.
+
+ >>> from ndindex import broadcast_shapes
+ >>> broadcast_shapes((2, 3), (3,), (4, 2, 1))
+ (4, 2, 3)
+ >>> broadcast_shapes((2, 3), (5,), (4, 2, 1))
+ Traceback (most recent call last):
+ ...
+ ndindex.shapetools.BroadcastError: shape mismatch: objects cannot be broadcast to a single shape. Mismatch is between arg 0 with shape (2, 3) and arg 1 with shape (5,).
+
+ Axes in `skip_axes` apply to each shape *before* being broadcasted. Each
+ shape will be broadcasted together with these axes removed. The dimensions
+ in skip_axes do not need to be equal or broadcast compatible with one
+ another. The final broadcasted shape be the result of broadcasting all the
+ non-skip axes.
+
+ >>> broadcast_shapes((10, 3, 2), (20, 2), skip_axes=(0,))
+ (3, 2)
+
+ """
+ shapes = [asshape(shape, allow_int=False) for shape in shapes]
+ skip_axes = normalize_skip_axes(shapes, skip_axes)
+
+ if not shapes:
+ return ()
+
+ non_skip_shapes = [remove_indices(shape, skip_axis) for shape, skip_axis in zip(shapes, skip_axes)]
+ dims = [len(shape) for shape in non_skip_shapes]
+ N = max(dims)
+
+ broadcasted_shape = [1]*N
+
+ arg = None
+ for i in range(-1, -N-1, -1):
+ for j in range(len(shapes)):
+ if dims[j] < -i:
+ continue
+ shape = non_skip_shapes[j]
+ broadcasted_side = broadcasted_shape[i]
+ shape_side = shape[i]
+ if shape_side == 1:
+ continue
+ elif broadcasted_side == 1:
+ broadcasted_side = shape_side
+ arg = j
+ elif shape_side != broadcasted_side:
+ raise BroadcastError(arg, shapes[arg], j, shapes[j])
+ broadcasted_shape[i] = broadcasted_side
+
+ return tuple(broadcasted_shape)
+
+def iter_indices(*shapes, skip_axes=(), _debug=False):
+ """
+ Iterate indices for every element of an arrays of shape `shapes`.
+
+ Each shape in `shapes` should be a shape tuple, which are broadcast
+ compatible along the non-skipped axes. Each iteration step will produce a
+ tuple of indices, one for each shape, which would correspond to the same
+ elements if the arrays of the given shapes were first broadcast together.
+
+ This is a generalization of the NumPy :py:class:`np.ndindex()
+ ` function (which otherwise has no relation).
+ `np.ndindex()` only iterates indices for a single shape, whereas
+ `iter_indices()` supports generating indices for multiple broadcast
+ compatible shapes at once. This is equivalent to first broadcasting the
+ arrays then generating indices for the single broadcasted shape.
+
+ Additionally, this function supports the ability to skip axes of the
+ shapes using `skip_axes`. These axes will be fully sliced in each index.
+ The remaining axes will be indexed one element at a time with integer
+ indices.
+
+ `skip_axes` should be a tuple of axes to skip. It can use negative
+ integers, e.g., `skip_axes=(-1,)` will skip the last axis (but note that
+ mixing negative and nonnegative skip axes is currently not supported). The
+ order of the axes in `skip_axes` does not matter. The axes in `skip_axes`
+ refer to the shapes *before* broadcasting (if you want to refer to the
+ axes after broadcasting, either broadcast the shapes and arrays first, or
+ refer to the axes using negative integers). For example,
+ `iter_indices((10, 2), (20, 1, 2), skip_axes=(0,))` will skip the size
+ `10` axis of `(10, 2)` and the size `20` axis of `(20, 1, 2)`. The result
+ is two sets of indices, one for each element of the non-skipped
+ dimensions:
+
+ >>> from ndindex import iter_indices
+ >>> for idx1, idx2 in iter_indices((10, 2), (20, 1, 2), skip_axes=(0,)):
+ ... print(idx1, idx2)
+ Tuple(slice(None, None, None), 0) Tuple(slice(None, None, None), 0, 0)
+ Tuple(slice(None, None, None), 1) Tuple(slice(None, None, None), 0, 1)
+
+ The skipped axes do not themselves need to be broadcast compatible, but
+ the shapes with all the skipped axes removed should be broadcast
+ compatible.
+
+ For example, suppose `a` is an array with shape `(3, 2, 4, 4)`, which we
+ wish to think of as a `(3, 2)` stack of 4 x 4 matrices. We can generate an
+ iterator for each matrix in the "stack" with `iter_indices((3, 2, 4, 4),
+ skip_axes=(-1, -2))`:
+
+ >>> for idx in iter_indices((3, 2, 4, 4), skip_axes=(-1, -2)):
+ ... print(idx)
+ (Tuple(0, 0, slice(None, None, None), slice(None, None, None)),)
+ (Tuple(0, 1, slice(None, None, None), slice(None, None, None)),)
+ (Tuple(1, 0, slice(None, None, None), slice(None, None, None)),)
+ (Tuple(1, 1, slice(None, None, None), slice(None, None, None)),)
+ (Tuple(2, 0, slice(None, None, None), slice(None, None, None)),)
+ (Tuple(2, 1, slice(None, None, None), slice(None, None, None)),)
+
+ .. note::
+
+ The iterates of `iter_indices` are always a tuple, even if only a
+ single shape is provided (one could instead use `for idx, in
+ iter_indices(...)` above).
+
+ As another example, say `a` is shape `(1, 3)` and `b` is shape `(2, 1)`,
+ and we want to generate indices for every value of the broadcasted
+ operation `a + b`. We can do this by using `a[idx1.raw] + b[idx2.raw]` for every
+ `idx1` and `idx2` as below:
+
+ >>> import numpy as np
+ >>> a = np.arange(3).reshape((1, 3))
+ >>> b = np.arange(100, 111, 10).reshape((2, 1))
+ >>> a
+ array([[0, 1, 2]])
+ >>> b
+ array([[100],
+ [110]])
+ >>> for idx1, idx2 in iter_indices((1, 3), (2, 1)):
+ ... print(f"{idx1 = }; {idx2 = }; {(a[idx1.raw], b[idx2.raw]) = }") # doctest: +SKIPNP1
+ idx1 = Tuple(0, 0); idx2 = Tuple(0, 0); (a[idx1.raw], b[idx2.raw]) = (np.int64(0), np.int64(100))
+ idx1 = Tuple(0, 1); idx2 = Tuple(0, 0); (a[idx1.raw], b[idx2.raw]) = (np.int64(1), np.int64(100))
+ idx1 = Tuple(0, 2); idx2 = Tuple(0, 0); (a[idx1.raw], b[idx2.raw]) = (np.int64(2), np.int64(100))
+ idx1 = Tuple(0, 0); idx2 = Tuple(1, 0); (a[idx1.raw], b[idx2.raw]) = (np.int64(0), np.int64(110))
+ idx1 = Tuple(0, 1); idx2 = Tuple(1, 0); (a[idx1.raw], b[idx2.raw]) = (np.int64(1), np.int64(110))
+ idx1 = Tuple(0, 2); idx2 = Tuple(1, 0); (a[idx1.raw], b[idx2.raw]) = (np.int64(2), np.int64(110))
+ >>> a + b
+ array([[100, 101, 102],
+ [110, 111, 112]])
+
+ To include an index into the final broadcasted array, you can simply
+ include the final broadcasted shape as one of the shapes (the NumPy
+ function :func:`np.broadcast_shapes() ` is
+ useful here).
+
+ >>> np.broadcast_shapes((1, 3), (2, 1))
+ (2, 3)
+ >>> for idx1, idx2, broadcasted_idx in iter_indices((1, 3), (2, 1), (2, 3)):
+ ... print(broadcasted_idx)
+ Tuple(0, 0)
+ Tuple(0, 1)
+ Tuple(0, 2)
+ Tuple(1, 0)
+ Tuple(1, 1)
+ Tuple(1, 2)
+
+ """
+ skip_axes = normalize_skip_axes(shapes, skip_axes)
+ shapes = [asshape(shape, allow_int=False) for shape in shapes]
+
+ if not shapes:
+ yield ()
+ return
+
+ shapes = [asshape(shape) for shape in shapes]
+ S = len(shapes)
+
+ iters = [[] for i in range(S)]
+ broadcasted_shape = broadcast_shapes(*shapes, skip_axes=skip_axes)
+
+ idxes = [-1]*S
+
+ while any(i is not None for i in idxes):
+ for s, it, shape, sk in zip(range(S), iters, shapes, skip_axes):
+ i = idxes[s]
+ if i is None:
+ continue
+ if -i > len(shape):
+ if not shape:
+ pass
+ elif len(shape) == len(sk):
+ # The whole shape is skipped. Just repeat the most recent slice
+ it[0] = ncycles(it[0], prod(broadcasted_shape))
+ else:
+ # Find the first non-skipped axis and repeat by however
+ # many implicit axes are left in the broadcasted shape
+ for j in range(-len(shape), 0):
+ if j not in sk:
+ break
+ it[j] = ncycles(it[j], prod(broadcasted_shape[:len(sk)-len(shape)+len(broadcasted_shape)]))
+
+ idxes[s] = None
+ continue
+
+ val = associated_axis(broadcasted_shape, i, sk)
+ if i in sk:
+ it.insert(0, [slice(None)])
+ else:
+ if val == 0:
+ return
+ elif val != 1 and shape[i] == 1:
+ it.insert(0, ncycles(range(shape[i]), val))
+ else:
+ it.insert(0, range(shape[i]))
+ idxes[s] -= 1
+
+ if _debug: # pragma: no cover
+ print(f"{iters = }")
+ for idxes in itertools.zip_longest(*[itertools.product(*i) for i in
+ iters], fillvalue=()):
+ yield tuple(ndindex(idx) for idx in idxes)
+
+#### Internal helpers
+
+
+def asshape(shape, axis=None, *, allow_int=True, allow_negative=False):
+ """
+ Cast `shape` as a valid NumPy shape.
+
+ The input can be an integer `n` (if `allow_int=True`), which is equivalent
+ to `(n,)`, or a tuple of integers.
+
+ If the `axis` argument is provided, an `IndexError` is raised if it is out
+ of bounds for the shape.
+
+ The resulting shape is always a tuple of nonnegative integers. If
+ `allow_negative=True`, negative integers are also allowed.
+
+ All ndindex functions that take a shape input should use::
+
+ shape = asshape(shape)
+
+ or::
+
+ shape = asshape(shape, axis=axis)
+
+ """
+ from .integer import Integer
+ from .tuple import Tuple
+ if isinstance(shape, (Tuple, Integer)):
+ raise TypeError("ndindex types are not meant to be used as a shape - "
+ "did you mean to use the built-in tuple type?")
+
+ if isinstance(shape, numbers.Number):
+ if allow_int:
+ shape = (operator_index(shape),)
+ else:
+ raise TypeError(f"expected sequence of integers, not {type(shape).__name__}")
+
+ if not isinstance(shape, Sequence) or isinstance(shape, str):
+ raise TypeError("expected sequence of integers" + allow_int*" or a single integer" + ", not " + type(shape).__name__)
+ l = len(shape)
+
+ newshape = []
+ # numpy uses __getitem__ rather than __iter__ to index into shape, so we
+ # match that
+ for i in range(l):
+ # Raise TypeError if invalid
+ val = shape[i]
+ if val is None:
+ raise ValueError("unknonwn (None) dimensions are not supported")
+
+ newshape.append(operator_index(shape[i]))
+
+ if not allow_negative and val < 0:
+ raise ValueError("unknown (negative) dimensions are not supported")
+
+ if axis is not None:
+ if len(newshape) <= axis:
+ raise IndexError(f"too many indices for array: array is {len(shape)}-dimensional, but {axis + 1} were indexed")
+
+ return tuple(newshape)
+
+def associated_axis(broadcasted_shape, i, skip_axes):
+ """
+ Return the associated element of `broadcasted_shape` corresponding to
+ `shape[i]` given `skip_axes`. If there is not such element (i.e., it's out
+ of bounds), returns None.
+
+ This function makes implicit assumptions about its input and is only
+ designed for internal use.
+
+ """
+ skip_axes = sorted(skip_axes, reverse=True)
+ if i >= 0:
+ raise NotImplementedError
+ if i in skip_axes:
+ return None
+ # We assume skip_axes are all negative and sorted
+ j = i
+ for sk in skip_axes:
+ if sk >= i:
+ j += 1
+ else:
+ break
+ if ndindex(j).isvalid(len(broadcasted_shape)):
+ return broadcasted_shape[j]
+ return None
+
+def remove_indices(x, idxes):
+ """
+ Return `x` with the indices `idxes` removed.
+
+ This function is only intended for internal usage.
+ """
+ if isinstance(idxes, int):
+ idxes = (idxes,)
+ dim = len(x)
+ _idxes = sorted({i if i >= 0 else i + dim for i in idxes})
+ _idxes = [i - a for i, a in zip(_idxes, range(len(_idxes)))]
+ _x = list(x)
+ for i in _idxes:
+ _x.pop(i)
+ return tuple(_x)
+
+def unremove_indices(x, idxes, *, val=None):
+ """
+ Insert `val` in `x` so that it appears at `idxes`.
+
+ Note that idxes must be either all negative or all nonnegative.
+
+ This function is only intended for internal usage.
+ """
+ if any(i >= 0 for i in idxes) and any(i < 0 for i in idxes):
+ # A mix of positive and negative indices presents a fundamental
+ # problem: sometimes the result is not unique. For example, x = [0];
+ # idxes = [1, -1] could be satisfied by both [0, None] or [0, None,
+ # None], depending on whether each index refers to a separate None or
+ # not (note that both cases are supported by remove_indices(), because
+ # there it is unambiguous). But even worse, in some cases, there may
+ # be no way to satisfy the given requirement. For example, given x =
+ # [0, 1, 2, 3]; idxes = [3, -3], there is no way to insert None into x
+ # so that remove_indices(res, idxes) == x. To see this, simply observe
+ # that there is no size list x such that remove_indices(x, [3, -3])
+ # returns a tuple of size 4:
+ #
+ # >>> [len(remove_indices(list(range(n)), [3, -3])) for n in range(4, 10)]
+ # [2, 3, 5, 5, 6, 7]
+ raise NotImplementedError("Mixing both negative and nonnegative idxes is not yet supported")
+ x = list(x)
+ n = len(idxes) + len(x)
+ _idxes = sorted({i if i >= 0 else i + n for i in idxes})
+ for i in _idxes:
+ x.insert(i, val)
+ return tuple(x)
+
+class ncycles:
+ """
+ Iterate `iterable` repeated `n` times.
+
+ This is based on a recipe from the `Python itertools docs
+ `_,
+ but improved to give a repr, and to denest when it can. This makes
+ debugging :func:`~.iter_indices` easier.
+
+ This is only intended for internal usage.
+
+ >>> from ndindex.shapetools import ncycles
+ >>> ncycles(range(3), 2)
+ ncycles(range(0, 3), 2)
+ >>> list(_)
+ [0, 1, 2, 0, 1, 2]
+ >>> ncycles(ncycles(range(3), 3), 2)
+ ncycles(range(0, 3), 6)
+
+ """
+ def __new__(cls, iterable, n):
+ if n == 1:
+ return iterable
+ return object.__new__(cls)
+
+ def __init__(self, iterable, n):
+ if isinstance(iterable, ncycles):
+ self.iterable = iterable.iterable
+ self.n = iterable.n*n
+ else:
+ self.iterable = iterable
+ self.n = n
+
+ def __repr__(self):
+ return f"ncycles({self.iterable!r}, {self.n!r})"
+
+ def __iter__(self):
+ return itertools.chain.from_iterable(itertools.repeat(tuple(self.iterable), self.n))
+
+def normalize_skip_axes(shapes, skip_axes):
+ """
+ Return a canonical form of `skip_axes` corresponding to `shapes`.
+
+ A canonical form of `skip_axes` is a list of tuples of integers, one for
+ each shape in `shapes`, which are a unique set of axes for each
+ corresponding shape.
+
+ If `skip_axes` is an integer, this is basically `[(skip_axes,) for s
+ in shapes]`. If `skip_axes` is a tuple, it is like `[skip_axes for s in
+ shapes]`.
+
+ The `skip_axes` must always refer to unique axes in each shape.
+
+ The returned `skip_axes` will always be negative integers and will be
+ sorted.
+
+ This function is only intended for internal usage.
+
+ """
+ # Note: we assume asshape has already been called on the shapes in shapes
+ if isinstance(skip_axes, Sequence):
+ if skip_axes and all(isinstance(i, Sequence) for i in skip_axes):
+ if len(skip_axes) != len(shapes):
+ raise ValueError(f"Expected {len(shapes)} skip_axes")
+ return [normalize_skip_axes([shape], skip_axis)[0] for shape, skip_axis in zip(shapes, skip_axes)]
+ else:
+ try:
+ [operator_index(i) for i in skip_axes]
+ except TypeError:
+ raise TypeError("skip_axes must be an integer, a tuple of integers, or a list of tuples of integers")
+
+ skip_axes = asshape(skip_axes, allow_negative=True)
+
+ # From here, skip_axes is a single tuple of integers
+
+ if not shapes and skip_axes:
+ raise ValueError("skip_axes must be empty if there are no shapes")
+
+ new_skip_axes = []
+ for shape in shapes:
+ s = tuple(sorted(ndindex(i).reduce(len(shape), negative_int=True, axiserror=True).raw for i in skip_axes))
+ if len(s) != len(set(s)):
+ err = ValueError(f"skip_axes {skip_axes} are not unique for shape {shape}")
+ # For testing
+ err.skip_axes = skip_axes
+ err.shape = shape
+ raise err
+ new_skip_axes.append(s)
+ return new_skip_axes
diff --git a/ndindex/slice.py b/ndindex/slice.py
index 00497d89..c73aca43 100644
--- a/ndindex/slice.py
+++ b/ndindex/slice.py
@@ -1,5 +1,6 @@
-from .ndindex import NDIndex, asshape, operator_index
+from .ndindex import NDIndex, operator_index
from .subindex_helpers import subindex_slice
+from .shapetools import asshape
class default:
"""
@@ -28,8 +29,8 @@ class Slice(NDIndex):
`Slice.args` always has three arguments, and does not make any distinction
between, for instance, `Slice(x, y)` and `Slice(x, y, None)`. This is
- because Python itself does not make the distinction between x:y and x:y:
- syntactically.
+ because Python itself does not make the distinction between `x:y` and
+ `x:y:` syntactically.
See :ref:`slices-docs` for a description of the semantic meaning of slices
on arrays.
@@ -75,9 +76,11 @@ def _typecheck(self, start, stop=default, step=None):
return args
def __hash__(self):
- # We can't use the default hash(self.raw) because slices are not
- # hashable
- return hash(self.args)
+ # Slices are only hashable in Python 3.12+
+ try:
+ return hash(self.raw)
+ except TypeError: # pragma: no cover
+ return hash(self.args)
@property
def raw(self):
@@ -199,7 +202,7 @@ def __len__(self):
return len(range(start, stop, step))
- def reduce(self, shape=None, axis=0):
+ def reduce(self, shape=None, *, axis=0, negative_int=False):
"""
`Slice.reduce` returns a slice that is canonicalized for an array of the
given shape, or for any shape if `shape` is `None` (the default).
@@ -470,6 +473,13 @@ def reduce(self, shape=None, axis=0):
stop = start % -step - 1
return self.__class__(start, stop, step)
+ def isvalid(self, shape):
+ # The docstring for this method is on the NDIndex base class
+ shape = asshape(shape)
+
+ # All slices are valid as long as there is at least one dimension
+ return bool(shape)
+
def newshape(self, shape):
# The docstring for this method is on the NDIndex base class
shape = asshape(shape)
diff --git a/ndindex/tests/doctest.py b/ndindex/tests/doctest.py
index d6917141..55a0a02e 100644
--- a/ndindex/tests/doctest.py
+++ b/ndindex/tests/doctest.py
@@ -3,12 +3,6 @@
This runs the doctests but ignores trailing ``` in Markdown documents.
-This also adds the flag SKIP37 to allow skipping doctests in Python 3.7.
-
->>> import sys
->>> sys.version_info[1] > 7 # doctest: +SKIP37
-True
-
Running this separately from pytest also allows us to not include the doctests
in the coverage. It also allows us to force a separate namespace for each
docstring's doctest, which the pytest doctest integration does not allow.
@@ -22,17 +16,22 @@
"""
+import numpy
+
import sys
import unittest
import glob
import os
from contextlib import contextmanager
from doctest import (DocTestRunner, DocFileSuite, DocTestSuite,
- NORMALIZE_WHITESPACE, register_optionflag, SKIP)
+ NORMALIZE_WHITESPACE, register_optionflag)
-SKIP37 = register_optionflag("SKIP37")
-
-PY37 = sys.version_info[:2] == (3, 7)
+SKIPNP1 = register_optionflag("SKIPNP1")
+NP1 = numpy.__version__.startswith('1')
+if NP1:
+ SKIP_THIS_VERSION = SKIPNP1
+else:
+ SKIP_THIS_VERSION = 0
@contextmanager
def patch_doctest():
@@ -44,13 +43,17 @@ def patch_doctest():
orig_run = DocTestRunner.run
def run(self, test, **kwargs):
+ filtered_examples = []
+
for example in test.examples:
- if PY37 and SKIP37 in example.options:
- example.options[SKIP] = True
+ if SKIP_THIS_VERSION not in example.options:
+ filtered_examples.append(example)
+
# Remove ```
example.want = example.want.replace('```\n', '')
example.exc_msg = example.exc_msg and example.exc_msg.replace('```\n', '')
+ test.examples = filtered_examples
return orig_run(self, test, **kwargs)
try:
diff --git a/ndindex/tests/helpers.py b/ndindex/tests/helpers.py
index a14ddd19..1cceaff9 100644
--- a/ndindex/tests/helpers.py
+++ b/ndindex/tests/helpers.py
@@ -1,7 +1,7 @@
import sys
from itertools import chain
-from functools import reduce
-from operator import mul
+import warnings
+from functools import wraps
from numpy import intp, bool_, array, broadcast_shapes
import numpy.testing
@@ -11,11 +11,13 @@
from hypothesis import assume, note
from hypothesis.strategies import (integers, none, one_of, lists, just,
builds, shared, composite, sampled_from,
- booleans)
+ nothing, tuples as hypothesis_tuples)
from hypothesis.extra.numpy import (arrays, mutually_broadcastable_shapes as
- mbs, BroadcastableShapes)
+ mbs, BroadcastableShapes, valid_tuple_axes)
from ..ndindex import ndindex
+from ..shapetools import remove_indices, unremove_indices
+from .._crt import prod
# Hypothesis strategies for generating indices. Note that some of these
# strategies are nominally already defined in hypothesis, but we redefine them
@@ -23,10 +25,6 @@
# hypothesis's slices strategy does not generate slices with negative indices.
# Similarly, hypothesis.extra.numpy.basic_indices only generates tuples.
-# np.prod has overflow and math.prod is Python 3.8+ only
-def prod(seq):
- return reduce(mul, seq, 1)
-
nonnegative_ints = integers(0, 10)
negative_ints = integers(-10, -1)
ints = lambda: one_of(nonnegative_ints, negative_ints)
@@ -50,19 +48,62 @@ def tuples(elements, *, min_size=0, max_size=None, unique_by=None, unique=False)
# See https://github.com/numpy/numpy/issues/15753
lambda shape: prod([i for i in shape if i]) < MAX_ARRAY_SIZE)
-_short_shapes = tuples(integers(0, 10)).filter(
+_short_shapes = lambda n: tuples(integers(0, 10), min_size=n).filter(
# numpy gives errors with empty arrays with large shapes.
# See https://github.com/numpy/numpy/issues/15753
lambda shape: prod([i for i in shape if i]) < SHORT_MAX_ARRAY_SIZE)
+# short_shapes should be used in place of shapes in any test function that
+# uses ndindices, boolean_arrays, or tuples
+short_shapes = shared(_short_shapes(0))
+
+_integer_arrays = arrays(intp, short_shapes)
+integer_scalars = arrays(intp, ()).map(lambda x: x[()])
+integer_arrays = one_of(integer_scalars, _integer_arrays.flatmap(lambda x: one_of(just(x), just(x.tolist()))))
+
+# We need to make sure shapes for boolean arrays are generated in a way that
+# makes them related to the test array shape. Otherwise, it will be very
+# difficult for the boolean array index to match along the test array, which
+# means we won't test any behavior other than IndexError.
+
+@composite
+def subsequences(draw, sequence):
+ seq = draw(sequence)
+ start = draw(integers(0, max(0, len(seq)-1)))
+ stop = draw(integers(start, len(seq)))
+ return seq[start:stop]
+
+_boolean_arrays = arrays(bool_, one_of(subsequences(short_shapes), short_shapes))
+boolean_scalars = arrays(bool_, ()).map(lambda x: x[()])
+boolean_arrays = one_of(boolean_scalars, _boolean_arrays.flatmap(lambda x: one_of(just(x), just(x.tolist()))))
+
+def _doesnt_raise(idx):
+ try:
+ ndindex(idx)
+ except (IndexError, ValueError, NotImplementedError):
+ return False
+ return True
+
+Tuples = tuples(one_of(ellipses(), ints(), slices(), newaxes(),
+ integer_arrays, boolean_arrays)).filter(_doesnt_raise)
+
+ndindices = one_of(
+ ints(),
+ slices(),
+ ellipses(),
+ newaxes(),
+ Tuples,
+ integer_arrays,
+ boolean_arrays,
+).filter(_doesnt_raise)
+
# Note: We could use something like this:
# mutually_broadcastable_shapes = shared(integers(1, 32).flatmap(lambda i: mbs(num_shapes=i).filter(
# lambda broadcastable_shapes: prod([i for i in broadcastable_shapes.result_shape if i]) < MAX_ARRAY_SIZE)))
-
@composite
-def _mutually_broadcastable_shapes(draw):
+def _mutually_broadcastable_shapes(draw, *, shapes=short_shapes, min_shapes=0, max_shapes=32, min_side=0):
# mutually_broadcastable_shapes() with the default inputs doesn't generate
# very interesting examples (see
# https://github.com/HypothesisWorks/hypothesis/issues/3170). It's very
@@ -77,23 +118,23 @@ def _mutually_broadcastable_shapes(draw):
# like. But it generates enough "real" interesting shapes that both of
# these workarounds are worth doing (plus I don't know if any other better
# way of handling the situation).
- base_shape = draw(short_shapes)
+ base_shape = draw(shapes)
input_shapes, result_shape = draw(
mbs(
- num_shapes=32,
+ num_shapes=max_shapes,
base_shape=base_shape,
- min_side=0,
+ min_side=min_side,
))
# The hypothesis mutually_broadcastable_shapes doesn't allow num_shapes to
# be a strategy. It's tempting to do something like num_shapes =
- # draw(integers(1, 32)), but this shrinks poorly. See
+ # draw(integers(min_shapes, max_shapes)), but this shrinks poorly. See
# https://github.com/HypothesisWorks/hypothesis/issues/3151. So instead of
- # using a strategy to draw the number of shapes, we just generate 32
+ # using a strategy to draw the number of shapes, we just generate max_shapes
# shapes and pick a subset of them.
- final_input_shapes = draw(lists(sampled_from(input_shapes), min_size=0, max_size=32,
- unique_by=id,))
+ final_input_shapes = draw(lists(sampled_from(input_shapes),
+ min_size=min_shapes, max_size=max_shapes))
# Note: result_shape is input_shapes broadcasted with base_shape, but
@@ -106,69 +147,230 @@ def _mutually_broadcastable_shapes(draw):
# is already somewhat limited by the mutually_broadcastable_shapes
# defaults, and pretty unlikely, but we filter again here just to be safe.
if not prod([i for i in final_result_shape if i]) < SHORT_MAX_ARRAY_SIZE: # pragma: no cover
- note(f"Filtering {result_shape}")
+ note(f"Filtering the shape {result_shape} (too many elements)")
assume(False)
return BroadcastableShapes(final_input_shapes, final_result_shape)
mutually_broadcastable_shapes = shared(_mutually_broadcastable_shapes())
-@composite
-def skip_axes(draw):
- shapes, result_shape = draw(mutually_broadcastable_shapes)
- n = len(result_shape)
- axes = draw(one_of(none(),
- lists(integers(-n, max(0, n-1)), max_size=n)))
- if isinstance(axes, list):
- axes = tuple(axes)
- # Sometimes return an integer
- if len(axes) == 1 and draw(booleans()): # pragma: no cover
- return axes[0]
- return axes
+def _fill_shape(draw,
+ *,
+ result_shape,
+ skip_axes,
+ skip_axes_values):
+ max_n = max([i + 1 if i >= 0 else -i for i in skip_axes], default=0)
+ assume(max_n <= len(skip_axes) + len(result_shape))
+ dim = draw(integers(min_value=max_n, max_value=len(skip_axes) + len(result_shape)))
+ new_shape = ['placeholder']*dim
+ for i in skip_axes:
+ assume(new_shape[i] is not None) # skip_axes must be unique
+ new_shape[i] = None
+ j = -1
+ for i in range(-1, -dim - 1, -1):
+ if new_shape[i] is None:
+ new_shape[i] = draw(skip_axes_values)
+ else:
+ new_shape[i] = draw(sampled_from([result_shape[j], 1]))
+ j -= 1
+ while new_shape and new_shape[0] == 'placeholder': # pragma: no cover
+ # Can happen if positive and negative skip_axes refer to the same
+ # entry
+ new_shape.pop(0)
+
+ # This will happen if the skip axes are too large
+ assume('placeholder' not in new_shape)
+
+ if prod([i for i in new_shape if i]) >= SHORT_MAX_ARRAY_SIZE:
+ note(f"Filtering the shape {new_shape} (too many elements)")
+ assume(False)
-# We need to make sure shapes for boolean arrays are generated in a way that
-# makes them related to the test array shape. Otherwise, it will be very
-# difficult for the boolean array index to match along the test array, which
-# means we won't test any behavior other than IndexError.
+ return tuple(new_shape)
-# short_shapes should be used in place of shapes in any test function that
-# uses ndindices, boolean_arrays, or tuples
-short_shapes = shared(_short_shapes)
+skip_axes_with_broadcasted_shape_type = shared(sampled_from([int, tuple, list]))
-_integer_arrays = arrays(intp, short_shapes)
-integer_scalars = arrays(intp, ()).map(lambda x: x[()])
-integer_arrays = one_of(integer_scalars, _integer_arrays.flatmap(lambda x: one_of(just(x), just(x.tolist()))))
+@composite
+def _mbs_and_skip_axes(
+ draw,
+ shapes=short_shapes,
+ min_shapes=0,
+ max_shapes=32,
+ skip_axes_type_st=skip_axes_with_broadcasted_shape_type,
+ skip_axes_values=integers(0, 20),
+ num_skip_axes=None,
+):
+ """
+ mutually_broadcastable_shapes except skip_axes() axes might not be
+ broadcastable
+
+ The result_shape will be None in the position of skip_axes.
+ """
+ skip_axes_type = draw(skip_axes_type_st)
+ _result_shape = draw(shapes)
+ if _result_shape == ():
+ assume(num_skip_axes is None)
+
+ ndim = len(_result_shape)
+ num_shapes = draw(integers(min_value=min_shapes, max_value=max_shapes))
+ if not num_shapes:
+ assume(num_skip_axes is None)
+ num_skip_axes = 0
+ if not ndim:
+ return BroadcastableShapes([()]*num_shapes, ()), ()
+
+ if num_skip_axes is not None:
+ min_skip_axes = max_skip_axes = num_skip_axes
+ else:
+ min_skip_axes = 0
+ max_skip_axes = None
+
+ # int and single tuple cases must be limited to N to ensure that they are
+ # correct for all shapes
+ if skip_axes_type == int:
+ assume(num_skip_axes in [None, 1])
+ skip_axes = draw(valid_tuple_axes(ndim, min_size=1, max_size=1))[0]
+ _skip_axes = [(skip_axes,)]*num_shapes
+ elif skip_axes_type == tuple:
+ skip_axes = draw(tuples(integers(-ndim, ndim-1), min_size=min_skip_axes,
+ max_size=max_skip_axes, unique=True))
+ _skip_axes = [skip_axes]*num_shapes
+ elif skip_axes_type == list:
+ skip_axes = []
+ for i in range(num_shapes):
+ skip_axes.append(draw(tuples(integers(-ndim, ndim+1), min_size=min_skip_axes,
+ max_size=max_skip_axes, unique=True)))
+ _skip_axes = skip_axes
+
+ shapes = []
+ for i in range(num_shapes):
+ shapes.append(_fill_shape(draw, result_shape=_result_shape, skip_axes=_skip_axes[i],
+ skip_axes_values=skip_axes_values))
+
+ non_skip_shapes = [remove_indices(shape, sk) for shape, sk in
+ zip(shapes, _skip_axes)]
+ # Broadcasting the result _fill_shape may produce a shape different from
+ # _result_shape because it might not have filled all dimensions, or it
+ # might have chosen 1 for a dimension every time. Ideally we would just be
+ # using shapes from mutually_broadcastable_shapes, but I don't know how to
+ # reverse inject skip axes into shapes in general (see the comment in
+ # unremove_indices). So for now, we just use the actual broadcast of the
+ # non-skip shapes. Note that we use np.broadcast_shapes here instead of
+ # ndindex.broadcast_shapes because test_broadcast_shapes itself uses this
+ # strategy.
+ broadcasted_shape = broadcast_shapes(*non_skip_shapes)
+
+ return BroadcastableShapes(shapes, broadcasted_shape), skip_axes
+
+mbs_and_skip_axes = shared(_mbs_and_skip_axes())
+
+mutually_broadcastable_shapes_with_skipped_axes = mbs_and_skip_axes.map(
+ lambda i: i[0])
+skip_axes_st = mbs_and_skip_axes.map(lambda i: i[1])
@composite
-def subsequences(draw, sequence):
- seq = draw(sequence)
- start = draw(integers(0, max(0, len(seq)-1)))
- stop = draw(integers(start, len(seq)))
- return seq[start:stop]
+def _cross_shapes_and_skip_axes(draw):
+ (shapes, _broadcasted_shape), skip_axes = draw(_mbs_and_skip_axes(
+ shapes=_short_shapes(2),
+ min_shapes=2,
+ max_shapes=2,
+ num_skip_axes=1,
+ # TODO: Test other skip axes types
+ skip_axes_type_st=just(list),
+ skip_axes_values=just(3),
+ ))
+
+ broadcasted_skip_axis = draw(integers(-len(_broadcasted_shape)-1, len(_broadcasted_shape)))
+ broadcasted_shape = unremove_indices(_broadcasted_shape,
+ [broadcasted_skip_axis], val=3)
+ skip_axes.append((broadcasted_skip_axis,))
+
+ return BroadcastableShapes(shapes, broadcasted_shape), skip_axes
+
+cross_shapes_and_skip_axes = shared(_cross_shapes_and_skip_axes())
+cross_shapes = cross_shapes_and_skip_axes.map(lambda i: i[0])
+cross_skip_axes = cross_shapes_and_skip_axes.map(lambda i: i[1])
-_boolean_arrays = arrays(bool_, one_of(subsequences(short_shapes), short_shapes))
-boolean_scalars = arrays(bool_, ()).map(lambda x: x[()])
-boolean_arrays = one_of(boolean_scalars, _boolean_arrays.flatmap(lambda x: one_of(just(x), just(x.tolist()))))
+@composite
+def cross_arrays_st(draw):
+ broadcastable_shapes = draw(cross_shapes)
+ shapes, broadcasted_shape = broadcastable_shapes
+
+ # Sanity check
+ assert len(shapes) == 2
+ # We need to generate fairly random arrays. Otherwise, if they are too
+ # similar to each other, like two arange arrays would be, the cross
+ # product will be 0. We also disable the fill feature in arrays() for the
+ # same reason, as it would otherwise generate too many vectors that are
+ # colinear.
+ a = draw(arrays(dtype=int, shape=shapes[0], elements=integers(-100, 100), fill=nothing()))
+ b = draw(arrays(dtype=int, shape=shapes[1], elements=integers(-100, 100), fill=nothing()))
+
+ return a, b
+
+@composite
+def _matmul_shapes_and_skip_axes(draw):
+ (shapes, _broadcasted_shape), skip_axes = draw(_mbs_and_skip_axes(
+ shapes=_short_shapes(2),
+ min_shapes=2,
+ max_shapes=2,
+ num_skip_axes=2,
+ # TODO: Test other skip axes types
+ skip_axes_type_st=just(list),
+ skip_axes_values=just(None),
+ ))
+
+ broadcasted_skip_axes = draw(hypothesis_tuples(*[
+ integers(-len(_broadcasted_shape)-1, len(_broadcasted_shape))
+ ]*2))
-def _doesnt_raise(idx):
try:
- ndindex(idx)
- except (IndexError, ValueError, NotImplementedError):
- return False
- return True
+ broadcasted_shape = unremove_indices(_broadcasted_shape,
+ broadcasted_skip_axes)
+ except NotImplementedError:
+ # TODO: unremove_indices only works with both positive or both negative
+ assume(False)
+ # Make sure the indices are unique
+ assume(len(set(broadcasted_skip_axes)) == len(broadcasted_skip_axes))
+
+ skip_axes.append(broadcasted_skip_axes)
+
+ # (n, m) @ (m, k) -> (n, k)
+ n, m, k = draw(hypothesis_tuples(integers(0, 10), integers(0, 10),
+ integers(0, 10)))
+ shape1, shape2 = map(list, shapes)
+ ax1, ax2 = skip_axes[0]
+ shape1[ax1] = n
+ shape1[ax2] = m
+ ax1, ax2 = skip_axes[1]
+ shape2[ax1] = m
+ shape2[ax2] = k
+ broadcasted_shape = list(broadcasted_shape)
+ ax1, ax2 = skip_axes[2]
+ broadcasted_shape[ax1] = n
+ broadcasted_shape[ax2] = k
+
+ shapes = (tuple(shape1), tuple(shape2))
+ broadcasted_shape = tuple(broadcasted_shape)
+
+ return BroadcastableShapes(shapes, broadcasted_shape), skip_axes
+
+matmul_shapes_and_skip_axes = shared(_matmul_shapes_and_skip_axes())
+matmul_shapes = matmul_shapes_and_skip_axes.map(lambda i: i[0])
+matmul_skip_axes = matmul_shapes_and_skip_axes.map(lambda i: i[1])
-Tuples = tuples(one_of(ellipses(), ints(), slices(), newaxes(),
- integer_arrays, boolean_arrays)).filter(_doesnt_raise)
+@composite
+def matmul_arrays_st(draw):
+ broadcastable_shapes = draw(matmul_shapes)
+ shapes, broadcasted_shape = broadcastable_shapes
-ndindices = one_of(
- ints(),
- slices(),
- ellipses(),
- newaxes(),
- Tuples,
- integer_arrays,
- boolean_arrays,
-).filter(_doesnt_raise)
+ # Sanity check
+ assert len(shapes) == 2
+ a = draw(arrays(dtype=int, shape=shapes[0], elements=integers(-100, 100)))
+ b = draw(arrays(dtype=int, shape=shapes[1], elements=integers(-100, 100)))
+
+ return a, b
+
+reduce_kwargs = sampled_from([{}, {'negative_int': False}, {'negative_int': True}])
def assert_equal(actual, desired, err_msg='', verbose=True):
"""
@@ -181,7 +383,16 @@ def assert_equal(actual, desired, err_msg='', verbose=True):
assert actual.shape == desired.shape, err_msg or f"{actual.shape} != {desired.shape}"
assert actual.dtype == desired.dtype, err_msg or f"{actual.dtype} != {desired.dtype}"
-def check_same(a, idx, raw_func=lambda a, idx: a[idx],
+def warnings_are_errors(f):
+ @wraps(f)
+ def inner(*args, **kwargs):
+ with warnings.catch_warnings():
+ warnings.simplefilter("error")
+ return f(*args, **kwargs)
+ return inner
+
+@warnings_are_errors
+def check_same(a, idx, *, raw_func=lambda a, idx: a[idx],
ndindex_func=lambda a, index: a[index.raw],
same_exception=True, assert_equal=assert_equal):
"""
@@ -215,8 +426,11 @@ def assert_equal(x, y):
try:
a_raw = raw_func(a, idx)
except Warning as w:
- if ("Using a non-tuple sequence for multidimensional indexing is deprecated" in w.args[0]):
- idx = array(idx)
+ # In NumPy < 1.23, this is a FutureWarning. In 1.23 the
+ # deprecation was removed and lists are always interpreted as
+ # array indices.
+ if ("Using a non-tuple sequence for multidimensional indexing is deprecated" in w.args[0]): # pragma: no cover
+ idx = array(idx, dtype=intp)
a_raw = raw_func(a, idx)
elif "Out of bound index found. This was previously ignored when the indexing result contained no elements. In the future the index error will be raised. This error occurs either due to an empty slice, or if an array has zero elements even before indexing." in w.args[0]:
same_exception = False
diff --git a/ndindex/tests/test_as_subindex.py b/ndindex/tests/test_as_subindex.py
index a664e0bb..f6a466a4 100644
--- a/ndindex/tests/test_as_subindex.py
+++ b/ndindex/tests/test_as_subindex.py
@@ -8,7 +8,7 @@
from ..ndindex import ndindex
from ..integerarray import IntegerArray
from ..tuple import Tuple
-from .helpers import ndindices, short_shapes, assert_equal
+from .helpers import ndindices, short_shapes, assert_equal, warnings_are_errors
@example((slice(0, 8), slice(0, 9), slice(0, 10)),
([2, 5, 6, 7], slice(1, 9, 1), slice(5, 10, 1)),
@@ -50,6 +50,7 @@
@example(0, (slice(None, 0, None), Ellipsis), 1)
@example(0, (slice(1, 2),), 1)
@given(ndindices, ndindices, one_of(integers(0, 100), short_shapes))
+@warnings_are_errors
def test_as_subindex_hypothesis(idx1, idx2, shape):
if isinstance(shape, int):
a = arange(shape)
diff --git a/ndindex/tests/test_booleanarray.py b/ndindex/tests/test_booleanarray.py
index f18f2e9c..171140b6 100644
--- a/ndindex/tests/test_booleanarray.py
+++ b/ndindex/tests/test_booleanarray.py
@@ -1,11 +1,13 @@
-from numpy import prod, arange, array, bool_, empty, full
+from numpy import prod, arange, array, bool_, empty, full, __version__ as np_version
+
+NP1 = np_version.startswith('1')
from hypothesis import given, example
from hypothesis.strategies import one_of, integers
from pytest import raises
-from .helpers import boolean_arrays, short_shapes, check_same, assert_equal
+from .helpers import boolean_arrays, short_shapes, check_same, assert_equal, reduce_kwargs
from ..booleanarray import BooleanArray
@@ -38,8 +40,8 @@ def test_booleanarray_hypothesis(idx, shape):
a = arange(prod(shape)).reshape(shape)
check_same(a, idx)
-@given(boolean_arrays, one_of(short_shapes, integers(0, 10)))
-def test_booleanarray_reduce_no_shape_hypothesis(idx, shape):
+@given(boolean_arrays, one_of(short_shapes, integers(0, 10)), reduce_kwargs)
+def test_booleanarray_reduce_no_shape_hypothesis(idx, shape, kwargs):
if isinstance(shape, int):
a = arange(shape)
else:
@@ -47,12 +49,12 @@ def test_booleanarray_reduce_no_shape_hypothesis(idx, shape):
index = BooleanArray(idx)
- check_same(a, index.raw, ndindex_func=lambda a, x: a[x.reduce().raw])
+ check_same(a, index.raw, ndindex_func=lambda a, x: a[x.reduce(**kwargs).raw])
-@example(full((1, 9), True), (3, 3))
-@example(full((1, 9), False), (3, 3))
-@given(boolean_arrays, one_of(short_shapes, integers(0, 10)))
-def test_booleanarray_reduce_hypothesis(idx, shape):
+@example(full((1, 9), True), (3, 3), {})
+@example(full((1, 9), False), (3, 3), {})
+@given(boolean_arrays, one_of(short_shapes, integers(0, 10)), reduce_kwargs)
+def test_booleanarray_reduce_hypothesis(idx, shape, kwargs):
if isinstance(shape, int):
a = arange(shape)
else:
@@ -60,10 +62,12 @@ def test_booleanarray_reduce_hypothesis(idx, shape):
index = BooleanArray(idx)
- check_same(a, index.raw, ndindex_func=lambda a, x: a[x.reduce(shape).raw])
+ same_exception = not NP1
+ check_same(a, index.raw, ndindex_func=lambda a, x: a[x.reduce(shape, **kwargs).raw],
+ same_exception=same_exception)
try:
- reduced = index.reduce(shape)
+ reduced = index.reduce(shape, **kwargs)
except IndexError:
pass
else:
@@ -72,8 +76,8 @@ def test_booleanarray_reduce_hypothesis(idx, shape):
assert reduced == index
# Idempotency
- assert reduced.reduce() == reduced
- assert reduced.reduce(shape) == reduced
+ assert reduced.reduce(**kwargs) == reduced
+ assert reduced.reduce(shape, **kwargs) == reduced
@given(boolean_arrays, one_of(short_shapes, integers(0, 10)))
def test_booleanarray_isempty_hypothesis(idx, shape):
diff --git a/ndindex/tests/test_broadcast_arrays.py b/ndindex/tests/test_broadcast_arrays.py
index 64858fda..75780a93 100644
--- a/ndindex/tests/test_broadcast_arrays.py
+++ b/ndindex/tests/test_broadcast_arrays.py
@@ -9,7 +9,7 @@
from ..integerarray import IntegerArray
from ..integer import Integer
from ..tuple import Tuple
-from .helpers import ndindices, check_same, short_shapes
+from .helpers import ndindices, check_same, short_shapes, warnings_are_errors
@example((..., False, False), 1)
@example((True, False), 1)
@@ -21,6 +21,7 @@
@example(False, 1)
@example([[True, False], [False, False]], (2, 2, 3))
@given(ndindices, one_of(short_shapes, integers(0, 10)))
+@warnings_are_errors
def test_broadcast_arrays_hypothesis(idx, shape):
if isinstance(shape, int):
a = arange(shape)
diff --git a/ndindex/tests/test_chunking.py b/ndindex/tests/test_chunking.py
index 0ccdb959..34a7ff7a 100644
--- a/ndindex/tests/test_chunking.py
+++ b/ndindex/tests/test_chunking.py
@@ -50,7 +50,7 @@ def test_ChunkSize_args(chunk_size_tuple, idx):
try:
ndindex(idx)
- except ValueError:
+ except ValueError: # pragma: no cover
# Filter out invalid slices (TODO: do this in the strategy)
assume(False)
diff --git a/ndindex/tests/test_ellipsis.py b/ndindex/tests/test_ellipsis.py
index 1b005172..4912cc6f 100644
--- a/ndindex/tests/test_ellipsis.py
+++ b/ndindex/tests/test_ellipsis.py
@@ -4,7 +4,7 @@
from hypothesis.strategies import one_of, integers
from ..ndindex import ndindex
-from .helpers import check_same, prod, shapes, ellipses
+from .helpers import check_same, prod, shapes, ellipses, reduce_kwargs
def test_ellipsis_exhaustive():
for n in range(10):
@@ -21,20 +21,20 @@ def test_ellipsis_reduce_exhaustive():
a = arange(n)
check_same(a, ..., ndindex_func=lambda a, x: a[x.reduce((n,)).raw])
-@given(ellipses(), shapes)
-def test_ellipsis_reduce_hypothesis(idx, shape):
+@given(ellipses(), shapes, reduce_kwargs)
+def test_ellipsis_reduce_hypothesis(idx, shape, kwargs):
a = arange(prod(shape)).reshape(shape)
- check_same(a, idx, ndindex_func=lambda a, x: a[x.reduce(shape).raw])
+ check_same(a, idx, ndindex_func=lambda a, x: a[x.reduce(shape, **kwargs).raw])
def test_ellipsis_reduce_no_shape_exhaustive():
for n in range(10):
a = arange(n)
check_same(a, ..., ndindex_func=lambda a, x: a[x.reduce().raw])
-@given(ellipses(), shapes)
-def test_ellipsis_reduce_no_shape_hypothesis(idx, shape):
+@given(ellipses(), shapes, reduce_kwargs)
+def test_ellipsis_reduce_no_shape_hypothesis(idx, shape, kwargs):
a = arange(prod(shape)).reshape(shape)
- check_same(a, idx, ndindex_func=lambda a, x: a[x.reduce().raw])
+ check_same(a, idx, ndindex_func=lambda a, x: a[x.reduce(**kwargs).raw])
@given(ellipses(), one_of(shapes, integers(0, 10)))
def test_ellipsis_isempty_hypothesis(idx, shape):
diff --git a/ndindex/tests/test_integer.py b/ndindex/tests/test_integer.py
index fd354ee4..53c3407a 100644
--- a/ndindex/tests/test_integer.py
+++ b/ndindex/tests/test_integer.py
@@ -7,7 +7,7 @@
from ..integer import Integer
from ..slice import Slice
-from .helpers import check_same, ints, prod, shapes, iterslice, assert_equal
+from .helpers import check_same, ints, prod, shapes, iterslice, assert_equal, reduce_kwargs
def test_integer_args():
zero = Integer(0)
@@ -46,51 +46,66 @@ def test_integer_len_hypothesis(i):
idx = Integer(i)
assert len(idx) == 1
-
def test_integer_reduce_exhaustive():
a = arange(10)
for i in range(-12, 12):
- check_same(a, i, ndindex_func=lambda a, x: a[x.reduce((10,)).raw])
+ for kwargs in [{'negative_int': False}, {'negative_int': True}, {}]:
+ check_same(a, i, ndindex_func=lambda a, x: a[x.reduce((10,), **kwargs).raw])
- try:
- reduced = Integer(i).reduce(10)
- except IndexError:
- pass
- else:
- assert reduced.raw >= 0
+ negative_int = kwargs.get('negative_int', False)
+
+ try:
+ reduced = Integer(i).reduce(10, **kwargs)
+ except IndexError:
+ pass
+ else:
+ if negative_int:
+ assert reduced.raw < 0
+ else:
+ assert reduced.raw >= 0
- # Idempotency
- assert reduced.reduce() == reduced
- assert reduced.reduce(10) == reduced
+ # Idempotency
+ assert reduced.reduce(**kwargs) == reduced
+ assert reduced.reduce(10, **kwargs) == reduced
-@given(ints(), shapes)
-def test_integer_reduce_hypothesis(i, shape):
+@given(ints(), shapes, reduce_kwargs)
+def test_integer_reduce_hypothesis(i, shape, kwargs):
a = arange(prod(shape)).reshape(shape)
# The axis argument is tested implicitly in the Tuple.reduce test. It is
# difficult to test here because we would have to pass in a Tuple to
# check_same.
- check_same(a, i, ndindex_func=lambda a, x: a[x.reduce(shape).raw])
+ check_same(a, i, ndindex_func=lambda a, x: a[x.reduce(shape, **kwargs).raw])
+
+ negative_int = kwargs.get('negative_int', False)
try:
- reduced = Integer(i).reduce(shape)
+ reduced = Integer(i).reduce(shape, **kwargs)
except IndexError:
pass
else:
- assert reduced.raw >= 0
+ if negative_int:
+ assert reduced.raw < 0
+ else:
+ assert reduced.raw >= 0
# Idempotency
- assert reduced.reduce() == reduced
- assert reduced.reduce(shape) == reduced
+ assert reduced.reduce(**kwargs) == reduced
+ assert reduced.reduce(shape, **kwargs) == reduced
def test_integer_reduce_no_shape_exhaustive():
a = arange(10)
for i in range(-12, 12):
check_same(a, i, ndindex_func=lambda a, x: a[x.reduce().raw])
-@given(ints(), shapes)
-def test_integer_reduce_no_shape_hypothesis(i, shape):
+@given(ints(), shapes, reduce_kwargs)
+def test_integer_reduce_no_shape_hypothesis(i, shape, kwargs):
a = arange(prod(shape)).reshape(shape)
- check_same(a, i, ndindex_func=lambda a, x: a[x.reduce().raw])
+ check_same(a, i, ndindex_func=lambda a, x: a[x.reduce(**kwargs).raw])
+
+@given(ints())
+def test_integer_reduce_no_shape_unchanged(i):
+ idx = Integer(i)
+ assert idx.reduce() == idx.reduce(negative_int=False) == idx.reduce(negative_int=True) == i
def test_integer_newshape_exhaustive():
shape = 5
diff --git a/ndindex/tests/test_integerarray.py b/ndindex/tests/test_integerarray.py
index 615c38af..27ae73cd 100644
--- a/ndindex/tests/test_integerarray.py
+++ b/ndindex/tests/test_integerarray.py
@@ -5,7 +5,7 @@
from pytest import raises
-from .helpers import integer_arrays, short_shapes, check_same, assert_equal
+from .helpers import integer_arrays, short_shapes, check_same, assert_equal, reduce_kwargs
from ..integer import Integer
from ..integerarray import IntegerArray
@@ -39,8 +39,8 @@ def test_integerarray_hypothesis(idx, shape):
a = arange(prod(shape)).reshape(shape)
check_same(a, idx)
-@given(integer_arrays, one_of(short_shapes, integers(0, 10)))
-def test_integerarray_reduce_no_shape_hypothesis(idx, shape):
+@given(integer_arrays, one_of(short_shapes, integers(0, 10)), reduce_kwargs)
+def test_integerarray_reduce_no_shape_hypothesis(idx, shape, kwargs):
if isinstance(shape, int):
a = arange(shape)
else:
@@ -48,13 +48,23 @@ def test_integerarray_reduce_no_shape_hypothesis(idx, shape):
index = IntegerArray(idx)
- check_same(a, index.raw, ndindex_func=lambda a, x: a[x.reduce().raw])
+ check_same(a, index.raw, ndindex_func=lambda a, x: a[x.reduce(**kwargs).raw])
-@example(array([2, 0]), (1, 0))
-@example(array(0), 1)
-@example(array([], dtype=intp), 0)
-@given(integer_arrays, one_of(short_shapes, integers(0, 10)))
-def test_integerarray_reduce_hypothesis(idx, shape):
+@given(integer_arrays)
+def test_integerarray_reduce_no_shape_unchanged(idx):
+ index = IntegerArray(idx)
+ assert index.reduce() == index.reduce(negative_int=False) == index.reduce(negative_int=True)
+ if index.ndim != 0:
+ assert index.reduce() == index
+
+
+@example(array([2, -2]), (4,), {'negative_int': True})
+@example(array(2), (4,), {'negative_int': True})
+@example(array([2, 0]), (1, 0), {})
+@example(array(0), 1, {})
+@example(array([], dtype=intp), 0, {})
+@given(integer_arrays, one_of(short_shapes, integers(0, 10)), reduce_kwargs)
+def test_integerarray_reduce_hypothesis(idx, shape, kwargs):
if isinstance(shape, int):
a = arange(shape)
else:
@@ -62,22 +72,30 @@ def test_integerarray_reduce_hypothesis(idx, shape):
index = IntegerArray(idx)
- check_same(a, index.raw, ndindex_func=lambda a, x: a[x.reduce(shape).raw])
+ check_same(a, index.raw, ndindex_func=lambda a, x: a[x.reduce(shape, **kwargs).raw])
+
+ negative_int = kwargs.get('negative_int', False)
try:
- reduced = index.reduce(shape)
+ reduced = index.reduce(shape, **kwargs)
except IndexError:
pass
else:
if isinstance(reduced, Integer):
- assert reduced.raw >= 0
+ if negative_int:
+ assert reduced.raw < 0
+ else:
+ assert reduced.raw >= 0
else:
assert isinstance(reduced, IntegerArray)
- assert (reduced.raw >= 0).all()
+ if negative_int:
+ assert (reduced.raw < 0).all()
+ else:
+ assert (reduced.raw >= 0).all()
# Idempotency
- assert reduced.reduce() == reduced
- assert reduced.reduce(shape) == reduced
+ assert reduced.reduce(**kwargs) == reduced
+ assert reduced.reduce(shape, **kwargs) == reduced
@example([], (1,))
@example([0], (1, 0))
diff --git a/ndindex/tests/test_isvalid.py b/ndindex/tests/test_isvalid.py
new file mode 100644
index 00000000..13995b81
--- /dev/null
+++ b/ndindex/tests/test_isvalid.py
@@ -0,0 +1,43 @@
+from hypothesis import given, example
+from hypothesis.strategies import one_of, integers
+
+from numpy import arange
+
+from .helpers import ndindices, shapes, MAX_ARRAY_SIZE, check_same, prod
+
+@example([0], (1,))
+@example(..., (1, 2, 3))
+@example(slice(0, 1), ())
+@example(slice(0, 1), (1,))
+@example((0, 1), (2, 2))
+@example((0,), ())
+@example([[1]], (0, 0, 1))
+@example(None, ())
+@given(ndindices, one_of(shapes, integers(0, MAX_ARRAY_SIZE)))
+def test_isvalid_hypothesis(idx, shape):
+ if isinstance(shape, int):
+ a = arange(shape)
+ else:
+ a = arange(prod(shape)).reshape(shape)
+
+ def raw_func(a, idx):
+ try:
+ a[idx]
+ return True
+ except Warning as w:
+ # check_same unconditionally turns this warning into raise
+ # IndexError, so we have to handle it separately here.
+ if "Out of bound index found. This was previously ignored when the indexing result contained no elements. In the future the index error will be raised. This error occurs either due to an empty slice, or if an array has zero elements even before indexing." in w.args[0]:
+ return False
+ raise # pragma: no cover
+ except IndexError:
+ return False
+
+ def ndindex_func(a, index):
+ return index.isvalid(a.shape)
+
+ def assert_equal(x, y):
+ assert x == y
+
+ check_same(a, idx, raw_func=raw_func, ndindex_func=ndindex_func,
+ assert_equal=assert_equal)
diff --git a/ndindex/tests/test_ndindex.py b/ndindex/tests/test_ndindex.py
index 0f90b8e1..8b537a99 100644
--- a/ndindex/tests/test_ndindex.py
+++ b/ndindex/tests/test_ndindex.py
@@ -1,21 +1,21 @@
import inspect
+import warnings
import numpy as np
from hypothesis import given, example, settings
-from hypothesis.strategies import integers
-from pytest import raises, warns
+from pytest import raises
-from ..ndindex import ndindex, asshape, iter_indices, ncycles, BroadcastError, AxisError
+from ..ndindex import ndindex
from ..booleanarray import BooleanArray
from ..integer import Integer
from ..ellipsis import ellipsis
from ..integerarray import IntegerArray
-from ..tuple import Tuple
-from .helpers import (ndindices, check_same, assert_equal, prod,
- mutually_broadcastable_shapes, skip_axes)
+from .helpers import ndindices, check_same, assert_equal
+
+@example([1, 2])
@given(ndindices)
def test_eq(idx):
index = ndindex(idx)
@@ -101,10 +101,15 @@ def test_ndindex_invalid():
np.array([])]:
check_same(a, idx)
- # This index is allowed by NumPy, but gives a deprecation warnings. We are
- # not going to allow indices that give deprecation warnings in ndindex.
- with warns(None) as r: # Make sure no warnings are emitted from ndindex()
- raises(IndexError, lambda: ndindex([1, []]))
+ # Older versions of NumPy gives a deprecation warning for this index. We
+ # are not going to allow indices that give deprecation warnings in
+ # ndindex.
+ with warnings.catch_warnings(record=True) as r:
+ # Make sure no warnings are emitted from ndindex()
+ warnings.simplefilter("error")
+ # Newer numpy versions raise ValueError with this index (although
+ # perhaps they shouldn't)
+ raises((IndexError, ValueError), lambda: ndindex([1, []]))
assert not r
def test_ndindex_ellipsis():
@@ -132,179 +137,3 @@ def test_repr_str(idx):
# Str may not be re-creatable. Just test that it doesn't give an exception.
str(index)
-
-def test_asshape():
- assert asshape(1) == (1,)
- assert asshape(np.int64(2)) == (2,)
- assert type(asshape(np.int64(2))[0]) == int
- assert asshape((1, 2)) == (1, 2)
- assert asshape([1, 2]) == (1, 2)
- assert asshape((np.int64(1), np.int64(2))) == (1, 2)
- assert type(asshape((np.int64(1), np.int64(2)))[0]) == int
- assert type(asshape((np.int64(1), np.int64(2)))[1]) == int
-
- raises(TypeError, lambda: asshape(1.0))
- raises(TypeError, lambda: asshape((1.0,)))
- raises(ValueError, lambda: asshape(-1))
- raises(ValueError, lambda: asshape((1, -1)))
- raises(TypeError, lambda: asshape(...))
- raises(TypeError, lambda: asshape(Integer(1)))
- raises(TypeError, lambda: asshape(Tuple(1, 2)))
- raises(TypeError, lambda: asshape((True,)))
-
-@example([((1, 1), (1, 1)), (1, 1)], (0, 0))
-@example([((), (0,)), (0,)], (0,))
-@example([((1, 2), (2, 1)), (2, 2)], 1)
-@given(mutually_broadcastable_shapes, skip_axes())
-def test_iter_indices(broadcastable_shapes, skip_axes):
- shapes, broadcasted_shape = broadcastable_shapes
-
- if skip_axes is None:
- res = iter_indices(*shapes)
- broadcasted_res = iter_indices(np.broadcast_shapes(*shapes))
- skip_axes = ()
- else:
- res = iter_indices(*shapes, skip_axes=skip_axes)
- broadcasted_res = iter_indices(np.broadcast_shapes(*shapes),
- skip_axes=skip_axes)
-
- if isinstance(skip_axes, int):
- skip_axes = (skip_axes,)
-
- sizes = [prod(shape) for shape in shapes]
- ndim = len(broadcasted_shape)
- arrays = [np.arange(size).reshape(shape) for size, shape in zip(sizes, shapes)]
- broadcasted_arrays = np.broadcast_arrays(*arrays)
-
- # Use negative indices to index the skip axes since only shapes that have
- # the skip axis will include a slice.
- normalized_skip_axes = sorted(ndindex(i).reduce(ndim).args[0] - ndim for i in skip_axes)
- skip_shapes = [tuple(shape[i] for i in normalized_skip_axes if -i <= len(shape)) for shape in shapes]
- broadcasted_skip_shape = tuple(broadcasted_shape[i] for i in normalized_skip_axes)
-
- broadcasted_non_skip_shape = tuple(broadcasted_shape[i] for i in range(-1, -ndim-1, -1) if i not in normalized_skip_axes)
- nitems = prod(broadcasted_non_skip_shape)
- broadcasted_nitems = prod(broadcasted_shape)
-
- vals = []
- n = -1
- try:
- for n, (idxes, bidxes) in enumerate(zip(res, broadcasted_res)):
- assert len(idxes) == len(shapes)
- for idx, shape in zip(idxes, shapes):
- assert isinstance(idx, Tuple)
- assert len(idx.args) == len(shape)
- for i in range(-1, -len(idx.args)-1, -1):
- if i in normalized_skip_axes and len(idx.args) >= -i:
- assert idx.args[i] == slice(None)
- else:
- assert isinstance(idx.args[i], Integer)
-
- aidxes = tuple([a[idx.raw] for a, idx in zip(arrays, idxes)])
- a_broadcasted_idxs = [a[idx.raw] for a, idx in
- zip(broadcasted_arrays, bidxes)]
-
- for aidx, abidx, skip_shape in zip(aidxes, a_broadcasted_idxs, skip_shapes):
- if skip_shape == broadcasted_skip_shape:
- assert_equal(aidx, abidx)
- assert aidx.shape == skip_shape
-
- if skip_axes:
- # If there are skipped axes, recursively call iter_indices to
- # get each individual element of the resulting subarrays.
- for subidxes in iter_indices(*[x.shape for x in aidxes]):
- items = [x[i.raw] for x, i in zip(aidxes, subidxes)]
- # An empty array means the iteration would be skipped.
- if any(a.size == 0 for a in items):
- continue
- vals.append(tuple(items))
- else:
- vals.append(aidxes)
- except ValueError as e:
- if "duplicate axes" in str(e):
- # There should be actual duplicate axes
- assert len({broadcasted_shape[i] for i in skip_axes}) < len(skip_axes)
- return
- raise # pragma: no cover
-
- assert len(set(vals)) == len(vals) == broadcasted_nitems
-
- # The indices should correspond to the values that would be matched up
- # if the arrays were broadcasted together.
- if not arrays:
- assert vals == [()]
- else:
- correct_vals = [tuple(i) for i in np.stack(broadcasted_arrays, axis=-1)
- .reshape((broadcasted_nitems, len(arrays)))]
- # Also test that the indices are produced in a lexicographic order
- # (even though this isn't strictly guaranteed by the iter_indices
- # docstring) in the case when there are no skip axes. The order when
- # there are skip axes is more complicated because the skipped axes are
- # iterated together.
- if not skip_axes:
- assert vals == correct_vals
- else:
- assert set(vals) == set(correct_vals)
-
- assert n == nitems - 1
-
-def test_iter_indices_errors():
- try:
- list(iter_indices((10,), skip_axes=(2,)))
- except AxisError as e:
- msg1 = str(e)
- else:
- raise RuntimeError("iter_indices did not raise AxisError") # pragma: no cover
-
- # Check that the message is the same one used by NumPy
- try:
- np.sum(np.arange(10), axis=2)
- except np.AxisError as e:
- msg2 = str(e)
- else:
- raise RuntimeError("np.sum() did not raise AxisError") # pragma: no cover
-
- assert msg1 == msg2
-
- try:
- list(iter_indices((2, 3), (3, 2)))
- except BroadcastError as e:
- msg1 = str(e)
- else:
- raise RuntimeError("iter_indices did not raise BroadcastError") # pragma: no cover
-
- # TODO: Check that the message is the same one used by NumPy
- # try:
- # np.broadcast_shapes((2, 3), (3, 2))
- # except np.Error as e:
- # msg2 = str(e)
- # else:
- # raise RuntimeError("np.broadcast_shapes() did not raise AxisError") # pragma: no cover
- #
- # assert msg1 == msg2
-
-@example(1, 1, 1)
-@given(integers(0, 100), integers(0, 100), integers(0, 100))
-def test_ncycles(i, n, m):
- N = ncycles(range(i), n)
- if n == 1:
- assert N == range(i)
- else:
- assert isinstance(N, ncycles)
- assert N.iterable == range(i)
- assert N.n == n
- assert f"range(0, {i})" in repr(N)
- assert str(n) in repr(N)
-
- L = list(N)
- assert len(L) == i*n
- for j in range(i*n):
- assert L[j] == j % i
-
- M = ncycles(N, m)
- if n*m == 1:
- assert M == range(i)
- else:
- assert isinstance(M, ncycles)
- assert M.iterable == range(i)
- assert M.n == n*m
diff --git a/ndindex/tests/test_newaxis.py b/ndindex/tests/test_newaxis.py
index bc655a7d..abe44503 100644
--- a/ndindex/tests/test_newaxis.py
+++ b/ndindex/tests/test_newaxis.py
@@ -4,7 +4,7 @@
from hypothesis.strategies import one_of, integers
from ..ndindex import ndindex
-from .helpers import check_same, prod, shapes, newaxes
+from .helpers import check_same, prod, shapes, newaxes, reduce_kwargs
def test_newaxis_exhaustive():
for n in range(10):
@@ -24,10 +24,10 @@ def test_newaxis_reduce_exhaustive():
check_same(a, newaxis, ndindex_func=lambda a, x: a[x.reduce((n,)).raw])
-@given(newaxes(), shapes)
-def test_newaxis_reduce_hypothesis(idx, shape):
+@given(newaxes(), shapes, reduce_kwargs)
+def test_newaxis_reduce_hypothesis(idx, shape, kwargs):
a = arange(prod(shape)).reshape(shape)
- check_same(a, idx, ndindex_func=lambda a, x: a[x.reduce(shape).raw])
+ check_same(a, idx, ndindex_func=lambda a, x: a[x.reduce(shape, **kwargs).raw])
def test_newaxis_reduce_no_shape_exhaustive():
@@ -35,10 +35,10 @@ def test_newaxis_reduce_no_shape_exhaustive():
a = arange(n)
check_same(a, newaxis, ndindex_func=lambda a, x: a[x.reduce().raw])
-@given(newaxes(), shapes)
-def test_newaxis_reduce_no_shape_hypothesis(idx, shape):
+@given(newaxes(), shapes, reduce_kwargs)
+def test_newaxis_reduce_no_shape_hypothesis(idx, shape, kwargs):
a = arange(prod(shape)).reshape(shape)
- check_same(a, idx, ndindex_func=lambda a, x: a[x.reduce().raw])
+ check_same(a, idx, ndindex_func=lambda a, x: a[x.reduce(**kwargs).raw])
@given(newaxes(), one_of(shapes, integers(0, 10)))
def test_newaxis_isempty_hypothesis(idx, shape):
diff --git a/ndindex/tests/test_shapetools.py b/ndindex/tests/test_shapetools.py
new file mode 100644
index 00000000..ba8333e3
--- /dev/null
+++ b/ndindex/tests/test_shapetools.py
@@ -0,0 +1,611 @@
+import numpy as np
+try:
+ from numpy import AxisError as np_AxisError
+except ImportError: # pragma: no cover
+ from numpy.exceptions import AxisError as np_AxisError
+
+from hypothesis import assume, given, example
+from hypothesis.strategies import (one_of, integers, tuples as
+ hypothesis_tuples, just, lists, shared,
+ )
+
+from pytest import raises
+
+from ..ndindex import ndindex
+from ..shapetools import (asshape, iter_indices, ncycles, BroadcastError,
+ AxisError, broadcast_shapes, remove_indices,
+ unremove_indices, associated_axis,
+ normalize_skip_axes)
+from ..integer import Integer
+from ..tuple import Tuple
+from .helpers import (prod, mutually_broadcastable_shapes_with_skipped_axes,
+ skip_axes_st, mutually_broadcastable_shapes, tuples,
+ shapes, assert_equal, cross_shapes, cross_skip_axes,
+ cross_arrays_st, matmul_shapes, matmul_skip_axes,
+ matmul_arrays_st)
+
+@example([[(1, 1), (1, 1)], (1,)], (0,))
+@example([[(0,), (0,)], ()], (0,))
+@example([[(1, 2), (2, 1)], (2,)], 1)
+@given(mutually_broadcastable_shapes_with_skipped_axes, skip_axes_st)
+def test_iter_indices(broadcastable_shapes, skip_axes):
+ # broadcasted_shape will contain None on the skip_axes, as those axes
+ # might not be broadcast compatible
+ shapes, broadcasted_shape = broadcastable_shapes
+ # We need no more than 31 dimensions so that the np.stack call below
+ # doesn't fail.
+ assume(len(broadcasted_shape) < 32)
+
+ # 1. Normalize inputs
+ _skip_axes = normalize_skip_axes(shapes, skip_axes)
+ _skip_axes_kwarg_default = [()]*len(shapes)
+
+ # Skipped axes may not be broadcast compatible. Since the index for a
+ # skipped axis should always be a slice(None), the result should be the
+ # same if the skipped axes are all moved to the end of the shape.
+ canonical_shapes = []
+ for s, sk in zip(shapes, _skip_axes):
+ c = remove_indices(s, sk)
+ canonical_shapes.append(c)
+
+ non_skip_shapes = [remove_indices(shape, sk) for shape, sk in zip(shapes, _skip_axes)]
+ assert np.broadcast_shapes(*non_skip_shapes) == broadcasted_shape
+
+ nitems = prod(broadcasted_shape)
+
+ if skip_axes == (): # kwarg default
+ res = iter_indices(*shapes)
+ else:
+ res = iter_indices(*shapes, skip_axes=skip_axes)
+ broadcasted_res = iter_indices(broadcasted_shape)
+
+ sizes = [prod(shape) for shape in shapes]
+ arrays = [np.arange(size).reshape(shape) for size, shape in zip(sizes, shapes)]
+ canonical_sizes = [prod(shape) for shape in canonical_shapes]
+ canonical_arrays = [np.arange(size).reshape(shape) for size, shape in zip(canonical_sizes, canonical_shapes)]
+ canonical_broadcasted_array = np.arange(nitems).reshape(broadcasted_shape)
+
+ # 2. Check that iter_indices is the same whether or not the shapes are
+ # broadcasted together first. Also check that every iterated index is the
+ # expected type and there are as many as expected.
+ vals = []
+ bvals = []
+ n = -1
+
+ def _remove_slices(idx):
+ assert isinstance(idx, Tuple)
+ idx2 = [i for i in idx.args if i != slice(None)]
+ return Tuple(*idx2)
+
+ for n, (idxes, bidxes) in enumerate(zip(res, broadcasted_res)):
+ assert len(idxes) == len(shapes)
+ assert len(bidxes) == 1
+ for idx, shape, sk in zip(idxes, shapes, _skip_axes):
+ assert isinstance(idx, Tuple)
+ assert len(idx.args) == len(shape)
+
+ for i in range(-1, -len(idx.args) - 1, -1):
+ if i in sk:
+ assert idx.args[i] == slice(None)
+ else:
+ assert isinstance(idx.args[i], Integer)
+
+ canonical_idxes = [_remove_slices(idx) for idx in idxes]
+ a_indexed = tuple([a[idx.raw] for a, idx in zip(arrays, idxes)])
+ canonical_a_indexed = tuple([a[idx.raw] for a, idx in
+ zip(canonical_arrays, canonical_idxes)])
+ canonical_b_indexed = canonical_broadcasted_array[bidxes[0].raw]
+
+ for c_indexed in canonical_a_indexed:
+ assert c_indexed.shape == ()
+ assert canonical_b_indexed.shape == ()
+
+ if _skip_axes != _skip_axes_kwarg_default:
+ vals.append(tuple(canonical_a_indexed))
+ else:
+ vals.append(a_indexed)
+
+ bvals.append(canonical_b_indexed)
+
+ # assert both iterators have the same length
+ raises(StopIteration, lambda: next(res))
+ raises(StopIteration, lambda: next(broadcasted_res))
+
+ # Check that the correct number of items are iterated
+ assert n == nitems - 1
+ assert len(set(vals)) == len(vals) == nitems
+
+ # 3. Check that every element of the (broadcasted) arrays is represented
+ # by an iterated index.
+
+ # The indices should correspond to the values that would be matched up
+ # if the arrays were broadcasted together.
+ if not arrays:
+ assert vals == [()]
+ else:
+ correct_vals = list(zip(*[x.flat for x in np.broadcast_arrays(*canonical_arrays)]))
+ # Also test that the indices are produced in a lexicographic order
+ # (even though this isn't strictly guaranteed by the iter_indices
+ # docstring) in the case when there are no skip axes. The order when
+ # there are skip axes is more complicated because the skipped axes are
+ # iterated together.
+ if _skip_axes == _skip_axes_kwarg_default:
+ assert vals == correct_vals
+ else:
+ assert set(vals) == set(correct_vals)
+ assert bvals == list(canonical_broadcasted_array.flat)
+
+@given(cross_arrays_st(), cross_shapes, cross_skip_axes)
+def test_iter_indices_cross(cross_arrays, broadcastable_shapes, _skip_axes):
+ # Test iter_indices behavior against np.cross, which effectively skips the
+ # crossed axis. Note that we don't test against cross products of size 2
+ # because a 2 x 2 cross product just returns the z-axis (i.e., it doesn't
+ # actually skip an axis in the result shape), and also that behavior is
+ # going to be removed in NumPy 2.0.
+ a, b = cross_arrays
+ shapes, broadcasted_shape = broadcastable_shapes
+
+ # Sanity check
+ skip_axes = normalize_skip_axes([*shapes, broadcasted_shape], _skip_axes)
+ for sh, sk in zip([*shapes, broadcasted_shape], skip_axes):
+ assert len(sk) == 1
+ assert sh[sk[0]] == 3
+
+ res = np.cross(a, b, axisa=skip_axes[0][0], axisb=skip_axes[1][0], axisc=skip_axes[2][0])
+ assert res.shape == broadcasted_shape
+
+ for idx1, idx2, idx3 in iter_indices(*shapes, broadcasted_shape, skip_axes=_skip_axes):
+ assert a[idx1.raw].shape == (3,)
+ assert b[idx2.raw].shape == (3,)
+ assert_equal(np.cross(
+ a[idx1.raw],
+ b[idx2.raw]),
+ res[idx3.raw])
+
+@given(matmul_arrays_st(), matmul_shapes, matmul_skip_axes)
+def test_iter_indices_matmul(matmul_arrays, broadcastable_shapes, skip_axes):
+ # Test iter_indices behavior against np.matmul, which effectively skips the
+ # contracted axis (they aren't broadcasted together, even when they are
+ # broadcast compatible).
+ a, b = matmul_arrays
+ shapes, broadcasted_shape = broadcastable_shapes
+
+ # Note, we don't use normalize_skip_axes here because it sorts the skip
+ # axes
+
+ ax1, ax2 = skip_axes[0]
+ ax3 = skip_axes[1][1]
+ n, m, k = shapes[0][ax1], shapes[0][ax2], shapes[1][ax3]
+
+ # Sanity check
+ sk0, sk1, sk2 = skip_axes
+ shape1, shape2 = shapes
+ assert a.shape == shape1
+ assert b.shape == shape2
+ assert shape1[sk0[0]] == n
+ assert shape1[sk0[1]] == m
+ assert shape2[sk1[0]] == m
+ assert shape2[sk1[1]] == k
+ assert broadcasted_shape[sk2[0]] == n
+ assert broadcasted_shape[sk2[1]] == k
+
+ res = np.matmul(a, b, axes=skip_axes)
+ assert res.shape == broadcasted_shape
+
+ is_ordered = lambda sk, shape: (Integer(sk[0]).reduce(len(shape)).raw <= Integer(sk[1]).reduce(len(shape)).raw)
+ orders = [
+ is_ordered(sk0, shapes[0]),
+ is_ordered(sk1, shapes[1]),
+ is_ordered(sk2, broadcasted_shape),
+ ]
+
+ for idx1, idx2, idx3 in iter_indices(*shapes, broadcasted_shape, skip_axes=skip_axes):
+ assert a[idx1.raw].shape == (n, m) if orders[0] else (m, n)
+ assert b[idx2.raw].shape == (m, k) if orders[1] else (k, m)
+ sub_res_axes = [
+ (0, 1) if orders[0] else (1, 0),
+ (0, 1) if orders[1] else (1, 0),
+ (0, 1) if orders[2] else (1, 0),
+ ]
+ sub_res = np.matmul(a[idx1.raw], b[idx2.raw], axes=sub_res_axes)
+ assert_equal(sub_res, res[idx3.raw])
+
+def test_iter_indices_errors():
+ try:
+ list(iter_indices((10,), skip_axes=(2,)))
+ except AxisError as e:
+ ndindex_msg = str(e)
+ else:
+ raise RuntimeError("iter_indices did not raise AxisError") # pragma: no cover
+
+ # Check that the message is the same one used by NumPy
+ try:
+ np.sum(np.arange(10), axis=2)
+ except np_AxisError as e:
+ np_msg = str(e)
+ else:
+ raise RuntimeError("np.sum() did not raise AxisError") # pragma: no cover
+
+ assert ndindex_msg == np_msg
+
+ try:
+ list(iter_indices((2, 3), (3, 2)))
+ except BroadcastError as e:
+ ndindex_msg = str(e)
+ else:
+ raise RuntimeError("iter_indices did not raise BroadcastError") # pragma: no cover
+
+ try:
+ np.broadcast_shapes((2, 3), (3, 2))
+ except ValueError as e:
+ np_msg = str(e)
+ else:
+ raise RuntimeError("np.broadcast_shapes() did not raise ValueError") # pragma: no cover
+
+
+ if 'Mismatch' in str(np_msg): # pragma: no cover
+ # Older versions of NumPy do not have the more helpful error message
+ assert ndindex_msg == np_msg
+
+ with raises(ValueError, match=r"not unique"):
+ list(iter_indices((1, 2), skip_axes=(0, 1, 0)))
+
+ raises(AxisError, lambda: list(iter_indices((0,), skip_axes=(3,))))
+ raises(ValueError, lambda: list(iter_indices(skip_axes=(0,))))
+ raises(TypeError, lambda: list(iter_indices(1, 2)))
+ raises(TypeError, lambda: list(iter_indices(1, 2, (2, 2))))
+ raises(TypeError, lambda: list(iter_indices([(1, 2), (2, 2)])))
+
+@example(1, 1, 1)
+@given(integers(0, 100), integers(0, 100), integers(0, 100))
+def test_ncycles(i, n, m):
+ N = ncycles(range(i), n)
+ if n == 1:
+ assert N == range(i)
+ else:
+ assert isinstance(N, ncycles)
+ assert N.iterable == range(i)
+ assert N.n == n
+ assert f"range(0, {i})" in repr(N)
+ assert str(n) in repr(N)
+
+ L = list(N)
+ assert len(L) == i*n
+ for j in range(i*n):
+ assert L[j] == j % i
+
+ M = ncycles(N, m)
+ if n*m == 1:
+ assert M == range(i)
+ else:
+ assert isinstance(M, ncycles)
+ assert M.iterable == range(i)
+ assert M.n == n*m
+
+@given(one_of(mutually_broadcastable_shapes,
+ hypothesis_tuples(tuples(shapes), just(None))))
+def test_broadcast_shapes(broadcastable_shapes):
+ shapes, broadcasted_shape = broadcastable_shapes
+ if broadcasted_shape is not None:
+ assert broadcast_shapes(*shapes) == broadcasted_shape
+
+ arrays = [np.empty(shape) for shape in shapes]
+ broadcastable = True
+ try:
+ broadcasted_shape = np.broadcast(*arrays).shape
+ except ValueError:
+ broadcastable = False
+
+ if broadcastable:
+ assert broadcast_shapes(*shapes) == broadcasted_shape
+ else:
+ raises(BroadcastError, lambda: broadcast_shapes(*shapes))
+
+
+@given(lists(shapes, max_size=32))
+def test_broadcast_shapes_errors(shapes):
+ error = True
+ try:
+ broadcast_shapes(*shapes)
+ except BroadcastError as exc:
+ e = exc
+ else:
+ error = False
+
+ # The ndindex and numpy errors won't match in general, because
+ # ndindex.broadcast_shapes gives an error with the first two shapes that
+ # aren't broadcast compatible, but numpy doesn't always, due to different
+ # implementation algorithms (e.g., the message from
+ # np.broadcast_shapes((0,), (0, 2), (2, 0)) mentions the last two shapes
+ # whereas ndindex.broadcast_shapes mentions the first two).
+
+ # Instead, just confirm that the error message is correct as stated, and
+ # check against the numpy error message when just broadcasting the two
+ # reportedly bad shapes.
+
+ if not error:
+ try:
+ np.broadcast_shapes(*shapes)
+ except: # pragma: no cover
+ raise RuntimeError("ndindex.broadcast_shapes raised but np.broadcast_shapes did not")
+ return
+
+ assert shapes[e.arg1] == e.shape1
+ assert shapes[e.arg2] == e.shape2
+
+ try:
+ np.broadcast_shapes(e.shape1, e.shape2)
+ except ValueError as np_exc:
+ # Check that they do in fact not broadcast, and the error messages are
+ # the same modulo the different arg positions.
+ if 'Mismatch' in str(np_exc): # pragma: no cover
+ # Older versions of NumPy do not have the more helpful error message
+ assert str(BroadcastError(0, e.shape1, 1, e.shape2)) == str(np_exc)
+ else: # pragma: no cover
+ raise RuntimeError("ndindex.broadcast_shapes raised but np.broadcast_shapes did not")
+
+ raises(TypeError, lambda: broadcast_shapes(1, 2))
+ raises(TypeError, lambda: broadcast_shapes(1, 2, (2, 2)))
+ raises(TypeError, lambda: broadcast_shapes([(1, 2), (2, 2)]))
+
+@given(mutually_broadcastable_shapes_with_skipped_axes, skip_axes_st)
+def test_broadcast_shapes_skip_axes(broadcastable_shapes, skip_axes):
+ shapes, broadcasted_shape = broadcastable_shapes
+ assert broadcast_shapes(*shapes, skip_axes=skip_axes) == broadcasted_shape
+
+@example([[], ()], (0,))
+@example([[(0, 1)], (0, 1)], (2,))
+@example([[(0, 1)], (0, 1)], (0, -1))
+@example([[(0, 1, 0, 0, 0), (2, 0, 0, 0)], (0, 2, 0, 0, 0)], [1])
+@given(mutually_broadcastable_shapes,
+ one_of(
+ integers(-20, 20),
+ tuples(integers(-20, 20), max_size=20),
+ lists(tuples(integers(-20, 20), max_size=20), max_size=32)))
+def test_broadcast_shapes_skip_axes_errors(broadcastable_shapes, skip_axes):
+ shapes, broadcasted_shape = broadcastable_shapes
+
+ # All errors should come from normalize_skip_axes, which is tested
+ # separately below.
+ try:
+ normalize_skip_axes(shapes, skip_axes)
+ except (TypeError, ValueError, IndexError) as e:
+ raises(type(e), lambda: broadcast_shapes(*shapes,
+ skip_axes=skip_axes))
+ return
+
+ try:
+ broadcast_shapes(*shapes, skip_axes=skip_axes)
+ except IndexError:
+ raise RuntimeError("broadcast_shapes raised but should not have") # pragma: no cover
+ except BroadcastError:
+ # Broadcastable shapes can become unbroadcastable after skipping axes
+ # (see the @example above).
+ pass
+
+remove_indices_n = shared(integers(0, 100))
+
+@given(remove_indices_n,
+ remove_indices_n.flatmap(lambda n: lists(integers(-n, n), unique=True)))
+def test_remove_indices(n, idxes):
+ if idxes:
+ assume(max(idxes) < n)
+ assume(min(idxes) >= -n)
+ a = tuple(range(n))
+ b = remove_indices(a, idxes)
+ if len(idxes) == 1:
+ assert remove_indices(a, idxes[0]) == b
+
+ A = list(a)
+ for i in idxes:
+ A[i] = None
+
+ assert set(A) - set(b) == ({None} if idxes else set())
+ assert set(b) - set(A) == set()
+
+ # Check the order is correct
+ j = 0
+ for i in range(n):
+ val = A[i]
+ if val == None:
+ assert val not in b
+ else:
+ assert b[j] == val
+ j += 1
+
+ # Test that unremove_indices is the inverse
+ if all(i >= 0 for i in idxes) or all(i < 0 for i in idxes):
+ assert unremove_indices(b, idxes) == tuple(A)
+ else:
+ raises(NotImplementedError, lambda: unremove_indices(b, idxes))
+
+# Meta-test for the hypothesis strategy
+@given(mutually_broadcastable_shapes_with_skipped_axes, skip_axes_st)
+def test_mutually_broadcastable_shapes_with_skipped_axes(broadcastable_shapes,
+ skip_axes): # pragma: no cover
+ shapes, broadcasted_shape = broadcastable_shapes
+ _skip_axes = normalize_skip_axes(shapes, skip_axes)
+
+ assert len(_skip_axes) == len(shapes)
+
+ for shape in shapes:
+ assert None not in shape
+ assert None not in broadcasted_shape
+
+ _shapes = [remove_indices(shape, sk) for shape, sk in zip(shapes, _skip_axes)]
+
+ assert broadcast_shapes(*_shapes) == broadcasted_shape
+
+@example([[(2, 10, 3, 4), (10, 3, 4)], (2, 3, 4)], (-3,))
+@example([[(0, 10, 2, 3, 10, 4), (1, 10, 1, 0, 10, 2, 3, 4)],
+ (1, 1, 0, 2, 3, 4)], (1, 4))
+@example([[(2, 0, 3, 4)], (2, 3, 4)], (1,))
+@example([[(0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0)], (0, 0, 0, 0)], (1, 2))
+@given(mutually_broadcastable_shapes_with_skipped_axes, skip_axes_st)
+def test_associated_axis(broadcastable_shapes, skip_axes):
+ shapes, broadcasted_shape = broadcastable_shapes
+ _skip_axes = normalize_skip_axes(shapes, skip_axes)
+
+ for shape, sk in zip(shapes, _skip_axes):
+ n = len(shape)
+ for i in range(-len(shape), 0):
+ val = shape[i]
+
+ bval = associated_axis(broadcasted_shape, i, sk)
+ if bval is None:
+ assert ndindex(i).reduce(n, negative_int=True) in sk, (shape, i)
+ else:
+ assert val == 1 or bval == val, (shape, i)
+
+
+ sk = max(_skip_axes, key=len, default=())
+ for i in range(-len(broadcasted_shape)-len(sk)-10, -len(broadcasted_shape)-len(sk)):
+ assert associated_axis(broadcasted_shape, i, sk) is None
+
+# TODO: add a hypothesis test for asshape
+def test_asshape():
+ assert asshape(1) == (1,)
+ assert asshape(np.int64(2)) == (2,)
+ assert type(asshape(np.int64(2))[0]) == int
+ assert asshape((1, 2)) == (1, 2)
+ assert asshape([1, 2]) == (1, 2)
+ assert asshape((1, 2), allow_int=False) == (1, 2)
+ assert asshape([1, 2], allow_int=False) == (1, 2)
+ assert asshape((np.int64(1), np.int64(2))) == (1, 2)
+ assert type(asshape((np.int64(1), np.int64(2)))[0]) == int
+ assert type(asshape((np.int64(1), np.int64(2)))[1]) == int
+ assert asshape((-1, -2), allow_negative=True) == (-1, -2)
+ assert asshape(-2, allow_negative=True) == (-2,)
+
+
+ raises(TypeError, lambda: asshape(1.0))
+ raises(TypeError, lambda: asshape((1.0,)))
+ raises(ValueError, lambda: asshape(-1))
+ raises(ValueError, lambda: asshape((1, -1)))
+ raises(ValueError, lambda: asshape((1, None)))
+ raises(TypeError, lambda: asshape(...))
+ raises(TypeError, lambda: asshape(Integer(1)))
+ raises(TypeError, lambda: asshape(Tuple(1, 2)))
+ raises(TypeError, lambda: asshape((True,)))
+ raises(TypeError, lambda: asshape({1, 2}))
+ raises(TypeError, lambda: asshape({1: 2}))
+ raises(TypeError, lambda: asshape('1'))
+ raises(TypeError, lambda: asshape(1, allow_int=False))
+ raises(TypeError, lambda: asshape(-1, allow_int=False))
+ raises(TypeError, lambda: asshape(-1, allow_negative=True, allow_int=False))
+ raises(TypeError, lambda: asshape(np.int64(1), allow_int=False))
+ raises(IndexError, lambda: asshape((2, 3), 3))
+
+@example([], [])
+@example([()], [])
+@example([(0, 1)], 0)
+@example([(2, 3), (2, 3, 4)], [(3,), (0,)])
+@example([(0, 1)], 0)
+@example([(2, 3)], (0, -2))
+@example([(2, 4), (2, 3, 4)], [(0,), (-3,)])
+@given(lists(tuples(integers(0))),
+ one_of(integers(), tuples(integers()), lists(tuples(integers()))))
+def test_normalize_skip_axes(shapes, skip_axes):
+ if not shapes:
+ if skip_axes in [(), []]:
+ assert normalize_skip_axes(shapes, skip_axes) == []
+ else:
+ raises(ValueError, lambda: normalize_skip_axes(shapes, skip_axes))
+ return
+
+ min_dim = min(len(shape) for shape in shapes)
+
+ if isinstance(skip_axes, int):
+ if not (-min_dim <= skip_axes < min_dim):
+ raises(AxisError, lambda: normalize_skip_axes(shapes, skip_axes))
+ return
+ _skip_axes = [(skip_axes,)]*len(shapes)
+ skip_len = 1
+ elif isinstance(skip_axes, tuple):
+ if not all(-min_dim <= s < min_dim for s in skip_axes):
+ raises(AxisError, lambda: normalize_skip_axes(shapes, skip_axes))
+ return
+ _skip_axes = [skip_axes]*len(shapes)
+ skip_len = len(skip_axes)
+ elif not skip_axes:
+ # empty list will be interpreted as a single skip_axes tuple
+ assert normalize_skip_axes(shapes, skip_axes) == [()]*len(shapes)
+ return
+ else:
+ if len(shapes) != len(skip_axes):
+ raises(ValueError, lambda: normalize_skip_axes(shapes, skip_axes))
+ return
+ _skip_axes = skip_axes
+ skip_len = len(skip_axes[0])
+
+ try:
+ res = normalize_skip_axes(shapes, skip_axes)
+ except AxisError as e:
+ axis, ndim = e.args
+ assert any(axis in s for s in _skip_axes)
+ assert any(ndim == len(shape) for shape in shapes)
+ assert axis < -ndim or axis >= ndim
+ return
+ except ValueError as e:
+ if 'not unique' in str(e):
+ bad_skip_axes, bad_shape = e.skip_axes, e.shape
+ assert str(bad_skip_axes) in str(e)
+ assert str(bad_shape) in str(e)
+ assert bad_skip_axes in _skip_axes
+ assert bad_shape in shapes
+ indexed = [bad_shape[i] for i in bad_skip_axes]
+ assert len(indexed) != len(set(indexed))
+ return
+ else: # pragma: no cover
+ raise
+
+ assert isinstance(res, list)
+ assert all(isinstance(x, tuple) for x in res)
+ assert all(isinstance(i, int) for x in res for i in x)
+
+ assert len(res) == len(shapes)
+ for shape, new_skip_axes in zip(shapes, res):
+ assert len(new_skip_axes) == len(set(new_skip_axes)) == skip_len
+ assert new_skip_axes == tuple(sorted(new_skip_axes))
+ for i in new_skip_axes:
+ assert i < 0
+ assert ndindex(i).reduce(len(shape), negative_int=True) == i
+
+ # TODO: Assert the order is maintained (doesn't actually matter for now
+ # but could for future applications)
+
+def test_normalize_skip_axes_errors():
+ raises(TypeError, lambda: normalize_skip_axes([(1,)], {0: 1}))
+ raises(TypeError, lambda: normalize_skip_axes([(1,)], {0}))
+ raises(TypeError, lambda: normalize_skip_axes([(1,)], [(0,), 0]))
+ raises(TypeError, lambda: normalize_skip_axes([(1,)], [0, (0,)]))
+
+@example(10, 5)
+@given(integers(), integers())
+def test_axiserror(axis, ndim):
+ if ndim == 0 and axis in [0, -1]:
+ # NumPy allows axis=0 or -1 for 0-d arrays
+ AxisError(axis, ndim)
+ return
+
+ try:
+ if ndim >= 0:
+ range(ndim)[axis]
+ except IndexError:
+ e = AxisError(axis, ndim)
+ else:
+ raises(ValueError, lambda: AxisError(axis, ndim))
+ return
+
+ try:
+ raise e
+ except AxisError as e2:
+ assert e2.args == (axis, ndim)
+ if ndim <= 32 and -1000 < axis < 1000:
+ a = np.empty((0,)*ndim)
+ try:
+ np.sum(a, axis=axis)
+ except np_AxisError as e3:
+ assert str(e2) == str(e3)
+ else:
+ raise RuntimeError("numpy didn't raise AxisError") # pragma: no cover
diff --git a/ndindex/tests/test_slice.py b/ndindex/tests/test_slice.py
index 6135cc9b..ee50e3c8 100644
--- a/ndindex/tests/test_slice.py
+++ b/ndindex/tests/test_slice.py
@@ -8,8 +8,8 @@
from ..slice import Slice
from ..integer import Integer
from ..ellipsis import ellipsis
-from ..ndindex import asshape
-from .helpers import check_same, slices, prod, shapes, iterslice, assert_equal
+from ..shapetools import asshape
+from .helpers import check_same, slices, prod, shapes, iterslice, assert_equal, reduce_kwargs
def test_slice_args():
# Test the behavior when not all three arguments are given
@@ -144,8 +144,8 @@ def test_slice_reduce_no_shape_exhaustive():
slices[B] = reduced
-@given(slices(), one_of(integers(0, 100), shapes))
-def test_slice_reduce_no_shape_hypothesis(s, shape):
+@given(slices(), one_of(integers(0, 100), shapes), reduce_kwargs)
+def test_slice_reduce_no_shape_hypothesis(s, shape, kwargs):
if isinstance(shape, int):
a = arange(shape)
else:
@@ -158,10 +158,10 @@ def test_slice_reduce_no_shape_hypothesis(s, shape):
# The axis argument is tested implicitly in the Tuple.reduce test. It is
# difficult to test here because we would have to pass in a Tuple to
# check_same.
- check_same(a, S.raw, ndindex_func=lambda a, x: a[x.reduce().raw])
+ check_same(a, S.raw, ndindex_func=lambda a, x: a[x.reduce(**kwargs).raw])
# Check the conditions stated by the Slice.reduce() docstring
- reduced = S.reduce()
+ reduced = S.reduce(**kwargs)
assert reduced.start != None
if S.start != None and S.start >= 0:
assert reduced.start >= 0
@@ -171,7 +171,7 @@ def test_slice_reduce_no_shape_hypothesis(s, shape):
if reduced.stop is None:
assert S.stop is None
# Idempotency
- assert reduced.reduce() == reduced, S
+ assert reduced.reduce(**kwargs) == reduced, S
def test_slice_reduce_exhaustive():
for n in range(30):
@@ -232,11 +232,11 @@ def test_slice_reduce_exhaustive():
assert reduced.reduce() == reduced, S
assert reduced.reduce((n,)) == reduced, S
-@example(slice(None, None, -1), 2)
-@example(slice(-10, 11, 3), 10)
-@example(slice(-1, 3, -3), 10)
-@given(slices(), one_of(integers(0, 100), shapes))
-def test_slice_reduce_hypothesis(s, shape):
+@example(slice(None, None, -1), 2, {})
+@example(slice(-10, 11, 3), 10, {})
+@example(slice(-1, 3, -3), 10, {})
+@given(slices(), one_of(integers(0, 100), shapes), reduce_kwargs)
+def test_slice_reduce_hypothesis(s, shape, kwargs):
if isinstance(shape, int):
a = arange(shape)
else:
@@ -250,11 +250,11 @@ def test_slice_reduce_hypothesis(s, shape):
# The axis argument is tested implicitly in the Tuple.reduce test. It is
# difficult to test here because we would have to pass in a Tuple to
# check_same.
- check_same(a, S.raw, ndindex_func=lambda a, x: a[x.reduce(shape).raw])
+ check_same(a, S.raw, ndindex_func=lambda a, x: a[x.reduce(shape, **kwargs).raw])
# Check the conditions stated by the Slice.reduce() docstring
try:
- reduced = S.reduce(shape)
+ reduced = S.reduce(shape, **kwargs)
except IndexError:
# shape == ()
return
@@ -294,8 +294,8 @@ def test_slice_reduce_hypothesis(s, shape):
assert reduced == Slice(reduced.start, reduced.start+1, 1)
# Idempotency
- assert reduced.reduce() == reduced, S
- assert reduced.reduce(shape) == reduced, S
+ assert reduced.reduce(**kwargs) == reduced, S
+ assert reduced.reduce(shape, **kwargs) == reduced, S
def test_slice_newshape_exhaustive():
def raw_func(a, idx):
diff --git a/ndindex/tests/test_tuple.py b/ndindex/tests/test_tuple.py
index b1ecdffa..8297ed7d 100644
--- a/ndindex/tests/test_tuple.py
+++ b/ndindex/tests/test_tuple.py
@@ -1,6 +1,6 @@
from itertools import product
-from numpy import arange, array, intp, empty
+from numpy import arange, array, intp, empty, all as np_all
from hypothesis import given, example
from hypothesis.strategies import integers, one_of
@@ -10,7 +10,8 @@
from ..ndindex import ndindex
from ..tuple import Tuple
from ..integer import Integer
-from .helpers import check_same, Tuples, prod, short_shapes, iterslice
+from ..integerarray import IntegerArray
+from .helpers import check_same, Tuples, prod, short_shapes, iterslice, reduce_kwargs
def test_tuple_constructor():
# Test things in the Tuple constructor that are not tested by the other
@@ -81,10 +82,10 @@ def ndindex_func(a, index):
check_same(a, t, ndindex_func=ndindex_func)
-@example((True, 0, False), 1)
-@example((..., None), ())
-@given(Tuples, one_of(short_shapes, integers(0, 10)))
-def test_tuple_reduce_no_shape_hypothesis(t, shape):
+@example((True, 0, False), 1, {})
+@example((..., None), (), {})
+@given(Tuples, one_of(short_shapes, integers(0, 10)), reduce_kwargs)
+def test_tuple_reduce_no_shape_hypothesis(t, shape, kwargs):
if isinstance(shape, int):
a = arange(shape)
else:
@@ -92,31 +93,33 @@ def test_tuple_reduce_no_shape_hypothesis(t, shape):
index = Tuple(*t)
- check_same(a, index.raw, ndindex_func=lambda a, x: a[x.reduce().raw],
+ check_same(a, index.raw, ndindex_func=lambda a, x: a[x.reduce(**kwargs).raw],
same_exception=False)
- reduced = index.reduce()
+ reduced = index.reduce(**kwargs)
if isinstance(reduced, Tuple):
assert len(reduced.args) != 1
assert reduced == () or reduced.args[-1] != ...
# Idempotency
- assert reduced.reduce() == reduced
-
-@example((..., None), ())
-@example((..., empty((0, 0), dtype=bool)), (0, 0))
-@example((empty((0, 0), dtype=bool), 0), (0, 0, 1))
-@example((array([], dtype=intp), 0), (0, 0))
-@example((array([], dtype=intp), array(0)), (0, 0))
-@example((array([], dtype=intp), [0]), (0, 0))
-@example((0, 1, ..., 2, 3), (2, 3, 4, 5, 6, 7))
-@example((0, slice(None), ..., slice(None), 3), (2, 3, 4, 5, 6, 7))
-@example((0, ..., slice(None)), (2, 3, 4, 5, 6, 7))
-@example((slice(None, None, -1),), (2,))
-@example((..., slice(None, None, -1),), (2, 3, 4))
-@example((..., False, slice(None)), 0)
-@given(Tuples, one_of(short_shapes, integers(0, 10)))
-def test_tuple_reduce_hypothesis(t, shape):
+ assert reduced.reduce(**kwargs) == reduced
+
+@example((..., empty((1, 0), dtype=intp)), (1, 0), {})
+@example((1, -1, [1, -1]), (3, 3, 3), {'negative_int': True})
+@example((..., None), (), {})
+@example((..., empty((0, 0), dtype=bool)), (0, 0), {})
+@example((empty((0, 0), dtype=bool), 0), (0, 0, 1), {})
+@example((array([], dtype=intp), 0), (0, 0), {})
+@example((array([], dtype=intp), array(0)), (0, 0), {})
+@example((array([], dtype=intp), [0]), (0, 0), {})
+@example((0, 1, ..., 2, 3), (2, 3, 4, 5, 6, 7), {})
+@example((0, slice(None), ..., slice(None), 3), (2, 3, 4, 5, 6, 7), {})
+@example((0, ..., slice(None)), (2, 3, 4, 5, 6, 7), {})
+@example((slice(None, None, -1),), (2,), {})
+@example((..., slice(None, None, -1),), (2, 3, 4), {})
+@example((..., False, slice(None)), 0, {})
+@given(Tuples, one_of(short_shapes, integers(0, 10)), reduce_kwargs)
+def test_tuple_reduce_hypothesis(t, shape, kwargs):
if isinstance(shape, int):
a = arange(shape)
else:
@@ -124,11 +127,13 @@ def test_tuple_reduce_hypothesis(t, shape):
index = Tuple(*t)
- check_same(a, index.raw, ndindex_func=lambda a, x: a[x.reduce(shape).raw],
+ check_same(a, index.raw, ndindex_func=lambda a, x: a[x.reduce(shape, **kwargs).raw],
same_exception=False)
+ negative_int = kwargs.get('negative_int', False)
+
try:
- reduced = index.reduce(shape)
+ reduced = index.reduce(shape, **kwargs)
except IndexError:
pass
else:
@@ -138,11 +143,23 @@ def test_tuple_reduce_hypothesis(t, shape):
# TODO: Check the other properties from the Tuple.reduce docstring.
# Idempotency
- assert reduced.reduce() == reduced
+ assert reduced.reduce(**kwargs) == reduced
# This is currently not implemented, for example, (..., False, :)
# takes two steps to remove the redundant slice.
# assert reduced.reduce(shape) == reduced
+ for arg in reduced.args:
+ if isinstance(arg, Integer):
+ if negative_int:
+ assert arg.raw < 0
+ else:
+ assert arg.raw >= 0
+ elif isinstance(arg, IntegerArray):
+ if negative_int:
+ assert np_all(arg.raw < 0)
+ else:
+ assert np_all(arg.raw >= 0)
+
def test_tuple_reduce_explicit():
# Some aspects of Tuple.reduce are hard to test as properties, so include
# some explicit tests here.
diff --git a/ndindex/tuple.py b/ndindex/tuple.py
index 46afe00c..30aa9646 100644
--- a/ndindex/tuple.py
+++ b/ndindex/tuple.py
@@ -1,7 +1,8 @@
import sys
-from .ndindex import NDIndex, ndindex, asshape
+from .ndindex import NDIndex, ndindex
from .subindex_helpers import subindex_slice
+from .shapetools import asshape, broadcast_shapes, BroadcastError
class Tuple(NDIndex):
"""
@@ -92,15 +93,12 @@ def _typecheck(self, *args):
if has_boolean_scalar:
raise NotImplementedError("Tuples mixing boolean scalars (True or False) with arrays are not yet supported.")
- from numpy import broadcast
try:
- broadcast(*[i for i in arrays])
- except ValueError as e:
- assert str(e).startswith("shape mismatch: objects cannot be broadcast to a single shape"), e.args
- # TODO: Newer versions of NumPy include where the mismatch is
- # in the error message in a more informative way than this
- # (but we can't use it directly because it talks about the
- # "arg"s to broadcast()).
+ broadcast_shapes(*[i.shape for i in arrays])
+ except BroadcastError:
+ # This matches the NumPy error message. The BroadcastError has
+ # a better error message, but it will be shown in the chained
+ # traceback.
raise IndexError("shape mismatch: indexing arrays could not be broadcast together with shapes %s" % ' '.join([str(i.shape) for i in arrays]))
return tuple(newargs)
@@ -183,7 +181,7 @@ def ellipsis_index(self):
def raw(self):
return tuple(i.raw for i in self.args)
- def reduce(self, shape=None):
+ def reduce(self, shape=None, *, negative_int=False):
r"""
Reduce a Tuple index on an array of shape `shape`
@@ -248,8 +246,8 @@ def reduce(self, shape=None):
Integer(1)
>>> a[..., 1]
array(1)
- >>> a[1]
- 1
+ >>> a[1] # doctest: +SKIPNP1
+ np.int64(1)
See https://github.com/Quansight-Labs/ndindex/issues/22.
@@ -280,7 +278,7 @@ def reduce(self, shape=None):
seen_boolean_scalar = True
else:
_args.append(s)
- return type(self)(*_args).reduce(shape)
+ return type(self)(*_args).reduce(shape, negative_int=negative_int)
arrays = []
for i in args:
@@ -292,9 +290,9 @@ def reduce(self, shape=None):
# TODO: Avoid explicitly calling nonzero
arrays.extend(i.raw.nonzero())
if arrays:
- from numpy import broadcast, broadcast_to
+ from numpy import broadcast_to
- broadcast_shape = broadcast(*arrays).shape
+ broadcast_shape = broadcast_shapes(*[a.shape for a in arrays])
else:
broadcast_shape = ()
@@ -342,7 +340,7 @@ def reduce(self, shape=None):
elif isinstance(s, BooleanArray):
begin_offset += s.ndim - 1
axis = ellipsis_i - i - begin_offset
- reduced = s.reduce(shape, axis=axis)
+ reduced = s.reduce(shape, axis=axis, negative_int=negative_int)
if (removable
and isinstance(reduced, Slice)
and reduced == Slice(0, shape[axis], 1)):
@@ -352,7 +350,7 @@ def reduce(self, shape=None):
preargs.insert(0, reduced)
if shape is None:
- endargs = [s.reduce() for s in args[ellipsis_i+1:]]
+ endargs = [s.reduce(negative_int=negative_int) for s in args[ellipsis_i+1:]]
else:
endargs = []
end_offset = 0
@@ -365,7 +363,7 @@ def reduce(self, shape=None):
if not (isinstance(s, IntegerArray) and (0 in broadcast_shape or
False in args)):
# Array bounds are not checked when the broadcast shape is empty
- s = s.reduce(shape, axis=axis)
+ s = s.reduce(shape, axis=axis, negative_int=negative_int)
endargs.insert(0, s)
if shape is not None:
@@ -428,9 +426,9 @@ def broadcast_arrays(self):
if not arrays:
return self
- from numpy import array, broadcast, broadcast_to, intp
+ from numpy import array, broadcast_to, intp
- broadcast_shape = broadcast(*arrays).shape
+ broadcast_shape = broadcast_shapes(*[a.shape for a in arrays])
newargs = []
for s in args:
@@ -487,9 +485,9 @@ def expand(self, shape):
arrays.extend(i.raw.nonzero())
if arrays:
- from numpy import broadcast, broadcast_to, array, intp
+ from numpy import broadcast_to, array, intp
- broadcast_shape = broadcast(*arrays).shape
+ broadcast_shape = broadcast_shapes(*[a.shape for a in arrays])
# If the broadcast shape is empty, out of bounds indices in
# non-empty arrays are ignored, e.g., ([], [10]) would broadcast to
# ([], []), so the bounds for 10 are not checked. Thus, we must do
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 00000000..7d960665
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,8 @@
+hypothesis
+numpy
+packaging
+pyflakes
+pytest
+pytest-cov
+pytest-doctestplus
+pytest-flakes
diff --git a/rever.xsh b/rever.xsh
index b443608e..33e66b3b 100644
--- a/rever.xsh
+++ b/rever.xsh
@@ -25,7 +25,8 @@ def run_tests():
@activity
def build_docs():
- with run_in_conda_env(['python=3.10', 'sphinx', 'myst-parser', 'numpy']):
+ with run_in_conda_env(['python=3.10', 'sphinx', 'myst-parser', 'numpy',
+ 'sphinx-copybutton', 'furo']):
cd docs
make html
cd ..
@@ -47,7 +48,7 @@ $ACTIVITIES = [
'pypi', # Sends the package to pypi
'push_tag', # Pushes the tag up to the $TAG_REMOTE
'ghrelease', # Creates a Github release entry for the new tag
- 'ghpages', # Update GitHub Pages
+ # 'ghpages', # Update GitHub Pages
]
$PUSH_TAG_REMOTE = 'git@github.com:Quansight-Labs/ndindex.git' # Repo to push tags to
diff --git a/setup.py b/setup.py
index 5d7e7efe..376e75cc 100644
--- a/setup.py
+++ b/setup.py
@@ -18,7 +18,9 @@ def check_cython():
from Cython.Build import cythonize
sys.argv = argv_org[:1] + ["build_ext"]
setuptools.setup(name="foo", version="1.0.0",
- ext_modules=cythonize(["ndindex/__init__.py"]))
+ ext_modules=cythonize(
+ ["ndindex/__init__.py"],
+ language_level="3"))
except:
return False
finally:
@@ -37,7 +39,8 @@ def check_cython():
if use_cython:
from Cython.Build import cythonize
- ext_modules = cythonize(["ndindex/*.py"])
+ ext_modules = cythonize(["ndindex/*.py"],
+ language_level="3")
else:
ext_modules = []
@@ -67,7 +70,7 @@ def check_cython():
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
- python_requires='>=3.7',
+ python_requires='>=3.8',
)
print("CYTHONIZE_NDINDEX: %r" % CYTHONIZE_NDINDEX)
diff --git a/versioneer.py b/versioneer.py
index 13901fcd..1e461ba0 100644
--- a/versioneer.py
+++ b/versioneer.py
@@ -339,9 +339,9 @@ def get_config_from_root(root):
# configparser.NoOptionError (if it lacks "VCS="). See the docstring at
# the top of versioneer.py for instructions on writing your setup.cfg .
setup_cfg = os.path.join(root, "setup.cfg")
- parser = configparser.SafeConfigParser()
+ parser = configparser.ConfigParser()
with open(setup_cfg, "r") as f:
- parser.readfp(f)
+ parser.read_file(f)
VCS = parser.get("versioneer", "VCS") # mandatory
def get(parser, name):