Subclassing module to deprecate a module level variable/constant?
Question:
Assuming I have a module and I want to deprecate something in that module. That’s very easy for functions, essentially this can be done using a decorator:
import warnings
def deprecated(func):
def old(*args, **kwargs):
warnings.warn("That has been deprecated, use the new features!", DeprecationWarning)
return func(*args, **kwargs)
return old
@deprecated
def func():
return 10
func()
DeprecationWarning: That has been deprecated, use the new features!
10
However if I want to deprecate a constant that’s not trivial, there is no way to apply a decorator to a variable. I was playing around and it seems possible to subclass a module and use __getattribute__
to issue the warning:
I use NumPy here just to illustrate the principle:
import numpy as np
class MyMod(type(np)): # I could also subclass "types.ModuleType" instead ...
def __getattribute__(self, name):
if name in {'float', 'int', 'bool', 'complex'}:
warnings.warn("that's deprecated!", DeprecationWarning)
return object.__getattribute__(self, name)
np.__class__ = MyMod
np.float
DeprecationWarning: that’s deprecated!
float
However that seems to be impossible to do from within the package (at least on the top-level) because I can’t access the own module. I would have to create another package that monkey-patches the primary package.
Is there a better way to “deprecate” accessing variables from a package than to subclass the “module” class and/or to use a metapackage that monkey-patches the top-level module of the other package?
Answers:
PEP 562 has been accepted and will be added in Python 3.7 (not released at the time of writing) and that will allow (or at least greatly simplify) deprecating module level constants.
It works by adding a __getattr__
function in the module. For example in this case:
import builtins
import warnings
def __getattr__(name):
if name == 'float':
warnings.warn("That has been deprecated, use the new features!", DeprecationWarning)
return builtins.float
raise AttributeError(f"module {__name__} has no attribute {name}")
This is basically the example in the PEP slightly adapted for the question.
a compy-paste example
MSeiferts answer is great. The link given (https://peps.python.org/pep-0562/) by him/her explains nicely, how to implement a deprecation mechanism for module constant. It also explains the missing return value in case of no deprecation (TLDR: getattr is only called if no attribute is found)
Here some example implementation:
(to keep thinks simple, I decided to go with print statements – sorry)
main.py
if __name__ == "__main__":
from lib import MY_CONST # deprecated
print(MY_CONST)
from lib import MY_CONST_VALID
print(MY_CONST_VALID)
lib.py
deprecated_names = ["MY_CONST"]
def __getattr__(name):
# https://peps.python.org/pep-0562/
if name in deprecated_names:
print(f"importing {name} from {__name__} is deprecated")
return globals()[f"_DEPRECATED_{name}"]
raise AttributeError(f"module {__name__} has no attribute {name}")
_DEPRECATED_MY_CONST = "dep"
MY_CONST_VALID = "valid"
Assuming I have a module and I want to deprecate something in that module. That’s very easy for functions, essentially this can be done using a decorator:
import warnings
def deprecated(func):
def old(*args, **kwargs):
warnings.warn("That has been deprecated, use the new features!", DeprecationWarning)
return func(*args, **kwargs)
return old
@deprecated
def func():
return 10
func()
DeprecationWarning: That has been deprecated, use the new features!
10
However if I want to deprecate a constant that’s not trivial, there is no way to apply a decorator to a variable. I was playing around and it seems possible to subclass a module and use __getattribute__
to issue the warning:
I use NumPy here just to illustrate the principle:
import numpy as np
class MyMod(type(np)): # I could also subclass "types.ModuleType" instead ...
def __getattribute__(self, name):
if name in {'float', 'int', 'bool', 'complex'}:
warnings.warn("that's deprecated!", DeprecationWarning)
return object.__getattribute__(self, name)
np.__class__ = MyMod
np.float
DeprecationWarning: that’s deprecated!
float
However that seems to be impossible to do from within the package (at least on the top-level) because I can’t access the own module. I would have to create another package that monkey-patches the primary package.
Is there a better way to “deprecate” accessing variables from a package than to subclass the “module” class and/or to use a metapackage that monkey-patches the top-level module of the other package?
PEP 562 has been accepted and will be added in Python 3.7 (not released at the time of writing) and that will allow (or at least greatly simplify) deprecating module level constants.
It works by adding a __getattr__
function in the module. For example in this case:
import builtins
import warnings
def __getattr__(name):
if name == 'float':
warnings.warn("That has been deprecated, use the new features!", DeprecationWarning)
return builtins.float
raise AttributeError(f"module {__name__} has no attribute {name}")
This is basically the example in the PEP slightly adapted for the question.
a compy-paste example
MSeiferts answer is great. The link given (https://peps.python.org/pep-0562/) by him/her explains nicely, how to implement a deprecation mechanism for module constant. It also explains the missing return value in case of no deprecation (TLDR: getattr is only called if no attribute is found)
Here some example implementation:
(to keep thinks simple, I decided to go with print statements – sorry)
main.py
if __name__ == "__main__":
from lib import MY_CONST # deprecated
print(MY_CONST)
from lib import MY_CONST_VALID
print(MY_CONST_VALID)
lib.py
deprecated_names = ["MY_CONST"]
def __getattr__(name):
# https://peps.python.org/pep-0562/
if name in deprecated_names:
print(f"importing {name} from {__name__} is deprecated")
return globals()[f"_DEPRECATED_{name}"]
raise AttributeError(f"module {__name__} has no attribute {name}")
_DEPRECATED_MY_CONST = "dep"
MY_CONST_VALID = "valid"