Commits

jason kirtland committed e3f892e

- Refined and documented CapturedResponse contract and default implementation.
- Fixed render() return value mismatch between Component and GenshiComponent
- Updates for new contract.

Comments (0)

Files changed (3)

 """Pegboard toolkit."""
 
 class CapturedResponse(object):
-    """A WSGI response."""
+    """A suspended WSGI response.
 
-    def __init__(self, environ):
-        self.environ = environ
+    Captures and holds the response of WSGI execution.  The captured
+    output may be emitted by invoking the :class:`CapturedResponse`
+    instance as a WSGI callable.
+
+    Instances provide 4 public, read/write attributes::
+
+     - :attr:`status` The captured HTTP status.
+
+     - :attr:`headers` The captured HTTP headers.
+
+     - :attr:`iterable` The captured HTTP response body iterable.
+
+     - :attr:`captured` A boolean, True if the instance has captured
+                        output.
+
+    Capture-related attributes will evaluate as false if no output has
+    been captured.
+
+    The default implementation does not support the capture of
+    streaming responses.
+
+    """
+
+    def __init__(self, peg):
         self.status = None
         self.headers = None
+        self.captured = False
         self.iterable = None
 
+    def capture(self, wsgi, environ):
+        """Invoke wsgi and capture it's entire response.
+
+        Returns True if a response iterable was captured.
+
+        The return value of the wsgi app may be either a
+        WSGI-compliant iterable or another callable object.
+        Evaluation of a callable response will be deferred until the
+        :class:`CapturedResponse` instance's :meth:`finalize` or
+        :meth:`__call__` methods are invoked.
+
+        """
+        response = wsgi(environ, self.start)
+        if response is not None:
+            self.iterable = response
+            self.captured = True
+            return True
+
     def __call__(self, environ, start_response, close_with=None):
         """Act as a WSGI app, returning captured content.
 
 
         """
         if callable(self.iterable):
-            iterator = self.iterable(environ, self.start)
-        else:
-            iterator = self.iterable
+            self.finalize(environ)
+        iterator = iter(self.iterable)
         if close_with:
             iterator = _ClosingIterator(iterator, close_with)
         start_response(self.status, self.headers)
         return iterator
 
-    def capture(self, wsgi, environ):
-        """Invoke wsgi and capture it's entire response.
-
-        Returns True if a response iterable was captured.
-
-        """
-        response = wsgi(environ, self.start)
-        if response is not None:
-            self.iterable = response
-            return True
-
     def start(self, status, headers, exc_info=None):
         """Act as a ``start_response`` callable."""
         self.status = status
         self.headers = headers
         return _unwritable
 
-    def __iter__(self):
-        if callable(self.iterable):
-            return self.iterable(self.environ, self.start)
-        else:
-            return iter(self.iterable)
+    def finalize(self, environ):
+        """Lazily evaluate a captured WSGI callable response."""
+        while callable(self.iterable):
+            if not self.capture(self.iterable, environ):
+                self.iterable = ()
+
 
 
 class Peg(object):
     """A copying function that returns at least a shallow copy of environ."""
 
     response_factory = CapturedResponse
+    """A factory for a CapturedResponse-compatible instance."""
 
     def __init__(self, name, app, environ, board, parent):
         self.name = name
         self.app = app
         self.environ = self.environ_copier(environ)
         self.environ['pegboard.peg'] = self
-        self.response = self.response_factory(self.environ)
+        self.response = self.response_factory(self)
         self.board = board
         self.parent = parent
         self.children = []

pegboard/contrib/component/genshi.py

             TemplateLoader(self.search_paths, auto_reload=True)
 
     def render(self):
-        if self.response.iterable is None:
+        if not self.response.captured:
             if isinstance(self.template, basestring):
                 tmpl = self.loader.load(self.template, encoding='utf-8')
             else:
                 tmpl = self.template
 
             self.response.iterable = tmpl.generate(**self.rendering_context)
+            self.response.captured = True
         if hasattr(self.response.iterable, 'serialize'):
             # if it quacks like a genshi stream then render it
             # recursively, otherwise just let it pass on.  This can be
                     item = item[2:-2]
                     instruction, value = item.split()
                     if instruction == "component":
-                        comp = self.context.board.pegs[value].app
-                        output.extend(comp.render())
+                        component = self.context.board.pegs[value].app
+                        iterable = component.render()
+                        output.extend(iterable)
+                        if hasattr(iterable, 'close'):
+                            iterable.close()
                 else:
                     output.append(item.encode('utf-8'))
             headers = self.response.headers or []
                 headers.append(('Content-Type', 'text/html; charset=utf-8'))
             self.response.start('200 OK', headers)
             self.response.iterable = output
-        return self.response
+        return self.response.iterable
 
 
 def component(expr):

pegboard/contrib/response.py

 
     """
 
-    def __init__(self, environ, response=None, **kw):
-        self.environ = environ
+    def __init__(self, peg, response=None, **kw):
         self.iterable = None
         wrappers.BaseResponse.__init__(self, response, **kw)
         if response is None:
             self.iterable = None
         elif isinstance(value, basestring):
             self.iterable = [value]
+        elif callable(value):
+            self.iterable = value
         else:
             self.iterable = iter(value)
 
     force_type = classmethod(force_type)
 
     def capture(self, wsgi, environ):
-        self.response = response = wsgi(environ, self.start)
+        response = wsgi(environ, self.start)
         if response is None:
             return False
+        elif response is not self:
+            self.response = response
         self.captured = True
         return True
 
     def __call__(self, environ, start_response, close_with=None):
         """Acts as a WSGI callable."""
         if callable(self.response):
-            self.response = iter(self.response(environ, self.start))
+            self.finalize(environ)
         if close_with:
             self.response = base._ClosingIterator(self.response, close_with)
 
         start_response(self.status, self.header_list)
         return resp
 
+
 for attr, value in CapturedWerkzeugResponse.__dict__.items():
     if not attr.startswith('_') and not value.__doc__:
         parent = getattr(base.CapturedResponse, attr, None)