Skip to content

Commit

Permalink
Make from_type work on Python 3.7
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD committed Jul 23, 2018
1 parent e0756a2 commit 6980b13
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 27 deletions.
4 changes: 0 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,6 @@ script:

matrix:
fast_finish: true
allow_failures:
- env: TASK=check-py37
sudo: required
dist: xenial

notifications:
email:
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 ensures that Hypothesis fully supports Python 3.7, by
upgrading :func:`~hypothesis.strategies.from_type` (:issue:`1264`)
and fixing some minor issues in our test suite (:issue:`1148`).
22 changes: 21 additions & 1 deletion hypothesis-python/src/hypothesis/internal/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,13 @@
from ordereddict import OrderedDict # type: ignore
from counter import Counter # type: ignore

try:
from collections import abc
except ImportError:
import collections as abc # type: ignore

if False:
from typing import Type # noqa
from typing import Type, Tuple # noqa


PY2 = sys.version_info[0] == 2
Expand Down Expand Up @@ -273,6 +278,21 @@ def qualname(f):
return f.__name__


try:
import typing
except ImportError:
typing_root_type = () # type: Tuple[type, ...]
ForwardRef = None
else:
if hasattr(typing, '_Final'): # new in Python 3.7
typing_root_type = (
typing._Final, typing._GenericAlias) # type: ignore
ForwardRef = typing.ForwardRef # type: ignore
else:
typing_root_type = (typing.TypingMeta, typing.TypeVar) # type: ignore
ForwardRef = typing._ForwardRef # type: ignore


if PY2:
FullArgSpec = namedtuple('FullArgSpec', 'args, varargs, varkw, defaults, '
'kwonlyargs, kwonlydefaults, annotations')
Expand Down
43 changes: 32 additions & 11 deletions hypothesis-python/src/hypothesis/searchstrategy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,38 @@

import hypothesis.strategies as st
from hypothesis.errors import InvalidArgument, ResolutionFailed
from hypothesis.internal.compat import PY2, text_type
from hypothesis.internal.compat import PY2, ForwardRef, abc, text_type, \
typing_root_type


def type_sorting_key(t):
"""Minimise to None, then non-container types, then container types."""
if not isinstance(t, type):
if not is_a_type(t):
raise InvalidArgument('thing=%s must be a type' % (t,))
if t is None or t is type(None): # noqa: E721
return (-1, repr(t))
return (issubclass(t, collections.Container), repr(t))
if not isinstance(t, type): # pragma: no cover
# Some generics in the typing module are not actually types in 3.7
return (2, repr(t))
return (int(issubclass(t, abc.Container)), repr(t))


def try_issubclass(thing, maybe_superclass):
def try_issubclass(thing, superclass):
thing = getattr(thing, '__origin__', None) or thing
superclass = getattr(superclass, '__origin__', None) or superclass
try:
return issubclass(thing, maybe_superclass)
return issubclass(thing, superclass)
except (AttributeError, TypeError): # pragma: no cover
# Some types can't be the subject or object of an instance or
# subclass check under Python 3.5
return False


def is_a_type(thing):
"""Return True if thing is a type or a generic type like thing."""
return isinstance(thing, type) or isinstance(thing, typing_root_type)


def from_typing_type(thing):
# We start with special-case support for Union and Tuple - the latter
# isn't actually a generic type. Support for Callable may be added to
Expand All @@ -65,22 +76,32 @@ def from_typing_type(thing):
if not args:
raise ResolutionFailed('Cannot resolve Union of no types.')
return st.one_of([st.from_type(t) for t in args])
if isinstance(thing, typing.TupleMeta):
if getattr(thing, '__origin__', None) == 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)
return st.tuples(*map(st.from_type, elem_types))
if isinstance(thing, typing.TypeVar):
if getattr(thing, '__bound__', None) is not None:
return st.from_type(thing.__bound__)
if getattr(thing, '__constraints__', None):
return st.shared(
st.sampled_from(thing.__constraints__),
key='typevar-with-constraint'
).flatmap(st.from_type)
# Constraints may be None or () on various Python versions.
return st.text() # An arbitrary type for the typevar
# Now, confirm that we're dealing with a generic type as we expected
if not isinstance(thing, typing.GenericMeta): # pragma: no cover
if not isinstance(thing, typing_root_type): # pragma: no cover
raise ResolutionFailed('Cannot resolve %s to a strategy' % (thing,))
# Parametrised generic types have their __origin__ attribute set to the
# un-parametrised version, which we need to use in the subclass checks.
# e.g.: typing.List[int].__origin__ == typing.List
mapping = {k: v for k, v in _global_type_lookup.items()
if isinstance(k, typing.GenericMeta) and
try_issubclass(k, getattr(thing, '__origin__', None) or thing)}
if isinstance(k, typing_root_type) and try_issubclass(k, thing)}
if typing.Dict in mapping:
# The subtype relationships between generic and concrete View types
# are sometimes inconsistent under Python 3.5, so we pop them out to
Expand Down Expand Up @@ -206,10 +227,10 @@ def resolve_Type(thing):
args = args[0].__args__
elif hasattr(args[0], '__union_params__'): # pragma: no cover
args = args[0].__union_params__
if isinstance(typing._ForwardRef, type): # pragma: no cover
if isinstance(ForwardRef, type): # pragma: no cover
# Duplicate check from from_type here - only paying when needed.
for a in args:
if type(a) == typing._ForwardRef:
if type(a) == ForwardRef:
raise ResolutionFailed(
'thing=%s cannot be resolved. Upgrading to '
'python>=3.6 may fix this problem via improvements '
Expand Down
15 changes: 8 additions & 7 deletions hypothesis-python/src/hypothesis/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
from hypothesis.internal.cache import LRUReusedCache
from hypothesis.searchstrategy import SearchStrategy, check_strategy
from hypothesis.internal.compat import gcd, ceil, floor, hrange, \
string_types, get_type_hints, getfullargspec, implements_iterator
string_types, get_type_hints, getfullargspec, typing_root_type, \
implements_iterator
from hypothesis.internal.floats import next_up, next_down, is_negative, \
float_to_int, int_to_float, count_between_floats
from hypothesis.internal.charmap import as_general_categories
Expand Down Expand Up @@ -1189,7 +1190,6 @@ def from_type(thing):
from hypothesis.searchstrategy import types

if typing is not None: # pragma: no branch
fr = typing._ForwardRef
if not isinstance(thing, type):
# At runtime, `typing.NewType` returns an identity function rather
# than an actual type, but we can check that for a possible match
Expand All @@ -1206,12 +1206,13 @@ def from_type(thing):
return one_of([from_type(t) for t in args])
# We can't resolve forward references, and under Python 3.5 (only)
# a forward reference is an instance of type. Hence, explicit check:
elif type(thing) == fr: # pragma: no cover
elif hasattr(typing, '_ForwardRef') and \
type(thing) == typing._ForwardRef: # pragma: no cover
raise ResolutionFailed(
'thing=%s cannot be resolved. Upgrading to python>=3.6 may '
'fix this problem via improvements to the typing module.'
% (thing,))
if not isinstance(thing, type):
if not types.is_a_type(thing):
raise InvalidArgument('thing=%s must be a type' % (thing,))
# Now that we know `thing` is a type, the first step is to check for an
# explicitly registered strategy. This is the best (and hopefully most
Expand All @@ -1232,7 +1233,7 @@ def from_type(thing):
# because there are several special cases that don't play well with
# subclass and instance checks.
if typing is not None: # pragma: no branch
if isinstance(thing, typing.TypingMeta):
if isinstance(thing, typing_root_type):
return types.from_typing_type(thing)
# If it's not from the typing module, we get all registered types that are
# a subclass of `thing` and are not themselves a subtype of any other such
Expand All @@ -1241,7 +1242,7 @@ def from_type(thing):
strategies = [
v if isinstance(v, SearchStrategy) else v(thing)
for k, v in types._global_type_lookup.items()
if issubclass(k, thing) and sum(
if isinstance(k, type) and issubclass(k, thing) and sum(
types.try_issubclass(k, typ) for typ in types._global_type_lookup
) == 1
]
Expand Down Expand Up @@ -2072,7 +2073,7 @@ def register_type_strategy(custom_type, strategy):
# TODO: We would like to move this to the top level, but pending some major
# refactoring it's hard to do without creating circular imports.
from hypothesis.searchstrategy import types
if not isinstance(custom_type, type):
if not types.is_a_type(custom_type):
raise InvalidArgument('custom_type=%r must be a type')
elif not (isinstance(strategy, SearchStrategy) or callable(strategy)):
raise InvalidArgument(
Expand Down
29 changes: 25 additions & 4 deletions hypothesis-python/tests/py3/test_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,24 @@
import pytest

import hypothesis.strategies as st
from hypothesis import find, given, infer, assume
from hypothesis import HealthCheck, find, given, infer, assume, settings
from hypothesis.errors import NoExamples, InvalidArgument, ResolutionFailed
from hypothesis.strategies import from_type
from hypothesis.searchstrategy import types
from hypothesis.internal.compat import integer_types, get_type_hints
from hypothesis.internal.compat import ForwardRef, integer_types, \
get_type_hints, typing_root_type

typing = pytest.importorskip('typing')
sentinel = object()
generics = sorted((t for t in types._global_type_lookup
if isinstance(t, typing.GenericMeta)), key=str)
if isinstance(t, typing_root_type)), key=str)


@pytest.mark.parametrize('typ', generics)
def test_resolve_typing_module(typ):
@settings(suppress_health_check=[
HealthCheck.too_slow, HealthCheck.filter_too_much
])
@given(from_type(typ))
def inner(ex):
if typ in (typing.BinaryIO, typing.TextIO):
Expand All @@ -49,6 +53,8 @@ def inner(ex):
assert ex == ()
elif isinstance(typ, typing._ProtocolMeta):
pass
elif typ is typing.Type and not isinstance(typing.Type, type):
assert isinstance(ex, typing.TypeVar)
else:
try:
assert isinstance(ex, typ)
Expand Down Expand Up @@ -227,6 +233,21 @@ def test_resolves_weird_types(typ):
from_type(typ).example()


@pytest.mark.parametrize('var,expected', [
(typing.TypeVar('V'), object),
(typing.TypeVar('V', bound=int), int),
(typing.TypeVar('V', int, str), (int, str)),
])
@given(data=st.data())
def test_typevar_type_is_consistent(data, var, expected):
strat = st.from_type(var)
v1 = data.draw(strat)
v2 = data.draw(strat)
assume(v1 != v2) # Values may vary, just not types
assert type(v1) == type(v2)
assert isinstance(v1, expected)


def annotated_func(a: int, b: int=2, *, c: int, d: int=4):
return a + b + c + d

Expand Down Expand Up @@ -391,7 +412,7 @@ def test_override_args_for_namedtuple(thing):
def test_cannot_resolve_bare_forward_reference(thing):
with pytest.raises(InvalidArgument):
t = thing['int']
if type(getattr(t, '__args__', [None])[0]) != typing._ForwardRef:
if type(getattr(t, '__args__', [None])[0]) != ForwardRef:
assert sys.version_info[:2] == (3, 5)
pytest.xfail('python 3.5 typing module is really weird')
st.from_type(t).example()
Expand Down

0 comments on commit 6980b13

Please sign in to comment.