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.

>>> 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

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()