Skip to content

Commit

Permalink
Fix abstract type bug
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD committed Apr 7, 2021
1 parent 25a5df8 commit 98a7891
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 12 deletions.
7 changes: 7 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
RELEASE_TYPE: patch

This patch fixes :func:`~hypothesis.strategies.from_type` with
:mod:`abstract types <python:abc>` which have either required but
non-type-annotated arguments to ``__init__``, or where
:func:`~hypothesis.strategies.from_type` can handle some concrete
subclasses but not others.
35 changes: 23 additions & 12 deletions hypothesis-python/src/hypothesis/strategies/_internal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1062,24 +1062,25 @@ def as_strategy(strat_or_callable, thing, final=True):
# may be able to fall back on type annotations.
if issubclass(thing, enum.Enum):
return sampled_from(thing)
# If we know that builds(thing) will fail, give a better error message
required = required_args(thing)
if required and not (
required.issubset(get_type_hints(thing))
or attr.has(thing)
or is_typed_named_tuple(thing) # weird enough that we have a specific check
):
raise ResolutionFailed(
f"Could not resolve {thing!r} to a strategy; consider "
"using register_type_strategy"
)

# Finally, try to build an instance by calling the type object. Unlike builds(),
# this block *does* try to infer strategies for arguments with default values.
# That's because of the semantic different; builds() -> "call this with ..."
# so we only infer when *not* doing so would be an error; from_type() -> "give
# me arbitrary instances" so the greater variety is acceptable.
# And if it's *too* varied, express your opinions with register_type_strategy()
if not isabstract(thing):
# If we know that builds(thing) will fail, give a better error message
required = required_args(thing)
if required and not (
required.issubset(get_type_hints(thing))
or attr.has(thing)
or is_typed_named_tuple(thing) # weird enough that we have a specific check
):
raise ResolutionFailed(
f"Could not resolve {thing!r} to a strategy; consider "
"using register_type_strategy"
)
try:
hints = get_type_hints(thing)
params = signature(thing).parameters
Expand All @@ -1102,7 +1103,17 @@ def as_strategy(strat_or_callable, thing, final=True):
f"Could not resolve {thing!r} to a strategy, because it is an abstract "
"type without any subclasses. Consider using register_type_strategy"
)
return sampled_from(subclasses).flatmap(from_type)
subclass_strategies = nothing()
for sc in subclasses:
try:
subclass_strategies |= _from_type(sc)
except Exception:
pass
if subclass_strategies.is_empty:
# We're unable to resolve subclasses now, but we might be able to later -
# so we'll just go back to the mixed distribution.
return sampled_from(subclasses).flatmap(from_type)
return subclass_strategies


@cacheable
Expand Down
65 changes: 65 additions & 0 deletions hypothesis-python/tests/cover/test_type_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#
# END HEADER

import abc
import enum
import sys
from typing import Callable, Generic, List, Sequence, TypeVar, Union
Expand All @@ -26,6 +27,7 @@
ResolutionFailed,
)
from hypothesis.strategies._internal import types
from hypothesis.strategies._internal.core import _from_type
from hypothesis.strategies._internal.types import _global_type_lookup
from hypothesis.strategies._internal.utils import _strategies

Expand Down Expand Up @@ -291,3 +293,66 @@ def test_generic_origin_concrete_builds():
assert_all_examples(
st.builds(using_generic), lambda example: isinstance(example, int)
)


class AbstractFoo(abc.ABC):
def __init__(self, x):
pass

@abc.abstractmethod
def qux(self):
pass


class ConcreteFoo1(AbstractFoo):
# Can't resolve this one due to unannotated `x` param
def qux(self):
pass


class ConcreteFoo2(AbstractFoo):
def __init__(self, x: int):
pass

def qux(self):
pass


@given(st.from_type(AbstractFoo))
def test_gen_abstract(foo):
# This requires that we correctly checked which of the subclasses
# could be resolved, rather than unconditionally using all of them.
assert isinstance(foo, ConcreteFoo2)


class AbstractBar(abc.ABC):
def __init__(self, x):
pass

@abc.abstractmethod
def qux(self):
pass


class ConcreteBar(AbstractBar):
def qux(self):
pass


def test_abstract_resolver_fallback():
# We create our distinct strategies for abstract and concrete types
gen_abstractbar = _from_type(AbstractBar)
gen_concretebar = st.builds(ConcreteBar, x=st.none())
assert gen_abstractbar != gen_concretebar

# And trying to generate an instance of the abstract type fails,
# UNLESS the concrete type is currently resolvable
with pytest.raises(ResolutionFailed):
gen_abstractbar.example()
with temp_registered(ConcreteBar, gen_concretebar):
gen = gen_abstractbar.example()
with pytest.raises(ResolutionFailed):
gen_abstractbar.example()

# which in turn means we resolve to the concrete subtype.
assert isinstance(gen, ConcreteBar)

0 comments on commit 98a7891

Please sign in to comment.