tushman.io

The musings of an insecure technologist

Module Properties | the Proxy Pattern

Have you ever tried to add a @property to a module?

I was working on a authentication_manager. And I wanted to usage of the module to be like so:

1
2
3
4
5
6
from login_manager import current_user

def do_sometime_with_a_user():
    print(current_user.name)
    current_user.speak()
    current_user.say_hi('lauren')

current_user is going to return the current User if it exists. In other words it needs to call a function.

This would be pretty trivial if I would be okay with using parens all over the place

1
2
3
4
5
6
print(current_user().name)
# gross ----------^^
current_user().speak()
# gross ----^^
current_user().say_hi('lauren')
# gross ----^^

But that makes me want to barf. Luckily python gives us the tools to clean this up. We are going to use the Proxy pattern to solve it. At its simplest we can do something like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Proxy(object):

    def __init__(self, local):
        self.local = local

    def __getattr__(self, name):
        return getattr(self.local(), name)

# aliasing for better syntax        
module_property = Proxy

class User(object):
    """Contrived User Object"""

    def __init__(self, **kwargs):
        self.name = kwargs.get('name', 'billy')

    def speak(self):
        print("Well hello there!")

    def say_hi(self, to_whom):
        print("Hi there {}".format(to_whom))

@module_property
def current_user():
  return User()

With this we have come close to achieving our goal:

1
2
3
4
5
6
from login_manager import current_user, User

if __name__ == '__main__':
    print current_user.name
    current_user.speak()
    current_user.say_hi('lauren')

This simple Proxy class that we defined. Takes a function, stores in a local variable, and then when it is accessed it is executed with names and arguments passed through to it. __getattr__ is a pretty special feature of python.

The big gotcha with this is that current_user does not return a User object (like the built-in @property will return), it is going to return the Proxy object. So without a little bit of additional care you might run into issues.

The werkzeug team has developed a fully featured Proxy within the werkzerg project. If you are using werkzeug, you can find it: from werkzeug.local import LocalProxy

Its takes the proxy pattern further by overwriting all of the python object methods such as __eq__, __le__, __str__ and so on, to use the proxied object as the underlying target.

If you are not using werkzeug I have created a mini library where you can get the extracted proxy code. You can find it here: (http://github.com/jtushman/proxy_tools)

Or install it like so:

1
pip install proxy_tools

And use it like so:

1
2
3
4
5
6
7
8
9
10
# your_module/__init__.py
from proxy_tools import module_property

@module_property
def current_user():
  return User.find_by_id(request['user_id'])

# Then elsewhere
from your_module import current_user
print(current_user.name)

Now — I am sure there was a very good reason why the python-powers-that-be chose not add the @property syntax to modules. But for the time being I have found it useful and elegant.

Comments