Skip to content

Commit

Permalink
Add @deprecate_func and @deprecate_arg decorators (Qiskit#9676)
Browse files Browse the repository at this point in the history
* Add `@deprecate_func` and `@deprecate_arg` decorators

* Update the release note

* Apply suggestions from code review

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

* lint

* Use PyPI package_name rather than loose English

---------

Co-authored-by: Luciano Bello <bel@zurich.ibm.com>
  • Loading branch information
Eric-Arellano and 1ucian0 committed Mar 28, 2023
1 parent 90b2439 commit 7e09125
Show file tree
Hide file tree
Showing 4 changed files with 430 additions and 25 deletions.
15 changes: 13 additions & 2 deletions qiskit/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
.. autosummary::
:toctree: ../stubs/
add_deprecation_to_docstring
deprecate_arg
deprecate_arguments
deprecate_func
deprecate_function
local_hardware_info
is_main_process
Expand Down Expand Up @@ -64,8 +67,13 @@
"""

from .quantum_instance import QuantumInstance
from .deprecation import deprecate_arguments
from .deprecation import deprecate_function
from .deprecation import (
add_deprecation_to_docstring,
deprecate_arg,
deprecate_arguments,
deprecate_func,
deprecate_function,
)
from .multiprocessing import local_hardware_info
from .multiprocessing import is_main_process
from .units import apply_prefix, detach_prefix
Expand Down Expand Up @@ -93,7 +101,10 @@
"has_aer",
"name_args",
"algorithm_globals",
"add_deprecation_to_docstring",
"deprecate_arg",
"deprecate_arguments",
"deprecate_func",
"deprecate_function",
"local_hardware_info",
"is_main_process",
Expand Down
238 changes: 222 additions & 16 deletions qiskit/utils/deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,173 @@

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


def deprecate_func(
*,
since: str,
additional_msg: Optional[str] = None,
pending: bool = False,
package_name: str = "qiskit-terra",
removal_timeline: str = "no earlier than 3 months after the release date",
is_property: bool = False,
):
"""Decorator to indicate a function has been deprecated.
It should be placed beneath other decorators like `@staticmethod` and property decorators.
When deprecating a class, set this decorator on its `__init__` function.
Args:
since: The version the deprecation started at. If the deprecation is pending, set
the version to when that started; but later, when switching from pending to
deprecated, update ``since`` to the new version.
additional_msg: Put here any additional information, such as what to use instead.
For example, "Instead, use the function ``new_func`` from the module
``<my_module>.<my_submodule>``, which is similar but uses GPU acceleration."
pending: Set to ``True`` if the deprecation is still pending.
package_name: The PyPI package name, e.g. "qiskit-nature".
removal_timeline: How soon can this deprecation be removed? Expects a value
like "no sooner than 6 months after the latest release" or "in release 9.99".
is_property: If the deprecated function is a `@property`, set this to True so that the
generated message correctly describes it as such. (This isn't necessary for
property setters, as their docstring is ignored by Python.)
Returns:
Callable: The decorated callable.
"""

def decorator(func):
qualname = func.__qualname__ # For methods, `qualname` includes the class name.
mod_name = func.__module__

# Detect what function type this is.
if is_property:
# `inspect.isdatadescriptor()` doesn't work because you must apply our decorator
# before `@property`, so it looks like the function is a normal method.
deprecated_entity = f"The property ``{mod_name}.{qualname}``"
# To determine if's a method, we use the heuristic of looking for a `.` in the qualname.
# This is because top-level functions will only have the function name. This is not
# perfect, e.g. it incorrectly classifies nested/inner functions, but we don't expect
# those to be deprecated.
#
# We can't use `inspect.ismethod()` because that only works when calling it on an instance
# of the class, rather than the class type itself, i.e. `ismethod(C().foo)` vs
# `ismethod(C.foo)`.
elif "." in qualname:
if func.__name__ == "__init__":
cls_name = qualname[: -len(".__init__")]
deprecated_entity = f"The class ``{mod_name}.{cls_name}``"
else:
deprecated_entity = f"The method ``{mod_name}.{qualname}()``"
else:
deprecated_entity = f"The function ``{mod_name}.{qualname}()``"

msg, category = _write_deprecation_msg(
deprecated_entity=deprecated_entity,
package_name=package_name,
since=since,
pending=pending,
additional_msg=additional_msg,
removal_timeline=removal_timeline,
)

@functools.wraps(func)
def wrapper(*args, **kwargs):
warnings.warn(msg, category=category, stacklevel=2)
return func(*args, **kwargs)

add_deprecation_to_docstring(wrapper, msg, since=since, pending=pending)
return wrapper

return decorator


def deprecate_arg(
name: str,
*,
since: str,
additional_msg: Optional[str] = None,
deprecation_description: Optional[str] = None,
pending: bool = False,
package_name: str = "qiskit-terra",
new_alias: Optional[str] = None,
predicate: Optional[Callable[[Any], bool]] = None,
removal_timeline: str = "no earlier than 3 months after the release date",
):
"""Decorator to indicate an argument has been deprecated in some way.
This decorator may be used multiple times on the same function, once per deprecated argument.
It should be placed beneath other decorators like ``@staticmethod`` and property decorators.
Args:
name: The name of the deprecated argument.
since: The version the deprecation started at. If the deprecation is pending, set
the version to when that started; but later, when switching from pending to
deprecated, update `since` to the new version.
deprecation_description: What is being deprecated? E.g. "Setting my_func()'s `my_arg`
argument to `None`." If not set, will default to "{func_name}'s argument `{name}`".
additional_msg: Put here any additional information, such as what to use instead
(if new_alias is not set). For example, "Instead, use the argument `new_arg`,
which is similar but does not impact the circuit's setup."
pending: Set to `True` if the deprecation is still pending.
package_name: The PyPI package name, e.g. "qiskit-nature".
new_alias: If the arg has simply been renamed, set this to the new name. The decorator will
dynamically update the `kwargs` so that when the user sets the old arg, it will be
passed in as the `new_alias` arg.
predicate: Only log the runtime warning if the predicate returns True. This is useful to
deprecate certain values or types for an argument, e.g.
`lambda my_arg: isinstance(my_arg, dict)`. Regardless of if a predicate is set, the
runtime warning will only log when the user specifies the argument.
removal_timeline: How soon can this deprecation be removed? Expects a value
like "no sooner than 6 months after the latest release" or "in release 9.99".
Returns:
Callable: The decorated callable.
"""

def decorator(func):
# For methods, `__qualname__` includes the class name.
func_name = f"{func.__module__}.{func.__qualname__}()"
deprecated_entity = deprecation_description or f"``{func_name}``'s argument ``{name}``"

if new_alias:
alias_msg = f"Instead, use the argument ``{new_alias}``, which behaves identically."
if additional_msg:
final_additional_msg = f"{alias_msg}. {additional_msg}"
else:
final_additional_msg = alias_msg
else:
final_additional_msg = additional_msg

msg, category = _write_deprecation_msg(
deprecated_entity=deprecated_entity,
package_name=package_name,
since=since,
pending=pending,
additional_msg=final_additional_msg,
removal_timeline=removal_timeline,
)

@functools.wraps(func)
def wrapper(*args, **kwargs):
if kwargs:
_maybe_warn_and_rename_kwarg(
func_name,
kwargs,
old_arg=name,
new_alias=new_alias,
warning_msg=msg,
category=category,
predicate=predicate,
)
return func(*args, **kwargs)

add_deprecation_to_docstring(wrapper, msg, since=since, pending=pending)
return wrapper

return decorator


def deprecate_arguments(
Expand All @@ -23,7 +189,7 @@ def deprecate_arguments(
*,
since: Optional[str] = None,
):
"""Decorator to automatically alias deprecated argument names and warn upon use.
"""Deprecated. Instead, use `@deprecate_arg`.
Args:
kwarg_map: A dictionary of the old argument name to the new name.
Expand Down Expand Up @@ -51,7 +217,16 @@ def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if kwargs:
_rename_kwargs(func_name, kwargs, old_kwarg_to_msg, kwarg_map, category)
for old, new in kwarg_map.items():
_maybe_warn_and_rename_kwarg(
func_name,
kwargs,
old_arg=old,
new_alias=new,
warning_msg=old_kwarg_to_msg[old],
category=category,
predicate=None,
)
return func(*args, **kwargs)

for msg in old_kwarg_to_msg.values():
Expand All @@ -70,7 +245,7 @@ def deprecate_function(
*,
since: Optional[str] = None,
):
"""Emit a warning prior to calling decorated function.
"""Deprecated. Instead, use `@deprecate_func`.
Args:
msg: Warning message to emit.
Expand Down Expand Up @@ -99,21 +274,52 @@ def wrapper(*args, **kwargs):
return decorator


def _rename_kwargs(
def _maybe_warn_and_rename_kwarg(
func_name: str,
kwargs: Dict[str, Any],
old_kwarg_to_msg: Dict[str, str],
kwarg_map: Dict[str, Optional[str]],
category: Type[Warning] = DeprecationWarning,
*,
old_arg: str,
new_alias: Optional[str],
warning_msg: str,
category: Type[Warning],
predicate: Optional[Callable[[Any], bool]],
) -> None:
for old_arg, new_arg in kwarg_map.items():
if old_arg not in kwargs:
continue
if new_arg in kwargs:
raise TypeError(f"{func_name} received both {new_arg} and {old_arg} (deprecated).")
warnings.warn(old_kwarg_to_msg[old_arg], category=category, stacklevel=3)
if new_arg is not None:
kwargs[new_arg] = kwargs.pop(old_arg)
if old_arg not in kwargs:
return
if new_alias and new_alias in kwargs:
raise TypeError(f"{func_name} received both {new_alias} and {old_arg} (deprecated).")
if predicate and not predicate(kwargs[old_arg]):
return
warnings.warn(warning_msg, category=category, stacklevel=3)
if new_alias is not None:
kwargs[new_alias] = kwargs.pop(old_arg)


def _write_deprecation_msg(
*,
deprecated_entity: str,
package_name: str,
since: str,
pending: bool,
additional_msg: str,
removal_timeline: str,
) -> Tuple[str, Union[Type[DeprecationWarning], Type[PendingDeprecationWarning]]]:
if pending:
category = PendingDeprecationWarning
deprecation_status = "pending deprecation"
removal_desc = f"marked deprecated in a future release, and then removed {removal_timeline}"
else:
category = DeprecationWarning
deprecation_status = "deprecated"
removal_desc = f"removed {removal_timeline}"

msg = (
f"{deprecated_entity} is {deprecation_status} as of {package_name} {since}. "
f"It will be {removal_desc}."
)
if additional_msg:
msg += f" {additional_msg}"
return msg, category


# We insert deprecations in-between the description and Napoleon's meta sections. The below is from
Expand Down
20 changes: 15 additions & 5 deletions releasenotes/notes/new-deprecation-utilities-066aff05e221d7b1.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
---
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.
Added the functions ``add_deprecation_to_docstring()``, ``@deprecate_arg``, and
``@deprecate_func`` to ``qiskit.util.deprecation``.
``add_deprecation_to_docstring()`` 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.
``@deprecate_func`` replaces ``@deprecate_function``. It will auto-generate most of the
deprecation message for you.
``@deprecate_arg`` replaces ``@deprecate_arguments``. It will generate a more useful message
than before. It is also more flexible, like allowing you to set a ``predicate`` so that you
only deprecate certain situations, such as using a deprecated value or data type.
Loading

0 comments on commit 7e09125

Please sign in to comment.