Repository: HypothesisWorks/hypothesis

Issue:
Composite decorator doesn't preserve docstring
Seems like a nice to have at least when creating a lib of strategies for others to use


Discussion:
Sampled from has the same problem. Maybe all decorators? Fix could be as simple as decorating all decorators with decorator.decorator and no, it's not a tongue twister.

Resolving patch:
diff --git a/src/hypothesis/core.py b/src/hypothesis/core.py
--- a/src/hypothesis/core.py
+++ b/src/hypothesis/core.py
@@ -39,8 +39,9 @@
 from hypothesis.statistics import note_engine_for_statistics
 from hypothesis.internal.compat import getargspec, str_to_bytes
 from hypothesis.internal.reflection import nicerepr, arg_string, \
-    impersonate, copy_argspec, function_digest, fully_qualified_name, \
-    convert_positional_arguments, get_pretty_function_description
+    impersonate, function_digest, fully_qualified_name, \
+    define_function_signature, convert_positional_arguments, \
+    get_pretty_function_description
 from hypothesis.searchstrategy.strategies import SearchStrategy
 
 
@@ -216,8 +217,8 @@ def wrapped_test(*arguments, **kwargs):
         )
 
         @impersonate(test)
-        @copy_argspec(
-            test.__name__, argspec
+        @define_function_signature(
+            test.__name__, test.__doc__, argspec
         )
         def wrapped_test(*arguments, **kwargs):
             settings = wrapped_test._hypothesis_internal_use_settings
diff --git a/src/hypothesis/internal/reflection.py b/src/hypothesis/internal/reflection.py
--- a/src/hypothesis/internal/reflection.py
+++ b/src/hypothesis/internal/reflection.py
@@ -365,9 +365,9 @@ def %(name)s(%(argspec)s):
 """.strip() + '\n'
 
 
-def copy_argspec(name, argspec):
-    """A decorator which sets the name and argspec of the function passed into
-    it."""
+def define_function_signature(name, docstring, argspec):
+    """A decorator which sets the name, argspec and docstring of the function
+    passed into it."""
     check_valid_identifier(name)
     for a in argspec.args:
         check_valid_identifier(a)
@@ -425,6 +425,7 @@ def accept(f):
             }).accept
 
         result = base_accept(f)
+        result.__doc__ = docstring
         result.__defaults__ = argspec.defaults
         return result
     return accept
@@ -453,5 +454,6 @@ def accept(f):
 def proxies(target):
     def accept(proxy):
         return impersonate(target)(wraps(target)(
-            copy_argspec(target.__name__, getargspec(target))(proxy)))
+            define_function_signature(
+                target.__name__, target.__doc__, getargspec(target))(proxy)))
     return accept
diff --git a/src/hypothesis/strategies.py b/src/hypothesis/strategies.py
--- a/src/hypothesis/strategies.py
+++ b/src/hypothesis/strategies.py
@@ -881,7 +881,7 @@ def composite(f):
 
     """
 
-    from hypothesis.internal.reflection import copy_argspec
+    from hypothesis.internal.reflection import define_function_signature
     argspec = getargspec(f)
 
     if (
@@ -902,7 +902,7 @@ def composite(f):
     )
 
     @defines_strategy
-    @copy_argspec(f.__name__, new_argspec)
+    @define_function_signature(f.__name__, f.__doc__, new_argspec)
     def accept(*args, **kwargs):
         class CompositeStrategy(SearchStrategy):
 


Test patch:
diff --git a/tests/cover/test_reflection.py b/tests/cover/test_reflection.py
--- a/tests/cover/test_reflection.py
+++ b/tests/cover/test_reflection.py
@@ -21,12 +21,14 @@
 from copy import deepcopy
 from functools import partial
 
+import pytest
+
 from tests.common.utils import raises
 from hypothesis.internal.compat import PY3, ArgSpec, getargspec
 from hypothesis.internal.reflection import proxies, arg_string, \
-    copy_argspec, unbind_method, eval_directory, function_digest, \
-    fully_qualified_name, source_exec_as_module, \
-    convert_keyword_arguments, convert_positional_arguments, \
+    unbind_method, eval_directory, function_digest, fully_qualified_name, \
+    source_exec_as_module, convert_keyword_arguments, \
+    define_function_signature, convert_positional_arguments, \
     get_pretty_function_description
 
 
@@ -423,42 +425,58 @@ def has_kwargs(**kwargs):
     pass
 
 
-def test_copying_preserves_argspec():
-    for f in [has_one_arg, has_two_args, has_varargs, has_kwargs]:
-        af = getargspec(f)
-        t = copy_argspec('foo', getargspec(f))(universal_acceptor)
-        at = getargspec(t)
-        assert af.args == at.args
-        assert af.varargs == at.varargs
-        assert af.keywords == at.keywords
-        assert len(af.defaults or ()) == len(at.defaults or ())
+@pytest.mark.parametrize('f', [
+    has_one_arg,
+    has_two_args,
+    has_varargs,
+    has_kwargs,
+])
+def test_copying_preserves_argspec(f):
+    af = getargspec(f)
+    t = define_function_signature(
+        'foo', 'docstring', getargspec(f))(universal_acceptor)
+    at = getargspec(t)
+    assert af.args == at.args
+    assert af.varargs == at.varargs
+    assert af.keywords == at.keywords
+    assert len(af.defaults or ()) == len(at.defaults or ())
 
 
 def test_name_does_not_clash_with_function_names():
     def f():
         pass
 
-    @copy_argspec('f', getargspec(f))
+    @define_function_signature('f', 'A docstring for f', getargspec(f))
     def g():
         pass
     g()
 
 
 def test_copying_sets_name():
-    f = copy_argspec(
-        'hello_world', getargspec(has_two_args))(universal_acceptor)
+    f = define_function_signature(
+        'hello_world', 'A docstring for hello_world',
+        getargspec(has_two_args))(universal_acceptor)
     assert f.__name__ == 'hello_world'
 
 
+def test_copying_sets_docstring():
+    f = define_function_signature(
+        'foo', 'A docstring for foo',
+        getargspec(has_two_args))(universal_acceptor)
+    assert f.__doc__ == 'A docstring for foo'
+
+
 def test_uses_defaults():
-    f = copy_argspec(
-        'foo', getargspec(has_a_default))(universal_acceptor)
+    f = define_function_signature(
+        'foo', 'A docstring for foo',
+        getargspec(has_a_default))(universal_acceptor)
     assert f(3, 2) == ((3, 2, 1), {})
 
 
 def test_uses_varargs():
-    f = copy_argspec(
-        'foo', getargspec(has_varargs))(universal_acceptor)
+    f = define_function_signature(
+        'foo', 'A docstring for foo',
+        getargspec(has_varargs))(universal_acceptor)
     assert f(1, 2) == ((1, 2), {})
 
 
@@ -486,36 +504,36 @@ def test_exec_leaves_sys_path_unchanged():
     assert sys.path == old_path
 
 
-def test_copy_argspec_works_with_conflicts():
+def test_define_function_signature_works_with_conflicts():
     def accepts_everything(*args, **kwargs):
         pass
 
-    copy_argspec('hello', ArgSpec(
+    define_function_signature('hello', 'A docstring for hello', ArgSpec(
         args=('f',), varargs=None, keywords=None, defaults=None
     ))(accepts_everything)(1)
 
-    copy_argspec('hello', ArgSpec(
+    define_function_signature('hello', 'A docstring for hello', ArgSpec(
         args=(), varargs='f', keywords=None, defaults=None
     ))(accepts_everything)(1)
 
-    copy_argspec('hello', ArgSpec(
+    define_function_signature('hello', 'A docstring for hello', ArgSpec(
         args=(), varargs=None, keywords='f', defaults=None
     ))(accepts_everything)()
 
-    copy_argspec('hello', ArgSpec(
+    define_function_signature('hello', 'A docstring for hello', ArgSpec(
         args=('f', 'f_3'), varargs='f_1', keywords='f_2', defaults=None
     ))(accepts_everything)(1, 2)
 
 
-def test_copy_argspec_validates_arguments():
+def test_define_function_signature_validates_arguments():
     with raises(ValueError):
-        copy_argspec('hello_world', ArgSpec(
+        define_function_signature('hello_world', None, ArgSpec(
             args=['a b'], varargs=None, keywords=None, defaults=None))
 
 
-def test_copy_argspec_validates_function_name():
+def test_define_function_signature_validates_function_name():
     with raises(ValueError):
-        copy_argspec('hello world', ArgSpec(
+        define_function_signature('hello world', None, ArgSpec(
             args=['a', 'b'], varargs=None, keywords=None, defaults=None))