Repository: numpy/numpy

Issue:
`deprecate` adds extra indentation
Suppose I do:

``` python
>>> def foo():
...     """This is the docstring for foo.
...
...     It has two lines.
...     """
...
>>> help(foo)
```

I get this:

```
Help on function foo in module __main__:

foo()
    This is the docstring for foo.

    It has two lines.
```

Now let's deprecate `foo`:

``` python
>>> foo = np.deprecate(foo, new_name='bar')
>>> help(foo)
```

The docstring now reads:

```
Help on function foo in module numpy.lib.utils:

foo(*args, **kwds)
    `foo` is deprecated, use `bar` instead!

    This is the docstring for foo.

        It has two lines.
```

The third line ("It has two lines") has gained an extra level of indentation.



Discussion:

Resolving patch:
diff --git a/numpy/lib/utils.py b/numpy/lib/utils.py
--- a/numpy/lib/utils.py
+++ b/numpy/lib/utils.py
@@ -105,6 +105,20 @@ def newfunc(*args,**kwds):
         if doc is None:
             doc = depdoc
         else:
+            lines = doc.expandtabs().split('\n')
+            indent = _get_indent(lines[1:])
+            if lines[0].lstrip():
+                # Indent the original first line to let inspect.cleandoc()
+                # dedent the docstring despite the deprecation notice.
+                doc = indent * ' ' + doc
+            else:
+                # Remove the same leading blank lines as cleandoc() would.
+                skip = len(lines[0]) + 1
+                for line in lines[1:]:
+                    if len(line) > indent:
+                        break
+                    skip += len(line) + 1
+                doc = doc[skip:]
             doc = '\n\n'.join([depdoc, doc])
         newfunc.__doc__ = doc
         try:
@@ -115,6 +129,21 @@ def newfunc(*args,**kwds):
             newfunc.__dict__.update(d)
         return newfunc
 
+
+def _get_indent(lines):
+    """
+    Determines the leading whitespace that could be removed from all the lines.
+    """
+    indent = sys.maxsize
+    for line in lines:
+        content = len(line.lstrip())
+        if content:
+            indent = min(indent, len(line) - content)
+    if indent == sys.maxsize:
+        indent = 0
+    return indent
+
+
 def deprecate(*args, **kwargs):
     """
     Issues a DeprecationWarning, adds warning to `old_name`'s


Test patch:
diff --git a/numpy/lib/tests/test_utils.py b/numpy/lib/tests/test_utils.py
--- a/numpy/lib/tests/test_utils.py
+++ b/numpy/lib/tests/test_utils.py
@@ -1,5 +1,6 @@
 from __future__ import division, absolute_import, print_function
 
+import inspect
 import sys
 import pytest
 
@@ -38,6 +39,32 @@ def old_func3(self, x):
 new_func3 = deprecate(old_func3, old_name="old_func3", new_name="new_func3")
 
 
+def old_func4(self, x):
+    """Summary.
+
+    Further info.
+    """
+    return x
+new_func4 = deprecate(old_func4)
+
+
+def old_func5(self, x):
+    """Summary.
+
+        Bizarre indentation.
+    """
+    return x
+new_func5 = deprecate(old_func5)
+
+
+def old_func6(self, x):
+    """
+    Also in PEP-257.
+    """
+    return x
+new_func6 = deprecate(old_func6)
+
+
 def test_deprecate_decorator():
     assert_('deprecated' in old_func.__doc__)
 
@@ -51,6 +78,25 @@ def test_deprecate_fn():
     assert_('new_func3' in new_func3.__doc__)
 
 
+@pytest.mark.skipif(sys.flags.optimize == 2, reason="-OO discards docstrings")
+def test_deprecate_help_indentation():
+    _compare_docs(old_func4, new_func4)
+    _compare_docs(old_func5, new_func5)
+    _compare_docs(old_func6, new_func6)
+
+
+def _compare_docs(old_func, new_func):
+    old_doc = inspect.getdoc(old_func)
+    new_doc = inspect.getdoc(new_func)
+    index = new_doc.index('\n\n') + 2
+    assert_equal(new_doc[index:], old_doc)
+
+
+@pytest.mark.skipif(sys.flags.optimize == 2, reason="-OO discards docstrings")
+def test_deprecate_preserve_whitespace():
+    assert_('\n        Bizarre' in new_func5.__doc__)
+
+
 def test_safe_eval_nameconstant():
     # Test if safe_eval supports Python 3.4 _ast.NameConstant
     utils.safe_eval('None')