Skip to content

Commit 4b20720

Browse files
authored
Merge branch 'master' into short-collection
2 parents d027dd6 + 0e905dc commit 4b20720

File tree

23 files changed

+295
-101
lines changed

23 files changed

+295
-101
lines changed

.github/workflows/main.yml

+5-4
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ jobs:
7878
- check-pandas15
7979
- check-pandas14
8080
- check-pandas13
81-
- check-py39-pandas12
82-
- check-py39-pandas11
81+
## FIXME: actions update means Python builds without eg _bz2, which was required
82+
# - check-py39-pandas12
83+
# - check-py39-pandas11
8384
## `-cover` is too slow under crosshair; use a custom split
8485
# - check-crosshair-custom-cover/test_[a-d]*
8586
# - check-crosshair-custom-cover/test_[e-i]*
@@ -226,8 +227,8 @@ jobs:
226227
NODE_VERSION: 18
227228
# Note that the versions below must be updated in sync; we've automated
228229
# that with `update_pyodide_versions()` in our weekly cronjob.
229-
PYODIDE_VERSION: 0.26.4
230-
PYTHON_VERSION: 3.12.1
230+
PYODIDE_VERSION: 0.27.0
231+
PYTHON_VERSION: 3.12.7
231232
EMSCRIPTEN_VERSION: 3.1.58
232233
steps:
233234
- uses: actions/checkout@v3

CODEOWNERS

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Engine changes need to be approved by Zac-HD, as per
22
# https://github.com/HypothesisWorks/hypothesis/blob/master/guides/review.rst#engine-changes
3-
/hypothesis-python/src/hypothesis/internal/conjecture/ @Zac-HD
3+
/hypothesis-python/src/hypothesis/internal/conjecture/ @DRMacIver @Zac-HD
44

55
# Changes to the paper also need to be approved by DRMacIver or Zac, as authors
66
/paper.md @DRMacIver @Zac-HD

build.sh

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ if [ -n "${GITHUB_ACTIONS-}" ] || [ -n "${CODESPACES-}" ] ; then
2525
else
2626
# Otherwise, we install it from scratch
2727
# NOTE: tooling keeps this version in sync with ci_version in tooling
28-
"$SCRIPTS/ensure-python.sh" 3.10.15
29-
PYTHON=$(pythonloc 3.10.15)/bin/python
28+
"$SCRIPTS/ensure-python.sh" 3.10.16
29+
PYTHON=$(pythonloc 3.10.16)/bin/python
3030
fi
3131

3232
TOOL_REQUIREMENTS="$ROOT/requirements/tools.txt"

hypothesis-python/docs/changes.rst

+9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ Hypothesis 6.x
1818

1919
.. include:: ../RELEASE.rst
2020

21+
.. _v6.123.3:
22+
23+
--------------------
24+
6.123.3 - 2025-01-06
25+
--------------------
26+
27+
This release further improves shrinking of strategies using :func:`~hypothesis.strategies.one_of`,
28+
allowing the shrinker to more reliably move between branches of the strategy.
29+
2130
.. _v6.123.2:
2231

2332
--------------------

hypothesis-python/setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def local_file(name):
6060
"pytest": ["pytest>=4.6"],
6161
"dpcontracts": ["dpcontracts>=0.4"],
6262
"redis": ["redis>=3.0.0"],
63-
"crosshair": ["hypothesis-crosshair>=0.0.18", "crosshair-tool>=0.0.78"],
63+
"crosshair": ["hypothesis-crosshair>=0.0.18", "crosshair-tool>=0.0.81"],
6464
# zoneinfo is an odd one: every dependency is platform-conditional.
6565
"zoneinfo": [
6666
"tzdata>=2024.2 ; sys_platform == 'win32' or sys_platform == 'emscripten'",

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -1196,7 +1196,9 @@ def integer_array_indices(
11961196
shape: Shape,
11971197
*,
11981198
result_shape: st.SearchStrategy[Shape] = array_shapes(),
1199-
dtype: "np.dtype[I] | np.dtype[np.signedinteger[Any]]" = np.dtype(int),
1199+
dtype: "np.dtype[I] | np.dtype[np.signedinteger[Any] | np.bool[bool]]" = np.dtype(
1200+
int
1201+
),
12001202
) -> "st.SearchStrategy[tuple[NDArray[I], ...]]":
12011203
"""Return a search strategy for tuples of integer-arrays that, when used
12021204
to index into an array of shape ``shape``, given an array whose shape

hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py

+118
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ def shrink(self):
483483
"""
484484

485485
try:
486+
self.initial_coarse_reduction()
486487
self.greedy_shrink()
487488
except StopShrinking:
488489
# If we stopped shrinking because we're making slow progress (instead of
@@ -689,6 +690,123 @@ def greedy_shrink(self):
689690
]
690691
)
691692

693+
def initial_coarse_reduction(self):
694+
"""Performs some preliminary reductions that should not be
695+
repeated as part of the main shrink passes.
696+
697+
The main reason why these can't be included as part of shrink
698+
passes is that they have much more ability to make the test
699+
case "worse". e.g. they might rerandomise part of it, significantly
700+
increasing the value of individual nodes, which works in direct
701+
opposition to the lexical shrinking and will frequently undo
702+
its work.
703+
"""
704+
self.reduce_each_alternative()
705+
706+
@derived_value # type: ignore
707+
def examples_starting_at(self):
708+
result = [[] for _ in self.shrink_target.ir_nodes]
709+
for i, ex in enumerate(self.examples):
710+
# We can have zero-length examples that start at the end
711+
if ex.ir_start < len(result):
712+
result[ex.ir_start].append(i)
713+
return tuple(map(tuple, result))
714+
715+
def reduce_each_alternative(self):
716+
"""This is a pass that is designed to rerandomise use of the
717+
one_of strategy or things that look like it, in order to try
718+
to move from later strategies to earlier ones in the branch
719+
order.
720+
721+
It does this by trying to systematically lower each value it
722+
finds that looks like it might be the branch decision for
723+
one_of, and then attempts to repair any changes in shape that
724+
this causes.
725+
"""
726+
i = 0
727+
while i < len(self.shrink_target.ir_nodes):
728+
nodes = self.shrink_target.ir_nodes
729+
node = nodes[i]
730+
if (
731+
node.ir_type == "integer"
732+
and not node.was_forced
733+
and node.value <= 10
734+
and node.kwargs["min_value"] == 0
735+
):
736+
assert isinstance(node.value, int)
737+
738+
# We've found a plausible candidate for a ``one_of`` choice.
739+
# We now want to see if the shape of the test case actually depends
740+
# on it. If it doesn't, then we don't need to do this (comparatively
741+
# costly) pass, and can let much simpler lexicographic reduction
742+
# handle it later.
743+
#
744+
# We test this by trying to set the value to zero and seeing if the
745+
# shape changes, as measured by either changing the number of subsequent
746+
# nodes, or changing the nodes in such a way as to cause one of the
747+
# previous values to no longer be valid in its position.
748+
zero_attempt = self.cached_test_function_ir(
749+
nodes[:i] + (nodes[i].copy(with_value=0),) + nodes[i + 1 :]
750+
)
751+
if (
752+
zero_attempt is not self.shrink_target
753+
and zero_attempt is not None
754+
and zero_attempt.status >= Status.VALID
755+
):
756+
changed_shape = len(zero_attempt.ir_nodes) != len(nodes)
757+
758+
if not changed_shape:
759+
for j in range(i + 1, len(nodes)):
760+
zero_node = zero_attempt.ir_nodes[j]
761+
orig_node = nodes[j]
762+
if (
763+
zero_node.ir_type != orig_node.ir_type
764+
or not ir_value_permitted(
765+
orig_node.value, zero_node.ir_type, zero_node.kwargs
766+
)
767+
):
768+
changed_shape = True
769+
break
770+
if changed_shape:
771+
for v in range(node.value):
772+
if self.try_lower_node_as_alternative(i, v):
773+
break
774+
i += 1
775+
776+
def try_lower_node_as_alternative(self, i, v):
777+
"""Attempt to lower `self.shrink_target.ir_nodes[i]` to `v`,
778+
while rerandomising and attempting to repair any subsequent
779+
changes to the shape of the test case that this causes."""
780+
nodes = self.shrink_target.ir_nodes
781+
initial_attempt = self.cached_test_function_ir(
782+
nodes[:i] + (nodes[i].copy(with_value=v),) + nodes[i + 1 :]
783+
)
784+
if initial_attempt is self.shrink_target:
785+
return True
786+
787+
prefix = nodes[:i] + (nodes[i].copy(with_value=v),)
788+
initial = self.shrink_target
789+
examples = self.examples_starting_at[i]
790+
for _ in range(3):
791+
random_attempt = self.engine.cached_test_function_ir(
792+
prefix, extend=len(nodes) * 2
793+
)
794+
if random_attempt.status < Status.VALID:
795+
continue
796+
self.incorporate_test_data(random_attempt)
797+
for j in examples:
798+
initial_ex = initial.examples[j]
799+
attempt_ex = random_attempt.examples[j]
800+
contents = random_attempt.ir_nodes[
801+
attempt_ex.ir_start : attempt_ex.ir_end
802+
]
803+
self.consider_new_tree(
804+
nodes[:i] + contents + nodes[initial_ex.ir_end :]
805+
)
806+
if initial is not self.shrink_target:
807+
return True
808+
return False
809+
692810
@derived_value # type: ignore
693811
def shrink_pass_choice_trees(self):
694812
return defaultdict(ChoiceTree)

hypothesis-python/src/hypothesis/strategies/_internal/strategies.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,7 @@ def do_draw(self, data: ConjectureData) -> Any:
843843
try:
844844
data.start_example(MAPPED_SEARCH_STRATEGY_DO_DRAW_LABEL)
845845
x = data.draw(self.mapped_strategy)
846-
result = self.pack(x) # type: ignore
846+
result = self.pack(x)
847847
data.stop_example()
848848
current_build_context().record_call(result, self.pack, [x], {})
849849
return result

hypothesis-python/src/hypothesis/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
99
# obtain one at https://mozilla.org/MPL/2.0/.
1010

11-
__version_info__ = (6, 123, 2)
11+
__version_info__ = (6, 123, 3)
1212
__version__ = ".".join(map(str, __version_info__))

hypothesis-python/tests/common/debug.py

-4
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@
2121
TIME_INCREMENT = 0.00001
2222

2323

24-
class Timeout(BaseException):
25-
pass
26-
27-
2824
def minimal(definition, condition=lambda x: True, settings=None):
2925
from tests.conftest import in_shrinking_benchmark
3026

hypothesis-python/tests/conjecture/test_engine.py

+1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ def generate_new_examples(self):
134134
runner.run()
135135
(last_data,) = runner.interesting_examples.values()
136136
assert last_data.status == Status.INTERESTING
137+
assert runner.exit_reason == ExitReason.max_shrinks
137138
assert runner.shrinks == n
138139
in_db = set(db.data[runner.secondary_key])
139140
assert len(in_db) == n

hypothesis-python/tests/conjecture/test_shrinker.py

+27
Original file line numberDiff line numberDiff line change
@@ -531,3 +531,30 @@ def shrinker(data: ConjectureData):
531531
shrinker.fixate_shrink_passes(["minimize_individual_nodes"])
532532
assert shrinker.choices == (b"\x00" * n,)
533533
assert shrinker.calls < 10
534+
535+
536+
def test_alternative_shrinking_will_lower_to_alternate_value():
537+
# We want to reject the first integer value we see when shrinking
538+
# this alternative, because it will be the result of transmuting the
539+
# bytes value, and we want to ensure that we can find other values
540+
# there when we detect the shape change.
541+
seen_int = None
542+
543+
@shrinking_from(ir(1, b"hello world"))
544+
def shrinker(data: ConjectureData):
545+
nonlocal seen_int
546+
i = data.draw_integer(min_value=0, max_value=1)
547+
if i == 1:
548+
if data.draw_bytes():
549+
data.mark_interesting()
550+
else:
551+
n = data.draw_integer(0, 100)
552+
if n == 0:
553+
return
554+
if seen_int is None:
555+
seen_int = n
556+
elif n != seen_int:
557+
data.mark_interesting()
558+
559+
shrinker.initial_coarse_reduction()
560+
assert shrinker.choices[0] == 0

hypothesis-python/tests/datetime/test_zoneinfo_timezones.py

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def test_timezone_keys_argument_validation(kwargs):
5656
st.timezone_keys(**kwargs).validate()
5757

5858

59+
@pytest.mark.xfail(strict=False, reason="newly failing on GitHub Actions")
5960
@pytest.mark.skipif(platform.system() != "Linux", reason="platform-specific")
6061
def test_can_generate_prefixes_if_allowed_and_available():
6162
"""

hypothesis-python/tests/nocover/test_precise_shrinking.py

+41-7
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,12 @@ def test_function(data):
135135

136136

137137
@lru_cache
138+
def minimal_for_strategy(s):
139+
return precisely_shrink(s, end_marker=st.none())
140+
141+
138142
def minimal_buffer_for_strategy(s):
139-
return precisely_shrink(s, end_marker=st.none())[0].buffer
143+
return minimal_for_strategy(s)[0].buffer
140144

141145

142146
def test_strategy_list_is_in_sorted_order():
@@ -274,12 +278,11 @@ def shortlex(s):
274278
result_list = []
275279

276280
for k, v in sorted(results.items(), key=lambda x: shortlex(x[0])):
277-
if shortlex(k) < shortlex(buffer):
278-
t = repr(v)
279-
if t in seen:
280-
continue
281-
seen.add(t)
282-
result_list.append((k, v))
281+
t = repr(v)
282+
if t in seen:
283+
continue
284+
seen.add(t)
285+
result_list.append((k, v))
283286
return result_list
284287

285288

@@ -296,3 +299,34 @@ def test_always_shrinks_to_none(a, seed, block_falsey, allow_sloppy):
296299
combined_strategy, result.buffer, allow_sloppy=allow_sloppy, seed=seed
297300
)
298301
assert shrunk_values[0][1] is None
302+
303+
304+
@pytest.mark.parametrize(
305+
"i,alts", [(i, alt) for alt in alternatives for i in range(1, len(alt))]
306+
)
307+
@pytest.mark.parametrize("force_small", [False, True])
308+
@pytest.mark.parametrize("seed", [0, 2452, 99085240570])
309+
def test_can_shrink_to_every_smaller_alternative(i, alts, seed, force_small):
310+
types = [t for t, _ in alts]
311+
strats = [s for _, s in alts]
312+
combined_strategy = st.one_of(*strats)
313+
if force_small:
314+
result, value = precisely_shrink(
315+
combined_strategy, is_interesting=lambda x: type(x) is types[i], seed=seed
316+
)
317+
else:
318+
result, value = find_random(
319+
combined_strategy, lambda x: type(x) is types[i], seed=seed
320+
)
321+
322+
shrunk = shrinks(
323+
combined_strategy,
324+
result.buffer,
325+
allow_sloppy=False,
326+
# Arbitrary change so we don't use the same seed for each Random.
327+
seed=seed * 17,
328+
)
329+
shrunk_values = [t for _, t in shrunk]
330+
331+
for j in range(i):
332+
assert any(isinstance(x, types[j]) for x in shrunk_values)

hypothesis-python/tox.ini

+3-3
Original file line numberDiff line numberDiff line change
@@ -161,21 +161,21 @@ commands =
161161
setenv=
162162
PYTHONWARNDEFAULTENCODING=1
163163
commands =
164-
pip install django==4.2.16
164+
pip install django==4.2.17
165165
python -bb -X dev -m tests.django.manage test tests.django {posargs}
166166

167167
[testenv:django50]
168168
setenv=
169169
PYTHONWARNDEFAULTENCODING=1
170170
commands =
171-
pip install django==5.0.9
171+
pip install django==5.0.10
172172
python -bb -X dev -m tests.django.manage test tests.django {posargs}
173173

174174
[testenv:django51]
175175
setenv=
176176
PYTHONWARNDEFAULTENCODING=1
177177
commands =
178-
pip install django==5.1.3
178+
pip install django==5.1.4
179179
python -bb -X dev -m tests.django.manage test tests.django {posargs}
180180

181181
[testenv:py{39}-nose]

0 commit comments

Comments
 (0)