Repository: HypothesisWorks/hypothesis

Issue:
Improve `extra.numpy.array_shapes()`
https://github.com/HypothesisWorks/hypothesis/blob/07f32bae6355bac2893eaf50a556f7957a8b2036/hypothesis-python/src/hypothesis/extra/numpy.py#L403

- [ ] The current list of arguments is pretty good, but it would be better if setting `min_dims` or `min_side` automatically chose an appropriate value for the maximum, instead of giving an error if it was greater than the current default.  The max args should therefore default to None, and be set inside the strategy if needed.
- [ ] We should validate that `max_dims` is not more than `32` (the limit set by Numpy).
- [ ] We should check that all the arguments are integers with `check_type`.

This would be a minor release, as the change is visible to users.  See our contributing guide for where to add your name to the list of authors and how to write a changelog!


Discussion:
Would it make sense to add an optional `max_size` parameter?  Often it's good to test long 1D arrays (to make sure it doesn't screw up with large amounts of data), and also small highly-dimensional arrays (to make sure it handles dimensions correctly), but large multi-dimensional arrays rapidly use up all available memory.  (Usually if it handles large 1D arrays, and small ND arrays, it will also handle large ND arrays.)
Hmm, that's definitely a useful feature!  However I can't think of a way to cap the element count by construction without either skewing the distribution or making shrinking much less efficient 😕 

Instead of adding an argument, I think we should suggest limiting element count with `.filter(lambda shape: np.prod(shape) <= MAX_ELEMENTS)` in the docstring for `array_shapes`.
If I may chip in: what I find missing in the signature is the ability to specify e.g. a shape that has up to length 3 in dimension 1, and lengths between 2 and 5 for all other dimensions (arbitrary many). 

I'm not sure whether this is in scope, and if it is, how to solve it. Simply passing a strategy from which lengths can be drawn would make this function completely redundant.
> What I find missing in the signature is the ability to specify e.g. a shape that has up to length 3 in dimension 1, and lengths between 2 and 5 for all other dimensions (arbitrary many).

Personally I think this kind of thing is specific enough that it should be built by users:

```python
# Fixed length is easier, obviously - just `tuples(integers(...), integers(...), ...)`
odd_shapes = tuples(integers(0, 5), lists(integers(2, 5))).map(lambda x, ys: tuple(ys + [x]))
```

It's definitely important to be able to define strategies like this, but if we make the `array_shapes` strategy too general it becomes a whole other API to learn - and we already have a perfectly good way to customise strategies *including* tuples of integers.  Perhaps another documentation issue - maybe we should have a "Numpy recipes" page?
> Perhaps another documentation issue - maybe we should have a "Numpy recipes" page?

That would definitely solve it. I agree that it would be highly convenient to have an easy-to-use interface to create shapes for most use cases. It is something that should be explicitly decided though, rather than turning out to be an unfortunate side-effect :)
Agreed!  If you have some specific (hopefully common) use-cases that are not well served by `array_shapes`, `broadcastable_shapes`, `st.lists(...)`, and `st.tuples(...)` in some simple combination, it would be great to discuss them and see if we can come up with a strategy that would be helpful :smile: 
Not yet, but maybe later :)
For now I just thought I had something to contribute to the discussion.
One strategy I was trying to create is `numeric_array_likes()`, which is all numeric dtypes (but with optional exclusion of inf/nan) or object arrays of other numeric types like decimal/fraction, in either array form or list of lists (also maybe matrices, masked/sparse arrays, etc.) to make sure that scipy functions work with the wide variety of possible numpy [`array_like`s](https://stackoverflow.com/a/40380014/125507) that people will throw at them.

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/extra/numpy.py b/hypothesis-python/src/hypothesis/extra/numpy.py
--- a/hypothesis-python/src/hypothesis/extra/numpy.py
+++ b/hypothesis-python/src/hypothesis/extra/numpy.py
@@ -400,11 +400,30 @@ def arrays(
 
 
 @st.defines_strategy
-def array_shapes(min_dims=1, max_dims=3, min_side=1, max_side=10):
+def array_shapes(min_dims=1, max_dims=None, min_side=1, max_side=None):
     # type: (int, int, int, int) -> st.SearchStrategy[Tuple[int, ...]]
     """Return a strategy for array shapes (tuples of int >= 1)."""
+    check_type(integer_types, min_dims, "min_dims")
+    check_type(integer_types, min_side, "min_side")
+    if min_dims > 32:
+        raise InvalidArgument(
+            "Got min_dims=%r, but numpy does not support arrays greater than 32 dimensions"
+            % min_dims
+        )
+    if max_dims is None:
+        max_dims = min(min_dims + 2, 32)
+    check_type(integer_types, max_dims, "max_dims")
+    if max_dims > 32:
+        raise InvalidArgument(
+            "Got max_dims=%r, but numpy does not support arrays greater than 32 dimensions"
+            % max_dims
+        )
+    if max_side is None:
+        max_side = min_side + 5
+    check_type(integer_types, max_side, "max_side")
     order_check("dims", 0, min_dims, max_dims)
     order_check("side", 0, min_side, max_side)
+
     return st.lists(
         st.integers(min_side, max_side), min_size=min_dims, max_size=max_dims
     ).map(tuple)


Test patch:
diff --git a/hypothesis-python/tests/numpy/test_argument_validation.py b/hypothesis-python/tests/numpy/test_argument_validation.py
--- a/hypothesis-python/tests/numpy/test_argument_validation.py
+++ b/hypothesis-python/tests/numpy/test_argument_validation.py
@@ -39,6 +39,12 @@ def e(a, **kwargs):
         e(nps.array_shapes, min_dims=3, max_dims=2),
         e(nps.array_shapes, min_dims=-1),
         e(nps.array_shapes, min_side=-1),
+        e(nps.array_shapes, min_side="not an int"),
+        e(nps.array_shapes, max_side="not an int"),
+        e(nps.array_shapes, min_dims="not an int"),
+        e(nps.array_shapes, max_dims="not an int"),
+        e(nps.array_shapes, min_dims=33),
+        e(nps.array_shapes, max_dims=33),
         e(nps.arrays, dtype=float, shape=(0.5,)),
         e(nps.arrays, dtype=object, shape=1),
         e(nps.arrays, dtype=float, shape=1, fill=3),
diff --git a/hypothesis-python/tests/numpy/test_gen_data.py b/hypothesis-python/tests/numpy/test_gen_data.py
--- a/hypothesis-python/tests/numpy/test_gen_data.py
+++ b/hypothesis-python/tests/numpy/test_gen_data.py
@@ -182,6 +182,13 @@ def test_minimise_array_shapes(min_dims, dim_range, min_side, side_range):
     assert len(smallest) == min_dims and all(k == min_side for k in smallest)
 
 
+@pytest.mark.parametrize(
+    "kwargs", [dict(min_side=100), dict(min_dims=15), dict(min_dims=32)]
+)
+def test_interesting_array_shapes_argument(kwargs):
+    nps.array_shapes(**kwargs).example()
+
+
 @given(nps.scalar_dtypes())
 def test_can_generate_scalar_dtypes(dtype):
     assert isinstance(dtype, np.dtype)