Skip to content

Commit

Permalink
Merge branch 'master' into short-collection
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD authored Jan 6, 2025
2 parents d027dd6 + 0e905dc commit 4b20720
Show file tree
Hide file tree
Showing 23 changed files with 295 additions and 101 deletions.
9 changes: 5 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ jobs:
- check-pandas15
- check-pandas14
- check-pandas13
- check-py39-pandas12
- check-py39-pandas11
## FIXME: actions update means Python builds without eg _bz2, which was required
# - check-py39-pandas12
# - check-py39-pandas11
## `-cover` is too slow under crosshair; use a custom split
# - check-crosshair-custom-cover/test_[a-d]*
# - check-crosshair-custom-cover/test_[e-i]*
Expand Down Expand Up @@ -226,8 +227,8 @@ jobs:
NODE_VERSION: 18
# Note that the versions below must be updated in sync; we've automated
# that with `update_pyodide_versions()` in our weekly cronjob.
PYODIDE_VERSION: 0.26.4
PYTHON_VERSION: 3.12.1
PYODIDE_VERSION: 0.27.0
PYTHON_VERSION: 3.12.7
EMSCRIPTEN_VERSION: 3.1.58
steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Engine changes need to be approved by Zac-HD, as per
# https://github.com/HypothesisWorks/hypothesis/blob/master/guides/review.rst#engine-changes
/hypothesis-python/src/hypothesis/internal/conjecture/ @Zac-HD
/hypothesis-python/src/hypothesis/internal/conjecture/ @DRMacIver @Zac-HD

# Changes to the paper also need to be approved by DRMacIver or Zac, as authors
/paper.md @DRMacIver @Zac-HD
Expand Down
4 changes: 2 additions & 2 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ if [ -n "${GITHUB_ACTIONS-}" ] || [ -n "${CODESPACES-}" ] ; then
else
# Otherwise, we install it from scratch
# NOTE: tooling keeps this version in sync with ci_version in tooling
"$SCRIPTS/ensure-python.sh" 3.10.15
PYTHON=$(pythonloc 3.10.15)/bin/python
"$SCRIPTS/ensure-python.sh" 3.10.16
PYTHON=$(pythonloc 3.10.16)/bin/python
fi

TOOL_REQUIREMENTS="$ROOT/requirements/tools.txt"
Expand Down
9 changes: 9 additions & 0 deletions hypothesis-python/docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ Hypothesis 6.x

.. include:: ../RELEASE.rst

.. _v6.123.3:

--------------------
6.123.3 - 2025-01-06
--------------------

This release further improves shrinking of strategies using :func:`~hypothesis.strategies.one_of`,
allowing the shrinker to more reliably move between branches of the strategy.

.. _v6.123.2:

--------------------
Expand Down
2 changes: 1 addition & 1 deletion hypothesis-python/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def local_file(name):
"pytest": ["pytest>=4.6"],
"dpcontracts": ["dpcontracts>=0.4"],
"redis": ["redis>=3.0.0"],
"crosshair": ["hypothesis-crosshair>=0.0.18", "crosshair-tool>=0.0.78"],
"crosshair": ["hypothesis-crosshair>=0.0.18", "crosshair-tool>=0.0.81"],
# zoneinfo is an odd one: every dependency is platform-conditional.
"zoneinfo": [
"tzdata>=2024.2 ; sys_platform == 'win32' or sys_platform == 'emscripten'",
Expand Down
4 changes: 3 additions & 1 deletion hypothesis-python/src/hypothesis/extra/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1196,7 +1196,9 @@ def integer_array_indices(
shape: Shape,
*,
result_shape: st.SearchStrategy[Shape] = array_shapes(),
dtype: "np.dtype[I] | np.dtype[np.signedinteger[Any]]" = np.dtype(int),
dtype: "np.dtype[I] | np.dtype[np.signedinteger[Any] | np.bool[bool]]" = np.dtype(
int
),
) -> "st.SearchStrategy[tuple[NDArray[I], ...]]":
"""Return a search strategy for tuples of integer-arrays that, when used
to index into an array of shape ``shape``, given an array whose shape
Expand Down
118 changes: 118 additions & 0 deletions hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ def shrink(self):
"""

try:
self.initial_coarse_reduction()
self.greedy_shrink()
except StopShrinking:
# If we stopped shrinking because we're making slow progress (instead of
Expand Down Expand Up @@ -689,6 +690,123 @@ def greedy_shrink(self):
]
)

def initial_coarse_reduction(self):
"""Performs some preliminary reductions that should not be
repeated as part of the main shrink passes.
The main reason why these can't be included as part of shrink
passes is that they have much more ability to make the test
case "worse". e.g. they might rerandomise part of it, significantly
increasing the value of individual nodes, which works in direct
opposition to the lexical shrinking and will frequently undo
its work.
"""
self.reduce_each_alternative()

@derived_value # type: ignore
def examples_starting_at(self):
result = [[] for _ in self.shrink_target.ir_nodes]
for i, ex in enumerate(self.examples):
# We can have zero-length examples that start at the end
if ex.ir_start < len(result):
result[ex.ir_start].append(i)
return tuple(map(tuple, result))

def reduce_each_alternative(self):
"""This is a pass that is designed to rerandomise use of the
one_of strategy or things that look like it, in order to try
to move from later strategies to earlier ones in the branch
order.
It does this by trying to systematically lower each value it
finds that looks like it might be the branch decision for
one_of, and then attempts to repair any changes in shape that
this causes.
"""
i = 0
while i < len(self.shrink_target.ir_nodes):
nodes = self.shrink_target.ir_nodes
node = nodes[i]
if (
node.ir_type == "integer"
and not node.was_forced
and node.value <= 10
and node.kwargs["min_value"] == 0
):
assert isinstance(node.value, int)

# We've found a plausible candidate for a ``one_of`` choice.
# We now want to see if the shape of the test case actually depends
# on it. If it doesn't, then we don't need to do this (comparatively
# costly) pass, and can let much simpler lexicographic reduction
# handle it later.
#
# We test this by trying to set the value to zero and seeing if the
# shape changes, as measured by either changing the number of subsequent
# nodes, or changing the nodes in such a way as to cause one of the
# previous values to no longer be valid in its position.
zero_attempt = self.cached_test_function_ir(
nodes[:i] + (nodes[i].copy(with_value=0),) + nodes[i + 1 :]
)
if (
zero_attempt is not self.shrink_target
and zero_attempt is not None
and zero_attempt.status >= Status.VALID
):
changed_shape = len(zero_attempt.ir_nodes) != len(nodes)

if not changed_shape:
for j in range(i + 1, len(nodes)):
zero_node = zero_attempt.ir_nodes[j]
orig_node = nodes[j]
if (
zero_node.ir_type != orig_node.ir_type
or not ir_value_permitted(
orig_node.value, zero_node.ir_type, zero_node.kwargs
)
):
changed_shape = True
break
if changed_shape:
for v in range(node.value):
if self.try_lower_node_as_alternative(i, v):
break
i += 1

def try_lower_node_as_alternative(self, i, v):
"""Attempt to lower `self.shrink_target.ir_nodes[i]` to `v`,
while rerandomising and attempting to repair any subsequent
changes to the shape of the test case that this causes."""
nodes = self.shrink_target.ir_nodes
initial_attempt = self.cached_test_function_ir(
nodes[:i] + (nodes[i].copy(with_value=v),) + nodes[i + 1 :]
)
if initial_attempt is self.shrink_target:
return True

prefix = nodes[:i] + (nodes[i].copy(with_value=v),)
initial = self.shrink_target
examples = self.examples_starting_at[i]
for _ in range(3):
random_attempt = self.engine.cached_test_function_ir(
prefix, extend=len(nodes) * 2
)
if random_attempt.status < Status.VALID:
continue
self.incorporate_test_data(random_attempt)
for j in examples:
initial_ex = initial.examples[j]
attempt_ex = random_attempt.examples[j]
contents = random_attempt.ir_nodes[
attempt_ex.ir_start : attempt_ex.ir_end
]
self.consider_new_tree(
nodes[:i] + contents + nodes[initial_ex.ir_end :]
)
if initial is not self.shrink_target:
return True
return False

@derived_value # type: ignore
def shrink_pass_choice_trees(self):
return defaultdict(ChoiceTree)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,7 @@ def do_draw(self, data: ConjectureData) -> Any:
try:
data.start_example(MAPPED_SEARCH_STRATEGY_DO_DRAW_LABEL)
x = data.draw(self.mapped_strategy)
result = self.pack(x) # type: ignore
result = self.pack(x)
data.stop_example()
current_build_context().record_call(result, self.pack, [x], {})
return result
Expand Down
2 changes: 1 addition & 1 deletion hypothesis-python/src/hypothesis/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.

__version_info__ = (6, 123, 2)
__version_info__ = (6, 123, 3)
__version__ = ".".join(map(str, __version_info__))
4 changes: 0 additions & 4 deletions hypothesis-python/tests/common/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@
TIME_INCREMENT = 0.00001


class Timeout(BaseException):
pass


def minimal(definition, condition=lambda x: True, settings=None):
from tests.conftest import in_shrinking_benchmark

Expand Down
1 change: 1 addition & 0 deletions hypothesis-python/tests/conjecture/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ def generate_new_examples(self):
runner.run()
(last_data,) = runner.interesting_examples.values()
assert last_data.status == Status.INTERESTING
assert runner.exit_reason == ExitReason.max_shrinks
assert runner.shrinks == n
in_db = set(db.data[runner.secondary_key])
assert len(in_db) == n
Expand Down
27 changes: 27 additions & 0 deletions hypothesis-python/tests/conjecture/test_shrinker.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,3 +531,30 @@ def shrinker(data: ConjectureData):
shrinker.fixate_shrink_passes(["minimize_individual_nodes"])
assert shrinker.choices == (b"\x00" * n,)
assert shrinker.calls < 10


def test_alternative_shrinking_will_lower_to_alternate_value():
# We want to reject the first integer value we see when shrinking
# this alternative, because it will be the result of transmuting the
# bytes value, and we want to ensure that we can find other values
# there when we detect the shape change.
seen_int = None

@shrinking_from(ir(1, b"hello world"))
def shrinker(data: ConjectureData):
nonlocal seen_int
i = data.draw_integer(min_value=0, max_value=1)
if i == 1:
if data.draw_bytes():
data.mark_interesting()
else:
n = data.draw_integer(0, 100)
if n == 0:
return
if seen_int is None:
seen_int = n
elif n != seen_int:
data.mark_interesting()

shrinker.initial_coarse_reduction()
assert shrinker.choices[0] == 0
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def test_timezone_keys_argument_validation(kwargs):
st.timezone_keys(**kwargs).validate()


@pytest.mark.xfail(strict=False, reason="newly failing on GitHub Actions")
@pytest.mark.skipif(platform.system() != "Linux", reason="platform-specific")
def test_can_generate_prefixes_if_allowed_and_available():
"""
Expand Down
48 changes: 41 additions & 7 deletions hypothesis-python/tests/nocover/test_precise_shrinking.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,12 @@ def test_function(data):


@lru_cache
def minimal_for_strategy(s):
return precisely_shrink(s, end_marker=st.none())


def minimal_buffer_for_strategy(s):
return precisely_shrink(s, end_marker=st.none())[0].buffer
return minimal_for_strategy(s)[0].buffer


def test_strategy_list_is_in_sorted_order():
Expand Down Expand Up @@ -274,12 +278,11 @@ def shortlex(s):
result_list = []

for k, v in sorted(results.items(), key=lambda x: shortlex(x[0])):
if shortlex(k) < shortlex(buffer):
t = repr(v)
if t in seen:
continue
seen.add(t)
result_list.append((k, v))
t = repr(v)
if t in seen:
continue
seen.add(t)
result_list.append((k, v))
return result_list


Expand All @@ -296,3 +299,34 @@ def test_always_shrinks_to_none(a, seed, block_falsey, allow_sloppy):
combined_strategy, result.buffer, allow_sloppy=allow_sloppy, seed=seed
)
assert shrunk_values[0][1] is None


@pytest.mark.parametrize(
"i,alts", [(i, alt) for alt in alternatives for i in range(1, len(alt))]
)
@pytest.mark.parametrize("force_small", [False, True])
@pytest.mark.parametrize("seed", [0, 2452, 99085240570])
def test_can_shrink_to_every_smaller_alternative(i, alts, seed, force_small):
types = [t for t, _ in alts]
strats = [s for _, s in alts]
combined_strategy = st.one_of(*strats)
if force_small:
result, value = precisely_shrink(
combined_strategy, is_interesting=lambda x: type(x) is types[i], seed=seed
)
else:
result, value = find_random(
combined_strategy, lambda x: type(x) is types[i], seed=seed
)

shrunk = shrinks(
combined_strategy,
result.buffer,
allow_sloppy=False,
# Arbitrary change so we don't use the same seed for each Random.
seed=seed * 17,
)
shrunk_values = [t for _, t in shrunk]

for j in range(i):
assert any(isinstance(x, types[j]) for x in shrunk_values)
6 changes: 3 additions & 3 deletions hypothesis-python/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -161,21 +161,21 @@ commands =
setenv=
PYTHONWARNDEFAULTENCODING=1
commands =
pip install django==4.2.16
pip install django==4.2.17
python -bb -X dev -m tests.django.manage test tests.django {posargs}

[testenv:django50]
setenv=
PYTHONWARNDEFAULTENCODING=1
commands =
pip install django==5.0.9
pip install django==5.0.10
python -bb -X dev -m tests.django.manage test tests.django {posargs}

[testenv:django51]
setenv=
PYTHONWARNDEFAULTENCODING=1
commands =
pip install django==5.1.3
pip install django==5.1.4
python -bb -X dev -m tests.django.manage test tests.django {posargs}

[testenv:py{39}-nose]
Expand Down
Loading

0 comments on commit 4b20720

Please sign in to comment.