diff --git a/hypothesis-python/src/hypothesis/internal/compat.py b/hypothesis-python/src/hypothesis/internal/compat.py index dabc4b99c9..59cee0fbf4 100644 --- a/hypothesis-python/src/hypothesis/internal/compat.py +++ b/hypothesis-python/src/hypothesis/internal/compat.py @@ -27,6 +27,7 @@ import codecs import platform import importlib +import collections from base64 import b64encode from collections import namedtuple @@ -35,6 +36,7 @@ except ImportError: from ordereddict import OrderedDict # type: ignore from counter import Counter # type: ignore + from collections import Container if False: from typing import Type # noqa @@ -70,6 +72,7 @@ def int_to_text(i): ARG_NAME_ATTRIBUTE = 'arg' integer_types = (int,) hunichr = chr + Container = collections.abc.Container # type: ignore def unicode_safe_repr(x): return repr(x) @@ -217,6 +220,7 @@ def shimrange(): ARG_NAME_ATTRIBUTE = 'id' integer_types = (int, long) hunichr = unichr + Container = collections.Container def escape_unicode_characters(s): return codecs.encode(s, 'string_escape') diff --git a/hypothesis-python/src/hypothesis/searchstrategy/attrs.py b/hypothesis-python/src/hypothesis/searchstrategy/attrs.py index 5c8f5ff50d..fd341f8974 100644 --- a/hypothesis-python/src/hypothesis/searchstrategy/attrs.py +++ b/hypothesis-python/src/hypothesis/searchstrategy/attrs.py @@ -26,6 +26,7 @@ from hypothesis.errors import ResolutionFailed from hypothesis.internal.compat import string_types, get_type_hints from hypothesis.utils.conventions import infer +from hypothesis.searchstrategy.types import type_sorting_key def from_attrs(target, args, kwargs, to_infer): @@ -107,17 +108,21 @@ def types_to_strategy(attrib, types): return st.one_of(*map(st.from_type, typ)) return st.from_type(typ) elif types: - # Multiple type validators. Pick common type(s) that satisfy all. - # TODO: use a more sophisticated aggregation that understands subtypes, - # generic types, etc. Consider pulling some code out of st.from_type? + # We have a list of tuples of types, and want to find a type + # (or tuple of types) that is a subclass of all of of them. type_tuples = [k if isinstance(k, tuple) else (k,) for k in types] - return st.one_of(*map(st.from_type, ordered_intersection(type_tuples))) + # Flatten the list, filter types that would fail validation, and + # sort so that ordering is stable between runs and shrinks well. + allowed = [t for t in set(sum(type_tuples, ())) + if all(issubclass(t, tup) for tup in type_tuples)] + allowed.sort(key=type_sorting_key) + return st.one_of([st.from_type(t) for t in allowed]) # Otherwise, try the `type` attribute as a fallback, and finally try # the type hints on a converter (desperate!) before giving up. if isinstance(getattr(attrib, 'type', None), type): # The convoluted test is because variable annotations may be stored - # in string form, and pass through attrs unevaluated. + # in string form; attrs doesn't evaluate them and we don't handle them. # See PEP 526, PEP 563, and Hypothesis issue #1004 for details. return st.from_type(attrib.type) diff --git a/hypothesis-python/src/hypothesis/searchstrategy/types.py b/hypothesis-python/src/hypothesis/searchstrategy/types.py index 86cfa581b3..a39a2c01e3 100644 --- a/hypothesis-python/src/hypothesis/searchstrategy/types.py +++ b/hypothesis-python/src/hypothesis/searchstrategy/types.py @@ -27,7 +27,7 @@ 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, Container, text_type def type_sorting_key(t): @@ -35,8 +35,8 @@ def type_sorting_key(t): if not isinstance(t, type): raise InvalidArgument('thing=%s must be a type' % (t,)) if t is None or t is type(None): # noqa: E721 - return -1 - return issubclass(t, collections.abc.Container) + return (-1, repr(t)) + return (issubclass(t, Container), repr(t)) def try_issubclass(thing, maybe_superclass): diff --git a/hypothesis-python/tests/cover/test_attrs_inference.py b/hypothesis-python/tests/cover/test_attrs_inference.py index 0b6e3fdc79..b9e18c729f 100644 --- a/hypothesis-python/tests/cover/test_attrs_inference.py +++ b/hypothesis-python/tests/cover/test_attrs_inference.py @@ -37,6 +37,11 @@ class Inferrables(object): attr.validators.instance_of(str), attr.validators.instance_of((str, int, bool)), ]) + validator_type_has_overlap = attr.ib(validator=[ + attr.validators.instance_of(str), + attr.validators.instance_of((str, list)), + attr.validators.instance_of(object), + ]) validator_optional = attr.ib( validator=attr.validators.optional(lambda inst, atrib, val: float(val)) )