Repository: optuna/optuna

Issue:
`experimental` decorator adds extra indents to docs
An extra indent appears in the docs when the [`experimental`](https://github.com/optuna/optuna/blob/2d5f24b06eed56ece72b8dfa878135bb4bb63779/optuna/_experimental.py#L60) decorator is added to a method of a class. This does not happen with a top level (i.e., not nested) class or function.

## Steps to reproduce

1. add an `experimental` decorator to a method of a class
2. make the document

## Reproducible examples
The following figure shows the docs of [`Study.enqueue_trial`](https://optuna.readthedocs.io/en/latest/reference/study.html#optuna.study.Study.enqueue_trial). An extra indent appears after the second paragraph.

![image](https://user-images.githubusercontent.com/1061922/81144389-e6f32f80-8fae-11ea-8dd2-33368293dafa.png)

## Expected behavior
no extra indent is shown after the second paragraph

For example, the docs of [`Study.get_trials`](https://optuna.readthedocs.io/en/latest/reference/study.html#optuna.study.Study.get_trials), which does not have the `experimental` decorator, appears as expected.

![image](https://user-images.githubusercontent.com/1061922/81143489-f1142e80-8fac-11ea-9896-e56086228168.png)

The following figure shows another example. The docs of the [`train`](https://optuna.readthedocs.io/en/latest/reference/integration.html#optuna.integration.lightgbm.train) function of LightGBMTuner has no extra indent. Although the function has an `experimental` decorator, it belongs to the top level classes.

![image](https://user-images.githubusercontent.com/1061922/81146606-98945f80-8fb3-11ea-8d67-5cf1cfbf8768.png)



Discussion:
It seems we need to indent the `.. note::` block when decorating a class method.

```bash
>>> print(Study.enqueue_trial.__doc__)
Enqueue a trial with given parameter values.

        You can fix the next sampling parameters which will be evaluated in your
        objective function.

        Example:

            .. testcode::

                import optuna

                def objective(trial):
                    x = trial.suggest_uniform('x', 0, 10)
                    return x ** 2

                study = optuna.create_study()
                study.enqueue_trial({'x': 5})
                study.enqueue_trial({'x': 0})
                study.optimize(objective, n_trials=2)

                assert study.trials[0].params == {'x': 5}
                assert study.trials[1].params == {'x': 0}

        Args:
            params:
                Parameter values to pass your objective function.
        

    .. note::
        Added in v1.2.0 as an experimental feature. The interface may change in newer versions
        without prior notice. See https://github.com/optuna/optuna/releases/tag/v1.2.0.
```

---

```
>>> print(lightgbm_tuner.train.__doc__)
Wrapper of LightGBM Training API to tune hyperparameters.

    It tunes important hyperparameters (e.g., `min_child_samples` and `feature_fraction`) in a
    stepwise manner. Arguments and keyword arguments for `lightgbm.train()
    <https://lightgbm.readthedocs.io/en/latest/pythonapi/lightgbm.train.html>`_ can be passed.
    

    .. note::
        Added in v0.18.0 as an experimental feature. The interface may change in newer versions
        without prior notice. See https://github.com/optuna/optuna/releases/tag/v0.18.0.
```
@g-votte 
Can I work on this?
@harupy 
Sure! Please go for it!

Resolving patch:
diff --git a/optuna/_experimental.py b/optuna/_experimental.py
--- a/optuna/_experimental.py
+++ b/optuna/_experimental.py
@@ -1,5 +1,6 @@
 import functools
 import inspect
+import textwrap
 from typing import Any
 from typing import Callable
 import warnings
@@ -7,14 +8,11 @@
 from optuna.exceptions import ExperimentalWarning
 
 
-# White spaces of each line are necessary to beautifully rendered documentation.
-# NOTE(crcrpar): When `experimental` decorator is applied to member methods, these lines require
-# another four spaces.
-_EXPERIMENTAL_DOCSTRING_TEMPLATE = """
+_EXPERIMENTAL_NOTE_TEMPLATE = """
 
-    .. note::
-        Added in v{ver} as an experimental feature. The interface may change in newer versions
-        without prior notice. See https://github.com/optuna/optuna/releases/tag/v{ver}.
+.. note::
+    Added in v{ver} as an experimental feature. The interface may change in newer versions
+    without prior notice. See https://github.com/optuna/optuna/releases/tag/v{ver}.
 """
 
 
@@ -28,6 +26,10 @@ def _validate_version(version: str) -> None:
         )
 
 
+def _get_docstring_indent(docstring: str) -> str:
+    return docstring.split("\n")[-1] if "\n" in docstring else ""
+
+
 def experimental(version: str, name: str = None) -> Any:
     """Decorate class or function as experimental.
 
@@ -43,16 +45,16 @@ def _experimental_wrapper(f: Any) -> Any:
 
         def _experimental_func(func: Callable[[Any], Any]) -> Callable[[Any], Any]:
 
-            docstring = _EXPERIMENTAL_DOCSTRING_TEMPLATE.format(ver=version)
             if func.__doc__ is None:
                 func.__doc__ = ""
-            func.__doc__ += docstring
+
+            note = _EXPERIMENTAL_NOTE_TEMPLATE.format(ver=version)
+            indent = _get_docstring_indent(func.__doc__)
+            func.__doc__ = func.__doc__.strip() + textwrap.indent(note, indent) + indent
 
             # TODO(crcrpar): Annotate this correctly.
             @functools.wraps(func)
             def new_func(*args: Any, **kwargs: Any) -> Any:
-                """Wrapped function."""
-
                 warnings.warn(
                     "{} is experimental (supported from v{}). "
                     "The interface can change in the future.".format(
@@ -70,7 +72,6 @@ def _experimental_class(cls: Any) -> Any:
 
             This decorator is supposed to be applied to the experimental class.
             """
-
             _original_init = cls.__init__
 
             @functools.wraps(_original_init)
@@ -89,7 +90,11 @@ def wrapped_init(self, *args, **kwargs) -> None:  # type: ignore
 
             if cls.__doc__ is None:
                 cls.__doc__ = ""
-            cls.__doc__ += _EXPERIMENTAL_DOCSTRING_TEMPLATE.format(ver=version)
+
+            note = _EXPERIMENTAL_NOTE_TEMPLATE.format(ver=version)
+            indent = _get_docstring_indent(cls.__doc__)
+            cls.__doc__ = cls.__doc__.strip() + textwrap.indent(note, indent) + indent
+
             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
@@ -15,53 +15,69 @@ class _Sample(object):
     def __init__(self, a: Any, b: Any, c: Any) -> None:
         pass
 
+    def _method(self) -> None:
+        """summary
 
-@pytest.mark.parametrize("version", ["1.1.0", "1.1", 100, None])
-def test_experimental_decorator(version: str) -> None:
-
-    if version != "1.1.0":
-        with pytest.raises(ValueError):
-            _experimental.experimental(version)
-    else:
-        decorator_experimental = _experimental.experimental(version)
-        assert (
-            callable(decorator_experimental)
-            and decorator_experimental.__name__ == "_experimental_wrapper"
-        )
-
-        decorated_sample_func = decorator_experimental(_sample_func)
-        assert decorated_sample_func.__name__ == "_sample_func"
-        assert (
-            decorated_sample_func.__doc__
-            == _experimental._EXPERIMENTAL_DOCSTRING_TEMPLATE.format(ver=version)
-        )
-
-        with pytest.warns(ExperimentalWarning):
-            decorated_sample_func(None)
-
-
-@pytest.mark.parametrize("version", ["1.1.0", "1.1", 100, None])
-def test_experimental_class_decorator(version: str) -> None:
-
-    if version != "1.1.0":
-        with pytest.raises(ValueError):
-            _experimental.experimental(version)
-    else:
-        decorator_experimental = _experimental.experimental(version)
-        assert (
-            callable(decorator_experimental)
-            and decorator_experimental.__name__ == "_experimental_wrapper"
-        )
-
-        decorated_sample = decorator_experimental(_Sample)
-        assert decorated_sample.__name__ == "_Sample"
-        assert decorated_sample.__init__.__name__ == "__init__"
-        assert decorated_sample.__doc__ == _experimental._EXPERIMENTAL_DOCSTRING_TEMPLATE.format(
-            ver=version
-        )
-
-        with pytest.warns(ExperimentalWarning):
-            decorated_sample("a", "b", "c")
+        detail
+        """
+        pass
+
+    def _method_expected(self) -> None:
+        """summary
+
+        detail
+
+        .. note::
+            Added in v1.1.0 as an experimental feature. The interface may change in newer versions
+            without prior notice. See https://github.com/optuna/optuna/releases/tag/v1.1.0.
+        """
+        pass
+
+
+@pytest.mark.parametrize("version", ["1.1", 100, None])
+def test_experimental_raises_error_for_invalid_version(version: Any) -> None:
+    with pytest.raises(ValueError):
+        _experimental.experimental(version)
+
+
+def test_experimental_decorator() -> None:
+    version = "1.1.0"
+    decorator_experimental = _experimental.experimental(version)
+    assert callable(decorator_experimental)
+
+    decorated_func = decorator_experimental(_sample_func)
+    assert decorated_func.__name__ == _sample_func.__name__
+    assert decorated_func.__doc__ == _experimental._EXPERIMENTAL_NOTE_TEMPLATE.format(ver=version)
+
+    with pytest.warns(ExperimentalWarning):
+        decorated_func(None)
+
+
+def test_experimental_method_decorator() -> None:
+    version = "1.1.0"
+    decorator_experimental = _experimental.experimental(version)
+    assert callable(decorator_experimental)
+
+    decorated_method = decorator_experimental(_Sample._method)
+    assert decorated_method.__name__ == _Sample._method.__name__
+    assert decorated_method.__doc__ == _Sample._method_expected.__doc__
+
+    with pytest.warns(ExperimentalWarning):
+        decorated_method(None)
+
+
+def test_experimental_class_decorator() -> None:
+    version = "1.1.0"
+    decorator_experimental = _experimental.experimental(version)
+    assert callable(decorator_experimental)
+
+    decorated_class = decorator_experimental(_Sample)
+    assert decorated_class.__name__ == "_Sample"
+    assert decorated_class.__init__.__name__ == "__init__"
+    assert decorated_class.__doc__ == _experimental._EXPERIMENTAL_NOTE_TEMPLATE.format(ver=version)
+
+    with pytest.warns(ExperimentalWarning):
+        decorated_class("a", "b", "c")
 
 
 def test_experimental_decorator_name() -> None: