============================== What You Can Deprecate and How ============================== *Most things should be easy to deprecate and everything should be possible.* This page attempts to demonstrate a variety of practical deprecations that library authors face, alongside how to perform the deprecation using :ref:`regret`. The `API Reference ` also contains a full list for completeness. .. testsetup:: import sys import warnings import regret def _redirected(message, category, filename, lineno, file=None, line=None): """ Show warnings on stdout so doctest sees them. """ showwarning(message, category, filename, lineno, sys.stdout, line) showwarning, warnings.showwarning = warnings.showwarning, _redirected warnings.simplefilter("default") .. testcleanup:: warnings.showwarning = showwarning Functions & Callables --------------------- Deprecating a single function or callable in its entirety is one of the simplest and most common deprecations to perform. Consider as an example a greeting function which we wish to deprecate: .. testcode:: def greeting(first_name, last_name): return f"Hello {first_name} {last_name}!" Doing so can be done via the `regret.callable` decorator by simply specifying the version in which the function has been deprecated: .. testcode:: @regret.callable(version="v2020-07-08") def greeting(first_name, last_name): return f"Hello {first_name} {last_name}!" at which point any code which uses the function will receive a suitable deprecation warning: .. testcode:: print(greeting("Joe", "Smith")) .. testoutput:: ...: DeprecationWarning: greeting is deprecated. print(greeting("Joe", "Smith")) Hello Joe Smith! .. note:: Class objects are themselves simply callables, and as such, deprecating an entire class can be done in the same manner. However, if you have an API that primarily encouraged subclassing of the class to be deprecated, the object returned by `regret.Deprecator.callable` is *not* guaranteed to be a type (i.e. a class), and therefore may not support being subclassed as the original object did. .. todo:: Link to ``regret.Deprecator.Class`` once it exists. Replacements ============ It is often the case when deprecating an object that a newer replacement API subsumes its functionality, and is meant to be used instead. `regret.callable` accommodates this common use case by allowing you to specify which object is the replacement while deprecating: .. testcode:: def better_greeting(first_name, last_name): return f"Hello {first_name} {last_name}! You are amazing!" @regret.callable(version="1.0.0", replacement=better_greeting) def greeting(first_name, last_name): return f"Hello {first_name} {last_name}!" which will then show the replacement object in warnings emitted: .. testcode:: print(greeting("Joe", "Smith")) .. testoutput:: ...: DeprecationWarning: greeting is deprecated. Please use better_greeting instead. print(greeting("Joe", "Smith")) Hello Joe Smith! Parameters ---------- There are various scenarios in which a callable's signature may require deprecation. Removing a Required Parameter ============================= :ref:`regret` can help deprecate a parameter (argument) which previously was required and which now is to be removed. Consider again our ``greeting`` function, but where we have decided to replace the separate specification of first and last names with a single ``name`` parameter\ `, `_ and therefore wish to deprecate providing the name in separate parameters: .. testcode:: @regret.parameter(version="v1.2.3", name="first_name") @regret.parameter(version="v1.2.3", name="last_name") def greeting(first_name=None, last_name=None, *, name=None): if first_name is not None: name = first_name if last_name is not None: name += f" {last_name}" return f"Hello {name}!" After the above, using the function with the previous parameters will show a deprecation warning: .. testcode:: print(greeting("Joe", "Smith")) .. testoutput:: ...: DeprecationWarning: The 'first_name' parameter is deprecated. print(greeting("Joe", "Smith")) ...: DeprecationWarning: The 'last_name' parameter is deprecated. print(greeting("Joe", "Smith")) Hello Joe Smith! but via the new parameter, will not: .. testcode:: print(greeting(name="Joe Smith")) .. testoutput:: Hello Joe Smith! Making a New or Previously-Optional Parameter Required ====================================================== :ref:`regret` can help make a parameter which previously was *not* required slowly become required. Again in our ``greeting`` function, perhaps we have decided to allow specifying how excited to make the greeting, by specifying whether to end it with a period or exclamation point. We wish to ultimately force users of the function to specify one or the other, but until then, a default is being chosen. .. testcode:: @regret.optional_parameter(version="v1.2.3", name="end", default="!") def greeting(first_name, last_name, end): return f"Hello {first_name} {last_name}{end}" .. note:: :ref:`regret` can and should handle ensuring that the default is used when not provided by the caller. Your wrapped function can assume a value will always be provided. `None` can be used as a default if appropriate (and will not be interpreted with any meaning), as can any other Python object. After the above, using the function without explicitly passing the ``end`` parameter will show a deprecation warning: .. testcode:: print(greeting("Joe", "Smith")) .. testoutput:: ...: DeprecationWarning: Calling greeting without providing the 'end' parameter is deprecated. Using '!' as a default. print(greeting("Joe", "Smith")) Hello Joe Smith! but when properly specifying the new parameter, will not: .. testcode:: print(greeting("Joe", "Smith", end=".")) .. testoutput:: Hello Joe Smith. Subclassability --------------- A deprecation library isn't necessarily the place to opine on `the pros and cons of inheritance `_. For library authors however who have released public APIs that heavily depend on or require users to inherit from provided superclasses, :ref:`regret` provides a mechanism for deprecating the inheritability of classes. Consider for example: .. testcode:: class Contact: name: str phone: str address: str which downstream users of the class extend via e.g.: .. testcode:: class EMailContact(Contact): email: str We can deprecate the downstream use of the contact class in inheritance hierarchies via: .. testcode:: @regret.inheritance(version="v1.2.3") class Contact: name: str phone: str address: str at which point, the act itself of subclassing will produce: .. testcode:: class EMailContact(Contact): email: str .. testoutput:: ...: DeprecationWarning: Subclassing from Contact is deprecated. class EMailContact(Contact):