I was looking at Python: Exception in the separated module works wrong which uses a multi-purpose GnuLibError class to ‘stand in’ for a variety of different errors. Each sub-error has its own ID number and error format string.
I figured it would be better written as a hierarchy of Exception classes, and set out to do so:
class GNULibError(Exception):
sub_exceptions = 0 # patched with dict of subclasses once subclasses are created
err_num = 0
err_format = None
def __new__(cls, *args):
print("new {}".format(cls)) # DEBUG
if len(args) and args[0] in GNULibError.sub_exceptions:
print(" factory -> {} {}".format(GNULibError.sub_exceptions[args[0]], args[1:])) # DEBUG
return super(GNULibError, cls).__new__(GNULibError.sub_exceptions[args[0]], *(args[1:]))
else:
print(" plain {} {}".format(cls, args)) # DEBUG
return super(GNULibError, cls).__new__(cls, *args)
def __init__(self, *args):
cls = type(self)
print("init {} {}".format(cls, args)) # DEBUG
self.args = args
if cls.err_format is None:
self.message = str(args)
else:
self.message = "[GNU Error {}] ".format(cls.err_num) + cls.err_format.format(*args)
def __str__(self):
return self.message
def __repr__(self):
return '{}{}'.format(type(self).__name__, self.args)
class GNULibError_Directory(GNULibError):
err_num = 1
err_format = "destination directory does not exist: {}"
class GNULibError_Config(GNULibError):
err_num = 2
err_format = "configure file does not exist: {}"
class GNULibError_Module(GNULibError):
err_num = 3
err_format = "selected module does not exist: {}"
class GNULibError_Cache(GNULibError):
err_num = 4
err_format = "{} is expected to contain gl_M4_BASE({})"
class GNULibError_Sourcebase(GNULibError):
err_num = 5
err_format = "missing sourcebase argument: {}"
class GNULibError_Docbase(GNULibError):
err_num = 6
err_format = "missing docbase argument: {}"
class GNULibError_Testbase(GNULibError):
err_num = 7
err_format = "missing testsbase argument: {}"
class GNULibError_Libname(GNULibError):
err_num = 8
err_format = "missing libname argument: {}"
# patch master class with subclass reference
# (TO DO: auto-detect all available subclasses instead of hardcoding them)
GNULibError.sub_exceptions = {
1: GNULibError_Directory,
2: GNULibError_Config,
3: GNULibError_Module,
4: GNULibError_Cache,
5: GNULibError_Sourcebase,
6: GNULibError_Docbase,
7: GNULibError_Testbase,
8: GNULibError_Libname
}
This starts out with GNULibError as a factory class – if you call it with an error number belonging to a recognized subclass, it returns an object belonging to that subclass, otherwise it returns itself as a default error type.
Based on this code, the following should be exactly equivalent (but aren’t):
e = GNULibError(3, 'missing.lib')
f = GNULibError_Module('missing.lib')
print e # -> '[GNU Error 3] selected module does not exist: 3'
print f # -> '[GNU Error 3] selected module does not exist: missing.lib'
I added some strategic print statements, and the error seems to be in GNULibError.__new__:
>>> e = GNULibError(3, 'missing.lib')
new <class '__main__.GNULibError'>
factory -> <class '__main__.GNULibError_Module'> ('missing.lib',) # good...
init <class '__main__.GNULibError_Module'> (3, 'missing.lib') # NO!
^
why?
I call the subclass constructor as subclass.__new__(*args[1:]) – this should drop the 3, the subclass type ID – and yet its __init__ is still getting the 3 anyway! How can I trim the argument list that gets passed to subclass.__init__?
You cannot affect what is passed to
__init__, as long as you’re doing it with a “factory class” like you have now that is returning subclasses of itself. The reason the “3” argument is still passed is because you are still returning an instance of GNULibError from__new__. By the time__new__is called, it’s too late to decide what will be passed to__init__. As stated in the documentation (emphasis added):In other words, when you call
GNULibError(3, 'missing.lib'), it’s too late — by calling the class with those arguments you have ensured that those are the arguments that will be passed to__init__.__new__can return a different instance than you might otherwise get, but it can’t stop the normal initialization from happening.As suggested by @Ned Batchelder, you are better off using a factory function instead of a “factory class”, because a function doesn’t have this
__new__/__init__machinery and you can just return an instance of the class you want.