Python: how to install a pass-through copy/deepcopy hook
Question: I have a library which stores additional data for foreign user objects in a
WeakKeyDictionary:

    
    
    extra_stuff = weakref.WeakKeyDictionary()
    def get_extra_stuff_for_obj(o):
        return extra_stuff[o]
    

When user object is copied, I want the copy to have the same extra stuff.
However, I have limited control over the user object. I would like to define a
class decorator for user object classes which will be used in this manner:

    
    
    def has_extra_stuff(klass):
        def copy_with_hook(self):
            new = magic_goes_here(self)
            extra_stuff[new] = extra_stuff[self]
        klass.__copy__ = copy_with_hook
        return klass
    

This is easy if klass already defines `__copy__`, because I can close
`copy_with_hook` over the original and call it. However, typically it's not
defined. What to call here? This obviously can't be `copy.copy`, because that
would result in infinite recursion.

I found [this question](http://stackoverflow.com/questions/3253439/python-
copy-how-to-inherit-the-default-copying-behaviour) which appears to ask the
exact same question, but afaict the answer is wrong because this results in a
deepcopy, not a copy. I would also be unable to do this, as I need to install
hooks for both deepcopy and copy. (Incidentally, I would have continued the
discussion in that question, but having no reputation I am not able to do
this.)

I looked at what the copy module does, which is a bunch of voodoo involving
`__reduce_ex()`. I can obviously cut/paste this into my code, or call its
private methods directly, but I would consider this an absolute last resort.
This seems like such a simple thing, I'm convinced I'm missing a simple
solution.


Answer: Essentially, you need to (A) copy and preserve the original `__copy__` if
present (and delegate to it), otherwise (B) trick `copy.copy` into **not**
using your newly-added `__copy__` (and delegate to `copy,copy`).

So, for example...:

    
    
    import copy
    import threading
    
    copylock = threading.RLock()
    
    def has_extra_stuff(klass):
    
        def simple_copy_with_hook(self):
            with copylock:
                new = original_copy(self)
                extra_stuff[new] = extra_stuff[self]
    
        def tricky_case(self):
            with copylock:
                try:
                    klass.__copy__ = None
                    new = copy.copy(self)
                finally:
                    klass.__copy__ = tricky_case
                extra_stuff[new] = extra_stuff[self]
    
        original_copy = getattr(klass, '__copy__', None)
        if original_copy is None:
            klass.__copy__ = tricky_case
        else:
            klass.__copy__ = simple_copy_with_hook
        return klass
    

Not the most elegant code ever written, but at least it just plays around with
`klass`, without monkey-patching nor copy-and-pasting `copy.py` itself:-)

Added: since the OP mentioned in a comment he can't use this solution because
the app is multi-threaded, added appropriate locking to make it actually
usable. Using a single global re-entrant lock to ensure against deadlocks due
to out-of-order acquires of multiple locks among multiple threads, and perhaps
over-locked "just in case" although I suspect the simple case and the dict
assignent in the tricky case probably don't need the lock... but, when
threading threatens, better safe than sorry:-)

