I thought, naively, that it would use isinstance or issubclass to know whether to enter the except clause or not, and so tried overriding __instancecheck__ and __subclasscheck__ in the metaclass of an exception, but they are not triggered when I’m catching it.
class MetaError(type):
def __new__(metacls, name, bases, namespace, **kwargs):
return super().__new__(metacls, name, bases, namespace)
def __init__(cls, name, bases, namespace, new):
cls.new = new
super().__init__(name, bases, namespace)
def __instancecheck__(self, instance):
if isinstance(instance, self.new):
print(f"DEPRECATED, catch {self.new!r} instead")
return True
return super().__instancecheck__(instance)
def __subclasscheck__(self, subclass):
if issubclass(subclass, self.new):
print(f"DEPRECATED, catch {self.new!r} instead")
return True
return super().__subclasscheck__(subclass)
class New(Exception):
...
class Old(Exception, metaclass=MetaError, new=New):
...
print(isinstance(New(), Old))
print(issubclass(New, Old))
try:
raise New()
except Old as error:
print("Caught Old")
except New as error:
print("Caught New")
OK. For the context, I’m trying to see how I can detect deprecated usage in downstream code (at runtime, to emit deprecation warnings), if I change the exception raised by a callable. But this would only work if I have control over the exception classes anyway (to set the metaclass, etc.), so not with builtin exceptions like ValueError, etc.
In short, I don’t think what I’m trying to achieve is possible without static analysis
I think what you can do is simply hook into the constructor of deprecated exception classes.
I’m not sure how you could possibly detect built-in exception handling as deprecated.
Hook into constructor, because why would just solely constructing the exception object not be deprecated in case of a deprecated exception class? In 99% cases you construct the exception object just before raising it. In 1% cases you construct it to raise it at some point in the future.
Basically, I want to raise a new exception, while users may still be trying to catch the old one, and I want that to still work, but to emit a deprecation warning to indicate they should catch the new exception instead.
The old exception might not be deprecated itself: it could still be used elsewhere. What is deprecated is catching a specific exception from a specific callable, because this callable now raises another one
To give an example, myfunc would change from raising ValueError into raising RuntimeError, and I want to emit a deprecation warning when downstream code tries to catch ValueError.
And it is worth noting that you emit deprecation warnings only if it is necessary (if the caught exception is not a base of both A and B, where A is the deprecated exception class and B is the new exception class).
Even static analysis may not be easy to get right here.
You could even perform it at runtime, by finding the code of the caller and inspecting the AST of it astor.code_to_ast(caller) (see the astor package).
But even then, imagine a scenario where the exception raised by your package is caught by a context manager such as suppress(). Inspecting try/except wouldn’t work here. Moreover, a function/module where a deprecated error is caught does not have to reference the error, as suppress() or other context managers can reference it out of the scope of the use (imagine there is a context manager with except Old: somewhere, reused in the function possibly statically examined for the use of Old), so inspecting co_names of the caller code object would be insufficient.
What I think this should lead to is maybe a proposal of ability to catch the catcher red-handed via a simple hook that would be called after sys.exc_info is available to be inspected.
…or you may simply end up emiting a deprecation warning in module-level __getattr__ of somewhere in your package as soon as some package other than yours imports the deprecated exception class?
Definitely. I can imagine hacks with inspecting code (not always available btw) or frames (super fragile as you mentioned). But a proper way of hooking into except thing would be much more robust, because even if I’m using built-in exceptions, I can create a custom one that inherits from the built-in ones, and raise that during the deprecation period.
class PreferRuntimeErrorOverValueError(ValueError, RuntimeError):
def __exceptcheck__(self, types):
# some logic that emits or not the deprecation warning