Friday, June 5, 2009

Fresh instances in Python with lazy evaluation

Testoob's --capture feature (written by Misha) replaces sys.stdout and sys.stderr for each test being run, and displays the output only if the test fails.

Leeor Aharon, of ff-activex-host fame, wanted to use it with Python's logging module, but the StreamHandler class stores a reference to the output stream on initialization - it already has a reference to sys.stdout, so replacing it won't affect it.

We originally thought of sublcassing StreamHandler and making retrieve the logger from a property, but we came up with this elegant code instead:

 
class LazyEvaluator(object):
    def __init__(self, factory):
        self.__factory = factory
    def __getattr__(self, name):
        return getattr(self.__factory(), name)

lazy_stdout = LazyEvaluator(lambda:sys.stdout)
handler = StreamHandler(strm=lazy_stdout)

The "lazy evaluator" is initialized with a factory callable, and every time it tries to access an attribute or method the object will be re-created by the factory. No changes necessary for StreamHandler, and ./alltests.py --capture works like a charm.

3 comments:

  1. As usual, adding one more level of indirection to solve the world's problems :)

    ReplyDelete
  2. An interesting solution! Nice..

    ReplyDelete
  3. Nice, but has subtle corner cases.
    Example: you have no __setattr__, so lazy_stdout.softspace will be written on lazy_stdout itself, which will work (because henceforth __dict__['softspace'] will override __getattr__) but will be distinct from stdout.softspace.
    So if StreamHandler prints will be intermixed with other printing, softspace will develop a split personality syndrome...
    OK, this might be lowest-impact bug I've ever seen!

    But what I;m getting at is that you want a proxy, and implementing a full proxy in Python is non-trivial, so consider taking a working one, like http://code.activestate.com/recipes/496741/.

    ReplyDelete