2

I have written a package where a sub-module contains a module-level variable deprecated_var that I want to remove, because it was a horrible mistake.

mypkg
  - mymodule
    - __init__.py

But instead of just leaving my end users with a generic ImportError, I want to print a message that says their import is deprecated, and what they should do. So instead of:

>>> from mypkg.mymodule import deprecated_var
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: cannot import name 'deprecated_var'

I want users to see something like this:

>>> from mypkg.mymodule import deprecated_var
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: mypkg.mymodule.deprecated_var was removed. Replace 
  with "from foo.bar import Baz; deprecated_var = Baz()"

How can I achieve that?

Erik Cederstrand
  • 9,643
  • 8
  • 39
  • 63
  • Can't you just `raise ImportError("that's deprecated")`? i.e. Just construct an `ImportError` with a custom string message. – martineau Aug 20 '18 at 14:27
  • @9769953: I agree and personally like Alex Martelli's [answer](https://stackoverflow.com/a/922645/355230) (even if he thinks it might be overkill). – martineau Aug 20 '18 at 14:38

2 Answers2

2

I don't think this is possible before python 3.7.

However, in python 3.7 or later, this can be achieved by using the module level __getattr__ added in PEP562.

You'd use like so:

#_deprecated_vars is a dict of keys -> alternatives (or None)
_deprecated_vars: Dict[str, Optional[str]] = {
        'deprecated_var': 'from foo.bar import Baz; deprecated_var = Baz()', 
        'other_deprecated_var': None
    }

def __getattr__(name):
    if name in _deprecated_vars:
        alt_text = '{name} was removed from module {__name__}'
        replace_text = _deprecated_vars[name]
        if replace_text is not None:
           alt_text += f'. Replace with {replace_text!r}.'
        raise AttributeError(alt_text)
    raise AttributeError(f"module {__name__} has no attribute {name}")

However, I'm not sure this works for your use case of from a.b import deprecated_var. This is more for import a.b; a.b.deprecated_var. See the other answer for the former.

FHTMitchell
  • 11,793
  • 2
  • 35
  • 47
2

For your specific example, you could use the following:

mymodule/__init__.py:

#deprecated_var = 5
replacement_var = 6

mymodule/deprecated_var.py:

raise ImportError("deprecated_var is deprecated. Use mypkg.mymodule.replacement_var instead")

While this raises the custom ImportError when importing the variable directly:

>>> from mymodule import deprecated_var
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../mymodule/deprecated_var.py", line 1, in <module>
    raise ImportError("deprecated_var is deprecated. Use mypkg.mymodule.replacement_var instead")
ImportError: deprecated_var is deprecated. Use mypkg.mymodule.replacement_var instead

it does nothing when accessing it as a module attribute. Or rather, it throws an AttributeError instead of a deprecation warning:

>>> import mymodule
>>> mymodule.deprecated_var
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'mymodule' has no attribute 'deprecated_var'
9769953
  • 10,344
  • 3
  • 26
  • 37