From a6db02f743121030ed22a062cf6bfee491997b75 Mon Sep 17 00:00:00 2001 From: Hal Blackburn Date: Sat, 14 Oct 2023 08:06:14 +0000 Subject: [PATCH 1/8] Fix flaky test test_self_ref_regression is failing randomly with FailedHealthCheck for too slow and filtering too much. We now suppress these health checks for this test. --- hypothesis-python/tests/nocover/test_recursive.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hypothesis-python/tests/nocover/test_recursive.py b/hypothesis-python/tests/nocover/test_recursive.py index 783cbfa31b..c6b0c640a7 100644 --- a/hypothesis-python/tests/nocover/test_recursive.py +++ b/hypothesis-python/tests/nocover/test_recursive.py @@ -177,6 +177,7 @@ def test(data): ) +@settings(suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much]) @given(SELF_REF) def test_self_ref_regression(_): # See https://github.com/HypothesisWorks/hypothesis/issues/2794 From d94243bd531aebbe13c6c25c829b07135c9d64e3 Mon Sep 17 00:00:00 2001 From: Hal Blackburn Date: Sat, 14 Oct 2023 08:28:11 +0000 Subject: [PATCH 2/8] Allow strategy-generating functions to not provide strategies Custom type strategies registered with register_type_strategy() can now choose not to provide a strategy for a type by returning NotImplemented instead of a strategy. This allows custom strategies to only provide a custom strategies for certain subtypes of a type, or certain generic parameters, while deferring to another strategy for the unhandled cases. --- AUTHORS.rst | 1 + hypothesis-python/RELEASE.rst | 5 + .../hypothesis/strategies/_internal/core.py | 34 +++-- .../hypothesis/strategies/_internal/types.py | 18 ++- hypothesis-python/tests/cover/test_lookup.py | 119 ++++++++++++++++++ .../tests/cover/test_type_lookup.py | 33 ++++- 6 files changed, 195 insertions(+), 15 deletions(-) create mode 100644 hypothesis-python/RELEASE.rst diff --git a/AUTHORS.rst b/AUTHORS.rst index dab937b42f..5acddff27c 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -73,6 +73,7 @@ their individual contributions. * `Gregory Petrosyan `_ * `Grzegorz Zieba `_ (g.zieba@erax.pl) * `Grigorios Giannakopoulos `_ +* `Hal Blackburn `_ * `Hugo van Kemenade `_ * `Humberto Rocha `_ * `Ilya Lebedev `_ (melevir@gmail.com) diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..819325d6b9 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,5 @@ +RELEASE_TYPE: minor + +This release allows strategy-generating functions registered with +:func:`~hypothesis.strategies.register_type_strategy` to conditionally not +return a strategy, by returning :data:`NotImplemented` (:issue:`3767`). diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py index a01fc7486e..70a58866cb 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py @@ -1235,6 +1235,8 @@ def as_strategy(strat_or_callable, thing): strategy = strat_or_callable(thing) else: strategy = strat_or_callable + if strategy is NotImplemented: + return NotImplemented if not isinstance(strategy, SearchStrategy): raise ResolutionFailed( f"Error: {thing} was registered for {nicerepr(strat_or_callable)}, " @@ -1277,7 +1279,9 @@ def from_type_guarded(thing): # Check if we have an explicitly registered strategy for this thing, # resolve it so, and otherwise resolve as for the base type. if thing in types._global_type_lookup: - return as_strategy(types._global_type_lookup[thing], thing) + strategy = as_strategy(types._global_type_lookup[thing], thing) + if strategy is not NotImplemented: + return strategy return _from_type(thing.__supertype__) # Unions are not instances of `type` - but we still want to resolve them! if types.is_a_union(thing): @@ -1287,7 +1291,9 @@ def from_type_guarded(thing): # They are represented as instances like `~T` when they come here. # We need to work with their type instead. if isinstance(thing, TypeVar) and type(thing) in types._global_type_lookup: - return as_strategy(types._global_type_lookup[type(thing)], thing) + strategy = as_strategy(types._global_type_lookup[type(thing)], thing) + if strategy is not NotImplemented: + return strategy if not types.is_a_type(thing): if isinstance(thing, str): # See https://github.com/HypothesisWorks/hypothesis/issues/3016 @@ -1312,7 +1318,9 @@ def from_type_guarded(thing): # convert empty results into an explicit error. try: if thing in types._global_type_lookup: - return as_strategy(types._global_type_lookup[thing], thing) + strategy = as_strategy(types._global_type_lookup[thing], thing) + if strategy is not NotImplemented: + return strategy except TypeError: # pragma: no cover # This is due to a bizarre divergence in behaviour under Python 3.9.0: # typing.Callable[[], foo] has __args__ = (foo,) but collections.abc.Callable @@ -1372,11 +1380,16 @@ def from_type_guarded(thing): # type. For example, `Number -> integers() | floats()`, but bools() is # not included because bool is a subclass of int as well as Number. strategies = [ - as_strategy(v, thing) - for k, v in sorted(types._global_type_lookup.items(), key=repr) - if isinstance(k, type) - and issubclass(k, thing) - and sum(types.try_issubclass(k, typ) for typ in types._global_type_lookup) == 1 + s + for s in ( + as_strategy(v, thing) + for k, v in sorted(types._global_type_lookup.items(), key=repr) + if isinstance(k, type) + and issubclass(k, thing) + and sum(types.try_issubclass(k, typ) for typ in types._global_type_lookup) + == 1 + ) + if s is not NotImplemented ] if any(not s.is_empty for s in strategies): return one_of(strategies) @@ -2142,7 +2155,10 @@ def register_type_strategy( for an argument with a default value. ``strategy`` may be a search strategy, or a function that takes a type and - returns a strategy (useful for generic types). + returns a strategy (useful for generic types). The function may return + :data:`NotImplemented` to conditionally not provide a strategy for the type + (the type will still be resolved by other methods, if possible, as if the + function was not registered). Note that you may not register a parametrised generic type (such as ``MyCollection[int]``) directly, because the resolution logic does not diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/types.py b/hypothesis-python/src/hypothesis/strategies/_internal/types.py index dd0233d5ce..48295b2eee 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/types.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/types.py @@ -444,9 +444,13 @@ def from_typing_type(thing): mapping.pop(t) # Sort strategies according to our type-sorting heuristic for stable output strategies = [ - v if isinstance(v, st.SearchStrategy) else v(thing) - for k, v in sorted(mapping.items(), key=lambda kv: type_sorting_key(kv[0])) - if sum(try_issubclass(k, T) for T in mapping) == 1 + s + for s in ( + v if isinstance(v, st.SearchStrategy) else v(thing) + for k, v in sorted(mapping.items(), key=lambda kv: type_sorting_key(kv[0])) + if sum(try_issubclass(k, T) for T in mapping) == 1 + ) + if s != NotImplemented ] empty = ", ".join(repr(s) for s in strategies if s.is_empty) if empty or not strategies: @@ -484,6 +488,14 @@ def _networks(bits): # As a general rule, we try to limit this to scalars because from_type() # would have to decide on arbitrary collection elements, and we'd rather # not (with typing module generic types and some builtins as exceptions). +# +# Strategy Callables may return NotImplemented, which should be treated in the +# same way as if the type was not registered. +# +# Note that NotImplemented cannot be typed in Python 3.8 because there's no type +# exposed for it, and NotImplemented itself is typed as Any so that it can be +# returned without being listed in a function signature: +# https://github.com/python/mypy/issues/6710#issuecomment-485580032 _global_type_lookup: typing.Dict[ type, typing.Union[st.SearchStrategy, typing.Callable[[type], st.SearchStrategy]] ] = { diff --git a/hypothesis-python/tests/cover/test_lookup.py b/hypothesis-python/tests/cover/test_lookup.py index 0510968c15..0daa9c90e6 100644 --- a/hypothesis-python/tests/cover/test_lookup.py +++ b/hypothesis-python/tests/cover/test_lookup.py @@ -11,6 +11,7 @@ import abc import builtins import collections +import contextlib import datetime import enum import inspect @@ -21,6 +22,7 @@ import sys import typing import warnings +from dataclasses import dataclass from inspect import signature from numbers import Real @@ -371,6 +373,25 @@ def test_typevars_can_be_redefine_with_factory(): assert_all_examples(st.from_type(A), lambda obj: obj == "A") +def test_typevars_can_be_resolved_conditionally(): + sentinel = object() + A = typing.TypeVar("A") + B = typing.TypeVar("B") + + def resolve_type_var(thing): + assert thing in (A, B) + if thing == A: + return st.just(sentinel) + return NotImplemented + + with temp_registered(typing.TypeVar, resolve_type_var): + assert st.from_type(A).example() is sentinel + # We've re-defined the default TypeVar resolver, so there is no fallback. + # This causes the lookup to fail. + with pytest.raises(InvalidArgument): + st.from_type(B).example() + + def annotated_func(a: int, b: int = 2, *, c: int, d: int = 4): return a + b + c + d @@ -465,6 +486,24 @@ def test_resolves_NewType(): assert isinstance(from_type(uni).example(), (int, type(None))) +@pytest.mark.parametrize("is_handled", [True, False]) +def test_resolves_NewType_conditionally(is_handled): + sentinel = object() + typ = typing.NewType("T", int) + + def resolve_custom_strategy(thing): + assert thing is typ + if is_handled: + return st.just(sentinel) + return NotImplemented + + with temp_registered(typ, resolve_custom_strategy): + if is_handled: + assert st.from_type(typ).example() is sentinel + else: + assert isinstance(st.from_type(typ).example(), int) + + E = enum.Enum("E", "a b c") @@ -802,6 +841,58 @@ def test_supportsop_types_support_protocol(protocol, data): assert issubclass(type(value), protocol) +@pytest.mark.parametrize("restrict_custom_strategy", [True, False]) +def test_generic_aliases_can_be_conditionally_resolved_by_registered_function( + restrict_custom_strategy, +): + # Check that a custom strategy function may provide no strategy for a + # generic alias request like Container[T]. We test this under two scenarios: + # - where CustomContainer CANNOT be generated from requests for Container[T] + # (only for requests for exactly CustomContainer[T]) + # - where CustomContainer CAN be generated from requests for Container[T] + T = typing.TypeVar("T") + + @dataclass + class CustomContainer(typing.Container[T]): + content: T + + def __contains__(self, value: object) -> bool: + return self.content == value + + def get_custom_container_strategy(thing): + if restrict_custom_strategy and typing.get_origin(thing) != CustomContainer: + return NotImplemented + return st.builds( + CustomContainer, content=st.from_type(typing.get_args(thing)[0]) + ) + + with temp_registered(CustomContainer, get_custom_container_strategy): + + def is_custom_container_with_str(example): + return isinstance(example, CustomContainer) and isinstance( + example.content, str + ) + + def is_non_custom_container(example): + return isinstance(example, typing.Container) and not isinstance( + example, CustomContainer + ) + + assert_all_examples( + st.from_type(CustomContainer[str]), is_custom_container_with_str + ) + # If the strategy function is restricting, it doesn't return a strategy + # for requests for Container[...], so it's never generated. When not + # restricting, it is generated. + if restrict_custom_strategy: + assert_all_examples( + st.from_type(typing.Container[str]), is_non_custom_container + ) + else: + find_any(st.from_type(typing.Container[str]), is_custom_container_with_str) + find_any(st.from_type(typing.Container[str]), is_non_custom_container) + + @pytest.mark.parametrize( "protocol, typ", [ @@ -1053,3 +1144,31 @@ def f(x: int): msg = "@no_type_check decorator prevented Hypothesis from inferring a strategy" with pytest.raises(TypeError, match=msg): st.builds(f).example() + + +def test_custom_strategy_function_resolves_types_conditionally(): + sentinel = object() + + class A: + pass + + class B(A): + pass + + class C(A): + pass + + def resolve_custom_strategy_for_b(thing): + if thing == B: + return st.just(sentinel) + return NotImplemented + + with contextlib.ExitStack() as stack: + stack.enter_context(temp_registered(B, resolve_custom_strategy_for_b)) + stack.enter_context(temp_registered(C, st.builds(C))) + + # C's strategy can be used for A, but B's cannot because its function + # only returns a strategy for requests for exactly B. + assert_all_examples(st.from_type(A), lambda example: type(example) == C) + assert_all_examples(st.from_type(B), lambda example: example is sentinel) + assert_all_examples(st.from_type(C), lambda example: type(example) == C) diff --git a/hypothesis-python/tests/cover/test_type_lookup.py b/hypothesis-python/tests/cover/test_type_lookup.py index 1b913a1672..0071718e86 100644 --- a/hypothesis-python/tests/cover/test_type_lookup.py +++ b/hypothesis-python/tests/cover/test_type_lookup.py @@ -95,10 +95,19 @@ def test_lookup_keys_are_types(): assert "int" not in types._global_type_lookup -def test_lookup_values_are_strategies(): +@pytest.mark.parametrize( + "typ, not_a_strategy", + [ + (int, 42), # Values must be strategies + # Can't register NotImplemented directly, even though strategy functions + # can return it. + (int, NotImplemented), + ], +) +def test_lookup_values_are_strategies(typ, not_a_strategy): with pytest.raises(InvalidArgument): - st.register_type_strategy(int, 42) - assert 42 not in types._global_type_lookup.values() + st.register_type_strategy(typ, not_a_strategy) + assert not_a_strategy not in types._global_type_lookup.values() @pytest.mark.parametrize("typ", sorted(types_with_core_strat, key=str)) @@ -147,6 +156,24 @@ def test_custom_type_resolution_with_function_non_strategy(): st.from_type(ParentUnknownType).example() +@pytest.mark.parametrize("strategy_returned", [True, False]) +def test_conditional_type_resolution_with_function(strategy_returned): + sentinel = object() + + def resolve_strategy(thing): + assert thing == UnknownType + if strategy_returned: + return st.just(sentinel) + return NotImplemented + + with temp_registered(UnknownType, resolve_strategy): + if strategy_returned: + assert st.from_type(UnknownType).example() is sentinel + else: + with pytest.raises(ResolutionFailed): + st.from_type(UnknownType).example() + + def test_errors_if_generic_resolves_empty(): with temp_registered(UnknownType, lambda _: st.nothing()): fails_1 = st.from_type(UnknownType) From 8b7f35a722b283330797c384f5b118ee0d1c8ff3 Mon Sep 17 00:00:00 2001 From: CI on behalf of the Hypothesis team Date: Sun, 15 Oct 2023 00:07:51 +0000 Subject: [PATCH 3/8] Update pinned dependencies --- ...-05-26-exploring-voting-with-hypothesis.md | 2 +- brand/README.rst | 2 +- .../tests/array_api/test_arrays.py | 2 +- .../cover/test_type_lookup_forward_ref.py | 2 +- .../tests/numpy/test_gen_data.py | 2 +- requirements/coverage.txt | 10 +++---- requirements/fuzzing.txt | 20 ++++++++----- requirements/test.txt | 2 +- requirements/tools.txt | 30 +++++++++---------- tooling/src/hypothesistooling/__main__.py | 10 +++---- 10 files changed, 43 insertions(+), 39 deletions(-) diff --git a/HypothesisWorks.github.io/_posts/2016-05-26-exploring-voting-with-hypothesis.md b/HypothesisWorks.github.io/_posts/2016-05-26-exploring-voting-with-hypothesis.md index 472705c165..502f84a3f0 100644 --- a/HypothesisWorks.github.io/_posts/2016-05-26-exploring-voting-with-hypothesis.md +++ b/HypothesisWorks.github.io/_posts/2016-05-26-exploring-voting-with-hypothesis.md @@ -87,7 +87,7 @@ some point). If we have zero, that's a draw. If we have one, that's a victory. It seems pretty plausible that these would not produce the same answer -all the time (it would be surpising if they did!), but it's maybe not +all the time (it would be surprising if they did!), but it's maybe not obvious how you would go about constructing an example that shows it. Fortunately, we don't have to because Hypothesis can do it for us! diff --git a/brand/README.rst b/brand/README.rst index afbe05f0c2..2d2227dc26 100644 --- a/brand/README.rst +++ b/brand/README.rst @@ -38,7 +38,7 @@ Colour palette in GIMP format A `colour palette in GIMP format `__ (``.gpl``) is also provided with the intent of making it easier to produce graphics and documents which -re-use the colours in the Hypothesis Dragonfly logo by Libby Berrie. +reuse the colours in the Hypothesis Dragonfly logo by Libby Berrie. The ``hypothesis.gpl`` file should be copied or imported to the appropriate location on your filesystem. For example: diff --git a/hypothesis-python/tests/array_api/test_arrays.py b/hypothesis-python/tests/array_api/test_arrays.py index 08d6750774..756576a730 100644 --- a/hypothesis-python/tests/array_api/test_arrays.py +++ b/hypothesis-python/tests/array_api/test_arrays.py @@ -391,7 +391,7 @@ def test_generate_unique_arrays_without_fill(xp, xps): Covers the collision-related branches for fully dense unique arrays. Choosing 25 of 256 possible values means we're almost certain to see - colisions thanks to the birthday paradox, but finding unique values should + collisions thanks to the birthday paradox, but finding unique values should still be easy. """ skip_on_missing_unique_values(xp) diff --git a/hypothesis-python/tests/cover/test_type_lookup_forward_ref.py b/hypothesis-python/tests/cover/test_type_lookup_forward_ref.py index a682b6a4d2..46e55c92b6 100644 --- a/hypothesis-python/tests/cover/test_type_lookup_forward_ref.py +++ b/hypothesis-python/tests/cover/test_type_lookup_forward_ref.py @@ -54,7 +54,7 @@ def test_bound_correct_forward_ref(built): assert isinstance(built, int) -# Alises: +# Aliases: _Alias = TypeVar("_Alias ", bound="OurAlias") diff --git a/hypothesis-python/tests/numpy/test_gen_data.py b/hypothesis-python/tests/numpy/test_gen_data.py index 20dc5b56fe..56fe1c84f8 100644 --- a/hypothesis-python/tests/numpy/test_gen_data.py +++ b/hypothesis-python/tests/numpy/test_gen_data.py @@ -439,7 +439,7 @@ def test_unique_array_with_fill_can_use_all_elements(arr): @given(nps.arrays(dtype="uint8", shape=25, unique=True, fill=st.nothing())) def test_unique_array_without_fill(arr): # This test covers the collision-related branches for fully dense unique arrays. - # Choosing 25 of 256 possible elements means we're almost certain to see colisions + # Choosing 25 of 256 possible elements means we're almost certain to see collisions # thanks to the 'birthday paradox', but finding unique elemennts is still easy. assume(len(set(arr)) == arr.size) diff --git a/requirements/coverage.txt b/requirements/coverage.txt index 0e48279260..ec4f7679f3 100644 --- a/requirements/coverage.txt +++ b/requirements/coverage.txt @@ -14,7 +14,7 @@ click==8.1.7 # via # -r requirements/coverage.in # black -coverage==7.3.1 +coverage==7.3.2 # via -r requirements/coverage.in dpcontracts==0.6.0 # via -r requirements/coverage.in @@ -30,17 +30,17 @@ iniconfig==2.0.0 # via pytest lark==1.1.7 # via -r requirements/coverage.in -libcst==1.0.1 +libcst==1.1.0 # via -r requirements/coverage.in mypy-extensions==1.0.0 # via # black # typing-inspect -numpy==1.26.0 +numpy==1.26.1 # via # -r requirements/coverage.in # pandas -packaging==23.1 +packaging==23.2 # via # black # pytest @@ -50,7 +50,7 @@ pathspec==0.11.2 # via black pexpect==4.8.0 # via -r requirements/test.in -platformdirs==3.10.0 +platformdirs==3.11.0 # via black pluggy==1.3.0 # via pytest diff --git a/requirements/fuzzing.txt b/requirements/fuzzing.txt index a2bf39951b..9dc731bd83 100644 --- a/requirements/fuzzing.txt +++ b/requirements/fuzzing.txt @@ -27,11 +27,11 @@ click==8.1.7 # black # flask # hypothesis -coverage==7.3.1 +coverage==7.3.2 # via # -r requirements/coverage.in # hypofuzz -dash==2.13.0 +dash==2.14.0 # via hypofuzz dash-core-components==2.0.0 # via dash @@ -54,10 +54,12 @@ flask==2.2.5 # via dash hypofuzz==23.7.1 # via -r requirements/fuzzing.in -hypothesis[cli]==6.87.0 +hypothesis[cli]==6.87.4 # via hypofuzz idna==3.4 # via requests +importlib-metadata==6.8.0 + # via dash iniconfig==2.0.0 # via pytest itsdangerous==2.1.2 @@ -66,7 +68,7 @@ jinja2==3.1.2 # via flask lark==1.1.7 # via -r requirements/coverage.in -libcst==1.0.1 +libcst==1.1.0 # via # -r requirements/coverage.in # hypofuzz @@ -84,11 +86,11 @@ mypy-extensions==1.0.0 # typing-inspect nest-asyncio==1.5.8 # via dash -numpy==1.26.0 +numpy==1.26.1 # via # -r requirements/coverage.in # pandas -packaging==23.1 +packaging==23.2 # via # black # plotly @@ -101,7 +103,7 @@ pathspec==0.11.2 # via black pexpect==4.8.0 # via -r requirements/test.in -platformdirs==3.10.0 +platformdirs==3.11.0 # via black plotly==5.17.0 # via dash @@ -166,12 +168,14 @@ typing-inspect==0.9.0 # via libcst tzdata==2023.3 # via pandas -urllib3==2.0.5 +urllib3==2.0.6 # via requests werkzeug==2.2.3 # via # dash # flask +zipp==3.17.0 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: setuptools==68.2.2 diff --git a/requirements/test.txt b/requirements/test.txt index 4a97655b8b..3ce63bcc4c 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -14,7 +14,7 @@ execnet==2.0.2 # via pytest-xdist iniconfig==2.0.0 # via pytest -packaging==23.1 +packaging==23.2 # via pytest pexpect==4.8.0 # via -r requirements/test.in diff --git a/requirements/tools.txt b/requirements/tools.txt index 1da4da7739..9e4eae4c83 100644 --- a/requirements/tools.txt +++ b/requirements/tools.txt @@ -14,7 +14,7 @@ attrs==23.1.0 # via hypothesis (hypothesis-python/setup.py) autoflake==2.2.1 # via shed -babel==2.12.1 +babel==2.13.0 # via sphinx backcall==0.2.0 # via ipython @@ -38,13 +38,13 @@ click==8.1.7 # via # black # pip-tools -codespell==2.2.5 +codespell==2.2.6 # via -r requirements/tools.in colorama==0.4.6 # via tox com2ann==0.3.0 # via shed -coverage==7.3.1 +coverage==7.3.2 # via -r requirements/tools.in cryptography==41.0.4 # via @@ -55,7 +55,7 @@ decorator==5.1.1 # via ipython distlib==0.3.7 # via virtualenv -django==4.2.5 +django==4.2.6 # via -r requirements/tools.in docutils==0.18.1 # via @@ -86,13 +86,13 @@ importlib-metadata==6.8.0 # twine iniconfig==2.0.0 # via pytest -ipython==8.16.0 +ipython==8.16.1 # via -r requirements/tools.in isort==5.12.0 # via shed jaraco-classes==3.3.0 # via keyring -jedi==0.19.0 +jedi==0.19.1 # via ipython jeepney==0.8.0 # via @@ -104,7 +104,7 @@ keyring==24.2.0 # via twine lark==1.1.7 # via -r requirements/tools.in -libcst==1.0.1 +libcst==1.1.0 # via # -r requirements/tools.in # shed @@ -118,7 +118,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==10.1.0 # via jaraco-classes -mypy==1.5.1 +mypy==1.6.0 # via -r requirements/tools.in mypy-extensions==1.0.0 # via @@ -129,7 +129,7 @@ nh3==0.2.14 # via readme-renderer nodeenv==1.8.0 # via pyright -packaging==23.1 +packaging==23.2 # via # black # build @@ -149,7 +149,7 @@ pip-tools==7.3.0 # via -r requirements/tools.in pkginfo==1.9.6 # via twine -platformdirs==3.10.0 +platformdirs==3.11.0 # via # black # tox @@ -178,13 +178,13 @@ pyproject-api==1.6.1 # via tox pyproject-hooks==1.0.0 # via build -pyright==1.1.329 +pyright==1.1.331 # via -r requirements/tools.in pytest==7.4.2 # via -r requirements/tools.in python-dateutil==2.8.2 # via -r requirements/tools.in -pyupgrade==3.13.0 +pyupgrade==3.15.0 # via shed pyyaml==6.0.1 # via libcst @@ -204,7 +204,7 @@ rfc3986==2.0.0 # via twine rich==13.6.0 # via twine -ruff==0.0.291 +ruff==0.0.292 # via -r requirements/tools.in secretstorage==3.3.3 # via keyring @@ -275,7 +275,7 @@ tomli==2.0.1 # tox tox==4.11.3 # via -r requirements/tools.in -traitlets==5.10.1 +traitlets==5.11.2 # via # ipython # matplotlib-inline @@ -301,7 +301,7 @@ typing-extensions==4.8.0 # typing-inspect typing-inspect==0.9.0 # via libcst -urllib3==2.0.5 +urllib3==2.0.6 # via # requests # twine diff --git a/tooling/src/hypothesistooling/__main__.py b/tooling/src/hypothesistooling/__main__.py index 348987aabd..84e6caf818 100644 --- a/tooling/src/hypothesistooling/__main__.py +++ b/tooling/src/hypothesistooling/__main__.py @@ -392,12 +392,12 @@ def run_tox(task, version, *args): "3.8": "3.8.18", "3.9": "3.9.18", "3.10": "3.10.13", - "3.11": "3.11.5", - "3.12": "3.12-dev", - "3.13": "3.13-dev", + "3.11": "3.11.6", + "3.12": "3.12.0", + "3.13": "3.13.0a1", "pypy3.8": "pypy3.8-7.3.11", - "pypy3.9": "pypy3.9-7.3.12", - "pypy3.10": "pypy3.10-7.3.12", + "pypy3.9": "pypy3.9-7.3.13", + "pypy3.10": "pypy3.10-7.3.13", } ci_version = "3.10" # Keep this in sync with GH Actions main.yml and .readthedocs.yml From 5bba33ad5296ad3cf3293b303164d076d8c8c794 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sat, 14 Oct 2023 09:11:40 -0700 Subject: [PATCH 4/8] Fix typo --- hypothesis-python/examples/test_basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/examples/test_basic.py b/hypothesis-python/examples/test_basic.py index 3e3bc762d5..45d4a206bd 100644 --- a/hypothesis-python/examples/test_basic.py +++ b/hypothesis-python/examples/test_basic.py @@ -20,7 +20,7 @@ def get_discount_price(self, discount_percentage: float): return self.price * (discount_percentage / 100) -# The @given decorater generates examples for us! +# The @given decorator generates examples for us! @given( price=st.floats(min_value=0, allow_nan=False, allow_infinity=False), discount_percentage=st.floats( From 0621e1b15674cf96ccedf7993da3ef8a6ee87562 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 15 Oct 2023 21:59:47 +0100 Subject: [PATCH 5/8] Fix nonsensical type in test --- .../tests/typing_extensions/test_backported_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/tests/typing_extensions/test_backported_types.py b/hypothesis-python/tests/typing_extensions/test_backported_types.py index ddfdd2e155..b1031e39df 100644 --- a/hypothesis-python/tests/typing_extensions/test_backported_types.py +++ b/hypothesis-python/tests/typing_extensions/test_backported_types.py @@ -71,7 +71,7 @@ def test_typing_extensions_Type_int(): assert from_type(Type[int]).example() is int -@given(from_type(Type[Union[str, list]])) +@given(from_type(Union[Type[str], Type[list]])) def test_typing_extensions_Type_Union(ex): assert ex in (str, list) From 8ad351b89da189993d3471b26f0a9b0054b91895 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 15 Oct 2023 22:00:58 +0100 Subject: [PATCH 6/8] Mypy now reveals correct type --- whole-repo-tests/test_mypy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/whole-repo-tests/test_mypy.py b/whole-repo-tests/test_mypy.py index f75f04eb67..0bf11e7399 100644 --- a/whole-repo-tests/test_mypy.py +++ b/whole-repo-tests/test_mypy.py @@ -115,7 +115,7 @@ def convert_lines(): "one_of(integers(), text(), none(), binary(), builds(list), builds(dict))", "Any", ), - ("tuples()", "tuple[]"), # Should be `tuple[()]`, but this is what mypy prints + ("tuples()", "tuple[()]"), ("tuples(integers())", "tuple[int]"), ("tuples(integers(), text())", "tuple[int, str]"), ( From aad4098a1e0411e9563c440720d7ae83101981e5 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 15 Oct 2023 13:11:13 -0400 Subject: [PATCH 7/8] remove stale target tests --- .../tests/cover/test_targeting.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/hypothesis-python/tests/cover/test_targeting.py b/hypothesis-python/tests/cover/test_targeting.py index c645643f9d..a80bab561c 100644 --- a/hypothesis-python/tests/cover/test_targeting.py +++ b/hypothesis-python/tests/cover/test_targeting.py @@ -102,21 +102,3 @@ def test_cannot_target_default_label_twice(_): target(0.0) with pytest.raises(InvalidArgument): target(1.0) - - -@given(st.lists(st.integers()), st.none()) -def test_targeting_with_following_empty(ls, n): - # This exercises some logic in the optimiser that prevents it from trying - # to mutate empty examples at the end of the test case. - target(float(len(ls))) - - -@given( - st.tuples( - *([st.none()] * 10 + [st.integers()] + [st.none()] * 10 + [st.integers()]) - ) -) -def test_targeting_with_many_empty(_): - # This exercises some logic in the optimiser that prevents it from trying - # to mutate empty examples in the middle of the test case. - target(1.0) From b4ddfd13e60f7813799acf82f32b9fdc15ba4767 Mon Sep 17 00:00:00 2001 From: CI on behalf of the Hypothesis team Date: Sun, 15 Oct 2023 22:23:14 +0000 Subject: [PATCH 8/8] Bump hypothesis-python version to 6.88.0 and update changelog [skip ci] --- hypothesis-python/RELEASE.rst | 5 ----- hypothesis-python/docs/changes.rst | 10 ++++++++++ hypothesis-python/src/hypothesis/version.py | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) delete mode 100644 hypothesis-python/RELEASE.rst diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst deleted file mode 100644 index 819325d6b9..0000000000 --- a/hypothesis-python/RELEASE.rst +++ /dev/null @@ -1,5 +0,0 @@ -RELEASE_TYPE: minor - -This release allows strategy-generating functions registered with -:func:`~hypothesis.strategies.register_type_strategy` to conditionally not -return a strategy, by returning :data:`NotImplemented` (:issue:`3767`). diff --git a/hypothesis-python/docs/changes.rst b/hypothesis-python/docs/changes.rst index 913670e3f8..6ffba2c9d4 100644 --- a/hypothesis-python/docs/changes.rst +++ b/hypothesis-python/docs/changes.rst @@ -18,6 +18,16 @@ Hypothesis 6.x .. include:: ../RELEASE.rst +.. _v6.88.0: + +------------------- +6.88.0 - 2023-10-15 +------------------- + +This release allows strategy-generating functions registered with +:func:`~hypothesis.strategies.register_type_strategy` to conditionally not +return a strategy, by returning :data:`NotImplemented` (:issue:`3767`). + .. _v6.87.4: ------------------- diff --git a/hypothesis-python/src/hypothesis/version.py b/hypothesis-python/src/hypothesis/version.py index 509f373ec3..4f14552eea 100644 --- a/hypothesis-python/src/hypothesis/version.py +++ b/hypothesis-python/src/hypothesis/version.py @@ -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, 87, 4) +__version_info__ = (6, 88, 0) __version__ = ".".join(map(str, __version_info__))