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
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
5 changes: 5 additions & 0 deletions hypothesis-python/RELEASE.rst
@@ -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
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
42 changes: 41 additions & 1 deletion hypothesis-python/tests/cover/test_lookup.py
Expand Up @@ -34,7 +34,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 @@ -1053,3 +1058,38 @@ 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)):
s = st.from_type(typing.Sequence[int])
assert_no_examples(s, lambda x: isinstance(x, tuple))


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():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd just skip this on Python 3.8, since it's using later typing features.

If we could also get a subclassing-based test working that'd make me more confident that future changes won't accidentally break something, but I'd be happy to merge without that too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I slept on it, but I'm drawing a blank on a subclassing-based test (as opposed to using Protocol) where the only valid types are tuple types. I'd be happy to add it if someone else can come up with one, though.

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