Repository: HypothesisWorks/hypothesis

Issue:
varargs handling doesn't work with methods
As mentioned on #113, the new varargs handling doesn't work with methods.

This is because all keyword arguments to `given` are converted into positional arguments in the wrapped tests and these catch any non-generated positional arguments (such a `self` for a method) instead of leaving them in varargs.

The fix is probably to handle `extra_kwargs` (keyword arguments to `given` that don't match named args in the argspec) differently.



Discussion:

Resolving patch:
diff --git a/src/hypothesis/core.py b/src/hypothesis/core.py
--- a/src/hypothesis/core.py
+++ b/src/hypothesis/core.py
@@ -423,7 +423,7 @@ def run_test_with_generator(test):
                 ' expected at most %d') % (
                     test.__name__, len(generator_arguments),
                     len(original_argspec.args)))
-        arguments = original_argspec.args + sorted(extra_kwargs)
+        arguments = original_argspec.args
         specifiers = list(generator_arguments)
         seen_kwarg = None
         for a in arguments:
@@ -446,6 +446,10 @@ def run_test_with_generator(test):
             defaults=tuple(map(HypothesisProvided, specifiers))
         )
 
+        unused_kwargs = {}
+        for k in extra_kwargs:
+            unused_kwargs[k] = HypothesisProvided(generator_kwargs[k])
+
         @copy_argspec(
             test.__name__, argspec
         )
@@ -453,11 +457,18 @@ def wrapped_test(*arguments, **kwargs):
             selfy = None
             arguments, kwargs = convert_positional_arguments(
                 wrapped_test, arguments, kwargs)
-            # Because we converted all kwargs to given into real args and
-            # error if we have neither args nor kwargs, this should always
-            # be valid
-            assert argspec.args
-            selfy = kwargs.get(argspec.args[0])
+            # Anything in unused_kwargs hasn't been injected through
+            # argspec.defaults, so we need to add them.
+            for k in unused_kwargs:
+                if k not in kwargs:
+                    kwargs[k] = unused_kwargs[k]
+            # If the test function is a method of some kind, the bound object
+            # will be the first named argument if there are any, otherwise the
+            # first vararg (if any).
+            if argspec.args:
+                selfy = kwargs.get(argspec.args[0])
+            elif arguments:
+                selfy = arguments[0]
             if isinstance(selfy, HypothesisProvided):
                 selfy = None
             test_runner = executor(selfy)


Test patch:
diff --git a/tests/cover/test_manual_given_invocation.py b/tests/cover/test_manual_given_invocation.py
--- a/tests/cover/test_manual_given_invocation.py
+++ b/tests/cover/test_manual_given_invocation.py
@@ -62,9 +62,18 @@ def test_argspec_lines_up(f, g):
     assert af.varargs == ag.varargs
 
 
-def test_converts_provided_kwargs_into_args():
+def test_does_not_convert_unknown_kwargs_into_args():
     @given(hello=int, world=int)
-    def greet(**kwargs):
+    def greet(hello, **kwargs):
         pass
 
-    assert inspect.getargspec(greet).args == ['hello', 'world']
+    assert inspect.getargspec(greet).args == ['hello']
+
+
+def test_provided_kwargs_are_defaults():
+    @given(hello=booleans(), world=booleans())
+    def greet(hello, **kwargs):
+        assert hello == 'salve'
+        assert kwargs == {'world': 'mundi'}
+
+    greet('salve', world='mundi')
diff --git a/tests/cover/test_testdecorators.py b/tests/cover/test_testdecorators.py
--- a/tests/cover/test_testdecorators.py
+++ b/tests/cover/test_testdecorators.py
@@ -93,6 +93,22 @@ class TestCases(object):
     @given(integers())
     def test_abs_non_negative(self, x):
         assert abs(x) >= 0
+        assert isinstance(self, TestCases)
+
+    @given(x=integers())
+    def test_abs_non_negative_varargs(self, x, *args):
+        assert abs(x) >= 0
+        assert isinstance(self, TestCases)
+
+    @given(x=integers())
+    def test_abs_non_negative_varargs_kwargs(self, *args, **kw):
+        assert abs(kw['x']) >= 0
+        assert isinstance(self, TestCases)
+
+    @given(x=integers())
+    def test_abs_non_negative_varargs_kwargs_only(*args, **kw):
+        assert abs(kw['x']) >= 0
+        assert isinstance(args[0], TestCases)
 
     @fails
     @given(integers())