Reputation: 80456
I was thinking of automatically adding child classes to parent for "chaining" using a metaclass. However, inheriting these attributes from parent classes messes thing up. Is there a nice way to avoid this?
class MetaError(type):
def __init__(cls, name, bases, attrs):
for base in bases:
setattr(base, name, cls)
super(MetaError, cls).__init__(name, bases, attrs)
class BaseError(Exception, object):
def __init__(self, message):
super(BaseError, self).__init__(message)
class HttpError(BaseError):
__metaclass__ = MetaError
class HttpBadRequest(HttpError):
pass
class HttpNotFound(HttpError):
pass
class FileNotFound(HttpNotFound):
pass
class InvalidJson(HttpBadRequest):
pass
http = HttpError
# now I can do
raise http.HttpNotFound('Not found')
raise http.HttpNotFound.FileNotFound('File not found')
raise http.HttpBadRequest.InvalidJson('Invalid json')
# unfortunately this also works
raise http.HttpBadRequest.HttpBadRequest('Bad request')
raise http.HttpBadRequest.HttpNotFound('Not found')
Upvotes: 3
Views: 480
Reputation: 80456
A fairly naive global mapping solution that also seems to be working:
m = {}
class MetaError(type):
def __init__(cls, name, bases, attrs):
for base in bases:
m[(base, name)] = cls
super(MetaError, cls).__init__(name, bases, attrs)
def __getattribute__(self, value):
if (self, value) in m:
return m[self, value]
return type.__getattribute__(self, value)
class BaseError(Exception):
__metaclass__ = MetaError
class HttpError(BaseError):
pass
class HttpBadRequest(HttpError):
pass
class HttpNotFound(HttpError):
pass
class FileNotFound(HttpNotFound):
pass
class InvalidJson(HttpBadRequest):
pass
Upvotes: 1
Reputation: 110811
Well, this turns out to be trickier than it seens at first -
because basically you want to have class inheritance relationship, but do not use the normal attribute lookup paths on class inheritance -
Otherwise, HTTPError, being a subclass of BaseError, for example, would always have all the attributs present in BaseError itself - Therefore,
the chain BaseError.HTTPError.HTTPError.HTTPError.HTTPError...
would always be valid.
Fortunately, Python does offer a mechanism to register classes as subclasses of other, without "physical" inheritance - that is, it is reported as subclass, but does not have the parent class in its bases or __mro__
- and therefore, attribute lookup on the derived class (adopted?) does not search attributes in the "foster" parent.
This mechanism is provided through the "abstract base classes" or "abc"s, through its ABCMeta Metaclass, and "register" method.
And now, due to the fact you also probably want to declare
your class hierarchy with the normal inheritance syntax - that is,
being able to write class HTTPError(BaseError):
to indicate the new
class derives from BaseError - you get the actual "physical" inheritance.
So, we can inherit from ABCMeta class (instead of type
) and write
the __new__
method so that the physical inheritance is excluded -
and we use the setattr
for containment you intended with your code as well, and also, we trigger the needed call to parentclass.register
directly on the metaclass.
(Note that as we are now changing the base classes, we need to fiddle
in the __new__
method of the metaclass, not on __init__
:
from abc import ABCMeta
class MetaError(ABCMeta):
def __new__(metacls, name, bases, attrs):
new_bases = []
base_iter = list(reversed(bases))
seen = []
register_this = None
while base_iter:
base = base_iter.pop(0)
if base in seen:
continue
seen.append(base)
if isinstance(base, MetaError):
register_this = base
base_iter = list(reversed(base.__mro__)) + base_iter
else:
new_bases.insert(0, base)
cls = super(MetaError, metacls).__new__(metacls, name, tuple(new_bases), attrs)
if register_this:
setattr(register_this, name, cls)
register_this.register(cls)
return cls
And for a quick test:
class BaseError(Exception):
__metaclass__ = MetaError
class HTTPError(BaseError):
pass
class HTTPBadRequest(HTTPError):
pass
In the interactive mode, check if it works as you intend:
In [38]: BaseError.HTTPError
Out[38]: __main__.HTTPError
In [39]: BaseError.HTTPError.HTTPError
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-39-5d5d03751646> in <module>()
----> 1 BaseError.HTTPError.HTTPError
AttributeError: type object 'HTTPError' has no attribute 'HTTPError'
In [40]: HTTPError.__mro__
Out[40]: (__main__.HTTPError, Exception, BaseException, object)
In [41]: issubclass(HTTPError, BaseError)
Out[41]: True
In [42]: issubclass(HTTPBadRequest, BaseError)
Out[42]: True
In [43]: BaseError.HTTPError.HTTPBadRequest
Out[43]: __main__.HTTPBadRequest
In [44]: BaseError.HTTPBadRequest
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-44-b40d65ca66c6> in <module>()
----> 1 BaseError.HTTPBadRequest
AttributeError: type object 'BaseError' has no attribute 'HTTPBadRequest'
And then, most important of all, testing if the Exception hierarchy actually works in this way:
In [45]: try:
....: raise HTTPError
....: except BaseError:
....: print("it works")
....: except HTTPError:
....: print("not so much")
....:
it works
A few notes: no need to inherit from both Exception
and object
explicitly - Exception
itself already inherits from object
. And, most important: whatever project you are working on, do whatever is possible to move it to Python 3.x instead of Python 2. Python 2 is with the days counted, and there are many, many new features in Python 3 you are excluding yourself of using. (The code in this answer is Python 2/3 compatible, but for the __metaclass__
usage declaration of course).
Upvotes: 3