Skip to content

Commit

Permalink
Merge pull request #3768 from tybug/namedtuple-generic
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD committed Oct 15, 2023
2 parents b4ddfd1 + 0d0ff88 commit a331d3e
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 5 deletions.
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ their individual contributions.
* `Lampros Mountrakis <https://www.github.com/lmount>`_
* `Lea Provenzano <https://github.com/leaprovenzano>`_
* `Lee Begg <https://www.github.com/llnz2>`_
* `Liam DeVoe <https://github.com/tybug>`_
* `Libor Martínek <https://github.com/bibajz>`_
* `Lisa Goeller <https://www.github.com/lgoeller>`_
* `Louis Taylor <https://github.com/kragniz>`_
Expand Down
5 changes: 5 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
RELEASE_TYPE: patch

This patch improves :func:`~hypothesis.strategies.register_type_strategy` when used with ``tuple`` subclasses,
by preventing them from being interpreted as generic and provided to strategies like ``st.from_type(Sequence[int])``
(:issue:`3767`).
15 changes: 11 additions & 4 deletions hypothesis-python/src/hypothesis/strategies/_internal/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,10 +391,16 @@ def from_typing_type(thing):
for k, v in _global_type_lookup.items()
if is_generic_type(k) and try_issubclass(k, thing)
}
# Drop some unusual cases for simplicity
for weird in (tuple, getattr(os, "_Environ", None)):
if len(mapping) > 1:
mapping.pop(weird, None)
# Drop some unusual cases for simplicity, including tuples or its
# subclasses (e.g. namedtuple)
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)]
if len(mapping) > len(tuple_types):
for tuple_type in tuple_types:
mapping.pop(tuple_type)

# After we drop Python 3.8 and can rely on having generic builtin types, we'll
# be able to simplify this logic by dropping the typing-module handling.
if {dict, set, typing.Dict, typing.Set}.intersection(mapping):
Expand All @@ -407,6 +413,7 @@ def from_typing_type(thing):
# the ghostwriter than it's worth, via undefined names in the repr.
mapping.pop(collections.deque, None)
mapping.pop(typing.Deque, None)

if len(mapping) > 1:
# issubclass treats bytestring as a kind of sequence, which it is,
# but treating it as such breaks everything else when it is presumed
Expand Down
18 changes: 17 additions & 1 deletion hypothesis-python/tests/cover/test_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@
from hypothesis.strategies import from_type
from hypothesis.strategies._internal import types

from tests.common.debug import assert_all_examples, find_any, minimal
from tests.common.debug import (
assert_all_examples,
assert_no_examples,
find_any,
minimal,
)
from tests.common.utils import fails_with, temp_registered

sentinel = object()
Expand Down Expand Up @@ -1146,6 +1151,17 @@ def f(x: int):
st.builds(f).example()


class TupleSubtype(tuple):
pass


def test_tuple_subclasses_not_generic_sequences():
# see https://github.com/HypothesisWorks/hypothesis/issues/3767.
with temp_registered(TupleSubtype, st.builds(TupleSubtype)):
s = st.from_type(typing.Sequence[int])
assert_no_examples(s, lambda x: isinstance(x, tuple))


def test_custom_strategy_function_resolves_types_conditionally():
sentinel = object()

Expand Down
24 changes: 24 additions & 0 deletions hypothesis-python/tests/cover/test_lookup_py39.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,27 @@ def test_can_register_builtin_list():
st.from_type(list[int]),
lambda ls: len(ls) <= 2 and {type(x) for x in ls}.issubset({int}),
)


T = typing.TypeVar("T")


@typing.runtime_checkable
class Fooable(typing.Protocol[T]):
def foo(self):
...


class FooableConcrete(tuple):
def foo(self):
pass


def test_only_tuple_subclasses_in_typing_type():
# A generic typing type (such as Fooable) whose only concrete
# instantiations are tuples should still generate tuples. This is in
# contrast to test_tuple_subclasses_not_generic_sequences, which discards
# tuples if there are any alternatives.
with temp_registered(FooableConcrete, st.builds(FooableConcrete)):
s = st.from_type(Fooable[int])
assert_all_examples(s, lambda x: type(x) is FooableConcrete)

0 comments on commit a331d3e

Please sign in to comment.