Posts in category hack

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

Type masquerading using dynamic base classes

Python allows us to construct new classes on-the-fly with type(). By (ab)using this feature we can construct a class that preserves its own interface while assuming all of the behaviour of an existing object. Useful for wrapping compound types such as dictionaries, lists, sets, etc. without having to proxy __getitem__, __iter__ and so on, or write a custom __getattr__.

One example of when this could be useful is a JSON-specific HTTP client object that assumes the type of the JSON data, while maintaining response-specific attributes such as headers and status:

import simplejson


class Response(object):
  """A response object that masquerades as the decoded content type."""
  def __new__(cls, content=None, headers=None, status=None):
    if content is not None:
      content = simplejson.loads(content)
      assumed_type = type(content)

      bases = (Response, assumed_type)
      name = assumed_type.__name__.title() + 'Response'
      cls = type(name, bases, {})

      self = assumed_type.__new__(cls, content)
      self.assumed_type = assumed_type
    else:
      self = object.__new__(cls)
    return self

  def __init__(self, content=None, headers=None, status=None):
    if content is not None:
      super(Response, self).__init__(content)
    self.headers = headers
    self.status = status


json_data = [
  '{"foo": 1, "bar": 2}',
  '123',
  '123.5',
  '["foo", "bar"]',
  ]


for data in json_data:
  response = Response(data, headers=[('Content-Type', 'application/json')],
                      status=200)
  decoded = simplejson.loads(data)
  print response
  print '  Same type?', isinstance(response, type(decoded))
  try:
    print '  Iteration:',
    print [i for i in response]
  except TypeError:
    print '(type does not support iteration)'
  print '  Headers:', response.headers
  print '  Status:', response.status
  print

Outputs this:

{u'foo': 1, u'bar': 2}
  Same type? True
  Iteration: [u'foo', u'bar']
  Headers: [('Content-Type', 'application/json')]
  Status: 200

123
  Same type? True
  Iteration: (type does not support iteration)
  Headers: [('Content-Type', 'application/json')]
  Status: 200

123.5
  Same type? True
  Iteration: (type does not support iteration)
  Headers: [('Content-Type', 'application/json')]
  Status: 200

[u'foo', u'bar']
  Same type? True
  Iteration: [u'foo', u'bar']
  Headers: [('Content-Type', 'application/json')]
  Status: 200