views:

533

answers:

4

I've seen decorators that let you mark a function a deprecated so that a warning is given whenever that function is used. I'd like to do the same thing but for a global variable, but I can't think of a way to detect global variable accesses. I know about the globals() function, and I could check its contents, but that would just tell me if the global is defined (which it still will be if the function is deprecated and not all out removed) not if it's actually being used. The best alternative I can think of is something like this:

# myglobal = 3
myglobal = DEPRECATED(3)

But besides the problem of how to get DEPRECATED to act exactly like a '3', I'm not sure what DEPRECATED could do that would let you detect every time it's accessed. I think the best it could do is iterate through all of the global's methods (since everything in Python is an object, so even '3' has methods, for converting to string and the like) and 'decorate' them to all be deprecated. But that's not ideal.

Any ideas? Has anyone else tackled this problem?

A: 

Is this just for your dev environment? If so, perhaps tweaking Python itself may be your best option, in which case pypy (the Python interpreter written in Python) may be helpful, but even that may be difficult.

yaauie
-1 Marking something deprecated only makes sense in the context of a public API, how could it be "just for a dev environment"? This answer makes no sense.
Carl Meyer
Tweaking Python itself (or pypy) is rather extreme... not a "best option", but an "option of last resort" I'd say. Doing this on a case-by-case basis for the deprecated types/variables in question, would be quite unpleasant and prone to side-effects.
Jarret Hardie
By tweaking pypy, I think he means something like using the thunk object space ( http://codespeak.net/pypy/dist/pypy/doc/objspace-proxies.html#the-thunk-object-space ) - you wouldn't have to modify python yourself. It's still not really practical for deprecating a public API though, unless pypy ever becomes the default python.
Brian
+4  A: 

You could make your module into a class (see e.g this SO question) and make that deprecated global into a property, so you can execute some of your code when it's accessed and provide the warning you desire. However, this does seem a bit of an overkill.

Alex Martelli
+7  A: 

You can't do this directly, since theres no way of intercepting the module access. However, you can replace that module with an object of your choosing that acts as a proxy, looking for accesses to certain properties:

import sys, warnings

def WrapMod(mod, deprecated):
    """Return a wrapped object that warns about deprecated accesses"""
    deprecated = set(deprecated)
    class Wrapper(object):
        def __getattr__(self, attr):
            if attr in deprecated:
                warnings.warn("Property %s is deprecated" % attr)

            return getattr(mod, attr)

        def __setattr__(self, attr, value):
            if attr in deprecated:
                warnings.warn("Property %s is deprecated" % attr)
            return setattr(mod, attr, value)
    return Wrapper()

oldVal = 6*9
newVal = 42

sys.modules[__name__] = WrapMod(sys.modules[__name__], 
                         deprecated = ['oldVal'])

Now, you can use it as:

>>> import mod1
>>> mod1.newVal
42
>>> mod1.oldVal
mod1.py:11: UserWarning: Property oldVal is deprecated
  warnings.warn("Property %s is deprecated" % attr)
54

The downside is that you are now performing two lookups when you access the module, so there is a slight performance hit.

Brian
See my reply to your comment below.
Unknown
+1  A: 

Behold:

Code

from types import *

def wrapper(f, warning):
    def new(*args, **kwargs):
        if not args[0].warned:
            print "Deprecated Warning: %s" % warning
            args[0].warned = True
        return f(*args, **kwargs)
    return new

class Deprecated(object):
    def __new__(self, o, warning):
        print "Creating Deprecated Object"
        class temp(o.__class__): pass
        temp.__name__ = "Deprecated_%s" % o.__class__.__name__
        output = temp.__new__(temp, o)

        output.warned = True
        wrappable_types = (type(int.__add__), type(zip), FunctionType)
        unwrappable_names = ("__str__", "__unicode__", "__repr__", "__getattribute__", "__setattr__")

        for method_name in dir(temp):
            if not type(getattr(temp, method_name)) in wrappable_types: continue
            if method_name in unwrappable_names: continue

            setattr(temp, method_name, wrapper(getattr(temp, method_name), warning))

        output.warned = False

        return output

Output

>>> a=Deprecated(1, "Don't use 1")
Creating Deprecated Object
>>> a+9
Deprecated Warning: Don't use 1
10
>>> a*4
4
>>> 2*a
2

This can obviously be refined, but the gist is there.

Unknown
+1 thats fairly neat. I think it would make more sense if Deprecated was a function though, rather than using a class with __new__. Also, using the warnings module will handle the "already warned about this" logic for you, and also allow users to filter/reset them the same way as for normal python warnings. (You also risk overwriting an existing "warned" attribute on the original object using the current way)There are a few corner cases where it will act slightly differently from the original object (eg "type(a) is int" which is bad style anyway), but 99.9% of the time it should work.
Brian
@Brian, actually you are supposed to do isinstance(a, int) which will work correctly because the new class inherits from the class of whatever you passed into Deprecated. Also about Deprecated not being a function, it is trivial to make it one.
Unknown
I agree (That's why I mentioned it's bad style), it just means there's a slight possibility that you'll break any poorly written code that relies on such specifics. I mentioned making it a function because it seems clearer, as the class object isn't used at all, __new__ returns a completely different class - all that's needed is removing the __new__ line, and changing the class line to "def Deprecated(o, warning):"
Brian