1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
|
#!/usr/bin/env python3
#
# __init__.py
"""
A wrapper around 'deprecation' providing support for deprecated aliases.
"""
#
# Copyright © 2020 Dominic Davis-Foster <dominic@davis-foster.co.uk>
# License: Apache Software License
# See the LICENSE file for details.
#
# Based on https://github.com/briancurtin/deprecation
# Modified to only change the docstring of the wrapper and not the original function.
#
# stdlib
import datetime
import functools
import textwrap
import warnings
from typing import Callable, Optional, Union
# 3rd party
import deprecation # type: ignore
from packaging import version
__author__: str = "Dominic Davis-Foster"
__copyright__: str = "2020 Dominic Davis-Foster"
__license__: str = "Apache Software License"
__version__: str = "0.4.0"
__email__: str = "dominic@davis-foster.co.uk"
__all__ = ["deprecated"]
def deprecated(
deprecated_in: Optional[str] = None,
removed_in: Union[str, datetime.date, None] = None,
current_version: Optional[str] = None,
details: str = '',
name: Optional[str] = None,
func: Optional[Callable] = None,
) -> Callable:
r"""Decorate a function to signify its deprecation.
This function wraps a method that will soon be removed and does two things:
* The docstring of the method will be modified to include a notice
about deprecation, e.g., "Deprecated since 0.9.11. Use foo instead."
* Raises a :class:`deprecation.DeprecatedWarning`
via the :mod:`warnings` module, which is a subclass of the built-in
:class:`DeprecationWarning`. Note that built-in
:class:`DeprecationWarning`\s are ignored by default, so for users
to be informed of said warnings they will need to enable them -- see
the :mod:`warnings` module documentation for more details.
:param deprecated_in: The version at which the decorated method is considered
deprecated. This will usually be the next version to be released when
the decorator is added. The default is :py:obj:`None`, which effectively
means immediate deprecation. If this is not specified, then the
``removed_in`` and ``current_version`` arguments are ignored.
:no-default deprecated_in:
:param removed_in: The version or :class:`datetime.date` when the decorated
method will be removed. The default is :py:obj:`None`, specifying that
the function is not currently planned to be removed.
.. note::
This parameter cannot be set to a value if ``deprecated_in=None``.
:no-default removed_in:
:param current_version: The source of version information for the currently
running code. This will usually be a ``__version__`` attribute in your
library. The default is :py:obj:`None`. When ``current_version=None``
the automation to determine if the wrapped function is actually in
a period of deprecation or time for removal does not work, causing a
:class:`~deprecation.DeprecatedWarning` to be raised in all cases.
:no-default current_version:
:param details: Extra details to be added to the method docstring and
warning. For example, the details may point users to a replacement
method, such as "Use the foo_bar method instead".
:param name: The name of the deprecated function, if an alias is being
deprecated. Default is to the name of the decorated function.
:no-default name:
:param func: The function to deprecate. Can be used as an alternative to using the ``@deprecated(...)`` decorator.
If provided ``deprecated`` can't be used as a decorator.
:no-default func:
.. versionchanged:: 0.2.0 Added the ``func`` argument.
.. versionchanged:: 0.3.0 The warning in the documentation is always shown.
"""
# You can't just jump to removal. It's weird, unfair, and also makes
# building up the docstring weird.
if deprecated_in is None and removed_in is not None:
raise TypeError("Cannot set removed_in to a value without also setting deprecated_in")
# Only warn when it's appropriate. There may be cases when it makes sense
# to add this decorator before a formal deprecation period begins.
# In CPython, PendingDeprecatedWarning gets used in that period,
# so perhaps mimick that at some point.
is_deprecated = False
is_unsupported = False
# StrictVersion won't take a None or a "", so make whatever goes to it
# is at least *something*. Compare versions only if removed_in is not
# of type datetime.date
if isinstance(removed_in, datetime.date):
if datetime.date.today() >= removed_in:
is_unsupported = True
else:
is_deprecated = True
elif current_version:
current_version = version.parse(current_version) # type: ignore
if removed_in is not None and current_version >= version.parse(removed_in): # type: ignore
is_unsupported = True
elif deprecated_in is not None and current_version >= version.parse(deprecated_in): # type: ignore
is_deprecated = True
else:
# If we can't actually calculate that we're in a period of
# deprecation...well, they used the decorator, so it's deprecated.
# This will cover the case of someone just using
# @deprecated("1.0") without the other advantages.
is_deprecated = True
should_warn = any([is_deprecated, is_unsupported])
def _function_wrapper(function):
# Everything *should* have a docstring, but just in case...
existing_docstring = function.__doc__ or ''
# split docstring at first occurrence of newline
string_list = existing_docstring.split('\n', 1)
# The various parts of this decorator being optional makes for
# a number of ways the deprecation notice could go. The following
# makes for a nicely constructed sentence with or without any
# of the parts.
parts = {"deprecated_in": '', "removed_in": '', "details": ''}
if deprecated_in:
parts["deprecated_in"] = f" {deprecated_in}"
if removed_in:
# If removed_in is a date, use "removed on"
# If removed_in is a version, use "removed in"
if isinstance(removed_in, datetime.date):
parts["removed_in"] = f"\n This will be removed on {removed_in}."
else:
parts["removed_in"] = f"\n This will be removed in {removed_in}."
if details:
parts["details"] = f" {details}"
deprecation_note = ".. deprecated::{deprecated_in}{removed_in}{details}".format_map(parts)
# default location for insertion of deprecation note
loc = 1
if len(string_list) > 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.
# in-place dedent docstring content
string_list[1] = textwrap.dedent(string_list[1])
# we need another newline
string_list.insert(loc, '\n')
# change the message_location if we add to end of docstring
# do this always if not "top"
if deprecation.message_location != "top":
loc = 3
# insert deprecation note and dual newline
string_list.insert(loc, deprecation_note)
string_list.insert(loc, "\n\n")
@functools.wraps(function)
def _inner(*args, **kwargs):
if should_warn:
if is_unsupported:
cls = deprecation.UnsupportedWarning
else:
cls = deprecation.DeprecatedWarning
the_warning = cls(name or function.__name__, deprecated_in, removed_in, details)
warnings.warn(the_warning, category=DeprecationWarning, stacklevel=2)
return function(*args, **kwargs)
_inner.__doc__ = ''.join(string_list)
return _inner
if func is None:
return _function_wrapper
else:
return _function_wrapper(func)
|