From cf5af6ae1dab2c793c9392e2763cef524dd6f432 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 11 Jan 2026 23:44:52 -0500 Subject: [PATCH 1/2] various test updates and fixes --- build.sh | 3 +- hypothesis-python/tests/cover/test_lookup.py | 12 ++++ .../tests/cover/test_random_module.py | 72 +++++++++++-------- hypothesis-python/tests/cover/test_randoms.py | 6 ++ .../tests/redis/test_redis_exampledatabase.py | 9 +++ .../tests/test_annotated_types.py | 7 +- tooling/src/hypothesistooling/installers.py | 3 + whole_repo_tests/types/test_mypy.py | 9 +-- 8 files changed, 84 insertions(+), 37 deletions(-) diff --git a/build.sh b/build.sh index 6fa2ecf388..c3043c58f7 100755 --- a/build.sh +++ b/build.sh @@ -31,7 +31,8 @@ fi TOOL_REQUIREMENTS="$ROOT/requirements/tools.txt" -TOOL_HASH=$("$PYTHON" "$SCRIPTS/tool-hash.py" < "$TOOL_REQUIREMENTS") +# append PYTHON_VERSION to bust caches when we upgrade versions +TOOL_HASH=$( (cat "$TOOL_REQUIREMENTS" && echo "$PYTHON_VERSION") | "$PYTHON" "$SCRIPTS/tool-hash.py") TOOL_VIRTUALENV="$VIRTUALENVS/build-$TOOL_HASH" TOOL_PYTHON="$TOOL_VIRTUALENV/bin/python" diff --git a/hypothesis-python/tests/cover/test_lookup.py b/hypothesis-python/tests/cover/test_lookup.py index 05f999a5ca..a00fdf04c6 100644 --- a/hypothesis-python/tests/cover/test_lookup.py +++ b/hypothesis-python/tests/cover/test_lookup.py @@ -1002,6 +1002,18 @@ def test_hashable_type_unhashable_value(): ) +def test_unhashable_type(): + class UnhashableMeta(type): + __hash__ = None + + class UnhashableType(metaclass=UnhashableMeta): + pass + + assert_simple_property( + st.from_type(UnhashableType), lambda x: isinstance(x, UnhashableType) + ) + + class _EmptyClass: def __init__(self, value=-1) -> None: pass diff --git a/hypothesis-python/tests/cover/test_random_module.py b/hypothesis-python/tests/cover/test_random_module.py index 53401bbee9..1c3c6a29fc 100644 --- a/hypothesis-python/tests/cover/test_random_module.py +++ b/hypothesis-python/tests/cover/test_random_module.py @@ -75,20 +75,22 @@ def test_cannot_register_non_Random(): @skipif_threading -@pytest.mark.filterwarnings( - "ignore:It looks like `register_random` was passed an object that could be garbage collected" -) -@pytest.mark.xfail( - sys.version_info[:2] == (3, 14), - reason="TODO_314: is this intentional semantics of the new gc?", -) def test_registering_a_Random_is_idempotent(): gc_collect() n_registered = len(entropy.RANDOMS_TO_MANAGE) - r = random.Random() + # on 3.14+, python introduced the LOAD_FAST_BORROW opcode, which does + # not increment the refcount. Passing a bare r to register_random here on 3.14+ + # would use LOAD_FAST_BORROW and entropy.py would see a non-increasing refcount + # and hard-error. On 3.13 and earlier, this is a warning instead. + # + # For compatibility with both versions, this test forces a refcount increment + # with a redundant container. + container = [random.Random()] + r = container[0] register_random(r) register_random(r) assert len(entropy.RANDOMS_TO_MANAGE) == n_registered + 1 + del container del r gc_collect() assert len(entropy.RANDOMS_TO_MANAGE) == n_registered @@ -174,13 +176,6 @@ def test_find_does_not_pollute_state(): assert state_a2 != state_b2 -@pytest.mark.filterwarnings( - "ignore:It looks like `register_random` was passed an object that could be garbage collected" -) -@pytest.mark.skipif( - sys.version_info[:2] == (3, 14), - reason="TODO_314: is this intentional semantics of the new gc?", -) @skipif_threading # we assume we're the only writer to entropy.RANDOMS_TO_MANAGE def test_evil_prng_registration_nonsense(): # my guess is that other tests may register randoms that are then marked for @@ -195,7 +190,10 @@ def test_evil_prng_registration_nonsense(): pass n_registered = len(entropy.RANDOMS_TO_MANAGE) - r1, r2, r3 = random.Random(1), random.Random(2), random.Random(3) + # put inside a list to increment ref count and avoid our warning/error about no + # referrers + c1, c2, c3 = [random.Random(1)], [random.Random(2)], [random.Random(3)] + r1, r2, r3 = c1[0], c2[0], c3[0] s2 = r2.getstate() # We're going to be totally evil here: register two randoms, then @@ -208,6 +206,7 @@ def test_evil_prng_registration_nonsense(): with deterministic_PRNG(): del r1 + del c1 gc_collect() assert k not in entropy.RANDOMS_TO_MANAGE, "r1 has been garbage-collected" assert len(entropy.RANDOMS_TO_MANAGE) == n_registered + 1 @@ -230,6 +229,7 @@ def test_passing_unreferenced_instance_raises(): register_random(random.Random(0)) +@xfail_if_gil_disabled @pytest.mark.skipif( PYPY, reason="We can't guard against bad no-reference patterns in pypy." ) @@ -240,44 +240,54 @@ def f(): with pytest.raises(ReferenceError): f() + # we have two error paths for register_random: one which warns and one which + # errors. We use an alias to bump the refcount while not adding a gc referrer, + # which covers the warning path. + def f(): + r = random.Random(0) + _r2 = r + register_random(r) + + with pytest.warns( + HypothesisWarning, + match="It looks like `register_random` was passed an object that could be" + " garbage collected", + ): + f() + @xfail_if_gil_disabled @pytest.mark.skipif( PYPY, reason="We can't guard against bad no-reference patterns in pypy." ) @pytest.mark.skipif( - sys.version_info[:2] == (3, 14), - reason="TODO_314: is this intentional semantics of the new gc?", + sys.version_info[:2] < (3, 14), + reason="warns instead of raises on 3.13 or earlier due to gc changes", ) -def test_passing_referenced_instance_within_function_scope_warns(): +def test_passing_referenced_instance_within_function_scope_raises(): def f(): r = random.Random(0) register_random(r) - with pytest.warns( - HypothesisWarning, - match="It looks like `register_random` was passed an object that could be" - " garbage collected", + with pytest.raises( + ReferenceError, + match=r"`register_random` was passed .* which will be garbage collected", ): f() -@pytest.mark.filterwarnings( - "ignore:It looks like `register_random` was passed an object that could be garbage collected" -) @pytest.mark.skipif( PYPY, reason="We can't guard against bad no-reference patterns in pypy." ) -@pytest.mark.skipif( - sys.version_info[:2] == (3, 14), - reason="TODO_314: is this intentional semantics of the new gc?", -) @skipif_threading # we assume we're the only writer to entropy.RANDOMS_TO_MANAGE def test_register_random_within_nested_function_scope(): n_registered = len(entropy.RANDOMS_TO_MANAGE) def f(): - r = random.Random() + # put inside a list to increment ref count and avoid our warning/error about no + # referrers + container = [random.Random()] + r = container[0] register_random(r) assert len(entropy.RANDOMS_TO_MANAGE) == n_registered + 1 diff --git a/hypothesis-python/tests/cover/test_randoms.py b/hypothesis-python/tests/cover/test_randoms.py index ff7af60c8d..0ee768e39a 100644 --- a/hypothesis-python/tests/cover/test_randoms.py +++ b/hypothesis-python/tests/cover/test_randoms.py @@ -236,6 +236,12 @@ def test_handles_singleton_regions_of_triangular_correctly(rnd): assert rnd.triangular(0.0, -0.0) == 0.0 +@given(st.randoms(use_true_random=False)) +def test_triangular_with_mode(rnd): + x = rnd.triangular(0.0, 1.0, mode=0.5) + assert 0.0 <= x <= 1.0 + + @pytest.mark.parametrize("use_true_random", [False, True]) def test_outputs_random_calls(use_true_random): @given(st.randoms(use_true_random=use_true_random, note_method_calls=True)) diff --git a/hypothesis-python/tests/redis/test_redis_exampledatabase.py b/hypothesis-python/tests/redis/test_redis_exampledatabase.py index 94987e3d70..031c97c8bc 100644 --- a/hypothesis-python/tests/redis/test_redis_exampledatabase.py +++ b/hypothesis-python/tests/redis/test_redis_exampledatabase.py @@ -164,6 +164,15 @@ def test_redis_move_into_key_with_value(): db.move(b"a", b"b", b"x") +def test_redis_move_to_same_key(): + # explicit covering test for: + # * moving a value where src == dest + redis = FakeRedis() + db = RedisExampleDatabase(redis) + db.move(b"a", b"a", b"x") + assert list(db.fetch(b"a")) == [b"x"] + + def test_redis_equality(): redis = FakeRedis() assert RedisExampleDatabase(redis) == RedisExampleDatabase(redis) diff --git a/hypothesis-python/tests/test_annotated_types.py b/hypothesis-python/tests/test_annotated_types.py index 796070a854..1aa3b68907 100644 --- a/hypothesis-python/tests/test_annotated_types.py +++ b/hypothesis-python/tests/test_annotated_types.py @@ -20,7 +20,7 @@ from hypothesis.strategies._internal.strategies import FilteredStrategy from hypothesis.strategies._internal.types import _get_constraints -from tests.common.debug import check_can_generate_examples +from tests.common.debug import assert_simple_property, check_can_generate_examples try: import annotated_types as at @@ -118,6 +118,11 @@ def test_collection_size_from_slice(data): assert 1 <= len(value) <= 10 +def test_unhashable_annotated_metadata(): + t = Annotated[int, {"key": "value"}] + assert_simple_property(st.from_type(t), lambda x: isinstance(x, int)) + + class GroupedStuff: __is_annotated_types_grouped_metadata__ = True diff --git a/tooling/src/hypothesistooling/installers.py b/tooling/src/hypothesistooling/installers.py index 2519cfa09f..f533579050 100644 --- a/tooling/src/hypothesistooling/installers.py +++ b/tooling/src/hypothesistooling/installers.py @@ -56,6 +56,9 @@ def ensure_stack(): if os.path.exists(STACK): return subprocess.check_call("mkdir -p ~/.local/bin", shell=True) + # if you're on macos, this will error with "--wildcards is not supported" + # or similar. You should put shellcheck on your PATH with your package + # manager of choice; eg `brew install shellcheck`. subprocess.check_call( "curl -L https://www.stackage.org/stack/linux-x86_64 " "| tar xz --wildcards --strip-components=1 -C $HOME" diff --git a/whole_repo_tests/types/test_mypy.py b/whole_repo_tests/types/test_mypy.py index eb2a330582..f5b1201f39 100644 --- a/whole_repo_tests/types/test_mypy.py +++ b/whole_repo_tests/types/test_mypy.py @@ -60,10 +60,11 @@ def get_mypy_output(fname, *extra_args): def get_mypy_analysed_type(fname): attempts = 0 while True: - out = get_mypy_output(fname).rstrip() - msg = "Success: no issues found in 1 source file" - if out.endswith(msg): - out = out[: -len(msg)] + out = ( + get_mypy_output(fname) + .rstrip() + .removesuffix("Success: no issues found in 1 source file") + ) # we've noticed some flakiness in getting an empty output here. Give it # a couple tries. if len(out.splitlines()) == 0: From b1bc5c25212abab8bbb8179cbd4eed2562265d6b Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Mon, 12 Jan 2026 00:11:32 -0500 Subject: [PATCH 2/2] update changelog --- hypothesis-python/docs/changelog.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hypothesis-python/docs/changelog.rst b/hypothesis-python/docs/changelog.rst index 24d6637b93..353015c7f8 100644 --- a/hypothesis-python/docs/changelog.rst +++ b/hypothesis-python/docs/changelog.rst @@ -579,7 +579,7 @@ The type annotations for |st.register_type_strategy| now indicate that it accept 6.138.1 - 2025-08-15 -------------------- -Internal refactoring and cleanup. As a result, ``hypothesis[black]`` now requires ``black>=20.8b0`` instead of the previous ``black>=19.10b0``. +Internal refactoring and cleanup. As a result, ``hypothesis[cli]`` and ``hypothesis[ghostwriter]`` now require ``black>=20.8b0`` instead of the previous ``black>=19.10b0``. .. _v6.138.0: @@ -2427,7 +2427,7 @@ This patch makes progress towards adding type hints to our internal conjecture e This release allows using :obj:`~python:typing.Annotated` and :obj:`!ReadOnly` types -for :class:`~python:typing.TypedDict` value types +for :obj:`~python:typing.TypedDict` value types with :func:`~hypothesis.strategies.from_type`. .. _v6.108.10: @@ -5075,7 +5075,7 @@ scans. ------------------- This patch by Cheuk Ting Ho adds support for :pep:`655` ``Required`` and ``NotRequired`` as attributes of -:class:`~python:typing.TypedDict` in :func:`~hypothesis.strategies.from_type` (:issue:`3339`). +:obj:`~python:typing.TypedDict` in :func:`~hypothesis.strategies.from_type` (:issue:`3339`). .. _v6.46.5: @@ -5248,7 +5248,7 @@ already been decorated with :func:`@given() `. Previously, 6.42.3 - 2022-04-10 ------------------- -This patch fixes :func:`~hypothesis.strategies.from_type` on a :class:`~python:typing.TypedDict` +This patch fixes :func:`~hypothesis.strategies.from_type` on a :obj:`~python:typing.TypedDict` with complex annotations, defined in a file using ``from __future__ import annotations``. Thanks to Katelyn Gigante for identifying and fixing this bug! @@ -6760,7 +6760,7 @@ creating import cycles. There is no user-visible change. ------------------ This patch enables :func:`~hypothesis.strategies.register_type_strategy` for subclasses of -:class:`python:typing.TypedDict`. Previously, :func:`~hypothesis.strategies.from_type` +:obj:`python:typing.TypedDict`. Previously, :func:`~hypothesis.strategies.from_type` would ignore the registered strategy (:issue:`2872`). Thanks to Ilya Lebedev for identifying and fixing this bug! @@ -8074,7 +8074,7 @@ deprecated - you can explicitly pass ``min_magnitude=0``, or omit the argument e ------------------- This patch fixes an internal error in :func:`~hypothesis.strategies.from_type` -for :class:`python:typing.NamedTuple` in Python 3.9. Thanks to Michel Salim +for :obj:`python:typing.NamedTuple` in Python 3.9. Thanks to Michel Salim for reporting and fixing :issue:`2427`! .. _v5.13.0: @@ -12603,7 +12603,7 @@ not matter. ------------------- This patch fixes inference in the :func:`~hypothesis.strategies.builds` -strategy with subtypes of :class:`python:typing.NamedTuple`, where the +strategy with subtypes of :obj:`python:typing.NamedTuple`, where the ``__init__`` method is not useful for introspection. We now use the field types instead - thanks to James Uther for identifying this bug.