Skip to content

Commit e05b372

Browse files
authored
Merge pull request #4611 from Zac-HD/claude/fix-hypothesis-4607-018dHECpz3mwL9NUfXjptxqC
Improve type annotations for array strategies and `assume()`
2 parents 93caa6d + 799c0c9 commit e05b372

File tree

9 files changed

+175
-12
lines changed

9 files changed

+175
-12
lines changed

.claude/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ When creating a PR:
4040
- **Idiomatic** - follows Python and Hypothesis conventions
4141
- **Minimally commented** - code should be self-documenting; only add comments where truly needed
4242
2. **Run `./build.sh format; ./build.sh lint`** immediately before committing to auto-format and lint code
43+
3. **Do not reference issues or PRs in commit messages** (e.g., avoid `Fixes #1234` or `See #5678`) - this clutters the issue timeline with unnecessary links

build.sh

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ SCRIPTS="$ROOT/tooling/scripts"
1919
# shellcheck source=tooling/scripts/common.sh
2020
source "$SCRIPTS/common.sh"
2121

22-
if [ -n "${GITHUB_ACTIONS-}" ] || [ -n "${CODESPACES-}" ] ; then
23-
# We're on GitHub Actions or Codespaces and already set up a suitable Python
24-
PYTHON=$(command -v python)
22+
if [ -n "${GITHUB_ACTIONS-}" ] || [ -n "${CODESPACES-}" ] || [ -n "${CLAUDECODE-}" ] ; then
23+
# We're on GitHub Actions, Codespaces, or Claude Code and already have a suitable Python
24+
PYTHON=$(command -v python3 || command -v python)
2525
else
2626
# Otherwise, we install it from scratch
2727
# NOTE: tooling keeps this version in sync with ci_version in tooling
@@ -40,9 +40,14 @@ export PYTHONPATH="$ROOT/tooling/src"
4040

4141
if ! "$TOOL_PYTHON" -m hypothesistooling check-installed ; then
4242
rm -rf "$TOOL_VIRTUALENV"
43-
"$PYTHON" -m pip install --upgrade pip
44-
"$PYTHON" -m pip install --upgrade virtualenv
45-
"$PYTHON" -m virtualenv "$TOOL_VIRTUALENV"
43+
if [ -n "${CLAUDECODE-}" ] ; then
44+
# Claude Code: use venv (available) and skip pip upgrades (debian-managed)
45+
"$PYTHON" -m venv "$TOOL_VIRTUALENV"
46+
else
47+
"$PYTHON" -m pip install --upgrade pip
48+
"$PYTHON" -m pip install --upgrade virtualenv
49+
"$PYTHON" -m virtualenv "$TOOL_VIRTUALENV"
50+
fi
4651
"$TOOL_PYTHON" -m pip install --no-warn-script-location -r requirements/tools.txt
4752
fi
4853

hypothesis-python/RELEASE.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
RELEASE_TYPE: patch
2+
3+
This patch improves the type annotations for :func:`~hypothesis.extra.numpy.basic_indices`.
4+
The return type now accurately reflects the ``allow_ellipsis`` and ``allow_newaxis``
5+
parameters, excluding ``EllipsisType`` or ``None`` from the union when those index
6+
types are disabled (:issue:`4607`).
7+
8+
Additionally, :func:`~hypothesis.assume` now has overloaded type annotations:
9+
``assume(True)`` returns ``Literal[True]``, while ``assume(False)`` and
10+
``assume(None)`` return ``NoReturn``.

hypothesis-python/src/hypothesis/control.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from collections import defaultdict
1515
from collections.abc import Callable, Sequence
1616
from contextlib import contextmanager
17-
from typing import Any, NoReturn, Optional
17+
from typing import Any, Literal, NoReturn, Optional, overload
1818
from weakref import WeakKeyDictionary
1919

2020
from hypothesis import Verbosity, settings
@@ -49,7 +49,13 @@ def reject() -> NoReturn:
4949
raise UnsatisfiedAssumption(where)
5050

5151

52-
def assume(condition: object) -> bool:
52+
@overload
53+
def assume(condition: Literal[False] | None) -> NoReturn: ...
54+
@overload
55+
def assume(condition: object) -> Literal[True]: ...
56+
57+
58+
def assume(condition: object) -> Literal[True]:
5359
"""Calling ``assume`` is like an :ref:`assert <python:assert>` that marks
5460
the example as bad, rather than failing the test.
5561

hypothesis-python/src/hypothesis/extra/_array_helpers.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@
2222

2323
__all__ = [
2424
"NDIM_MAX",
25+
"_BIE",
2526
"BasicIndex",
2627
"BasicIndexStrategy",
2728
"BroadcastableShapes",
2829
"MutuallyBroadcastableShapesStrategy",
2930
"Shape",
31+
"_BIENoEllipsis",
32+
"_BIENoEllipsisNoNewaxis",
33+
"_BIENoNewaxis",
3034
"array_shapes",
3135
"broadcastable_shapes",
3236
"check_argument",
@@ -38,7 +42,15 @@
3842

3943

4044
Shape = tuple[int, ...]
41-
BasicIndex = tuple[int | slice | None | EllipsisType, ...]
45+
46+
# Type aliases for basic array index elements. Variants exist to accurately
47+
# type the return value of basic_indices() based on allow_ellipsis/allow_newaxis.
48+
_BIE = int | slice | None | EllipsisType
49+
_BIENoEllipsis = int | slice | None
50+
_BIENoNewaxis = int | slice | EllipsisType
51+
_BIENoEllipsisNoNewaxis = int | slice
52+
53+
BasicIndex = _BIE | tuple[_BIE, ...]
4254

4355

4456
class BroadcastableShapes(NamedTuple):

hypothesis-python/src/hypothesis/extra/numpy.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@
3030
from hypothesis._settings import note_deprecation
3131
from hypothesis.errors import HypothesisException, InvalidArgument
3232
from hypothesis.extra._array_helpers import (
33+
_BIE,
3334
NDIM_MAX,
3435
BasicIndex,
3536
BasicIndexStrategy,
3637
BroadcastableShapes,
3738
Shape,
39+
_BIENoEllipsis,
40+
_BIENoEllipsisNoNewaxis,
41+
_BIENoNewaxis,
3842
array_shapes,
3943
broadcastable_shapes,
4044
check_argument,
@@ -1092,6 +1096,52 @@ def mutually_broadcastable_shapes(*args, **kwargs):
10921096
"""
10931097

10941098

1099+
@overload
1100+
def basic_indices(
1101+
shape: Shape,
1102+
*,
1103+
min_dims: int = 0,
1104+
max_dims: int | None = None,
1105+
allow_newaxis: Literal[False] = ...,
1106+
allow_ellipsis: Literal[False],
1107+
) -> st.SearchStrategy[
1108+
_BIENoEllipsisNoNewaxis | tuple[_BIENoEllipsisNoNewaxis, ...]
1109+
]: ...
1110+
1111+
1112+
@overload
1113+
def basic_indices(
1114+
shape: Shape,
1115+
*,
1116+
min_dims: int = 0,
1117+
max_dims: int | None = None,
1118+
allow_newaxis: Literal[False] = ...,
1119+
allow_ellipsis: Literal[True] = ...,
1120+
) -> st.SearchStrategy[_BIENoNewaxis | tuple[_BIENoNewaxis, ...]]: ...
1121+
1122+
1123+
@overload
1124+
def basic_indices(
1125+
shape: Shape,
1126+
*,
1127+
min_dims: int = 0,
1128+
max_dims: int | None = None,
1129+
allow_newaxis: Literal[True],
1130+
allow_ellipsis: Literal[False],
1131+
) -> st.SearchStrategy[_BIENoEllipsis | tuple[_BIENoEllipsis, ...]]: ...
1132+
1133+
1134+
@overload
1135+
def basic_indices(
1136+
shape: Shape,
1137+
*,
1138+
min_dims: int = 0,
1139+
max_dims: int | None = None,
1140+
allow_newaxis: Literal[True],
1141+
allow_ellipsis: Literal[True] = ...,
1142+
) -> st.SearchStrategy[_BIE | tuple[_BIE, ...]]: ...
1143+
1144+
10951145
@defines_strategy()
10961146
def basic_indices(
10971147
shape: Shape,

whole_repo_tests/types/revealed_types.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@
2222
else:
2323
NP1 = np_version.startswith("1.")
2424

25+
ASSUME_REVEALED_TYPES = [
26+
("assume(False)", "Never"),
27+
("assume(None)", "Never"),
28+
("assume(True)", "Literal[True]"),
29+
("assume(1)", "Literal[True]"),
30+
]
31+
2532
REVEALED_TYPES = [
2633
("integers()", "int"),
2734
("text()", "str"),
@@ -202,4 +209,30 @@ class DifferingRevealedTypes(NamedTuple):
202209
'integer_array_indices(shape=(2, 3), dtype=np.dtype("uint8"))',
203210
"tuple[ndarray[tuple[int, ...], dtype[unsignedinteger[_8Bit]]], ...]",
204211
),
212+
# basic_indices with allow_ellipsis=False (no EllipsisType differences)
213+
(
214+
"basic_indices((3, 4), allow_ellipsis=False)",
215+
"int | slice[Any, Any, Any] | tuple[int | slice[Any, Any, Any], ...]",
216+
),
217+
]
218+
219+
# basic_indices tests where mypy/pyright differ in EllipsisType representation
220+
NUMPY_DIFF_REVEALED_TYPES = [
221+
# mypy uses types.EllipsisType, pyright uses EllipsisType
222+
DifferingRevealedTypes(
223+
"basic_indices((3, 4))",
224+
"int | slice[Any, Any, Any] | types.EllipsisType | tuple[int | slice[Any, Any, Any] | types.EllipsisType, ...]",
225+
"int | slice[Any, Any, Any] | EllipsisType | tuple[int | slice[Any, Any, Any] | EllipsisType, ...]",
226+
),
227+
# pyright also reorders None to the end
228+
DifferingRevealedTypes(
229+
"basic_indices((3, 4), allow_newaxis=True, allow_ellipsis=False)",
230+
"int | slice[Any, Any, Any] | None | tuple[int | slice[Any, Any, Any] | None, ...]",
231+
"int | slice[Any, Any, Any] | tuple[int | slice[Any, Any, Any] | None, ...] | None",
232+
),
233+
DifferingRevealedTypes(
234+
"basic_indices((3, 4), allow_newaxis=True)",
235+
"int | slice[Any, Any, Any] | None | types.EllipsisType | tuple[int | slice[Any, Any, Any] | None | types.EllipsisType, ...]",
236+
"int | slice[Any, Any, Any] | EllipsisType | tuple[int | slice[Any, Any, Any] | EllipsisType | None, ...] | None",
237+
),
205238
]

whole_repo_tests/types/test_mypy.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
from hypothesistooling.scripts import pip_tool, tool_path
1919

2020
from .revealed_types import (
21+
ASSUME_REVEALED_TYPES,
2122
DIFF_REVEALED_TYPES,
23+
NUMPY_DIFF_REVEALED_TYPES,
2224
NUMPY_REVEALED_TYPES,
2325
PYTHON_VERSIONS,
2426
REVEALED_TYPES,
@@ -142,7 +144,27 @@ def test_revealed_types(tmp_path, val, expect):
142144
assert typ == f"SearchStrategy[{expect}]"
143145

144146

145-
@pytest.mark.parametrize("val,expect", NUMPY_REVEALED_TYPES)
147+
@pytest.mark.parametrize("val,expect", ASSUME_REVEALED_TYPES)
148+
def test_assume_revealed_types(tmp_path, val, expect):
149+
"""Check that Mypy infers the correct return type for assume()."""
150+
f = tmp_path / "check.py"
151+
f.write_text(
152+
textwrap.dedent(
153+
f"""
154+
from hypothesis import assume
155+
reveal_type({val})
156+
"""
157+
),
158+
encoding="utf-8",
159+
)
160+
typ = get_mypy_analysed_type(f)
161+
assert typ == expect
162+
163+
164+
@pytest.mark.parametrize(
165+
"val,expect",
166+
[*NUMPY_REVEALED_TYPES, *((x.value, x.mypy) for x in NUMPY_DIFF_REVEALED_TYPES)],
167+
)
146168
def test_numpy_revealed_types(tmp_path, val, expect):
147169
f = tmp_path / "check.py"
148170
f.write_text(

whole_repo_tests/types/test_pyright.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
from hypothesistooling.scripts import pip_tool, tool_path
2525

2626
from .revealed_types import (
27+
ASSUME_REVEALED_TYPES,
2728
DIFF_REVEALED_TYPES,
29+
NUMPY_DIFF_REVEALED_TYPES,
2830
NUMPY_REVEALED_TYPES,
2931
PYTHON_VERSIONS,
3032
REVEALED_TYPES,
@@ -200,9 +202,31 @@ def test_revealed_types(tmp_path, val, expect):
200202
assert typ == f"SearchStrategy[{expect}]"
201203

202204

203-
@pytest.mark.parametrize("val,expect", NUMPY_REVEALED_TYPES)
205+
@pytest.mark.parametrize("val,expect", ASSUME_REVEALED_TYPES)
206+
def test_assume_revealed_types(tmp_path, val, expect):
207+
"""Check that Pyright infers the correct return type for assume()."""
208+
f = tmp_path / "check.py"
209+
f.write_text(
210+
textwrap.dedent(
211+
f"""
212+
from hypothesis import assume
213+
reveal_type({val})
214+
"""
215+
),
216+
encoding="utf-8",
217+
)
218+
_write_config(tmp_path)
219+
typ = get_pyright_analysed_type(f)
220+
# Pyright uses NoReturn, mypy uses Never
221+
assert typ == (expect if expect != "Never" else "NoReturn")
222+
223+
224+
@pytest.mark.parametrize(
225+
"val,expect",
226+
[*NUMPY_REVEALED_TYPES, *((x.value, x.pyright) for x in NUMPY_DIFF_REVEALED_TYPES)],
227+
)
204228
def test_numpy_revealed_types(tmp_path, val, expect):
205-
f = tmp_path / (expect + ".py")
229+
f = tmp_path / "check.py"
206230
f.write_text(
207231
textwrap.dedent(
208232
f"""

0 commit comments

Comments
 (0)