3

Situation: I have some custom Exception classes in my library, which is used by users in their code. For some reason, I want to rename one of the exceptions and deprecate the old one.

Keeping the old exception as an alias is not difficult:

class MyNewError(ValueError):
    pass

MyOldError = MyNewError

But eventually I would like to remove the old error name for my library, and therefore I want users who use this custom exception in downstream code to be notified with a DeprecationWarning that this error will be removed.

But I want to raise the DeprecationWarning in the following usecase (say that my library containing the custom exceptions is called mypackage):

# downstream user code
import mypackage

...

try:
    ....
except mypackage.MyOldError:
    ....

So I want to raise the warning when the user tries to catch the error, not only when the user would raise the error.

Is it possible to do this in some way? (as the user is not calling a function here in which I can raise a deprecation warning)

joris
  • 133,120
  • 36
  • 247
  • 202
  • I don't think you can - you presumably don't want to raise a warning every time someone imports `mypackage`, and I don't think there's a sensible way to override `__getitem__` at the module level (see e.g. http://stackoverflow.com/q/10438894/3001761 - it's awkward). – jonrsharpe Nov 11 '16 at 10:57

2 Answers2

4

You can apply the hack/trick from this answer, converting your module into a class and intercepting the __getattr__() operation:

mypackage/__init__.py:

import sys
import warnings

class MyNewError(ValueError):
    pass

MyOldError = MyNewError

def foo():
    print('mypackage.foo(): raising MyNewError')
    raise MyNewError()

class Wrapper(object):
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def __getattr__(self, name):
        if name == 'MyOldError':
            warnings.warn('MyOldError is deprecated, use MyNewError')

        return getattr(self.wrapped, name)

sys.modules[__name__] = Wrapper(sys.modules[__name__])

test.py:

import mypackage

try:
    mypackage.foo()
except mypackage.MyOldError:
    print('Caught mypackage.MyOldError')

Output:

$ python test.py 
mypackage.foo(): raising MyNewError
mypackage/__init__.py:18: UserWarning: MyOldError is deprecated, use MyNewError
  warnings.warn('MyOldError is deprecated, please use MyNewError')
Caught mypackage.MyOldError
Community
  • 1
  • 1
Leon
  • 31,443
  • 4
  • 72
  • 97
2

It's certainly possible to raise an exception within an exception, but if the user isn't expecting it, it could cause issues. Assuming that was just a misstep in your post, and you don't have a DeprecationWarning exception you want to raise, you can do the following:

class MyOldError(MyNewError):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        deprecation_warning('Deprecated, please use MyNewError')

>>> raise MyOldError
Deprecated, please use MyNewError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
__main__.MyOldError
>>>
Adam Barnes
  • 2,922
  • 21
  • 27
  • Thanks! This works for raising an error, but I would also like to show a deprecation warning in case someone tries to *catch* the error (see the example use case in my question) – joris Nov 11 '16 at 10:14
  • Aah I see what you mean. It would be possible to do this with [metaclasses and magic methods](https://docs.python.org/3/reference/datamodel.html#customizing-instance-and-subclass-checks), but I'm not certain how useful that would be. I'll think on an elegant solution in the meantime, but no promises. Someone might beat me to it, too. – Adam Barnes Nov 11 '16 at 10:38