Posts for the month of May 2008

Emulating !ActionScript's "with" statement in Python

If you haven't had much to do with ActionScript or Visual Basic, right now you might be thinking, as I did, "huh?" Basically the with statement in both of these languages allows one to access the attributes of an object as if they were in the local scope.

I know, I know. Why would you want to do this? I have no idea, but I thought it was cool that it was even possible in Python. However, there are some fairly major limitations:

  • It only works in a global scope (I neglected to mention this originally, apologies - see #74 for details). One can s/f_locals/f_globals/g but this introduces its own problems.
  • It only works for objects with a mutable __dict__.

I also thought the approach of injecting symbols into the context could be useful for providing DSL-like features. eg.

with query('a', 'b', 'c') as result:
    SELECT(a.foo, b.bar, c.baz)
    WHERE(AND(a.foo == 10, b.bar == 20))
    ORDER_BY(a.foo)
    GROUP_BY(b.bar)
    for row in result:
        print row.foo, row.bar, row.baz

etc.

But enough speculation, here is an actual example. It requires at least Python 2.5.

Error: Failed to load processor pycon
No macro or processor named 'pycon' found

And finally, here's the actual code.

from __future__ import with_statement
import inspect


class scope(object):
    """A context that maps an objects attributes into the current lexical
    scope, only for the duration of the context.

    NB. Properties will NOT work.

    >>> import math
    >>> with scope(math):
    ...     print sqrt(9)
    3.0

    >>> class Vector:
    ...     def __init__(self, x, y):
    ...         self.x = x
    ...         self.y = y
    ...     def length(self):
    ...         return math.sqrt(self.x ** 2 + self.y ** 2)
    >>> v = Vector(3, 3)
    >>> with scope(v):
    ...     print x, '%0.2f' % length()
    ...     x = 4
    ...     print x, '%0.2f' % length()
    3 4.24
    4 5.00
    >>> print v.x, v.y, v.length()
    4 3 5.0
    >>> print x
    Traceback (most recent call last):
    ...
    NameError: name 'x' is not defined
    """
    def __init__(self, obj):
        self.members = dict(inspect.getmembers(obj))
        self.obj = obj

    def __enter__(self):
        parent = inspect.currentframe().f_back
        # Preserve parent locals() and the objects dictionaries
        self.old_locals = dict(parent.f_locals)
        self.old_dict = self.obj.__dict__
        # Update locals with object members
        parent.f_locals.update(self.members)
        # If possible, replace the object's __dict__ with our updated locals.
        # Functions on the object that operate on the attributes will then
        # continue to work as expected. The downside is that all other objects
        # in the parents scope will be included as well.
        try:
            self.obj.__dict__ = parent.f_locals
            self.fake_dict = True
        except (TypeError, AttributeError):
            self.fake_dict = False

    def __exit__(self, type, value, traceback):
        parent = inspect.currentframe().f_back
        if self.fake_dict:
            object_dict = self.old_dict
        else:
            object_dict = self.obj.__dict__
        # Replicate member state from locals to object's __dict__.
        for key in self.members:
            if key in parent.f_locals:
                object_dict[key] = parent.f_locals[key]
                if key not in self.old_locals:
                    del parent.f_locals[key]
            else:
                del object_dict[key]
        if self.fake_dict:
            self.obj.__dict__ = object_dict
        del self.old_locals
        del self.old_dict
        return False


if __name__ == '__main__':
    import doctest
    doctest.testmod()