Skip to content

Commit

Permalink
Merge pull request #1253 from Zac-HD/typed
Browse files Browse the repository at this point in the history
Add type hints to the public API, with generic Strategy type
  • Loading branch information
Zac-HD committed May 9, 2018
2 parents b340323 + ddfb6e6 commit 661fd78
Show file tree
Hide file tree
Showing 14 changed files with 320 additions and 96 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ exclude =
test_imports.py,
hypothesis-python/tests/py2/*,
test_lambda_formatting.py
ignore = D1,D205,D209,D213,D400,D401,D999
ignore = F811,D1,D205,D209,D213,D400,D401,D999
4 changes: 4 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
RELEASE_TYPE: patch

This patch contains further internal work to support Mypy.
There are no user-visible changes... yet.
9 changes: 8 additions & 1 deletion hypothesis-python/src/hypothesis/_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from hypothesis.utils.dynamicvariables import DynamicVariable

if False:
from typing import Dict, List # noqa
from typing import Any, Dict, List # noqa

__all__ = [
'settings',
Expand Down Expand Up @@ -140,6 +140,7 @@ def __getattr__(self, name):
raise AttributeError('settings has no attribute %s' % (name,))

def __init__(self, parent=None, **kwargs):
# type: (settings, **Any) -> None
if (
kwargs.get('database', not_set) is not_set and
kwargs.get('database_file', not_set) is not not_set
Expand Down Expand Up @@ -326,6 +327,7 @@ def __exit__(self, *args, **kwargs):

@staticmethod
def register_profile(name, parent=None, **kwargs):
# type: (str, settings, **Any) -> None
"""Registers a collection of values to be used as a settings profile.
Settings profiles can be loaded by name - for example, you might
Expand Down Expand Up @@ -354,6 +356,7 @@ def register_profile(name, parent=None, **kwargs):

@staticmethod
def get_profile(name):
# type: (str) -> settings
"""Return the profile with the given name."""
if not isinstance(name, (str, text_type)):
note_deprecation('name=%r must be a string' % (name,))
Expand All @@ -364,6 +367,7 @@ def get_profile(name):

@staticmethod
def load_profile(name):
# type: (str) -> None
"""Loads in the settings defined in the profile provided.
If the profile does not exist, InvalidArgument will be raised.
Expand Down Expand Up @@ -568,6 +572,7 @@ def __repr__(self):

@classmethod
def all(cls):
# type: () -> List[HealthCheck]
bad = (HealthCheck.exception_in_generation, HealthCheck.random_module)
return [h for h in list(cls) if h not in bad]

Expand Down Expand Up @@ -623,6 +628,7 @@ class Verbosity(IntEnum):

@staticmethod
def _get_default():
# type: () -> Verbosity
var = os.getenv('HYPOTHESIS_VERBOSITY_LEVEL')
if var is not None: # pragma: no cover
try:
Expand Down Expand Up @@ -792,6 +798,7 @@ class PrintSettings(Enum):


def note_deprecation(message, s=None):
# type: (str, settings) -> None
if s is None:
s = settings.default
assert s is not None
Expand Down
6 changes: 6 additions & 0 deletions hypothesis-python/src/hypothesis/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@
from hypothesis.reporting import report
from hypothesis.utils.dynamicvariables import DynamicVariable

if False:
from typing import Any, AnyStr # noqa


def reject():
raise UnsatisfiedAssumption()


def assume(condition):
# type: (Any) -> bool
"""Calling ``assume`` is like an :ref:`assert <python:assert>` that marks
the example as bad, rather than failing the test.
Expand Down Expand Up @@ -103,6 +107,7 @@ def cleanup(teardown):


def note(value):
# type: (AnyStr) -> None
"""Report this value in the final execution."""
context = _current_build_context.value
if context is None:
Expand All @@ -114,6 +119,7 @@ def note(value):


def event(value):
# type: (AnyStr) -> None
"""Record an event that occurred this test. Statistics on number of test
runs with each event will be reported at the end if you run Hypothesis in
statistics reporting mode.
Expand Down
10 changes: 7 additions & 3 deletions hypothesis-python/src/hypothesis/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@
from coverage.collector import FileDisposition

if False:
from typing import Any, Dict, Callable, Optional # noqa
from typing import Any, Dict, Callable, Optional, Union # noqa
from hypothesis.utils.conventions import InferType # noqa


running_under_pytest = False
Expand Down Expand Up @@ -892,8 +893,11 @@ def fake_subTest(self, msg=None, **__):
yield


def given(*given_arguments, **given_kwargs):
# type: (*SearchStrategy, **SearchStrategy) -> Callable
def given(
*given_arguments, # type: Union[SearchStrategy, InferType]
**given_kwargs # type: Union[SearchStrategy, InferType]
):
# type: (...) -> Callable[[Callable[..., None]], Callable[..., None]]
"""A decorator for turning a test function that accepts arguments into a
randomized test.
Expand Down
28 changes: 21 additions & 7 deletions hypothesis-python/src/hypothesis/extra/django/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@
from hypothesis.extra.pytz import timezones
from hypothesis.provisional import emails, ip4_addr_strings, \
ip6_addr_strings
from hypothesis.utils.conventions import UniqueIdentifier
from hypothesis.utils.conventions import DefaultValueType

if False:
from datetime import tzinfo # noqa
from typing import Any, Type, Optional, List, Text, Callable, Union # noqa


def get_tz_strat():
# type: () -> st.SearchStrategy[Optional[tzinfo]]
if getattr(django_settings, 'USE_TZ', False):
return timezones()
return st.none()
Expand Down Expand Up @@ -85,13 +90,15 @@ def field_mappings():


def add_default_field_mapping(field_type, strategy):
# type: (Type[dm.Field], st.SearchStrategy[Any]) -> None
field_mappings()[field_type] = strategy


default_value = UniqueIdentifier(u'default_value')
default_value = DefaultValueType(u'default_value')


def validator_to_filter(f):
# type: (Type[dm.Field]) -> Callable[[Any], bool]
"""Converts the field run_validators method to something suitable for use
in filter."""
def validate(value):
Expand All @@ -105,8 +112,9 @@ def validate(value):


def _get_strategy_for_field(f):
# type: (Type[dm.Field]) -> st.SearchStrategy[Any]
if f.choices:
choices = []
choices = [] # type: list
for value, name_or_optgroup in f.choices:
if isinstance(name_or_optgroup, (list, tuple)):
choices.extend(key for key, _ in name_or_optgroup)
Expand Down Expand Up @@ -161,7 +169,11 @@ def _get_strategy_for_field(f):
return strategy


def models(model, **field_strategies):
def models(
model, # type: Type[dm.Model]
**field_strategies # type: Union[st.SearchStrategy[Any], DefaultValueType]
):
# type: (...) -> st.SearchStrategy[Any]
"""Return a strategy for examples of ``model``.
.. warning::
Expand All @@ -187,9 +199,11 @@ def models(model, **field_strategies):
shop_strategy = models(Shop, company=models(Company))
"""
result = {k: v for k, v in field_strategies.items()
if v is not default_value}
missed = []
result = {}
for k, v in field_strategies.items():
if not isinstance(v, DefaultValueType):
result[k] = v
missed = [] # type: List[Text]
for f in model._meta.concrete_fields:
if not (f.name in field_strategies or isinstance(f, dm.AutoField)):
result[f.name] = _get_strategy_for_field(f)
Expand Down
52 changes: 43 additions & 9 deletions hypothesis-python/src/hypothesis/extra/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@
from hypothesis.internal.coverage import check_function
from hypothesis.internal.reflection import proxies

if False:
from typing import Any, Union, Sequence, Tuple # noqa
from hypothesis.searchstrategy.strategies import T # noqa

TIME_RESOLUTIONS = tuple('Y M D h m s ms us ns ps fs as'.split())


@st.defines_strategy_with_reusable_values
def from_dtype(dtype):
# type: (np.dtype) -> st.SearchStrategy[Any]
# Compound datatypes, eg 'f4,f4,f4'
if dtype.names is not None:
# mapping np.void.type over a strategy is nonsense, so return now.
Expand All @@ -47,7 +52,7 @@ def from_dtype(dtype):

# Scalar datatypes
if dtype.kind == u'b':
result = st.booleans()
result = st.booleans() # type: SearchStrategy[Any]
elif dtype.kind == u'f':
result = st.floats()
elif dtype.kind == u'c':
Expand Down Expand Up @@ -227,8 +232,14 @@ def fill_for(elements, unique, fill, name=''):

@st.composite
def arrays(
draw, dtype, shape, elements=None, fill=None, unique=False
draw, # type: Any
dtype, # type: Any
shape, # type: Union[int, Sequence[int]]
elements=None, # type: st.SearchStrategy[Any]
fill=None, # type: st.SearchStrategy[Any]
unique=False, # type: bool
):
# type: (...) -> st.SearchStrategy[np.ndarray]
"""Returns a strategy for generating :class:`numpy's
ndarrays<numpy.ndarray>`.
Expand Down Expand Up @@ -320,6 +331,7 @@ def arrays(

@st.defines_strategy
def array_shapes(min_dims=1, max_dims=3, min_side=1, max_side=10):
# type: (int, int, int, int) -> st.SearchStrategy[Tuple[int, ...]]
"""Return a strategy for array shapes (tuples of int >= 1)."""
order_check('dims', 1, min_dims, max_dims)
order_check('side', 1, min_side, max_side)
Expand All @@ -329,6 +341,7 @@ def array_shapes(min_dims=1, max_dims=3, min_side=1, max_side=10):

@st.defines_strategy
def scalar_dtypes():
# type: () -> st.SearchStrategy[np.dtype]
"""Return a strategy that can return any non-flexible scalar dtype."""
return st.one_of(boolean_dtypes(),
integer_dtypes(), unsigned_integer_dtypes(),
Expand All @@ -337,6 +350,7 @@ def scalar_dtypes():


def defines_dtype_strategy(strat):
# type: (T) -> T
@st.defines_strategy
@proxies(strat)
def inner(*args, **kwargs):
Expand All @@ -346,6 +360,7 @@ def inner(*args, **kwargs):

@defines_dtype_strategy
def boolean_dtypes():
# type: () -> st.SearchStrategy[np.dtype]
return st.just('?')


Expand Down Expand Up @@ -374,6 +389,7 @@ def dtype_factory(kind, sizes, valid_sizes, endianness):

@defines_dtype_strategy
def unsigned_integer_dtypes(endianness='?', sizes=(8, 16, 32, 64)):
# type: (str, Sequence[int]) -> st.SearchStrategy[np.dtype]
"""Return a strategy for unsigned integer dtypes.
endianness may be ``<`` for little-endian, ``>`` for big-endian,
Expand All @@ -388,6 +404,7 @@ def unsigned_integer_dtypes(endianness='?', sizes=(8, 16, 32, 64)):

@defines_dtype_strategy
def integer_dtypes(endianness='?', sizes=(8, 16, 32, 64)):
# type: (str, Sequence[int]) -> st.SearchStrategy[np.dtype]
"""Return a strategy for signed integer dtypes.
endianness and sizes are treated as for
Expand All @@ -398,6 +415,7 @@ def integer_dtypes(endianness='?', sizes=(8, 16, 32, 64)):

@defines_dtype_strategy
def floating_dtypes(endianness='?', sizes=(16, 32, 64)):
# type: (str, Sequence[int]) -> st.SearchStrategy[np.dtype]
"""Return a strategy for floating-point dtypes.
sizes is the size in bits of floating-point number. Some machines support
Expand All @@ -412,6 +430,7 @@ def floating_dtypes(endianness='?', sizes=(16, 32, 64)):

@defines_dtype_strategy
def complex_number_dtypes(endianness='?', sizes=(64, 128)):
# type: (str, Sequence[int]) -> st.SearchStrategy[np.dtype]
"""Return a strategy for complex-number dtypes.
sizes is the total size in bits of a complex number, which consists
Expand Down Expand Up @@ -439,6 +458,7 @@ def validate_time_slice(max_period, min_period):

@defines_dtype_strategy
def datetime64_dtypes(max_period='Y', min_period='ns', endianness='?'):
# type: (str, str, str) -> st.SearchStrategy[np.dtype]
"""Return a strategy for datetime64 dtypes, with various precisions from
year to attosecond."""
return dtype_factory('datetime64[{}]',
Expand All @@ -448,6 +468,7 @@ def datetime64_dtypes(max_period='Y', min_period='ns', endianness='?'):

@defines_dtype_strategy
def timedelta64_dtypes(max_period='Y', min_period='ns', endianness='?'):
# type: (str, str, str) -> st.SearchStrategy[np.dtype]
"""Return a strategy for timedelta64 dtypes, with various precisions from
year to attosecond."""
return dtype_factory('timedelta64[{}]',
Expand All @@ -457,6 +478,7 @@ def timedelta64_dtypes(max_period='Y', min_period='ns', endianness='?'):

@defines_dtype_strategy
def byte_string_dtypes(endianness='?', min_len=0, max_len=16):
# type: (str, int, int) -> st.SearchStrategy[np.dtype]
"""Return a strategy for generating bytestring dtypes, of various lengths
and byteorder."""
order_check('len', 0, min_len, max_len)
Expand All @@ -466,6 +488,7 @@ def byte_string_dtypes(endianness='?', min_len=0, max_len=16):

@defines_dtype_strategy
def unicode_string_dtypes(endianness='?', min_len=0, max_len=16):
# type: (str, int, int) -> st.SearchStrategy[np.dtype]
"""Return a strategy for generating unicode string dtypes, of various
lengths and byteorder."""
order_check('len', 0, min_len, max_len)
Expand All @@ -474,23 +497,34 @@ def unicode_string_dtypes(endianness='?', min_len=0, max_len=16):


@defines_dtype_strategy
def array_dtypes(subtype_strategy=scalar_dtypes(),
min_size=1, max_size=5, allow_subarrays=False):
def array_dtypes(
subtype_strategy=scalar_dtypes(), # type: st.SearchStrategy[np.dtype]
min_size=1, # type: int
max_size=5, # type: int
allow_subarrays=False, # type: bool
):
# type: (...) -> st.SearchStrategy[np.dtype]
"""Return a strategy for generating array (compound) dtypes, with members
drawn from the given subtype strategy."""
order_check('size', 0, min_size, max_size)
native_strings = st.text if text_type is str else st.binary
elements = st.tuples(native_strings(), subtype_strategy)
native_strings = st.text() # type: SearchStrategy[Any]
if text_type is not str: # pragma: no cover
native_strings = st.binary()
elements = st.tuples(native_strings, subtype_strategy)
if allow_subarrays:
elements |= st.tuples(native_strings(), subtype_strategy,
elements |= st.tuples(native_strings, subtype_strategy,
array_shapes(max_dims=2, max_side=2))
return st.lists(elements=elements, min_size=min_size, max_size=max_size,
unique_by=lambda d: d[0])


@st.defines_strategy
def nested_dtypes(subtype_strategy=scalar_dtypes(),
max_leaves=10, max_itemsize=None):
def nested_dtypes(
subtype_strategy=scalar_dtypes(), # type: st.SearchStrategy[np.dtype]
max_leaves=10, # type: int
max_itemsize=None, # type: int
):
# type: (...) -> st.SearchStrategy[np.dtype]
"""Return the most-general dtype strategy.
Elements drawn from this strategy may be simple (from the
Expand Down

0 comments on commit 661fd78

Please sign in to comment.