deprecated
deprecated copied to clipboard
Deprecate a function parameter — Contribution Guide
Introduction
This article concerns the addition of one (or more) decorator(s) to deprecate the use of a function parameter. For example:
@deprecated_param(version="0.2.3",
reason="you may consider using *styles* instead.",
deprecated_args='color background_color')
def paragraph(text, color=None, background_color=None, styles=None):
styles = styles or {}
if color:
styles['color'] = color
if background_color:
styles['background-color'] = background_color
html_styles = " ".join("{k}: {v};".format(k=k, v=v) for k, v in styles.items())
html_text = xml.sax.saxutils.escape(text)
return ('<p styles="{html_styles}">{html_text}</p>'
.format(html_styles=html_styles, html_text=html_text))
Such a decorator could be coded as follows:
import functools
def deprecated_param(version, reason, deprecated_args):
def decorate(func):
@functools.wraps(func)
def call(*args, **kwargs):
# todo: check deprecated arguments here...
return func(*args, **kwargs)
return call
return decorate
Advantages and disadvantages of the decorator
Using a decorator to deprecate the use of a function parameter is interesting and offers some advantages:
- The decorator allows you to isolate the code that checks the use of this parameter from the rest of the function processing: Separation of Concerns design pattern.
- The decorator allows explicit documentation of the code, so the use of comments is unnecessary: “Readability matters”.
- The decorator could be used to enhance the code documentation (the docstring of the decorated function).
Disadvantages:
- The implementation of such a decorator is necessarily more complex than a specific solution.
- The resulting function (after decoration) is necessarily slower than the decorated function because of the introspection and parameters usage check. Another important consideration:
- The resulting function (after decoration) must be of the same nature as the decorated function: function, method, static method, etc.
Generalization:
In addition to the case of deprecating a parameter usage, a warning could also be issued in the following cases:
- A parameter has been renamed: the new name is more meaningful, more explicit, etc.
- One parameter is replaced by another of more general use.
- The parameter type changes, it is better to use another one.
- A parameter is considered obsolete, it will be deleted in a future version.
- A new parameter has been added, its use is strongly recommended.
- Etc.
The use cases mentioned above fall into 3 categories:
- Deleted parameter,
- Added parameter,
- Modified parameter (default value, modified type or use).
The third case (modified parameter) is the most difficult to specify in general terms.
Examples of deprecations
(This section is incomplete) Before starting a complex implementation, it would be necessary to study how it is done, on one hand in the Standard Library, and on the other hand in the popular libraries of the Open Source world such as: pip, urllib3, boto3, six, requests, setuptools, futures, Flask, Django… At least two questions must be considered regarding deprecation:
- How is it implemented in the source code?
- How is it documented?
Implementation
(This section is incomplete)
A parameter deprecation decorator implementation must be flexible. It is necessary to be as flexible as possible and to use what exists for @deprecated.basic.deprecated
and @deprecated.sphinx.deprecated
. Especially, the use of Wrapt is a must-have.
It is also necessary to define test cases that correspond to the most common scenarios. It is also likely that some atypical situations are clearly not implemented.
For now, I would like to continue supporting Python 2.7, as it is still used by 25% of users (according to the 2018 State of Developer Ecosystem survey).
This looks pretty elegant: https://stackoverflow.com/questions/49802412/how-to-implement-deprecation-in-python-with-argument-alias:
import functools
import warnings
def deprecated_alias(**aliases):
def deco(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
rename_kwargs(f.__name__, kwargs, aliases)
return f(*args, **kwargs)
return wrapper
return deco
def rename_kwargs(func_name, kwargs, aliases):
for alias, new in aliases.items():
if alias in kwargs:
if new in kwargs:
raise TypeError('{} received both {} and {}'.format(
func_name, alias, new))
warnings.warn('{} is deprecated; use {}'.format(alias, new),
DeprecationWarning)
kwargs[new] = kwargs.pop(alias)
class MyClass(object):
@deprecated_alias(object_id='id_object')
def __init__(self, id_object):
self.id = id_object
It’s a good starting point. We also need to add a warning message.
Since it looks like it's taking some time to discuss and implement this improvement, or find someone that could, can I suggest to introduce a function to emit properly a deprecation warning, to be used inside the code for complex situations that can only be represented using runtime checks by the programmer? Those runtime checks are probably already there.
Something like:
deprecation_warning(version=..., reason=...)
That can be used in any place needed?
hi @tantale, consider checking this PR https://github.com/tantale/deprecated/pull/51, if you think I can contribute something similar!