Repository: HypothesisWorks/hypothesis

Issue:
@given decorator doesn't supply "self" argument when used inline
If I "decorate" a class's method inline, it doesn't supply the `self` argument when the resultant function is called. If I accept an `*args` argument instead (as with `show`), it *is* passed to the method, so I do not understand why it doesn't work for a specific argument instead of all positional arguments.

Please note that because this is raising exceptions without handling, you will of course have to run the remaining lines by copy-pasting into the interpreter.

(I'm aware this is not the intended use case for `given`. It's a reproducible example of inconsistent behaviour that I found, but I will not be using this code in my project. It may be related to #198 as that also involves the `@given` decorator.)

Python 3.5.2 (x86_64, Win10), Hypothesis 3.33.0 (installed to a virtual environment).

```python
from hypothesis import given, strategies as strat

class MyTest:
    @given(dq=strat.booleans())
    def decorated(self, **kwargs):
        # No *args as we don't have any extra positional arguments
        print(kwargs)

    def bare(self, **kwargs):
        print(kwargs)

    def show(*args, **kwargs):
        print(args, kwargs)

instance = MyTest()

# Works, even if not given functools.wraps(instance.bare)
(lambda **kwargs: instance.bare(**kwargs))()
instance.decorated()

# "self" not supplied, calling this gives this error:
# TypeError: bare() missing 1 required positional argument: 'self'
broken = given(dq=strat.booleans())(instance.bare)
broken()
instance.decorated = broken
instance.decorated()  # Still broken.

# With no argument, you can see it's still supplied, but not as "self".
given(dq=strat.booleans())(instance.show)()

# 2 Arguments supplied
given(dq=strat.booleans())(instance.bare)(instance)
```





Discussion:
Hmm, this *is* pretty strange!  

I think we might be able to do something with [`inspect.ismethod` and `inspect.isclass`](https://docs.python.org/3/library/inspect.html#inspect.isclass) (which I did for `builds()` in Hypothesis 3.33.1), but it's going to be pretty complicated even to work out where the change should go.

For now: thanks for reporting it; I'll mark this as a bug but don't plan to work on it any time soon 😄
Yeah, I think this is the same issue as with builds - given has to do a bunch of really complicated argspec juggling, and when getting the argspec of a bound method you get the one for the unbound function, so it doesn't notice that self has been bound.

I'm in a similar boat to @Zac-HD. It's definitely a bug, it's definitely weird, but it has a pretty high pain to motivation ratio for working on it. :-) What's your actual use case that lead to you finding this?
The code I wrote was pretty terrible code, and it's since been deleted (I had an unsaved copy and then Atom crashed/updated and dropped unsaved changes). It was something like this:

```python
for i in range(len(someiterable)):
    for combo in itertools.combinations(someiterable, n+1):
        functools.reduce(
            lambda fn, gv: given(**{gv[0]: gv[1]})(fn),
            combo, self.original_fn)()  # Call it too
```

The general premise being to generate multiple strategy sets, one per combination from a larger set of strategies.

I now know this "double-wraps" the function and is a bad idea, so I rewrote it to do something like this:

```python
combos = [combo
          for n in range(len(somedict))
          for combo in itertools.combinations(somedict.items(), n+1)]

return strat.sampled_from(combos).map(dict).flatmap(strat.fixed_dictionaries)
```

(That's within a function which is passed to the `@given` decorator directly, instead.)

TLDR: I don't have a valid use case, so low priority is fine by me.
Cool, low priority bug it is!

As a side note, `itertools.combinations` makes me wince.  Assuming that `somedict` isn't tiny, you'll probably get better examples if you avoid generating combinations up front - which you'll need to do for `sampled_from` to work, even though `combinations` is lazy.

```python
st.sets(st.sampled_from(sorted(somedict.items()))).map(dict).flatmap(st.fixed_dictionaries)
```

That is: construct a sorted list of items (so order, and therefore sampling, is reproducible across runs), take a subset, turn it back into a dict, and construct a strategy with `fixed_dictionaries`.
For future reference, the place to fix this is probably `process_arguments_to_given` in `core.py` - check if the test is a method; if so get the class it's bound to and use that as `selfy`.  Not entirely clear to me whether this would work when the current code doesn't, but it's worth trying.
I saw that the other issues that I was working on have been fixed already (#2164) So I'm going to take a crack at this one.  I'm going to look at process_arguements_to_given and see if I can find the problem.

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py
--- a/hypothesis-python/src/hypothesis/core.py
+++ b/hypothesis-python/src/hypothesis/core.py
@@ -28,7 +28,6 @@
 import warnings
 import zlib
 from collections import defaultdict
-from inspect import getfullargspec
 from io import StringIO
 from random import Random
 from typing import Any, BinaryIO, Callable, Hashable, List, Optional, TypeVar, Union
@@ -83,6 +82,7 @@
     define_function_signature,
     function_digest,
     get_pretty_function_description,
+    getfullargspec_except_self as getfullargspec,
     impersonate,
     is_mock,
     proxies,
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
@@ -46,6 +46,13 @@ def is_mock(obj):
     return hasattr(obj, "hypothesis_internal_is_this_a_mock_check")
 
 
+def getfullargspec_except_self(target):
+    spec = inspect.getfullargspec(target)
+    if inspect.ismethod(target):
+        del spec.args[0]
+    return spec
+
+
 def function_digest(function):
     """Returns a string that is stable across multiple invocations across
     multiple processes and is prone to changing significantly in response to
@@ -67,7 +74,7 @@ def function_digest(function):
     except AttributeError:
         pass
     try:
-        hasher.update(repr(inspect.getfullargspec(function)).encode())
+        hasher.update(repr(getfullargspec_except_self(function)).encode())
     except TypeError:
         pass
     try:
@@ -90,7 +97,7 @@ def required_args(target, args=(), kwargs=()):
     if inspect.isclass(target) and is_typed_named_tuple(target):
         provided = set(kwargs) | set(target._fields[: len(args)])
         return set(target._fields) - provided
-    # Then we try to do the right thing with inspect.getfullargspec
+    # Then we try to do the right thing with getfullargspec_except_self
     # Note that for classes we inspect the __init__ method, *unless* the class
     # has an explicit __signature__ attribute.  This allows us to support
     # runtime-generated constraints on **kwargs, as for e.g. Pydantic models.
@@ -121,7 +128,7 @@ def convert_keyword_arguments(function, args, kwargs):
     passed as positional and keyword args to the function. Unless function has
     kwonlyargs or **kwargs the dictionary will always be empty.
     """
-    argspec = inspect.getfullargspec(function)
+    argspec = getfullargspec_except_self(function)
     new_args = []
     kwargs = dict(kwargs)
 
@@ -167,7 +174,7 @@ def convert_positional_arguments(function, args, kwargs):
 
     new_args will only be non-empty if function has a variadic argument.
     """
-    argspec = inspect.getfullargspec(function)
+    argspec = getfullargspec_except_self(function)
     new_kwargs = dict(argspec.kwonlydefaults or {})
     new_kwargs.update(kwargs)
     if not argspec.varkw:
@@ -398,7 +405,7 @@ def arg_string(f, args, kwargs, reorder=True):
     if reorder:
         args, kwargs = convert_positional_arguments(f, args, kwargs)
 
-    argspec = inspect.getfullargspec(f)
+    argspec = getfullargspec_except_self(f)
 
     bits = []
 
@@ -476,7 +483,7 @@ def define_function_signature(name, docstring, argspec):
         check_valid_identifier(a)
 
     def accept(f):
-        fargspec = inspect.getfullargspec(f)
+        fargspec = getfullargspec_except_self(f)
         must_pass_as_kwargs = []
         invocation_parts = []
         for a in argspec.args:
@@ -560,7 +567,7 @@ def accept(proxy):
                 define_function_signature(
                     target.__name__.replace("<lambda>", "_lambda_"),
                     target.__doc__,
-                    inspect.getfullargspec(target),
+                    getfullargspec_except_self(target),
                 )(proxy)
             )
         )


Test patch:
diff --git a/hypothesis-python/tests/cover/test_reflection.py b/hypothesis-python/tests/cover/test_reflection.py
--- a/hypothesis-python/tests/cover/test_reflection.py
+++ b/hypothesis-python/tests/cover/test_reflection.py
@@ -23,7 +23,7 @@
 import pytest
 from pytest import raises
 
-from hypothesis import strategies as st
+from hypothesis import given, strategies as st
 from hypothesis.internal import reflection
 from hypothesis.internal.reflection import (
     arg_string,
@@ -676,3 +676,14 @@ def test_too_many_posargs_fails():
 def test_overlapping_posarg_kwarg_fails():
     with pytest.raises(TypeError):
         st.times(time.min, time.max, st.none(), timezones=st.just(None)).validate()
+
+
+def test_inline_given_handles_self():
+    # Regression test for https://github.com/HypothesisWorks/hypothesis/issues/961
+    class Cls:
+        def method(self, **kwargs):
+            assert isinstance(self, Cls)
+            assert kwargs["k"] is sentinel
+
+    sentinel = object()
+    given(k=st.just(sentinel))(Cls().method)()