Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

prevent tuple subclasses from being interpreted as generic #3768

Merged
merged 10 commits into from
Oct 15, 2023
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,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
3 changes: 3 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
RELEASE_TYPE: patch

This patch improves `st.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])`.
tybug marked this conversation as resolved.
Show resolved Hide resolved
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
15 changes: 15 additions & 0 deletions hypothesis-python/tests/cover/test_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1053,3 +1053,18 @@ 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()


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)):

@given(st.from_type(typing.Sequence[int]))
def f(val):
assert not isinstance(val, tuple)

f()