Repository: HypothesisWorks/hypothesis

Issue:
Objects for which getfullargspec() fails are handled incorrectly by from_type() and builds().
Some types of objects are considered callable but getfullargspec() fails on them. This is handled by required_args() by returning None. The functions from_type() and builds() don't seem to anticipate this and assume required_args always returns set. In case of from_type it ends up with:
`AttributeError: 'NoneType' object has no attribute 'issubset'` while builds assumes this means the object should be called with no arguments.
I've added tests to check for this in https://github.com/ccxcz/hypothesis/tree/check-uninspectable-callables
__init__ inherited from object fails required_args() on Python-2.7
On Python 2.7 `getargspec()` fails on default `__init__` method. This in turn triggers bug #1655. The `required_args()` function can use `target.__init__ is object.__init__` to detect this case.
Test for checking correct result is provided in https://github.com/ccxcz/hypothesis/tree/check-default-init


Discussion:

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/internal/reflection.py b/hypothesis-python/src/hypothesis/internal/reflection.py
--- a/hypothesis-python/src/hypothesis/internal/reflection.py
+++ b/hypothesis-python/src/hypothesis/internal/reflection.py
@@ -116,7 +116,9 @@ def required_args(target, args=(), kwargs=()):
     # Then we try to do the right thing with getfullargspec
     try:
         spec = getfullargspec(
-            target.__init__ if inspect.isclass(target) else target)
+            getattr(target, '__init__', target)
+            if inspect.isclass(target) else target
+        )
     except TypeError:  # pragma: no cover
         return None
     # self appears in the argspec of __init__ and bound methods, but it's an
diff --git a/hypothesis-python/src/hypothesis/strategies.py b/hypothesis-python/src/hypothesis/strategies.py
--- a/hypothesis-python/src/hypothesis/strategies.py
+++ b/hypothesis-python/src/hypothesis/strategies.py
@@ -1191,7 +1191,7 @@ def builds(
         # Avoid an implementation nightmare juggling tuples and worse things
         raise InvalidArgument('infer was passed as a positional argument to '
                               'builds(), but is only allowed as a keyword arg')
-    required = required_args(target, args, kwargs)
+    required = required_args(target, args, kwargs) or set()
     to_infer = set(k for k, v in kwargs.items() if v is infer)
     if required or to_infer:
         if isclass(target) and attr.has(target):
@@ -1332,8 +1332,7 @@ def from_type(thing):
         return sampled_from(thing)
     # If we know that builds(thing) will fail, give a better error message
     required = required_args(thing)
-    if not any([
-        not required,
+    if required and not any([
         required.issubset(get_type_hints(thing.__init__)),
         attr.has(thing),
         # NamedTuples are weird enough that we need a specific check for them.


Test patch:
diff --git a/hypothesis-python/tests/cover/test_type_lookup.py b/hypothesis-python/tests/cover/test_type_lookup.py
--- a/hypothesis-python/tests/cover/test_type_lookup.py
+++ b/hypothesis-python/tests/cover/test_type_lookup.py
@@ -91,6 +91,13 @@ class ParentUnknownType(object):
     pass
 
 
+def test_can_resolve_trivial_types():
+    # Under Python 2, this inherits a special wrapper_descriptor slots
+    # thing from object.__init__, which chokes inspect.getargspec.
+    # from_type should and does work anyway; see issues #1655 and #1656.
+    st.from_type(ParentUnknownType).example()
+
+
 class UnknownType(ParentUnknownType):
     def __init__(self, arg):
         pass
@@ -159,3 +166,17 @@ class EmptyEnum(enum.Enum):
 
 def test_error_if_enum_is_empty():
     assert st.from_type(EmptyEnum).is_empty
+
+
+class BrokenClass(object):
+    __init__ = 'Hello!'
+
+
+def test_uninspectable_builds():
+    with pytest.raises(TypeError, match='object is not callable'):
+        st.builds(BrokenClass).example()
+
+
+def test_uninspectable_from_type():
+    with pytest.raises(TypeError, match='object is not callable'):
+        st.from_type(BrokenClass).example()
diff --git a/hypothesis-python/tests/py2/test_from_type.py b/hypothesis-python/tests/py2/test_from_type.py
--- a/hypothesis-python/tests/py2/test_from_type.py
+++ b/hypothesis-python/tests/py2/test_from_type.py
@@ -29,3 +29,11 @@ def test_from_int_is_int(x):
 @given(st.from_type(long))
 def test_from_long_is_long(x):
     assert isinstance(x, long)
+
+
+class OldStyleInitlessClass:
+    pass
+
+
+def test_builds_old_style_initless_class():
+    st.builds(OldStyleInitlessClass).example()