First, I think the sanest option would be to patch the digraph instance to add the methods you need, and include patching __copy__ in that, or even stick with your wrapper, and use a metaclass to add proxies for magic methods, as suggested in this answer to the question you linked to.
That said, I was recently toying around with the idea of "magically" subclassing an instance, and figured I'd share my findings with you, since you're toying with the very same thing. Here's the code I came up with:
def retype_instance(recvinst, sendtype, metaklass=type):
""" Turn recvinst into an instance of sendtype.
Given an instance (recvinst) of some class, turn that instance
into an instance of class `sendtype`, which inherits from
type(recvinst). The output instance will still retain all
the instance methods and attributes it started with, however.
For example:
Input:
type(recvinst) == Connection
sendtype == AioConnection
metaklass == CoroBuilder (metaclass used for creating AioConnection)
Output:
recvinst.__class__ == AioConnection
recvinst.__bases__ == bases_of_AioConnection +
Connection + bases_of_Connection
"""
# Bases of our new instance's class should be all the current
# bases, all of sendtype's bases, and the current type of the
# instance. The set->tuple conversion is done to remove duplicates
# (this is required for Python 3.x).
bases = tuple(set((type(recvinst),) + type(recvinst).__bases__ +
sendtype.__bases__))
# We change __class__ on the instance to a new type,
# which should match sendtype in every where, except it adds
# the bases of recvinst (and type(recvinst)) to its bases.
recvinst.__class__ = metaklass(sendtype.__name__, bases, {})
# This doesn't work because of http://bugs.python.org/issue672115
#sendtype.__bases__ = bases
#recv_inst.__class__ = sendtype
# Now copy the dict of sendtype to the new type.
dct = sendtype.__dict__
for objname in dct:
if not objname.startswith('__'):
setattr(type(recvinst), objname, dct[objname])
return recvinst
The idea is to redefine the __class__ of the instance, changing it to be a new class of our choosing, and add the original value of __class__ to inst.__bases__ (along with the __bases__ of the new type). Additionally, we copy the __dict__ of the new type into the instance. This sounds fairly crazy and probably is, but in the little bit of testing I did with it, it seemed to (mostly) actually work:
class MagicThread(object):
def magic_method(self):
print("This method is magic")
t = Thread()
m = retype_instance(t, MagicThread)
print m.__class__
print type(m)
print type(m).__mro__
print isinstance(m, Thread)
print dir(m)
m.magic_method()
print t.is_alive()
print t.name
print isinstance(m, MagicThread)
Output:
<class '__main__.MagicThread'>
<class '__main__.MagicThread'>
(<class '__main__.MagicThread'>, <class 'threading.Thread'>, <class 'threading._Verbose'>, <type 'object'>)
True
['_Thread__args', '_Thread__block', '_Thread__bootstrap', '_Thread__bootstrap_inner', '_Thread__daemonic', '_Thread__delete', '_Thread__exc_clear', '_Thread__exc_info', '_Thread__ident', '_Thread__initialized', '_Thread__kwargs', '_Thread__name', '_Thread__started', '_Thread__stderr', '_Thread__stop', '_Thread__stopped', '_Thread__target', '_Verbose__verbose', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_block', '_note', '_reset_internal_locks', '_set_daemon', '_set_ident', 'daemon', 'getName', 'ident', 'isAlive', 'isDaemon', 'is_alive', 'join', 'magic_method', 'name', 'run', 'setDaemon', 'setName', 'start']
This method is magic
False
Thread-1
False
All of that output is exactly as we'd like, except for the last line - isinstance(m, MagicThread) is False. This is because we didn't actually assign __class__ to the MagicMethod class we defined. Instead, we created a separate class with the same name and all the same methods/attributes. Ideally this could be addressed by actually redefining the __bases__ of MagicThread inside retype_instance, but Python won't allow this:
TypeError: __bases__ assignment: 'Thread' deallocator differs from 'object'
This appears to be a bug in Python dating all the way back to 2003. It hasn't been fixed yet, probably because dynamically redefining __bases__ on an instance is a strange and probably bad idea!
Now, if you don't care about being able to use isinstance(obj, ColliderGraph), the above might work for you. Or it might fail in strange, unexpected ways. I really wouldn't recommend using this in any production code, but I figured I'd share it.