[Python-Dev] More on contextlib - adding back a contextmanager decorator

Nick Coghlan ncoghlan at iinet.net.au
Sun Apr 30 15:23:46 CEST 2006


A few things from the pre-alpha2 context management terminology review have 
had a chance to run around in the back of my head for a while now, and I'd 
like to return to a topic Paul Moore brought up during that discussion.

Paul had a feeling there should be two generator decorators in contextlib - 
one for __context__ methods and one for standalone generator functions. 
However, contextfactory seemed to meet both needs, so we didn't follow the 
question up for alpha 2.

The second link in this chain is the subsequent discussion with Guido about 
making the context manager and managed context protocols orthogonal. With this 
clearer separation of the two terms to happen in alpha 3, it becomes more 
reasonable to have two different generator decorators, one for defining 
managed contexts and one for defining context managers.

The final link is a use case for such a context manager decorator, in the form 
of a couple of HTML tag example contexts in the contextlib documentation. 
(contextlib.nested is also a good use case, where caching can be used to 
ensure certain resources are always acquired in the same order, but the issue 
is easier to demonstrate using the HTML tag examples)

Firstly, the class-based HTML tag context manager example:

--------------------
class TagClass:
     def __init__(self, name):
         self.name = name
     @contextfactory
     def __context__(self):
         print "<%s>" % self.name
         yield self
         print "</%s>" % self.name

 >>> h1_cls = TagClass('h1')
 >>> with h1_cls:
...     print "Header A"
...
<h1>
Header A
</h1>
 >>> with h1_cls:
...     print "Header B"
...
<h1>
Header B
</h1>
--------------------

Each with statement creates a new context object, so caching the tag object 
itself works just as you would expect. Unfortunately, the same cannot be said 
for the generator based version:

--------------------
@contextfactory
def tag_gen(name):
     print "<%s>" % name
     yield
     print "</%s>" % name

 >>> h1_gen = tag_gen('h1')
 >>> with h1_gen:
...     print "Header A"
...
<h1>
Header A
</h1>
 >>> with h1_gen:
...     print "Header B"
...
Traceback (most recent call last):
   ...
RuntimeError: generator didn't yield
--------------------

The managed contexts produced by the context factory aren't reusable, so 
caching them doesn't work properly - they need to be created afresh for each 
with statement.

Adding another decorator to define context managers, as Paul suggested, solves 
this problem neatly. Here's a possible implementation:

   def managerfactory(gen_func):
       # Create a context manager factory from a generator function
       context_factory = contextfactory(gen_func)
       def wrapper(*args, **kwds):
           class ContextManager(object):
               def __context__(self):
                   return context_factory(*args, **kwds)
           mgr = ContextManager()
           mgr.__context__() # Throwaway context to check arguments
           return mgr
       # Having @functools.decorator would eliminate the next 4 lines
       wrapper.__name__ = context_factory.__name__
       wrapper.__module__ = context_factory.__module__
       wrapper.__doc__ = context_factory.__doc__
       wrapper.__dict__.update(context_factory.__dict__)
       return wrapper

   @managerfactory
   def tag_gen2(name):
     print "<%s>" % name
     yield
     print "</%s>" % name

 >>> h1_gen2 = tag_gen2('h1')
 >>> with h1_gen2:
...     print "Header A"
...
<h1>
Header A
</h1>
 >>> with h1_gen2:
...     print "Header B"
...
<h1>
Header B
</h1>


-- 
Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia
---------------------------------------------------------------
             http://www.boredomandlaziness.org


More information about the Python-Dev mailing list