Skip to content

Commit 377b4ee

Browse files
committed
Merge branch 'master' into refactor
2 parents cf92ea0 + decd12a commit 377b4ee

File tree

11 files changed

+176
-56
lines changed

11 files changed

+176
-56
lines changed

.github/workflows/main.yml

+9-16
Original file line numberDiff line numberDiff line change
@@ -223,17 +223,17 @@ jobs:
223223
- name: Run tests
224224
run: TASK=${{ matrix.task }} ./build.sh
225225

226-
# See https://pyodide.org/en/stable/development/building-and-testing-packages.html#testing-packages-against-pyodide
226+
# See https://pyodide.org/en/stable/usage/building-and-testing-packages.html
227227
# and https://github.com/numpy/numpy/blob/9a650391651c8486d8cb8b27b0e75aed5d36033e/.github/workflows/emscripten.yml
228228
test-pyodide:
229229
runs-on: ubuntu-latest
230230
env:
231-
NODE_VERSION: 18
232-
# Note that the versions below must be updated in sync; we've automated
233-
# that with `update_pyodide_versions()` in our weekly cronjob.
234-
PYODIDE_VERSION: 0.27.3
231+
NODE_VERSION: 22
232+
# Note that the versions below are updated by `update_pyodide_versions()` in our weekly cronjob.
233+
# The versions of pyodide-build and the Pyodide runtime may differ.
234+
PYODIDE_VERSION: 0.27.5
235+
PYODIDE_BUILD_VERSION: 0.30.0
235236
PYTHON_VERSION: 3.12.7
236-
EMSCRIPTEN_VERSION: 3.1.58
237237
steps:
238238
- uses: actions/checkout@v3
239239
with:
@@ -246,17 +246,10 @@ jobs:
246246
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
247247
with:
248248
node-version: ${{ env.NODE_VERSION }}
249-
- name: Set up Emscripten
250-
uses: mymindstorm/setup-emsdk@6ab9eb1bda2574c4ddb79809fc9247783eaf9021 # v14
251-
with:
252-
version: ${{ env.EMSCRIPTEN_VERSION }}
253-
- name: Build
249+
- name: Install pyodide-build and Pyodide cross-build environment
254250
run: |
255-
# TODO remove https://github.com/pyodide/pyodide/issues/5585
256-
pip install -U wheel==0.45.1
257-
pip install pyodide-build==$PYODIDE_VERSION
258-
cd hypothesis-python/
259-
CFLAGS=-g2 LDFLAGS=-g2 pyodide build
251+
pip install pyodide-build==$PYODIDE_BUILD_VERSION
252+
pyodide xbuildenv install $PYODIDE_VERSION
260253
- name: Set up Pyodide venv and install dependencies
261254
run: |
262255
pip install --upgrade setuptools pip wheel build

AUTHORS.rst

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ their individual contributions.
1414
* `Adam Sven Johnson <https://www.github.com/pkqk>`_
1515
* `Afrida Tabassum <https://github.com/oxfordhalfblood>`_ ([email protected])
1616
* `Afonso Silva <https://github.com/ajcerejeira>`_ ([email protected])
17+
* `Agriya Khetarpal <https://github.com/agriyakhetarpal>`_
1718
* `Agustín Covarrubias <https://github.com/agucova>`_ ([email protected])
1819
* `Akash Suresh <https://www.github.com/akash-suresh>`_ ([email protected])
1920
* `Alex Gaynor <https://github.com/alex>`_

hypothesis-python/RELEASE.rst

-3
This file was deleted.

hypothesis-python/docs/changelog.rst

+16
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,22 @@ Hypothesis 6.x
1818

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

21+
.. _v6.130.12:
22+
23+
---------------------
24+
6.130.12 - 2025-04-09
25+
---------------------
26+
27+
Lays some groundwork for future work on collecting interesting literals from the code being tested, for increased bug-finding power (:issue:`3127`). There is no user-visible change (yet!)
28+
29+
.. _v6.130.11:
30+
31+
---------------------
32+
6.130.11 - 2025-04-08
33+
---------------------
34+
35+
Fix the caching behavior of |st.sampled_from|, which in rare cases led to failing an internal assertion (:issue:`4339`).
36+
2137
.. _v6.130.10:
2238

2339
---------------------

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

+11-9
Original file line numberDiff line numberDiff line change
@@ -540,28 +540,30 @@ def explain(self) -> None:
540540
attempt = choices[:start] + tuple(replacement) + choices[end:]
541541
result = self.engine.cached_test_function(attempt, extend="full")
542542

543-
# Turns out this was a variable-length part, so grab the infix...
544543
if result.status is Status.OVERRUN:
545544
continue # pragma: no cover # flakily covered
546545
result = cast(ConjectureResult, result)
547546
if not (
548547
len(attempt) == len(result.choices)
549548
and endswith(result.nodes, nodes[end:])
550549
):
551-
for ex, res in zip(shrink_target.spans, result.spans):
552-
assert ex.start == res.start
553-
assert ex.start <= start
554-
assert ex.label == res.label
555-
if start == ex.start and end == ex.end:
556-
res_end = res.end
550+
# Turns out this was a variable-length part, so grab the infix...
551+
for span1, span2 in zip(shrink_target.spans, result.spans):
552+
assert span1.start == span2.start
553+
assert span1.start <= start
554+
assert span1.label == span2.label
555+
if span1.start == start and span1.end == end:
556+
result_end = span2.end
557557
break
558558
else:
559559
raise NotImplementedError("Expected matching prefixes")
560560

561561
attempt = (
562-
choices[:start] + result.choices[start:res_end] + choices[end:]
562+
choices[:start]
563+
+ result.choices[start:result_end]
564+
+ choices[end:]
563565
)
564-
chunks[(start, end)].append(result.choices[start:res_end])
566+
chunks[(start, end)].append(result.choices[start:result_end])
565567
result = self.engine.cached_test_function(attempt)
566568

567569
if result.status is Status.OVERRUN:

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

+3-4
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ def calc_is_cacheable(self, recur: RecurT) -> bool:
104104
return False
105105
return True
106106

107+
def calc_label(self) -> int:
108+
return self.wrapped_strategy.label
109+
107110
@property
108111
def wrapped_strategy(self) -> SearchStrategy[Ex]:
109112
if self.__wrapped_strategy is None:
@@ -176,7 +179,3 @@ def __repr__(self) -> str:
176179

177180
def do_draw(self, data: ConjectureData) -> Ex:
178181
return data.draw(self.wrapped_strategy)
179-
180-
@property
181-
def label(self) -> int:
182-
return self.wrapped_strategy.label

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

+47-8
Original file line numberDiff line numberDiff line change
@@ -564,14 +564,53 @@ def __repr__(self) -> str:
564564
)
565565

566566
def calc_label(self) -> int:
567-
return combine_labels(
568-
self.class_label,
569-
*(
570-
(calc_label_from_hash(self.elements),)
571-
if is_hashable(self.elements)
572-
else ()
573-
),
574-
)
567+
# strategy.label is effectively an under-approximation of structural
568+
# equality (i.e., some strategies may have the same label when they are not
569+
# structurally identical). More importantly for calculating the
570+
# SampledFromStrategy label, we might have hash(s1) != hash(s2) even
571+
# when s1 and s2 are structurally identical. For instance:
572+
#
573+
# s1 = st.sampled_from([st.none()])
574+
# s2 = st.sampled_from([st.none()])
575+
# assert hash(s1) != hash(s2)
576+
#
577+
# (see also test cases in test_labels.py).
578+
#
579+
# We therefore use the labels of any component strategies when calculating
580+
# our label, and only use the hash if it is not a strategy.
581+
#
582+
# That's the ideal, anyway. In reality the logic is more complicated than
583+
# necessary in order to be efficient in the presence of (very) large sequences:
584+
# * add an unabashed special case for range, to avoid iteration over an
585+
# enormous range when we know it is entirely integers.
586+
# * if there is at least one strategy in self.elements, use strategy label,
587+
# and the element hash otherwise.
588+
# * if there are no strategies in self.elements, take the hash of the
589+
# entire sequence. This prevents worst-case performance of hashing each
590+
# element when a hash of the entire sequence would have sufficed.
591+
#
592+
# The worst case performance of this scheme is
593+
# itertools.chain(range(2**100), [st.none()]), where it degrades to
594+
# hashing every int in the range.
595+
596+
if isinstance(self.elements, range) or (
597+
is_hashable(self.elements)
598+
and not any(isinstance(e, SearchStrategy) for e in self.elements)
599+
):
600+
return combine_labels(self.class_label, calc_label_from_hash(self.elements))
601+
602+
labels = [self.class_label]
603+
for element in self.elements:
604+
if not is_hashable(element):
605+
continue
606+
607+
labels.append(
608+
element.label
609+
if isinstance(element, SearchStrategy)
610+
else calc_label_from_hash(element)
611+
)
612+
613+
return combine_labels(*labels)
575614

576615
def calc_has_reusable_values(self, recur: RecurT) -> bool:
577616
# Because our custom .map/.filter implementations skip the normal

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, 130, 10)
11+
__version_info__ = (6, 130, 12)
1212
__version__ = ".".join(map(str, __version_info__))

hypothesis-python/tests/nocover/test_labels.py

+22
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
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+
import pytest
12+
1113
from hypothesis import strategies as st
1214

1315

@@ -47,3 +49,23 @@ def test_lists_label_by_element():
4749
def test_label_of_deferred_strategy_is_well_defined():
4850
recursive = st.deferred(lambda: st.lists(recursive))
4951
recursive.label
52+
53+
54+
@pytest.mark.parametrize(
55+
"strategy",
56+
[
57+
lambda: [st.none()],
58+
lambda: [st.integers()],
59+
lambda: [st.lists(st.floats())],
60+
lambda: [st.none(), st.integers(), st.lists(st.floats())],
61+
],
62+
)
63+
def test_sampled_from_label_with_strategies_does_not_change(strategy):
64+
s1 = st.sampled_from(strategy())
65+
s2 = st.sampled_from(strategy())
66+
assert s1.label == s2.label
67+
68+
69+
def test_label_of_enormous_sampled_range():
70+
# this should not take forever.
71+
st.sampled_from(range(2**30)).label

hypothesis-python/tests/nocover/test_regressions.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import pytest
1212

13-
from hypothesis import given, strategies as st
13+
from hypothesis import given, settings, strategies as st
1414
from hypothesis._settings import note_deprecation
1515
from hypothesis.errors import HypothesisDeprecationWarning
1616

@@ -66,3 +66,20 @@ def test_unique_floats_with_nan_is_not_flaky_3926(ls):
6666
@given(st.integers(min_value=0, max_value=1 << 25_000))
6767
def test_overrun_during_datatree_simulation_3874(n):
6868
pass
69+
70+
71+
def test_explain_phase_label_assertion_4339():
72+
# st.composite causes a re-creation of the SampledFromStrategy each time
73+
# (one_of is implemented using sampled_from internally), which previously
74+
# had different labels which triggered an assertion in the explain code.
75+
@st.composite
76+
def g(draw):
77+
draw(st.none() | st.booleans())
78+
79+
@given(g(), st.none() | st.booleans())
80+
@settings(database=None)
81+
def f(a, b):
82+
raise ValueError("marker")
83+
84+
with pytest.raises(ValueError, match="marker"):
85+
f()

tooling/src/hypothesistooling/__main__.py

+48-14
Original file line numberDiff line numberDiff line change
@@ -403,31 +403,65 @@ def update_django_versions():
403403

404404

405405
def update_pyodide_versions():
406+
407+
def version_tuple(v: str) -> tuple[int, int, int]:
408+
return tuple(int(x) for x in v.split(".")) # type: ignore
409+
406410
vers_re = r"(\d+\.\d+\.\d+)"
407-
all_versions = re.findall(
411+
all_pyodide_build_versions = re.findall(
408412
f"pyodide_build-{vers_re}-py3-none-any.whl", # excludes pre-releases
409413
requests.get("https://pypi.org/simple/pyodide-build/").text,
410414
)
411-
for pyodide_version in sorted(
415+
pyodide_build_version = max(
412416
# Don't just pick the most recent version; find the highest stable version.
413-
set(all_versions),
414-
key=lambda version: tuple(int(x) for x in version.split(".")),
415-
reverse=True,
416-
):
417-
makefile_url = f"https://raw.githubusercontent.com/pyodide/pyodide/{pyodide_version}/Makefile.envs"
418-
match = re.search(
419-
rf"export PYVERSION \?= {vers_re}\nexport PYODIDE_EMSCRIPTEN_VERSION \?= {vers_re}\n",
420-
requests.get(makefile_url).text,
417+
set(all_pyodide_build_versions),
418+
key=version_tuple,
419+
)
420+
421+
cross_build_environments_url = "https://raw.githubusercontent.com/pyodide/pyodide/refs/heads/main/pyodide-cross-build-environments.json"
422+
cross_build_environments_data = requests.get(cross_build_environments_url).json()
423+
424+
# Find the latest stable release for the Pyodide runtime/xbuildenv that is compatible
425+
# with the pyodide-build version we found
426+
stable_releases = [
427+
release
428+
for release in cross_build_environments_data["releases"].values()
429+
if re.fullmatch(vers_re, release["version"])
430+
]
431+
432+
compatible_releases = []
433+
for release in stable_releases: # sufficiently large values
434+
min_build_version = release.get("min_pyodide_build_version", "0.0.0")
435+
max_build_version = release.get("max_pyodide_build_version", "999.999.999")
436+
437+
# Perform version comparisons to avoid getting an incompatible pyodide-build version
438+
# with the Pyodide runtime
439+
if (
440+
version_tuple(min_build_version)
441+
<= version_tuple(pyodide_build_version)
442+
<= version_tuple(max_build_version)
443+
):
444+
compatible_releases.append(release)
445+
446+
if not compatible_releases:
447+
raise RuntimeError(
448+
f"No compatible Pyodide release found for pyodide-build {pyodide_build_version}"
421449
)
422-
if match is not None:
423-
python_version, emscripten_version = match.groups()
424-
break
450+
451+
pyodide_release = max(
452+
compatible_releases,
453+
key=lambda release: version_tuple(release["version"]),
454+
)
455+
456+
pyodide_version = pyodide_release["version"]
457+
python_version = pyodide_release["python_version"]
458+
425459
ci_file = tools.ROOT / ".github/workflows/main.yml"
426460
config = ci_file.read_text(encoding="utf-8")
427461
for name, var in [
428462
("PYODIDE", pyodide_version),
463+
("PYODIDE_BUILD", pyodide_build_version),
429464
("PYTHON", python_version),
430-
("EMSCRIPTEN", emscripten_version),
431465
]:
432466
config = re.sub(f"{name}_VERSION: {vers_re}", f"{name}_VERSION: {var}", config)
433467
ci_file.write_text(config, encoding="utf-8")

0 commit comments

Comments
 (0)