Repository: optuna/optuna

Issue:
`experimental` decorator breaks class documentation.
The `experimental` decorator used on classes break documentation. This could be one manifestation but there is an issue with how the documentation including type hints are propagated to the decorated class. This does not apply for free functions.

See https://github.com/optuna/optuna/pull/1265#issuecomment-633195955 for how it may break.

## Expected behavior

Class documentation should not be altered by applying the experimental decorator.

## Steps to reproduce

1. Apply the experimental decorator to a class.
1. Build the document (`cd docs && make html`) 
1. Open the rendered documentation and note that the class signatures is broken.

## Additional context (optional)

- An issue regarding the indentation https://github.com/optuna/optuna/issues/1213.



Discussion:
@hvy I think the root cause of this issue is [_make_func_spec_str](https://github.com/optuna/optuna/blob/master/optuna/_experimental.py#L121) which seems to ignore keyword-only arguments properly.

I'm not sure why we need to add this `func_spec_str` when decorating a class.
I found `wrapped_init` is not decorated by `functools.wraps`.

https://github.com/optuna/optuna/blob/e48e4624a486a68484c4824fed243207df000361/optuna/_experimental.py#L104-L106

This changes the signature of a class that `experimental` decorates to `(*args, **kwargs) -> None`.

```python
>>> import optuna
>>> import signature
>>> inspect.signature(optuna.integration.AllenNLPExecutor)
<Signature (*args, **kwargs) -> None>
```

`func_spec_str` is inserted to override this incorrect class signature. For example,

<img width="301" alt="Screen Shot 2020-05-25 at 19 54 41" src="https://user-images.githubusercontent.com/17039389/82806840-99892480-9ec1-11ea-8797-be776d32e26c.png">

generates

<img width="635" alt="Screen Shot 2020-05-25 at 19 53 52" src="https://user-images.githubusercontent.com/17039389/82806806-86765480-9ec1-11ea-9420-ea1edd5c83ac.png">

Decorating `wrapped_init` by `functools.wraps` and removing `func_spec_str` should solve this issue.
It looks fine. I'll submit a PR.

Before:

<img width="709" alt="Screen Shot 2020-05-25 at 20 11 42" src="https://user-images.githubusercontent.com/17039389/82808016-030a3280-9ec4-11ea-8ee7-ce7c90d3e3d7.png">


After:

<img width="709" alt="Screen Shot 2020-05-25 at 20 06 31" src="https://user-images.githubusercontent.com/17039389/82807774-6778c200-9ec3-11ea-8f9c-90409c32800a.png">

Thank you for the swift debugging. Looking forward to your PR.

Resolving patch:
diff --git a/optuna/_experimental.py b/optuna/_experimental.py
--- a/optuna/_experimental.py
+++ b/optuna/_experimental.py
@@ -18,35 +18,6 @@
 """
 
 
-def _make_func_spec_str(func: Callable[..., Any]) -> str:
-
-    name = func.__name__
-    argspec = inspect.getfullargspec(func)
-
-    n_defaults = len(argspec.defaults) if argspec.defaults is not None else 0
-    offset = int(len(argspec.args) > 0 and argspec.args[0] == "self")
-
-    if n_defaults > 0:
-        args = ", ".join(argspec.args[offset:-n_defaults])
-        with_default_values = ", ".join(
-            [
-                "{}={}".format(a, d)
-                for a, d in zip(argspec.args[-n_defaults:], argspec.defaults)  # type: ignore
-            ]
-        )
-    else:
-        args = ", ".join(argspec.args[offset:])
-        with_default_values = ""
-
-    if len(args) > 0 and len(with_default_values) > 0:
-        args += ", "
-
-    # NOTE(crcrpar): The four spaces are necessary to correctly render documentation.
-    # Different classes or methods require more spaces.
-    str_args_description = "(" + args + with_default_values + ")\n\n    "
-    return name + str_args_description
-
-
 def _validate_version(version: str) -> None:
 
     if not isinstance(version, str) or len(version.split(".")) != 3:
@@ -102,6 +73,7 @@ def _experimental_class(cls: Any) -> Any:
 
             _original_init = cls.__init__
 
+            @functools.wraps(_original_init)
             def wrapped_init(self, *args, **kwargs) -> None:  # type: ignore
                 warnings.warn(
                     "{} is experimental (supported from v{}). "
@@ -117,11 +89,7 @@ def wrapped_init(self, *args, **kwargs) -> None:  # type: ignore
 
             if cls.__doc__ is None:
                 cls.__doc__ = ""
-            cls.__doc__ = (
-                _make_func_spec_str(_original_init)
-                + cls.__doc__
-                + _EXPERIMENTAL_DOCSTRING_TEMPLATE.format(ver=version)
-            )
+            cls.__doc__ += _EXPERIMENTAL_DOCSTRING_TEMPLATE.format(ver=version)
             return cls
 
         return _experimental_class(f) if inspect.isclass(f) else _experimental_func(f)


Test patch:
diff --git a/tests/test_experimental.py b/tests/test_experimental.py
--- a/tests/test_experimental.py
+++ b/tests/test_experimental.py
@@ -11,30 +11,11 @@ def _sample_func(_: Any) -> int:
     return 10
 
 
-def _f() -> None:
-    pass
-
-
-def _g(a: Any, b: Any = None) -> None:
-    pass
-
-
-def _h(a: Any = None, b: int = 10) -> None:
-    pass
-
-
 class _Sample(object):
     def __init__(self, a: Any, b: Any, c: Any) -> None:
         pass
 
 
-def test_str() -> None:
-
-    assert _experimental._make_func_spec_str(_f) == "_f()\n\n    "
-    assert _experimental._make_func_spec_str(_g) == "_g(a, b=None)\n\n    "
-    assert _experimental._make_func_spec_str(_h) == "_h(a=None, b=10)\n\n    "
-
-
 @pytest.mark.parametrize("version", ["1.1.0", "1.1", 100, None])
 def test_experimental_decorator(version: str) -> None:
 
@@ -74,10 +55,9 @@ def test_experimental_class_decorator(version: str) -> None:
 
         decorated_sample = decorator_experimental(_Sample)
         assert decorated_sample.__name__ == "_Sample"
-        assert (
-            decorated_sample.__doc__
-            == "__init__(a, b, c)\n\n    "
-            + _experimental._EXPERIMENTAL_DOCSTRING_TEMPLATE.format(ver=version)
+        assert decorated_sample.__init__.__name__ == "__init__"
+        assert decorated_sample.__doc__ == _experimental._EXPERIMENTAL_DOCSTRING_TEMPLATE.format(
+            ver=version
         )
 
         with pytest.warns(ExperimentalWarning):