Skip to content

Commit

Permalink
Merge pull request #3785 from Zac-HD/tuples-cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD authored Nov 5, 2023
2 parents 5401530 + 345207a commit 5afa502
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 17 deletions.
7 changes: 7 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
RELEASE_TYPE: patch

This patch refactors ``from_type(typing.Tuple)``, allowing
:func:`~hypothesis.strategies.register_type_strategy` to take effect
for tuples instead of being silently ignored (:issue:`3750`).

Thanks to Nick Collins for reporting and extensive work on this issue.
33 changes: 16 additions & 17 deletions hypothesis-python/src/hypothesis/strategies/_internal/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,27 +324,12 @@ def _try_import_forward_ref(thing, bound): # pragma: no cover


def from_typing_type(thing):
# We start with special-case support for Tuple, which isn't actually a generic
# type; then Final, Literal, and Annotated since they don't support `isinstance`.
# We start with Final, Literal, and Annotated since they don't support `isinstance`.
#
# We then explicitly error on non-Generic types, which don't carry enough
# information to sensibly resolve to strategies at runtime.
# Finally, we run a variation of the subclass lookup in `st.from_type`
# among generic types in the lookup.
if get_origin(thing) == tuple or isinstance(
thing, getattr(typing, "TupleMeta", ())
):
elem_types = getattr(thing, "__tuple_params__", None) or ()
elem_types += getattr(thing, "__args__", None) or ()
if (
getattr(thing, "__tuple_use_ellipsis__", False)
or len(elem_types) == 2
and elem_types[-1] is Ellipsis
):
return st.lists(st.from_type(elem_types[0])).map(tuple)
elif len(elem_types) == 1 and elem_types[0] == ():
return st.tuples() # Empty tuple; see issue #1583
return st.tuples(*map(st.from_type, elem_types))
if get_origin(thing) == typing.Final:
return st.one_of([st.from_type(t) for t in thing.__args__])
if is_typing_literal(thing):
Expand Down Expand Up @@ -396,7 +381,11 @@ def from_typing_type(thing):
if len(mapping) > 1:
_Environ = getattr(os, "_Environ", None)
mapping.pop(_Environ, None)
tuple_types = [t for t in mapping if isinstance(t, type) and issubclass(t, tuple)]
tuple_types = [
t
for t in mapping
if (isinstance(t, type) and issubclass(t, tuple)) or t is typing.Tuple
]
if len(mapping) > len(tuple_types):
for tuple_type in tuple_types:
mapping.pop(tuple_type)
Expand Down Expand Up @@ -760,6 +749,16 @@ def resolve_List(thing):
return st.lists(st.from_type(thing.__args__[0]))


@register(typing.Tuple, st.builds(tuple))
def resolve_Tuple(thing):
elem_types = getattr(thing, "__args__", None) or ()
if len(elem_types) == 2 and elem_types[-1] is Ellipsis:
return st.lists(st.from_type(elem_types[0])).map(tuple)
elif len(elem_types) == 1 and elem_types[0] == ():
return st.tuples() # Empty tuple; see issue #1583
return st.tuples(*map(st.from_type, elem_types))


def _can_hash(val):
try:
hash(val)
Expand Down
8 changes: 8 additions & 0 deletions hypothesis-python/tests/cover/test_lookup_py39.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,11 @@ def test_only_tuple_subclasses_in_typing_type():
with temp_registered(FooableConcrete, st.builds(FooableConcrete)):
s = st.from_type(Fooable[int])
assert_all_examples(s, lambda x: type(x) is FooableConcrete)


def test_lookup_registered_tuple():
sentinel = object()
typ = tuple[int]
with temp_registered(tuple, st.just(sentinel)):
assert st.from_type(typ).example() is sentinel
assert st.from_type(typ).example() is not sentinel

0 comments on commit 5afa502

Please sign in to comment.