codecov-io: # [Codecov](https://codecov.io/gh/briancurtin/deprecation/pull/12?src=pr&el=h1) Report
> Merging [#12](https://codecov.io/gh/briancurtin/deprecation/pull/12?src=pr&el=desc) into [master](https://codecov.io/gh/briancurtin/deprecation/commit/70bb27fef996b214d7b698ed3124fb598220eea9?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/briancurtin/deprecation/pull/12/graphs/tree.svg?width=650&height=150&token=6w8zqu2CEQ&src=pr)](https://codecov.io/gh/briancurtin/deprecation/pull/12?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master    #12   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files           1      1           
  Lines          56     64    +8     
  Branches        9     10    +1     
=====================================
+ Hits           56     64    +8
```


| [Impacted Files](https://codecov.io/gh/briancurtin/deprecation/pull/12?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [deprecation.py](https://codecov.io/gh/briancurtin/deprecation/pull/12/diff?src=pr&el=tree#diff-ZGVwcmVjYXRpb24ucHk=) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/briancurtin/deprecation/pull/12?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/briancurtin/deprecation/pull/12?src=pr&el=footer). Last update [70bb27f...adfcd2e](https://codecov.io/gh/briancurtin/deprecation/pull/12?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/deprecation.py b/deprecation.py
index 28a21ee..00598db 100644
--- a/deprecation.py
+++ b/deprecation.py
@@ -10,6 +10,7 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 import functools
+import textwrap
 import warnings
 
 from packaging import version
@@ -167,8 +168,31 @@ def deprecated(deprecated_in=None, removed_in=None, current_version=None,
             deprecation_note = ("*Deprecated{deprecated_in}{removed_in}"
                                 "{period}{details}*".format(**parts))
 
-            function.__doc__ = "\n\n".join([existing_docstring,
+            pos = existing_docstring.find("\n")
+
+            if pos != -1:
+                # With a multi-line docstring, when we modify
+                # existing_docstring to add our deprecation_note,
+                # if we're not careful we'll interfere with the
+                # indentation levels of the contents below the
+                # first line, or as PEP 257 calls it, the summary
+                # line. Since the summary line can start on the
+                # same line as the """, dedenting the whole thing
+                # won't help. Split the summary and contents up,
+                # dedent the contents independently, then join
+                # summary, dedent'ed contents, and our
+                # deprecation_note.
+
+                summary = existing_docstring[:pos]
+                contents = existing_docstring[pos:]
+
+                function.__doc__ = "".join([summary,
+                                            textwrap.dedent(contents),
+                                            "\n\n",
                                             deprecation_note])
+            else:
+                function.__doc__ = "\n\n".join([existing_docstring,
+                                                deprecation_note])
 
         @functools.wraps(function)
         def _inner(*args, **kwargs):
diff --git a/sample.py b/sample.py
index be00938..9373753 100644
--- a/sample.py
+++ b/sample.py
@@ -14,6 +14,19 @@ def won():
     return 1
 
 
+@deprecation.deprecated(deprecated_in="1.0", removed_in="2.0",
+                        current_version=__version__,
+                        details="Use the ``one`` function instead")
+def uno():
+    """Esta función regresa 1
+
+    This is Spanish for 'This function returns 1'
+
+    This is also here to show that multiline docstrings work
+    """
+    return 1
+
+
 def one():
     """This function returns 1"""
     return 1
diff --git a/setup.py b/setup.py
index ffe453a..3da8a87 100644
--- a/setup.py
+++ b/setup.py
@@ -28,6 +28,7 @@ setup(name="deprecation",
           "Programming Language :: Python :: 3.4",
           "Programming Language :: Python :: 3.5",
           "Programming Language :: Python :: 3.6",
+          "Programming Language :: Python :: 3.7",
           "Programming Language :: Python :: Implementation :: CPython",
           "Programming Language :: Python :: Implementation :: PyPy",
           "Topic :: Software Development :: Libraries :: Python Modules"]
diff --git a/tox.ini b/tox.ini
index 3458187..026704c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py35,py34,py27,pypy,flake8
+envlist = py37,py36,py35,py34,py27,pypy3,pypy,flake8
 skipsdist = True
 
 [testenv]

deprecation_note brakes docstring processing
While joining the `function.__doc__`-string with the `deprecation_note`-string the indentation isn't handled properly. Let's look at this example:

```python
@deprecated(deprecated_in="0.0.3", removed_in="0.1.0",
            current_version="0.0.4",
            details="Do not use this function any more.")
def testfunc():
    """Test function.

    Returns
    -------
    name : str
        Some string
    """
    return "Teststring"

def newfunc():
    """Test function.

    Returns
    -------
    name : str
        Some string
    """
    return "Teststring"
```

Output of `help(testfunc)` (broken):

```
testfunc()
    Test function.
    
        Returns
        -------
        name : str
            Some string
        
    
    *Deprecated in 0.0.3, to be removed in 0.1.0. Do not use this function any more.*
```

Output of `help(newfunc)` (correct):

```
newfunc()
    Test function.
    
    Returns
    -------
    name : str
        Some string
```

The indentation of the function (numpy-style) docstring has been broken by the addition of the `deprecation_note`. While this might seem like a minor cosmetic issue, this breaks proper docstring processing in eg. `sphinx`.
diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py
index 3364f9c..cf9d420 100644
--- a/tests/test_deprecation.py
+++ b/tests/test_deprecation.py
@@ -43,6 +43,30 @@ class Test_deprecated(unittest2.TestCase):
 
                 self.assertEqual(fn.__doc__, test["__doc__"])
 
+    def test_multiline_docstring(self):
+        docstring = "summary line\n\ndetails\nand more details\n"
+        for test in [{"args": {},
+                      "__doc__": "%s\n\n*Deprecated*"},
+                     {"args": {"deprecated_in": "1.0"},
+                      "__doc__": "%s\n\n*Deprecated in 1.0.*"},
+                     {"args": {"deprecated_in": "1.0", "removed_in": "2.0"},
+                      "__doc__": "%s\n\n*Deprecated in 1.0, "
+                                 "to be removed in 2.0.*"},
+                     {"args": {"deprecated_in": "1.0", "removed_in": "2.0",
+                               "details": "some details"},
+                      "__doc__": "%s\n\n*Deprecated in 1.0, "
+                                 "to be removed in 2.0. some details*"}]:
+            with self.subTest(**test):
+                @deprecation.deprecated(**test["args"])
+                def fn():
+                    """summary line
+
+                    details
+                    and more details
+                    """
+
+                self.assertEqual(fn.__doc__, test["__doc__"] % docstring)
+
     def test_warning_raised(self):
         ret_val = "lololol"
 

pip install -e .
parse_log_pytest
requirements.txt
coverage codecov flake8 pytest
3.7
test-requirements.txt docs-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
codecov==2.1.13
coverage==7.2.7
-e git+https://github.com/briancurtin/deprecation.git@70bb27fef996b214d7b698ed3124fb598220eea9#egg=deprecation
exceptiongroup==1.2.2
flake8==5.0.4
idna==3.10
importlib-metadata==4.2.0
iniconfig==2.0.0
linecache2==1.0.0
mccabe==0.7.0
packaging==24.0
pluggy==1.2.0
pycodestyle==2.9.1
pyflakes==2.5.0
pytest==7.4.4
requests==2.31.0
six==1.17.0
tomli==2.0.1
traceback2==1.4.0
typing_extensions==4.7.1
unittest2==1.1.0
urllib3==2.0.7
zipp==3.15.0

name: deprecation
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - argparse==1.4.0
      - charset-normalizer==3.4.1
      - codecov==2.1.13
      - coverage==7.2.7
      - exceptiongroup==1.2.2
      - flake8==5.0.4
      - idna==3.10
      - importlib-metadata==4.2.0
      - iniconfig==2.0.0
      - linecache2==1.0.0
      - mccabe==0.7.0
      - packaging==24.0
      - pluggy==1.2.0
      - pycodestyle==2.9.1
      - pyflakes==2.5.0
      - pytest==7.4.4
      - requests==2.31.0
      - six==1.17.0
      - tomli==2.0.1
      - traceback2==1.4.0
      - typing-extensions==4.7.1
      - unittest2==1.1.0
      - urllib3==2.0.7
      - zipp==3.15.0
prefix: /opt/conda/envs/deprecation

tests/test_deprecation.py::Test_deprecated::test_multiline_docstring
tests/test_deprecation.py::Test_deprecated::test_DeprecatedWarning_not_raised tests/test_deprecation.py::Test_deprecated::test_docstring tests/test_deprecation.py::Test_deprecated::test_removing_without_deprecating tests/test_deprecation.py::Test_deprecated::test_warning_raised tests/test_deprecation.py::Test_fail_if_not_removed::test_DeprecatedWarning_doesnt_fail tests/test_deprecation.py::Test_fail_if_not_removed::test_UnsupportedWarning_causes_failure tests/test_deprecation.py::Test_fail_if_not_removed::test_literal_DeprecatedWarning
setup.py sample.py tox.ini deprecation.py
setup.py sample.py tox.ini deprecation.py