Skip to content

Commit 0d0ff88

Browse files
authored
Merge branch 'master' into namedtuple-generic
2 parents 7c7375c + b4ddfd1 commit 0d0ff88

22 files changed

Lines changed: 248 additions & 76 deletions

File tree

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ their individual contributions.
7373
* `Gregory Petrosyan <https://github.com/flyingmutant>`_
7474
* `Grzegorz Zieba <https://github.com/gzaxel>`_ (g.zieba@erax.pl)
7575
* `Grigorios Giannakopoulos <https://github.com/grigoriosgiann>`_
76+
* `Hal Blackburn <https://github.com/h4l>`_
7677
* `Hugo van Kemenade <https://github.com/hugovk>`_
7778
* `Humberto Rocha <https://github.com/humrochagf>`_
7879
* `Ilya Lebedev <https://github.com/melevir>`_ (melevir@gmail.com)

HypothesisWorks.github.io/_posts/2016-05-26-exploring-voting-with-hypothesis.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ some point). If we have zero, that's a draw. If we have one, that's a
8787
victory.
8888

8989
It seems pretty plausible that these would not produce the same answer
90-
all the time (it would be surpising if they did!), but it's maybe not
90+
all the time (it would be surprising if they did!), but it's maybe not
9191
obvious how you would go about constructing an example that shows it.
9292

9393
Fortunately, we don't have to because Hypothesis can do it for us!

brand/README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Colour palette in GIMP format
3838

3939
A `colour palette in GIMP format <hypothesis.gpl>`__ (``.gpl``) is also provided
4040
with the intent of making it easier to produce graphics and documents which
41-
re-use the colours in the Hypothesis Dragonfly logo by Libby Berrie.
41+
reuse the colours in the Hypothesis Dragonfly logo by Libby Berrie.
4242

4343
The ``hypothesis.gpl`` file should be copied or imported to the appropriate
4444
location on your filesystem. For example:

hypothesis-python/docs/changes.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ Hypothesis 6.x
1818

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

21+
.. _v6.88.0:
22+
23+
-------------------
24+
6.88.0 - 2023-10-15
25+
-------------------
26+
27+
This release allows strategy-generating functions registered with
28+
:func:`~hypothesis.strategies.register_type_strategy` to conditionally not
29+
return a strategy, by returning :data:`NotImplemented` (:issue:`3767`).
30+
2131
.. _v6.87.4:
2232

2333
-------------------

hypothesis-python/examples/test_basic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def get_discount_price(self, discount_percentage: float):
2020
return self.price * (discount_percentage / 100)
2121

2222

23-
# The @given decorater generates examples for us!
23+
# The @given decorator generates examples for us!
2424
@given(
2525
price=st.floats(min_value=0, allow_nan=False, allow_infinity=False),
2626
discount_percentage=st.floats(

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

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1235,6 +1235,8 @@ def as_strategy(strat_or_callable, thing):
12351235
strategy = strat_or_callable(thing)
12361236
else:
12371237
strategy = strat_or_callable
1238+
if strategy is NotImplemented:
1239+
return NotImplemented
12381240
if not isinstance(strategy, SearchStrategy):
12391241
raise ResolutionFailed(
12401242
f"Error: {thing} was registered for {nicerepr(strat_or_callable)}, "
@@ -1277,7 +1279,9 @@ def from_type_guarded(thing):
12771279
# Check if we have an explicitly registered strategy for this thing,
12781280
# resolve it so, and otherwise resolve as for the base type.
12791281
if thing in types._global_type_lookup:
1280-
return as_strategy(types._global_type_lookup[thing], thing)
1282+
strategy = as_strategy(types._global_type_lookup[thing], thing)
1283+
if strategy is not NotImplemented:
1284+
return strategy
12811285
return _from_type(thing.__supertype__)
12821286
# Unions are not instances of `type` - but we still want to resolve them!
12831287
if types.is_a_union(thing):
@@ -1287,7 +1291,9 @@ def from_type_guarded(thing):
12871291
# They are represented as instances like `~T` when they come here.
12881292
# We need to work with their type instead.
12891293
if isinstance(thing, TypeVar) and type(thing) in types._global_type_lookup:
1290-
return as_strategy(types._global_type_lookup[type(thing)], thing)
1294+
strategy = as_strategy(types._global_type_lookup[type(thing)], thing)
1295+
if strategy is not NotImplemented:
1296+
return strategy
12911297
if not types.is_a_type(thing):
12921298
if isinstance(thing, str):
12931299
# See https://github.com/HypothesisWorks/hypothesis/issues/3016
@@ -1312,7 +1318,9 @@ def from_type_guarded(thing):
13121318
# convert empty results into an explicit error.
13131319
try:
13141320
if thing in types._global_type_lookup:
1315-
return as_strategy(types._global_type_lookup[thing], thing)
1321+
strategy = as_strategy(types._global_type_lookup[thing], thing)
1322+
if strategy is not NotImplemented:
1323+
return strategy
13161324
except TypeError: # pragma: no cover
13171325
# This is due to a bizarre divergence in behaviour under Python 3.9.0:
13181326
# typing.Callable[[], foo] has __args__ = (foo,) but collections.abc.Callable
@@ -1372,11 +1380,16 @@ def from_type_guarded(thing):
13721380
# type. For example, `Number -> integers() | floats()`, but bools() is
13731381
# not included because bool is a subclass of int as well as Number.
13741382
strategies = [
1375-
as_strategy(v, thing)
1376-
for k, v in sorted(types._global_type_lookup.items(), key=repr)
1377-
if isinstance(k, type)
1378-
and issubclass(k, thing)
1379-
and sum(types.try_issubclass(k, typ) for typ in types._global_type_lookup) == 1
1383+
s
1384+
for s in (
1385+
as_strategy(v, thing)
1386+
for k, v in sorted(types._global_type_lookup.items(), key=repr)
1387+
if isinstance(k, type)
1388+
and issubclass(k, thing)
1389+
and sum(types.try_issubclass(k, typ) for typ in types._global_type_lookup)
1390+
== 1
1391+
)
1392+
if s is not NotImplemented
13801393
]
13811394
if any(not s.is_empty for s in strategies):
13821395
return one_of(strategies)
@@ -2142,7 +2155,10 @@ def register_type_strategy(
21422155
for an argument with a default value.
21432156
21442157
``strategy`` may be a search strategy, or a function that takes a type and
2145-
returns a strategy (useful for generic types).
2158+
returns a strategy (useful for generic types). The function may return
2159+
:data:`NotImplemented` to conditionally not provide a strategy for the type
2160+
(the type will still be resolved by other methods, if possible, as if the
2161+
function was not registered).
21462162
21472163
Note that you may not register a parametrised generic type (such as
21482164
``MyCollection[int]``) directly, because the resolution logic does not

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -451,9 +451,13 @@ def from_typing_type(thing):
451451
mapping.pop(t)
452452
# Sort strategies according to our type-sorting heuristic for stable output
453453
strategies = [
454-
v if isinstance(v, st.SearchStrategy) else v(thing)
455-
for k, v in sorted(mapping.items(), key=lambda kv: type_sorting_key(kv[0]))
456-
if sum(try_issubclass(k, T) for T in mapping) == 1
454+
s
455+
for s in (
456+
v if isinstance(v, st.SearchStrategy) else v(thing)
457+
for k, v in sorted(mapping.items(), key=lambda kv: type_sorting_key(kv[0]))
458+
if sum(try_issubclass(k, T) for T in mapping) == 1
459+
)
460+
if s != NotImplemented
457461
]
458462
empty = ", ".join(repr(s) for s in strategies if s.is_empty)
459463
if empty or not strategies:
@@ -491,6 +495,14 @@ def _networks(bits):
491495
# As a general rule, we try to limit this to scalars because from_type()
492496
# would have to decide on arbitrary collection elements, and we'd rather
493497
# not (with typing module generic types and some builtins as exceptions).
498+
#
499+
# Strategy Callables may return NotImplemented, which should be treated in the
500+
# same way as if the type was not registered.
501+
#
502+
# Note that NotImplemented cannot be typed in Python 3.8 because there's no type
503+
# exposed for it, and NotImplemented itself is typed as Any so that it can be
504+
# returned without being listed in a function signature:
505+
# https://github.com/python/mypy/issues/6710#issuecomment-485580032
494506
_global_type_lookup: typing.Dict[
495507
type, typing.Union[st.SearchStrategy, typing.Callable[[type], st.SearchStrategy]]
496508
] = {

hypothesis-python/src/hypothesis/version.py

Lines changed: 1 addition & 1 deletion
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, 87, 4)
11+
__version_info__ = (6, 88, 0)
1212
__version__ = ".".join(map(str, __version_info__))

hypothesis-python/tests/array_api/test_arrays.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ def test_generate_unique_arrays_without_fill(xp, xps):
391391
392392
Covers the collision-related branches for fully dense unique arrays.
393393
Choosing 25 of 256 possible values means we're almost certain to see
394-
colisions thanks to the birthday paradox, but finding unique values should
394+
collisions thanks to the birthday paradox, but finding unique values should
395395
still be easy.
396396
"""
397397
skip_on_missing_unique_values(xp)

hypothesis-python/tests/cover/test_lookup.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import abc
1212
import builtins
1313
import collections
14+
import contextlib
1415
import datetime
1516
import enum
1617
import inspect
@@ -21,6 +22,7 @@
2122
import sys
2223
import typing
2324
import warnings
25+
from dataclasses import dataclass
2426
from inspect import signature
2527
from numbers import Real
2628

@@ -376,6 +378,25 @@ def test_typevars_can_be_redefine_with_factory():
376378
assert_all_examples(st.from_type(A), lambda obj: obj == "A")
377379

378380

381+
def test_typevars_can_be_resolved_conditionally():
382+
sentinel = object()
383+
A = typing.TypeVar("A")
384+
B = typing.TypeVar("B")
385+
386+
def resolve_type_var(thing):
387+
assert thing in (A, B)
388+
if thing == A:
389+
return st.just(sentinel)
390+
return NotImplemented
391+
392+
with temp_registered(typing.TypeVar, resolve_type_var):
393+
assert st.from_type(A).example() is sentinel
394+
# We've re-defined the default TypeVar resolver, so there is no fallback.
395+
# This causes the lookup to fail.
396+
with pytest.raises(InvalidArgument):
397+
st.from_type(B).example()
398+
399+
379400
def annotated_func(a: int, b: int = 2, *, c: int, d: int = 4):
380401
return a + b + c + d
381402

@@ -470,6 +491,24 @@ def test_resolves_NewType():
470491
assert isinstance(from_type(uni).example(), (int, type(None)))
471492

472493

494+
@pytest.mark.parametrize("is_handled", [True, False])
495+
def test_resolves_NewType_conditionally(is_handled):
496+
sentinel = object()
497+
typ = typing.NewType("T", int)
498+
499+
def resolve_custom_strategy(thing):
500+
assert thing is typ
501+
if is_handled:
502+
return st.just(sentinel)
503+
return NotImplemented
504+
505+
with temp_registered(typ, resolve_custom_strategy):
506+
if is_handled:
507+
assert st.from_type(typ).example() is sentinel
508+
else:
509+
assert isinstance(st.from_type(typ).example(), int)
510+
511+
473512
E = enum.Enum("E", "a b c")
474513

475514

@@ -807,6 +846,58 @@ def test_supportsop_types_support_protocol(protocol, data):
807846
assert issubclass(type(value), protocol)
808847

809848

849+
@pytest.mark.parametrize("restrict_custom_strategy", [True, False])
850+
def test_generic_aliases_can_be_conditionally_resolved_by_registered_function(
851+
restrict_custom_strategy,
852+
):
853+
# Check that a custom strategy function may provide no strategy for a
854+
# generic alias request like Container[T]. We test this under two scenarios:
855+
# - where CustomContainer CANNOT be generated from requests for Container[T]
856+
# (only for requests for exactly CustomContainer[T])
857+
# - where CustomContainer CAN be generated from requests for Container[T]
858+
T = typing.TypeVar("T")
859+
860+
@dataclass
861+
class CustomContainer(typing.Container[T]):
862+
content: T
863+
864+
def __contains__(self, value: object) -> bool:
865+
return self.content == value
866+
867+
def get_custom_container_strategy(thing):
868+
if restrict_custom_strategy and typing.get_origin(thing) != CustomContainer:
869+
return NotImplemented
870+
return st.builds(
871+
CustomContainer, content=st.from_type(typing.get_args(thing)[0])
872+
)
873+
874+
with temp_registered(CustomContainer, get_custom_container_strategy):
875+
876+
def is_custom_container_with_str(example):
877+
return isinstance(example, CustomContainer) and isinstance(
878+
example.content, str
879+
)
880+
881+
def is_non_custom_container(example):
882+
return isinstance(example, typing.Container) and not isinstance(
883+
example, CustomContainer
884+
)
885+
886+
assert_all_examples(
887+
st.from_type(CustomContainer[str]), is_custom_container_with_str
888+
)
889+
# If the strategy function is restricting, it doesn't return a strategy
890+
# for requests for Container[...], so it's never generated. When not
891+
# restricting, it is generated.
892+
if restrict_custom_strategy:
893+
assert_all_examples(
894+
st.from_type(typing.Container[str]), is_non_custom_container
895+
)
896+
else:
897+
find_any(st.from_type(typing.Container[str]), is_custom_container_with_str)
898+
find_any(st.from_type(typing.Container[str]), is_non_custom_container)
899+
900+
810901
@pytest.mark.parametrize(
811902
"protocol, typ",
812903
[
@@ -1069,3 +1160,31 @@ def test_tuple_subclasses_not_generic_sequences():
10691160
with temp_registered(TupleSubtype, st.builds(TupleSubtype)):
10701161
s = st.from_type(typing.Sequence[int])
10711162
assert_no_examples(s, lambda x: isinstance(x, tuple))
1163+
1164+
1165+
def test_custom_strategy_function_resolves_types_conditionally():
1166+
sentinel = object()
1167+
1168+
class A:
1169+
pass
1170+
1171+
class B(A):
1172+
pass
1173+
1174+
class C(A):
1175+
pass
1176+
1177+
def resolve_custom_strategy_for_b(thing):
1178+
if thing == B:
1179+
return st.just(sentinel)
1180+
return NotImplemented
1181+
1182+
with contextlib.ExitStack() as stack:
1183+
stack.enter_context(temp_registered(B, resolve_custom_strategy_for_b))
1184+
stack.enter_context(temp_registered(C, st.builds(C)))
1185+
1186+
# C's strategy can be used for A, but B's cannot because its function
1187+
# only returns a strategy for requests for exactly B.
1188+
assert_all_examples(st.from_type(A), lambda example: type(example) == C)
1189+
assert_all_examples(st.from_type(B), lambda example: example is sentinel)
1190+
assert_all_examples(st.from_type(C), lambda example: type(example) == C)

0 commit comments

Comments
 (0)