Skip to content

Commit

Permalink
Add add_deprecation_to_docstring for docsite deprecation support (Q…
Browse files Browse the repository at this point in the history
…iskit#9685)

* Add deprecations to function docstring

* Work around issue with multiline strings

* Better variable name

* Jake's review feedback

* Make Napoleon check case-insensitive

* Promote add_deprecation_to_docstring to be public

It may be useful for Applications/Ecosystem, when they want this functionality but want to keep using their own deprecation decorators

* Properly error when metadata line is the first line

* Tests feedback: don't use helper function and simplify instructions to generate tests

* Don't use the function with @deprecate_function and @deprecate_arguments yet

This makes the PR a smaller diff, so easier to review and also less risk when we land that it breaks something. Now, this PR only adds new functionality, the public `add_deprecation_to_docstring` function

* Update qiskit/utils/deprecation.py

Co-authored-by: Luciano Bello <bel@zurich.ibm.com>

* Update qiskit/utils/deprecation.py

Co-authored-by: Luciano Bello <bel@zurich.ibm.com>

* Add release note

---------

Co-authored-by: Luciano Bello <bel@zurich.ibm.com>
  • Loading branch information
Eric-Arellano and 1ucian0 committed Mar 13, 2023
1 parent c2affb1 commit 3005806
Show file tree
Hide file tree
Showing 3 changed files with 498 additions and 2 deletions.
118 changes: 116 additions & 2 deletions qiskit/utils/deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@

import functools
import warnings
from typing import Any, Dict, Optional, Type
from typing import Any, Callable, Dict, Optional, Type


def deprecate_arguments(
kwarg_map: Dict[str, str],
kwarg_map: Dict[str, Optional[str]],
category: Type[Warning] = DeprecationWarning,
*,
since: Optional[str] = None,
Expand Down Expand Up @@ -113,3 +113,117 @@ def _rename_kwargs(
)

kwargs[new_arg] = kwargs.pop(old_arg)


# We insert deprecations in-between the description and Napoleon's meta sections. The below is from
# https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#docstring-sections. We use
# lowercase because Napoleon is case-insensitive.
_NAPOLEON_META_LINES = frozenset(
{
"args:",
"arguments:",
"attention:",
"attributes:",
"caution:",
"danger:",
"error:",
"example:",
"examples:",
"hint:",
"important:",
"keyword args:",
"keyword arguments:",
"note:",
"notes:",
"other parameters:",
"parameters:",
"return:",
"returns:",
"raises:",
"references:",
"see also:",
"tip:",
"todo:",
"warning:",
"warnings:",
"warn:",
"warns:",
"yield:",
"yields:",
}
)


def add_deprecation_to_docstring(
func: Callable, msg: str, *, since: Optional[str], pending: bool
) -> None:
"""Dynamically insert the deprecation message into ``func``'s docstring.
Args:
func: The function to modify.
msg: The full deprecation message.
since: The version the deprecation started at.
pending: Is the deprecation still pending?
"""
if "\n" in msg:
raise ValueError(
"Deprecation messages cannot contain new lines (`\\n`), but the deprecation for "
f'{func.__qualname__} had them. Usually this happens when using `"""` multiline '
f"strings; instead, use string concatenation.\n\n"
"This is a simplification to facilitate deprecation messages being added to the "
"documentation. If you have a compelling reason to need "
"new lines, feel free to improve this function or open a request at "
"https://github.com/Qiskit/qiskit-terra/issues."
)

if since is None:
version_str = "unknown"
else:
version_str = f"{since}_pending" if pending else since

indent = ""
meta_index = None
if func.__doc__:
original_lines = func.__doc__.splitlines()
content_encountered = False
for i, line in enumerate(original_lines):
stripped = line.strip()

# Determine the indent based on the first line with content. But, we don't consider the
# first line, which corresponds to the format """Docstring.""", as it does not properly
# capture the indentation of lines beneath it.
if not content_encountered and i != 0 and stripped:
num_leading_spaces = len(line) - len(line.lstrip())
indent = " " * num_leading_spaces
content_encountered = True

if stripped.lower() in _NAPOLEON_META_LINES:
meta_index = i
if not content_encountered:
raise ValueError(
"add_deprecation_to_docstring cannot currently handle when a Napoleon "
"metadata line like 'Args' is the very first line of docstring, "
f'e.g. `"""Args:`. So, it cannot process {func.__qualname__}. Instead, '
f'move the metadata line to the second line, e.g.:\n\n"""\nArgs:'
)
# We can stop checking since we only care about the first meta line, and
# we've validated content_encountered is True to determine the indent.
break
else:
original_lines = []

# We defensively include new lines in the beginning and end. This is sometimes necessary,
# depending on the original docstring. It is not a big deal to have extra, other than `help()`
# being a little longer.
new_lines = [
indent,
f"{indent}.. deprecated:: {version_str}",
f"{indent} {msg}",
indent,
]

if meta_index:
original_lines[meta_index - 1 : meta_index - 1] = new_lines
else:
original_lines.extend(new_lines)
func.__doc__ = "\n".join(original_lines)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
features:
- |
Added the function ``qiskit.util.deprecation.add_deprecation_to_docstring()``.
It will rewrite the function's docstring to include a Sphinx ``.. deprecated::` directive
so that the deprecation shows up in docs and with ``help()``. The deprecation decorators
from ``qiskit.util.deprecation`` call ``add_deprecation_to_docstring()`` already for you;
but you can call it directly if you are using different mechanisms for deprecations.
Loading

0 comments on commit 3005806

Please sign in to comment.