Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 7 additions & 7 deletions hypothesis-python/docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -5248,7 +5248,7 @@ already been decorated with :func:`@given() <hypothesis.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!

Expand Down Expand Up @@ -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!
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.

Expand Down
12 changes: 12 additions & 0 deletions hypothesis-python/tests/cover/test_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 41 additions & 31 deletions hypothesis-python/tests/cover/test_random_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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."
)
Expand All @@ -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

Expand Down
6 changes: 6 additions & 0 deletions hypothesis-python/tests/cover/test_randoms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
9 changes: 9 additions & 0 deletions hypothesis-python/tests/redis/test_redis_exampledatabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion hypothesis-python/tests/test_annotated_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions tooling/src/hypothesistooling/installers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 5 additions & 4 deletions whole_repo_tests/types/test_mypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down