13

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?

Joseph Garvin
  • 20,727
  • 18
  • 94
  • 165
  • Can't you just delete the variable and rerun all the unit tests? – S.Lott May 28 '09 at 23:25
  • 4
    That would defeat the point of marking something deprecated. The idea is to inform users of an API that *their* code will break *in the future*. If you're writing a library, you don't necessarily know who is using your library or have access to the code they've written using it. So you mark it as deprecated to communicate to them that the function or variable marked is something you intend to remove in the future. – Joseph Garvin May 29 '09 at 14:23
  • 1
    https://stackoverflow.com/questions/2447353/getattr-on-a-module – n611x007 Dec 17 '14 at 12:38

4 Answers4

14

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
  • 116,865
  • 28
  • 107
  • 112
  • See my reply to your comment below. – Unknown May 29 '09 at 20:20
  • good to know, Guido van Rossum creator of python [writes that they have actively added support](https://mail.python.org/pipermail/python-ideas/2012-May/014969.html) for this `sys.modules[__name__]` way of tinkering with the import system, as pointed out in [answer](http://stackoverflow.com/a/7668273/611007) of [Ethan Furman](http://stackoverflow.com/users/208880/ethan-furman) to question [`__getattr__` on module](https://stackoverflow.com/questions/2447353/getattr-on-a-module) by [Matt Joiner](https://stackoverflow.com/users/149482/matt-joiner) – n611x007 Dec 17 '14 at 12:34
  • 1
    The 2nd argument to `warnings.warn` is the warning category. There is a category specifically for deprecations, `DeprecationWarning`. So, use `warnings.warn("Property %s is deprecated" % attr, DeprecationWarning)`. – cowlinator Jan 13 '21 at 00:37
5

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.

Community
  • 1
  • 1
Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395
5

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
  • 45,913
  • 27
  • 138
  • 182
  • +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 May 29 '09 at 15:51
  • @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 May 29 '09 at 20:20
  • 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 May 29 '09 at 21:32
3

This is one of the main rationale for PEP 562 (implemented in Python 3.7):

Typical workarounds are assigning __class__ of a module object to a custom subclass of types.ModuleType or replacing the sys.modules item with a custom wrapper instance. It would be convenient to simplify this procedure by recognizing __getattr__ defined directly in a module that would act like a normal __getattr__ method, except that it will be defined on module instances. For example:

# lib.py

from warnings import warn

deprecated_names = ["old_function", ...]

def _deprecated_old_function(arg, other):
    ...

def __getattr__(name):
    if name in deprecated_names:
        warn(f"{name} is deprecated", DeprecationWarning)
        return globals()[f"_deprecated_{name}"]
    raise AttributeError(f"module {__name__} has no attribute {name}")

# main.py

from lib import old_function  # Works, but emits the warning
AXO
  • 8,198
  • 6
  • 62
  • 63