[Zope3-checkins] SVN: Zope3/branches/stephan_and_jim-response-refactor/src/zope/ Some initial refactoring of the publisher; more comments when checking in

Stephan Richter srichter at cosmos.phy.tufts.edu
Fri Sep 2 15:50:56 EDT 2005


Log message for revision 38247:
  Some initial refactoring of the publisher; more comments when checking in 
  to the trunk.
  

Changed:
  U   Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/debug/debug.py
  U   Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/file/browser/file.txt
  U   Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/file/browser/ftests.py
  U   Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/ftests/doctest.txt
  U   Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/publication/methodnotallowed.txt
  U   Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/testing/functional.py
  U   Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/base.py
  U   Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/browser.py
  U   Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/ftp.py
  U   Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/http.py
  U   Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/interfaces/__init__.py
  U   Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/interfaces/http.py
  U   Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/publish.py
  U   Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/xmlrpc.py

-=-
Modified: Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/debug/debug.py
===================================================================
--- Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/debug/debug.py	2005-09-02 19:25:40 UTC (rev 38246)
+++ Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/debug/debug.py	2005-09-02 19:50:56 UTC (rev 38247)
@@ -51,7 +51,7 @@
         return self.db.open().root()[ZopePublication.root_name]
 
     def _request(self,
-                 path='/', stdin='', stdout=None, basic=None,
+                 path='/', stdin='', basic=None,
                  environment = None, form=None,
                  request=None, publication=BrowserPublication):
         """Create a request
@@ -59,9 +59,6 @@
 
         env = {}
 
-        if stdout is None:
-            stdout = StringIO()
-
         if type(stdin) is str:
             stdin = StringIO(stdin)
 
@@ -83,9 +80,9 @@
         pub = publication(self.db)
 
         if request is not None:
-            request = request(stdin, stdout, env)
+            request = request(stdin, env)
         else:
-            request = TestRequest(stdin, stdout, env)
+            request = TestRequest(stdin, env)
             setDefaultSkin(request)
         request.setPublication(pub)
         if form:

Modified: Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/file/browser/file.txt
===================================================================
--- Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/file/browser/file.txt	2005-09-02 19:25:40 UTC (rev 38246)
+++ Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/file/browser/file.txt	2005-09-02 19:50:56 UTC (rev 38247)
@@ -32,7 +32,7 @@
   >>> print http(r"""
   ... GET /@@+/action.html?type_name=zope.app.file.File HTTP/1.1
   ... Authorization: Basic mgr:mgrpw
-  ... """)
+  ... """, handle_errors=False)
   HTTP/1.1 303 See Other
   Content-Length: ...
   Location: http://localhost/+/zope.app.file.File=
@@ -171,7 +171,7 @@
   ... """)
   HTTP/1.1 200 Ok
   Content-Length: 0
-  Content-Type: text/plain;charset=utf-8
+  Content-Type: text/plain
   <BLANKLINE>
 
 Since it is a text file, we can edit it directly in a web form.
@@ -268,7 +268,7 @@
   ... """)
   HTTP/1.1 200 Ok
   Content-Length: ...
-  Content-Type: text/plain;charset=utf-8
+  Content-Type: text/plain
   <BLANKLINE>
   This is a sample text file.
   <BLANKLINE>

Modified: Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/file/browser/ftests.py
===================================================================
--- Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/file/browser/ftests.py	2005-09-02 19:25:40 UTC (rev 38246)
+++ Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/file/browser/ftests.py	2005-09-02 19:50:56 UTC (rev 38247)
@@ -26,7 +26,7 @@
 
 class FileTest(BrowserTestCase):
 
-    content = u'File <Data>' 
+    content = u'File <Data>'
 
     def addFile(self):
         file = File(self.content)
@@ -130,7 +130,7 @@
         file = root['file']
         self.assertEqual(file.data, '<h1>A file</h1>')
         self.assertEqual(file.contentType, 'text/plain')
-        
+
     def testIndex(self):
         self.addFile()
         response = self.publish(

Modified: Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/ftests/doctest.txt
===================================================================
--- Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/ftests/doctest.txt	2005-09-02 19:25:40 UTC (rev 38246)
+++ Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/ftests/doctest.txt	2005-09-02 19:50:56 UTC (rev 38247)
@@ -23,19 +23,19 @@
   HTTP/1.1 401 Unauthorized
   Content-Length: ...
   Content-Type: text/html;charset=utf-8
-  Www-Authenticate: basic realm=zope
+  WWW-Authenticate: basic realm=zope
   <BLANKLINE>
   <!DOCTYPE html PUBLIC ...
 
 Here we see that we got:
 
   - A 404 response,
-  - A Www-Authenticate header, and
+  - A WWW-Authenticate header, and
   - An html body with an error message
 
 Note that we used ellipeses to indicate ininteresting details.
 
-Next, we'll access the same page with credentials: 
+Next, we'll access the same page with credentials:
 
   >>> print http(r"""
   ... GET /@@contents.html HTTP/1.1
@@ -48,11 +48,11 @@
   <!DOCTYPE html PUBLIC ...
 
 Important note: you must use the user named "mgr" with a password
-"mgrpw". 
+"mgrpw".
 
 And we get a normal output.
 
-Next we'll try accessing site management. Since we used "/manage", 
+Next we'll try accessing site management. Since we used "/manage",
 we got redirected:
 
   >>> print http(r"""
@@ -125,7 +125,7 @@
   ... Authorization: Basic mgr:mgrpw
   ... Content-Length: 73
   ... Content-Type: application/x-www-form-urlencoded
-  ... 
+  ...
   ... type_name=BrowserAdd__zope.app.folder.folder.Folder&new_value=f1""")
   HTTP/1.1 303 See Other
   Content-Length: ...

Modified: Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/publication/methodnotallowed.txt
===================================================================
--- Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/publication/methodnotallowed.txt	2005-09-02 19:25:40 UTC (rev 38246)
+++ Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/publication/methodnotallowed.txt	2005-09-02 19:50:56 UTC (rev 38247)
@@ -10,6 +10,7 @@
   HTTP/1.1 405 Method Not Allowed
   Allow: DELETE, MKCOL, OPTIONS, PROPFIND, PROPPATCH, PUT
   Content-Length: 18
+  Content-Type: text/plain
   <BLANKLINE>
   Method Not Allowed
 

Modified: Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/testing/functional.py
===================================================================
--- Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/testing/functional.py	2005-09-02 19:25:40 UTC (rev 38246)
+++ Zope3/branches/stephan_and_jim-response-refactor/src/zope/app/testing/functional.py	2005-09-02 19:50:56 UTC (rev 38247)
@@ -55,30 +55,39 @@
 from zope.publisher.interfaces.browser import IBrowserRequest
 from zope.app.component.hooks import setSite, getSite
 
-HTTPTaskStub = StringIO
 
-
 class ResponseWrapper(object):
     """A wrapper that adds several introspective methods to a response."""
 
-    def __init__(self, response, outstream, path):
+    def __init__(self, response, path, omit=()):
         self._response = response
-        self._outstream = outstream
         self._path = path
+        self.omit = omit
+        self._body = None
 
     def getOutput(self):
         """Returns the full HTTP output (headers + body)"""
-        return self._outstream.getvalue()
+        body = self.getBody()
+        omit = self.omit
+        headers = [x
+                   for x in self._response.getHeaders()
+                   if x[0].lower() not in omit]
+        headers.sort()
+        headers = '\n'.join([("%s: %s" % (n, v)) for (n, v) in headers])
+        statusline = '%s %s' % (self._response._request['SERVER_PROTOCOL'],
+                                self._response.getStatusString())
+        if body:
+            return '%s\n%s\n\n%s' %(statusline, headers, body)
+        else:
+            return '%s\n%s\n' % (statusline, headers)
 
     def getBody(self):
         """Returns the response body"""
-        output = self._outstream.getvalue()
-        idx = output.find('\r\n\r\n')
-        if idx == -1:
-            return None
-        else:
-            return output[idx+4:]
+        if self._body is None:
+            self._body = ''.join(self._response.result.body)
 
+        return self._body
+
     def getPath(self):
         """Returns the path of the request"""
         return self._path
@@ -86,7 +95,9 @@
     def __getattr__(self, attr):
         return getattr(self._response, attr)
 
+    __str__ = getOutput
 
+
 class IManagerSetup(zope.interface.Interface):
     """Utility for enabling up a functional testing manager with needed grants
 
@@ -238,8 +249,7 @@
         """Returns the site which is used to look up local components"""
         return getSite()
 
-    def makeRequest(self, path='', basic=None, form=None, env={},
-                    outstream=None):
+    def makeRequest(self, path='', basic=None, form=None, env={}):
         """Creates a new request object.
 
         Arguments:
@@ -251,16 +261,13 @@
                     (You can emulate HTTP request header
                        X-Header: foo
                      by adding 'HTTP_X_HEADER': 'foo' to env)
-          outstream -- a stream where the HTTP response will be written
         """
-        if outstream is None:
-            outstream = HTTPTaskStub()
         environment = {"HTTP_HOST": 'localhost',
                        "HTTP_REFERER": 'localhost',
                        "HTTP_COOKIE": self.httpCookie(path)}
         environment.update(env)
         app = FunctionalTestSetup().getApplication()
-        request = app._request(path, '', outstream,
+        request = app._request(path, '',
                                environment=environment,
                                basic=basic, form=form,
                                request=BrowserRequest)
@@ -281,7 +288,6 @@
           getBody()      -- returns the full response body as a string
           getPath()      -- returns the path used in the request
         """
-        outstream = HTTPTaskStub()
         old_site = self.getSite()
         self.setSite(None)
         # A cookie header has been sent - ensure that future requests
@@ -292,9 +298,8 @@
             self.loadCookies(env['HTTP_COOKIE'])
             del env['HTTP_COOKIE'] # Added again in makeRequest
 
-        request = self.makeRequest(path, basic=basic, form=form, env=env,
-                                   outstream=outstream)
-        response = ResponseWrapper(request.response, outstream, path)
+        request = self.makeRequest(path, basic=basic, form=form, env=env)
+        response = ResponseWrapper(request.response, path)
         if env.has_key('HTTP_COOKIE'):
             self.loadCookies(env['HTTP_COOKIE'])
         publish(request, handle_errors=handle_errors)
@@ -362,7 +367,7 @@
 
                 # Make sure we don't have pending changes
                 abort()
-                
+
                 # The request should always be closed to free resources
                 # held by the request
                 if request:
@@ -376,7 +381,7 @@
     """Functional test case for HTTP requests."""
 
     def makeRequest(self, path='', basic=None, form=None, env={},
-                    instream=None, outstream=None):
+                    instream=None):
         """Creates a new request object.
 
         Arguments:
@@ -389,17 +394,14 @@
                        X-Header: foo
                      by adding 'HTTP_X_HEADER': 'foo' to env)
           instream  -- a stream from where the HTTP request will be read
-          outstream -- a stream where the HTTP response will be written
         """
-        if outstream is None:
-            outstream = HTTPTaskStub()
         if instream is None:
             instream = ''
         environment = {"HTTP_HOST": 'localhost',
                        "HTTP_REFERER": 'localhost'}
         environment.update(env)
         app = FunctionalTestSetup().getApplication()
-        request = app._request(path, instream, outstream,
+        request = app._request(path, instream,
                                environment=environment,
                                basic=basic, form=form,
                                request=HTTPRequest, publication=HTTPPublication)
@@ -419,67 +421,13 @@
           getBody()      -- returns the full response body as a string
           getPath()      -- returns the path used in the request
         """
-        outstream = HTTPTaskStub()
         request = self.makeRequest(path, basic=basic, form=form, env=env,
-                                   instream=request_body, outstream=outstream)
-        response = ResponseWrapper(request.response, outstream, path)
+                                   instream=request_body)
+        response = ResponseWrapper(request.response, path)
         publish(request, handle_errors=handle_errors)
         return response
 
 
-class HTTPHeaderOutput:
-
-    interface.implements(zope.publisher.interfaces.http.IHeaderOutput)
-
-    def __init__(self, protocol, omit):
-        self.headers = {}
-        self.headersl = []
-        self.protocol = protocol
-        self.omit = omit
-
-    def setResponseStatus(self, status, reason):
-        self.status, self.reason = status, reason
-
-    def setResponseHeaders(self, mapping):
-        self.headers.update(dict(
-            [('-'.join([s.capitalize() for s in name.split('-')]), v)
-             for name, v in mapping.items()
-             if name.lower() not in self.omit]
-        ))
-
-    def appendResponseHeaders(self, lst):
-        headers = [split_header(header) for header in lst]
-        self.headersl.extend(
-            [('-'.join([s.capitalize() for s in name.split('-')]), v)
-             for name, v in headers
-             if name.lower() not in self.omit]
-        )
-
-    def __str__(self):
-        out = ["%s: %s" % header for header in self.headers.items()]
-        out.extend(["%s: %s" % header for header in self.headersl])
-        out.sort()
-        out.insert(0, "%s %s %s" % (self.protocol, self.status, self.reason))
-        return '\n'.join(out)
-
-class DocResponseWrapper(ResponseWrapper):
-    """Response Wrapper for use in doc tests
-    """
-
-    def __init__(self, response, outstream, path, header_output):
-        ResponseWrapper.__init__(self, response, outstream, path)
-        self.header_output = header_output
-
-    def __str__(self):
-        body = self.getOutput()
-        if body:
-            return "%s\n\n%s" % (self.header_output, body)
-        return "%s\n" % (self.header_output)
-
-    def getBody(self):
-        return self.getOutput()
-
-
 headerre = re.compile(r'(\S+): (.+)$')
 def split_header(header):
     return headerre.match(header).group(1, 2)
@@ -571,8 +519,6 @@
         if environment.has_key(auth_key):
             environment[auth_key] = auth_header(environment[auth_key])
 
-        outstream = HTTPTaskStub()
-
         old_site = getSite()
         setSite(None)
 
@@ -581,7 +527,7 @@
         app = FunctionalTestSetup().getApplication()
 
         request = app._request(
-            path, instream, outstream,
+            path, instream,
             environment=environment,
             request=request_cls, publication=publication_cls)
         if IBrowserRequest.providedBy(request):
@@ -593,11 +539,10 @@
                 raise ValueError("only one set of form values can be provided")
             request.form = form
 
-        header_output = HTTPHeaderOutput(
-            protocol, ('x-content-type-warning', 'x-powered-by'))
-        request.response.setHeaderOutput(header_output)
-        response = DocResponseWrapper(
-            request.response, outstream, path, header_output)
+        response = ResponseWrapper(
+            request.response, path,
+            omit=('x-content-type-warning', 'x-powered-by'),
+            )
 
         publish(request, handle_errors=handle_errors)
         self.saveCookies(response)
@@ -612,8 +557,8 @@
         """Choose and return a request class and a publication class"""
         # note that `path` is unused by the default implementation (BBB)
         return chooseClasses(method, environment)
- 
 
+
 def FunctionalDocFileSuite(*paths, **kw):
     globs = kw.setdefault('globs', {})
     globs['http'] = HTTPCaller()

Modified: Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/base.py
===================================================================
--- Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/base.py	2005-09-02 19:25:40 UTC (rev 38246)
+++ Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/base.py	2005-09-02 19:50:56 UTC (rev 38247)
@@ -36,35 +36,16 @@
     """
 
     __slots__ = (
-        '_body',      # The response body
-        '_outstream', # The output stream
-        '_request',    # The associated request (if any)
+        'result',     # The result of the application call
+        '_request',   # The associated request (if any)
         )
 
     implements(IResponse)
 
-
-    def __init__(self, outstream):
-        self._body = ''
-        self._outstream = outstream
-
-    def outputBody(self):
+    def setResult(self, result):
         'See IPublisherResponse'
-        self._outstream.write(self._getBody())
+        self.result = result
 
-    def setBody(self, body):
-        'See IPublisherResponse'
-        self._body = body
-
-    # This method is not part of this interface
-    def _getBody(self):
-        'Returns a string representing the currently set body.'
-        return self._body
-
-    def reset(self):
-        'See IPublisherResponse'
-        self._body = ""
-
     def handleException(self, exc_info):
         'See IPublisherResponse'
         traceback.print_exception(
@@ -74,14 +55,19 @@
         'See IPublisherResponse'
         pass
 
+    def reset(self):
+        'See IPublisherResponse'
+        pass
+
     def retry(self):
         'See IPublisherResponse'
-        return self.__class__(self.outstream)
+        return self.__class__()
 
-    def write(self, string):
-        'See IApplicationResponse'
-        self._body += string
+    # XXX: Do BBB properly
+    def setBody(self, body):
+        return self.setResult(body)
 
+
 class RequestDataGetter(object):
 
     implements(IReadMapping)
@@ -196,7 +182,7 @@
 
     environment = RequestDataProperty(RequestEnvironment)
 
-    def __init__(self, body_instream, outstream, environ, response=None,
+    def __init__(self, body_instream, environ, response=None,
                  positional=()):
         self._traversal_stack = []
         self._last_obj_traversed = None
@@ -205,7 +191,7 @@
 
         self._args = positional
         if response is None:
-            self._response = self._createResponse(outstream)
+            self._response = self._createResponse()
         else:
             self._response = response
         self._response._request = self
@@ -283,7 +269,7 @@
         for held in self._held:
             if IHeld.providedBy(held):
                 held.release()
-        
+
         self._held = None
         self._response = None
         self._body_instream = None
@@ -381,9 +367,9 @@
 
     has_key = __contains__
 
-    def _createResponse(self, outstream):
+    def _createResponse(self):
         # Should be overridden by subclasses
-        return BaseResponse(outstream)
+        return BaseResponse()
 
     def __nonzero__(self):
         # This is here to avoid calling __len__ for boolean tests
@@ -398,7 +384,7 @@
         path = self.get(attr, "/").strip()
         if path.endswith('/'):
             # Remove trailing backslash, so that we will not get an empty
-            # last entry when splitting the path. 
+            # last entry when splitting the path.
             path = path[:-1]
             self._endswithslash = True
         else:
@@ -424,16 +410,14 @@
 
     __slots__ = ('_presentation_type', )
 
-    def __init__(self, path, body_instream=None, outstream=None, environ=None):
+    def __init__(self, path, body_instream=None, environ=None):
         if environ is None:
             environ = {}
         environ['PATH_INFO'] = path
         if body_instream is None:
             body_instream = StringIO('')
-        if outstream is None:
-            outstream = StringIO()
 
-        super(TestRequest, self).__init__(body_instream, outstream, environ)
+        super(TestRequest, self).__init__(body_instream, environ)
 
 
 class DefaultPublication(object):

Modified: Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/browser.py
===================================================================
--- Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/browser.py	2005-09-02 19:25:40 UTC (rev 38246)
+++ Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/browser.py	2005-09-02 19:50:56 UTC (rev 38247)
@@ -213,16 +213,14 @@
     # effective and actual URLs differ.
     use_redirect = False 
 
-    def __init__(self, body_instream, outstream, environ, response=None):
+    def __init__(self, body_instream, environ, response=None):
         self.form = {}
         self.charsets = None
-        super(BrowserRequest, self).__init__(
-            body_instream, outstream, environ, response)
+        super(BrowserRequest, self).__init__(body_instream, environ, response)
 
 
-    def _createResponse(self, outstream):
-        # Should be overridden by subclasses
-        return BrowserResponse(outstream)
+    def _createResponse(self):
+        return BrowserResponse()
 
     def _decode(self, text):
         """Try to decode the text using one of the available charsets."""
@@ -600,8 +598,7 @@
     """Browser request with a constructor convenient for testing
     """
 
-    def __init__(self,
-                 body_instream=None, outstream=None, environ=None, form=None,
+    def __init__(self, body_instream=None, environ=None, form=None,
                  skin=None,
                  **kw):
 
@@ -620,11 +617,7 @@
             from StringIO import StringIO
             body_instream = StringIO('')
 
-        if outstream is None:
-            from StringIO import StringIO
-            outstream = StringIO()
-
-        super(TestRequest, self).__init__(body_instream, outstream, _testEnv)
+        super(TestRequest, self).__init__(body_instream, _testEnv)
         if form:
             self.form.update(form)
 
@@ -655,60 +648,12 @@
         '_base', # The base href
         )
 
-    def setBody(self, body):
-        """Sets the body of the response
-
-        Sets the return body equal to the (string) argument "body". Also
-        updates the "content-length" return header and sets the status to
-        200 if it has not already been set.
-        """
-        if body is None:
-            return
-
-        if not isinstance(body, StringTypes):
-            body = unicode(body)
-
-        if 'content-type' not in self._headers:
-            c = (self.__isHTML(body) and 'text/html' or 'text/plain')
-            if self._charset is not None:
-                c += ';charset=' + self._charset
-            self.setHeader('content-type', c)
-            self.setHeader('x-content-type-warning', 'guessed from content')
-            # TODO: emit a warning once all page templates are changed to
-            # specify their content type explicitly.
-
+    def _implicitResult(self, body):
+        body, headers = super(BrowserResponse, self)._implicitResult(body)
         body = self.__insertBase(body)
-        self._body = body
-        self._updateContentLength()
-        if not self._status_set:
-            self.setStatus(200)
+        return body, headers
 
 
-    def __isHTML(self, str):
-        """Try to determine whether str is HTML or not."""
-        s = str.lstrip().lower()
-        if s.startswith('<!doctype html'):
-            return True
-        if s.startswith('<html') and (s[5:6] in ' >'):
-            return True
-        if s.startswith('<!--'):
-            idx = s.find('<html')
-            return idx > 0 and (s[idx+5:idx+6] in ' >')
-        else:
-            return False
-
-
-    def __wrapInHTML(self, title, content):
-        t = escape(title)
-        return (
-            "<html><head><title>%s</title></head>\n"
-            "<body><h2>%s</h2>\n"
-            "%s\n"
-            "</body></html>\n" %
-            (t, t, content)
-            )
-
-
     def __insertBase(self, body):
         # Only insert a base tag if content appears to be html.
         content_type = self.getHeader('content-type', '')

Modified: Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/ftp.py
===================================================================
--- Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/ftp.py	2005-09-02 19:25:40 UTC (rev 38246)
+++ Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/ftp.py	2005-09-02 19:50:56 UTC (rev 38247)
@@ -29,7 +29,7 @@
     def getResult(self):
         if getattr(self, '_exc', None) is not None:
             raise self._exc[0], self._exc[1], self._exc[2]
-        return self._getBody()
+        return self.result
 
     def handleException(self, exc_info):
         self._exc = exc_info
@@ -39,12 +39,11 @@
 
     __slots__ = '_auth'
 
-    def __init__(self, body_instream, outstream, environ, response=None):
+    def __init__(self, body_instream, environ, response=None):
         self._auth = environ.get('credentials')
         del environ['credentials']
 
-        super(FTPRequest, self).__init__(
-            body_instream, outstream, environ, response)
+        super(FTPRequest, self).__init__(body_instream, environ, response)
 
         path = environ['path']
         if path.startswith('/'):
@@ -55,9 +54,9 @@
             self.setTraversalStack(path)
 
 
-    def _createResponse(self, outstream):
+    def _createResponse(self):
         """Create a specific XML-RPC response object."""
-        return FTPResponse(outstream)
+        return FTPResponse()
 
     def _authUserPW(self):
         'See IFTPCredentials'

Modified: Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/http.py
===================================================================
--- Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/http.py	2005-09-02 19:25:40 UTC (rev 38246)
+++ Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/http.py	2005-09-02 19:50:56 UTC (rev 38247)
@@ -30,13 +30,14 @@
 from zope.publisher.interfaces.http import IHTTPPublisher
 
 from zope.publisher.interfaces import Redirect
-from zope.publisher.interfaces.http import IHTTPResponse
+from zope.publisher.interfaces.http import IHTTPResponse, IResult
 from zope.publisher.interfaces.http import IHTTPApplicationResponse
 from zope.publisher.interfaces.logginginfo import ILoggingInfo
 from zope.i18n.interfaces import IUserPreferredCharsets
 from zope.i18n.interfaces import IUserPreferredLanguages
 from zope.i18n.locales import locales, LoadLocaleError
 
+from zope.publisher import contenttype
 from zope.publisher.base import BaseRequest, BaseResponse
 from zope.publisher.base import RequestDataProperty, RequestDataMapper
 from zope.publisher.base import RequestDataGetter
@@ -234,9 +235,8 @@
 
     retry_max_count = 3    # How many times we're willing to retry
 
-    def __init__(self, body_instream, outstream, environ, response=None):
-        super(HTTPRequest, self).__init__(
-            body_instream, outstream, environ, response)
+    def __init__(self, body_instream, environ, response=None):
+        super(HTTPRequest, self).__init__(body_instream, environ, response)
 
         self._orig_env = environ
         environ = sane_environment(environ)
@@ -258,7 +258,6 @@
         self.__setupLocale()
 
     def __setupLocale(self):
-        self.response.setCharsetUsingRequest(self)
         envadapter = IUserPreferredLanguages(self, None)
         if envadapter is None:
             self._locale = None
@@ -372,7 +371,6 @@
         new_response = self.response.retry()
         request = self.__class__(
             body_instream=self._body_instream,
-            outstream=None,
             environ=self._orig_env,
             response=new_response,
             )
@@ -435,15 +433,13 @@
     def setPrincipal(self, principal):
         'See IPublicationRequest'
         super(HTTPRequest, self).setPrincipal(principal)
+        logging_info = ILoggingInfo(principal)
+        message = logging_info.getLogMessage()
+        self.response.authUser = message
 
-        if self.response.http_transaction is not None:
-            logging_info = ILoggingInfo(principal)
-            message = logging_info.getLogMessage()
-            self.response.http_transaction.setAuthUserName(message)
-
-    def _createResponse(self, outstream):
+    def _createResponse(self):
         # Should be overridden by subclasses
-        return HTTPResponse(outstream)
+        return HTTPResponse()
 
 
     def getURL(self, level=0, path_only=False):
@@ -540,25 +536,22 @@
     implements(IHTTPResponse, IHTTPApplicationResponse)
 
     __slots__ = (
+        'authUser',             # Authenticated user string
         '_header_output',       # Hook object to collaborate with a server
                                 # for header generation.
         '_headers',
         '_cookies',
-        '_accumulated_headers', # Headers that can have multiples
-        '_wrote_headers',
         '_status',              # The response status (usually an integer)
         '_reason',              # The reason that goes with the status
         '_status_set',          # Boolean: status explicitly set
         '_charset',             # String: character set for the output
-        'http_transaction',     # HTTPTask object
         )
 
 
-    def __init__(self, outstream, header_output=None, http_transaction=None):
+    def __init__(self, header_output=None, http_transaction=None):
         self._header_output = header_output
-        self.http_transaction = http_transaction
 
-        super(HTTPResponse, self).__init__(outstream)
+        super(HTTPResponse, self).__init__()
         self.reset()
 
     def reset(self):
@@ -566,19 +559,11 @@
         super(HTTPResponse, self).reset()
         self._headers = {}
         self._cookies = {}
-        self._accumulated_headers = []
-        self._wrote_headers = False
         self._status = 599
         self._reason = 'No status set'
         self._status_set = False
         self._charset = None
 
-    def setHeaderOutput(self, header_output):
-        self._header_output = header_output
-
-    def setHTTPTransaction(self, http_transaction):
-        self.http_transaction = http_transaction
-
     def setStatus(self, status, reason=None):
         'See IHTTPResponse'
         if status is None:
@@ -607,67 +592,56 @@
         'See IHTTPResponse'
         return self._status
 
+    def getStatusString(self):
+        'See IHTTPResponse'
+        return '%i %s' % (self._status, self._reason)
 
     def setHeader(self, name, value, literal=False):
         'See IHTTPResponse'
-
         name = str(name)
         value = str(value)
 
-        key = name.lower()
-        if key == 'set-cookie':
-            self.addHeader(name, value)
-        else:
-            name = literal and name or key
-            self._headers[name]=value
+        if not literal:
+            name = name.lower()
 
+        self._headers[name] = [value]
 
+
     def addHeader(self, name, value):
         'See IHTTPResponse'
-        accum = self._accumulated_headers
-        accum.append('%s: %s' % (name, value))
+        values = self._headers.setdefault(name, [])
+        values.append(value)
 
 
     def getHeader(self, name, default=None, literal=False):
         'See IHTTPResponse'
         key = name.lower()
         name = literal and name or key
-        return self._headers.get(name, default)
+        result = self._headers.get(name)
+        if result:
+            return result[0]
+        return default
 
+
     def getHeaders(self):
         'See IHTTPResponse'
-        result = {}
+        result = []
         headers = self._headers
 
-        result["X-Powered-By"] = "Zope (www.zope.org), Python (www.python.org)"
+        result.append(
+            ("X-Powered-By", "Zope (www.zope.org), Python (www.python.org)"))
 
-        for key, val in headers.items():
+        for key, values in headers.items():
             if key.lower() == key:
                 # only change non-literal header names
-                key = key.capitalize()
-                start = 0
-                location = key.find('-', start)
-                while location >= start:
-                    key = "%s-%s" % (key[:location],
-                                     key[location+1:].capitalize())
-                    start = location + 1
-                    location = key.find('-', start)
-            result[key] = val
+                key = '-'.join([k.capitalize() for k in key.split('-')])
+            result.extend([(key, val) for val in values])
 
+        result.extend([cookie.split(':', 1) for cookie in self._cookie_list()])
+
         return result
 
 
-    def appendToHeader(self, name, value, delimiter=','):
-        'See IHTTPResponse'
-        headers = self._headers
-        if name in headers:
-            h = self._header[name]
-            h = "%s%s\r\n\t%s" % (h, delimiter, value)
-        else:
-            h = value
-        self.setHeader(name, h)
-
-
     def appendToCookie(self, name, value):
         'See IHTTPResponse'
         cookies = self._cookies
@@ -711,42 +685,55 @@
         return self._cookies.get(name, default)
 
 
-    def setCharset(self, charset=None):
-        'See IHTTPResponse'
-        self._charset = charset
+    def setResult(self, result):
+        r = IResult(result, None)
+        if r is None:
+            if isinstance(result, basestring):
+                body, headers = self._implicitResult(result)
+                r = DirectResult((body,), headers)
+            elif result is None:
+                body, headers = self._implicitResult('')
+                r = DirectResult((body,), headers)
+            else:
+                raise TypeError('The result should be adaptable to IResult.')
+        self.result = r
+        self._headers.update([(k, [v]) for (k, v) in r.headers])
+        if not self._status_set:
+            self.setStatus(200)
 
-    def _updateContentType(self):
-        if self._charset:
-            ctype = self.getHeader('content-type', '')
-            if ctype.lower().startswith('text'):
-                ctinfo = contenttype.parseOrdered(ctype)
-                for param, value in ctinfo[2]:
-                    if param == "charset":
-                        break
-                else:
-                    ctinfo[2].append(("charset", self._charset))
-                    self.setHeader('content-type', contenttype.join(ctinfo))
 
-    def setCharsetUsingRequest(self, request):
-        'See IHTTPResponse'
-        envadapter = IUserPreferredCharsets(request, None)
-        if envadapter is None:
-            return
+    def _implicitResult(self, body):
+        encoding = getCharsetUsingRequest(self._request) or 'utf-8'
+        content_type = self.getHeader('content-type')
 
-        try:
-            charset = envadapter.getPreferredCharsets()[0]
-        except IndexError:
-            # Exception caused by empty list! This is okay though, since the
-            # browser just could have sent a '*', which means we can choose
-            # the encoding, which we do here now.
-            charset = 'utf-8'
-        self.setCharset(charset)
+        if content_type is None:
+            if isHTML(body):
+                content_type = 'text/html'
+            else:
+                content_type = 'text/plain'
+            self.setHeader('x-content-type-warning', 'guessed from content')
 
-    def setBody(self, body):
-        self._body = unicode(body)
-        if not self._status_set:
-            self.setStatus(200)
+        if isinstance(body, unicode):
 
+            if not content_type.startswith('text/'):
+                raise ValueError(
+                    'Unicode results must have a text content type.')
+
+            major, minor, params = contenttype.parse(content_type)
+
+            if 'charset' in params:
+                encoding = params['charset']
+            else:
+                content_type += ';charset=%s' %encoding
+
+            body = body.encode(encoding)
+
+        headers = [('content-type', content_type),
+                   ('content-length', len(body))]
+
+        return body, headers
+
+
     def handleException(self, exc_info):
         """
         Calls self.setBody() with an error response.
@@ -765,7 +752,7 @@
         self.setStatus(tname)
 
         body = self._html(title, "A server error occurred." )
-        self.setBody(body)
+        self.setResult(body)
 
 
     def internalError(self):
@@ -788,17 +775,8 @@
         """
         Returns a response object to be used in a retry attempt
         """
-        return self.__class__(self._outstream,
-                              self._header_output)
+        return self.__class__(self._header_output)
 
-    def _updateContentLength(self, data=None):
-        if data is None:
-            blen = str(len(self._body))
-        else:
-            blen = str(len(data))
-        if blen.endswith('L'):
-            blen = blen[:-1]
-        self.setHeader('content-length', blen)
 
     def redirect(self, location, status=None):
         """Causes a redirection without raising an error"""
@@ -809,7 +787,7 @@
                 status=302
             else:
                 status=303
-                
+
         self.setStatus(status)
         self.setHeader('Location', location)
         return location
@@ -834,114 +812,7 @@
                 c[name][k] = str(v)
         return str(c).splitlines()
 
-    def getHeaderText(self, m):
-        lst = ['Status: %s %s' % (self._status, self._reason)]
-        items = m.items()
-        items.sort()
-        lst.extend(['%s: %s' % i for i in items])
-        lst.extend(self._cookie_list())
-        lst.extend(self._accumulated_headers)
-        return ('%s\r\n\r\n' % '\r\n'.join(lst))
 
-
-    def outputHeaders(self):
-        """This method outputs all headers.
-        Since it is a final output method, it must take care of all possible
-        unicode strings and encode them! 
-        """
-        if self._charset is None:
-            self.setCharset('utf-8')
-        self._updateContentType()
-        encode = self._encode
-        headers = self.getHeaders()
-        # Clean these headers from unicode by possibly encoding them
-        headers = dict([(encode(key), encode(val))
-                        for key, val in headers.iteritems()])
-        # Cleaning done.
-        header_output = self._header_output
-        if header_output is not None:
-            # Use the IHeaderOutput interface.
-            header_output.setResponseStatus(self._status, encode(self._reason))
-            header_output.setResponseHeaders(headers)
-            cookie_list = map(encode, self._cookie_list())
-            header_output.appendResponseHeaders(cookie_list)
-            accumulated_headers = map(encode, self._accumulated_headers)
-            header_output.appendResponseHeaders(accumulated_headers)
-        else:
-            # Write directly to outstream.
-            headers_text = self.getHeaderText(headers)
-            self._outstream.write(encode(headers_text))
-
-    def write(self, string):
-        """See IApplicationResponse
-
-        Return data as a stream
-
-        HTML data may be returned using a stream-oriented interface.
-        This allows the browser to display partial results while
-        computation of a response to proceed.
-
-        The published object should first set any output headers or
-        cookies on the response object and encode the string into
-        appropriate encoding.
-
-        Note that published objects must not generate any errors
-        after beginning stream-oriented output.
-
-        """
-        if not self._wrote_headers:
-            self.outputHeaders()
-            self._wrote_headers = True
-
-        self._outstream.write(string)
-
-    def output(self, data):
-        """Output the data to the world.
-        
-        There are a couple of steps we have to do:
-
-        1. Check that there is a character encoding for the data. If not,
-           choose UTF-8. Note that if the charset is None, this is a sign of a
-           bug! The method setCharsetUsingRequest() specifically sets the
-           encoding to UTF-8, if none was found in the HTTP header. This
-           method should always be called when reading the HTTP request.
-
-        2. Now that the encoding has been finalized, we can output the
-           headers.
-
-        3. If the content type is text-based, let's encode the data and send
-           it also out the door.
-
-        4. Make sure that a Content-Length or Transfer-Encoding header is
-           present.
-        """
-        if self._charset is None:
-            self.setCharset('utf-8')
-
-        if self.getHeader('content-type', '').startswith('text'):
-            data = self._encode(data)
-            self._updateContentLength(data)
-        
-        if (not ('content-length' in self._headers)
-            and not ('transfer-encoding' in self._headers)):
-            self._updateContentLength()
-
-        self.write(data)
-
-
-    def outputBody(self):
-        """Outputs the response body."""
-        self.output(self._body)
-
-
-    def _encode(self, text):
-        # Any method that calls this method has the responsibility to set
-        # the _charset variable (if None) to a non-None value (usually UTF-8)
-        if isinstance(text, unicode):
-            return text.encode(self._charset)
-        return text
-
-
 def sort_charsets(x, y):
     if y[1] == 'utf-8':
         return 1
@@ -998,3 +869,56 @@
         # always good to use UTF-8.
         charsets.sort(sort_charsets)
         return [c[1] for c in charsets]
+
+
+def isHTML(str):
+     """Try to determine whether str is HTML or not."""
+     s = str.lstrip().lower()
+     if s.startswith('<!doctype html'):
+         return True
+     if s.startswith('<html') and (s[5:6] in ' >'):
+         return True
+     if s.startswith('<!--'):
+         idx = s.find('<html')
+         return idx > 0 and (s[idx+5:idx+6] in ' >')
+     else:
+         return False
+
+
+def getCharsetUsingRequest(request):
+    'See IHTTPResponse'
+    envadapter = IUserPreferredCharsets(request, None)
+    if envadapter is None:
+        return
+
+    try:
+        charset = envadapter.getPreferredCharsets()[0]
+    except IndexError:
+        # Exception caused by empty list! This is okay though, since the
+        # browser just could have sent a '*', which means we can choose
+        # the encoding, which we do here now.
+        charset = 'utf-8'
+    return charset
+
+
+class DirectResult(object):
+    """A generic result object.
+
+    The result's body can be any iteratable. It is the responsibility of the
+    application to specify all headers related to the content, such as the
+    content type and length.
+    """
+    implements(IResult)
+
+    def __init__(self, body, headers=()):
+        self.body = body
+        self.headers = headers
+
+
+def StrResult(body, headers=()):
+    """A simple string result that represents any type of data.
+
+    It is the responsibility of the application to specify all the headers,
+    including content type and length.
+    """
+    return DirectResult((body,), headers)

Modified: Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/interfaces/__init__.py
===================================================================
--- Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/interfaces/__init__.py	2005-09-02 19:25:40 UTC (rev 38246)
+++ Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/interfaces/__init__.py	2005-09-02 19:50:56 UTC (rev 38247)
@@ -171,7 +171,7 @@
 
         This method should return an object having the specified name and
         `self` as parent. The method can use the request to determine the
-        correct object. 
+        correct object.
         """
 
 
@@ -183,18 +183,13 @@
         The request must be an IPublisherRequest.
         """
 
-class IPublisherResponse(Interface):
-    """Interface used by the publsher
-    """
+class IResponse(Interface):
+    """Interface used by the publsher"""
 
-    def setBody(result):
+    def setResult(result):
         """Sets the response result value.
         """
 
-    def reset():
-        """Resets response state on exceptions.
-        """
-
     def handleException(exc_info):
         """Handles an otherwise unhandled exception.
 
@@ -209,8 +204,10 @@
         Should report back to the client that an internal error occurred.
         """
 
-    def outputBody():
-        """Outputs the response to the client
+    def reset():
+        """Reset the output result.
+
+        Reset the response by nullifying already set variables.
         """
 
     def retry():
@@ -247,7 +244,6 @@
 
         This is called before traversing each object.  The ob argument
         is the object that is about to be traversed.
-        
         """
 
     def traverseName(request, ob, name):
@@ -289,15 +285,6 @@
         """
 
 
-class IApplicationResponse(Interface):
-    """Features that support application logic
-    """
-
-    def write(string):
-        """Output a string to the response body.
-        """
-
-
 class IPublicationRequest(IPresentationRequest, IParticipation):
     """Interface provided by requests to IPublication objects
     """
@@ -316,7 +303,6 @@
 
         The object should be an IHeld.  If it is an IHeld, it's
         release method will be called when it is released.
-        
         """
 
     def getTraversalStack():
@@ -449,23 +435,20 @@
         virtue of including the dotted name of a package as a prefex.  A
         package name is used to limit the authority for picking names for
         a package to the people using that package.
-    
+
         For example, when implementing annotations for hypothetical
         request-persistent adapters in a hypothetical zope.persistentadapter
         package, the key would be (or at least begin with) the following::
-    
+
           "zope.persistentadapter"
         """)
 
-class IResponse(IPublisherResponse, IApplicationResponse):
-    """The basic response contract
-    """
 
-
 class IRequest(IPublisherRequest, IPublicationRequest, IApplicationRequest):
     """The basic request contract
     """
-    
+
+
 class ILayer(IInterface):
     """A grouping of related views for a request."""
 

Modified: Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/interfaces/http.py
===================================================================
--- Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/interfaces/http.py	2005-09-02 19:25:40 UTC (rev 38246)
+++ Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/interfaces/http.py	2005-09-02 19:50:56 UTC (rev 38247)
@@ -19,7 +19,6 @@
 from zope.interface import Attribute
 
 from zope.publisher.interfaces import IApplicationRequest
-from zope.publisher.interfaces import IApplicationResponse
 from zope.publisher.interfaces import IPublishTraverse
 from zope.publisher.interfaces import IRequest
 from zope.publisher.interfaces import IResponse
@@ -36,7 +35,7 @@
     def setVirtualHostRoot(names):
         """Marks the currently traversed object as the root of a virtual host.
 
-        Any path elements traversed up to that 
+        Any path elements traversed up to that
 
         Set the names which compose the application path.
         These are the path elements that appear in the beginning of
@@ -47,7 +46,7 @@
 
     def getVirtualHostRoot():
         """Returns the object which is the virtual host root for this request
-        
+
         Return None if setVirtualHostRoot hasn't been called.
         """
 
@@ -217,7 +216,7 @@
         The challenge is the value of the WWW-Authenticate header."""
 
 
-class IHTTPApplicationResponse(IApplicationResponse):
+class IHTTPApplicationResponse(Interface):
     """HTTP Response
     """
 
@@ -282,6 +281,8 @@
     passed into the object must be used.
     """
 
+    authUser = Attribute('The authenticated user message.')
+
     def getStatus():
         """Returns the current HTTP status code as an integer.
         """
@@ -297,6 +298,9 @@
         correct integer value.
         """
 
+    def getStatusString(self):
+        """Return the status followed by the reason."""
+
     def setHeader(name, value, literal=False):
         """Sets an HTTP return header "name" with value "value"
 
@@ -322,7 +326,7 @@
         """
 
     def getHeaders():
-        """Returns a mapping of correctly-cased header names to values.
+        """Returns a list of header name, value tuples.
         """
 
     def appendToCookie(name, value):
@@ -363,14 +367,6 @@
         yet.
         """
 
-    def appendToHeader(name, value, delimiter=","):
-        """Appends a value to a header
-
-        Sets an HTTP return header "name" with value "value",
-        appending it following a comma if there was a previous value
-        set for the header.
-        """
-
     def setCharset(charset=None):
         """Set the character set into which the response body should be
            encoded. If None is passed in then no encoding will be done to
@@ -384,8 +380,26 @@
            HTTP header information.
         """
 
-    def setHTTPTransaction(http_transaction):
-        """Sets an HTTP transaction.
+    def setResult(result):
+        """Sets the response result value that is adaptable to ``IResult``.
+        """
 
-        Returns an HTTPTask or None. It is used for logging.
-        """
+
+class IResult(Interface):
+    """HTTP result.
+
+    The result provides the result in a form suitable for delivery to HTTP
+    clients.
+
+    IMPORTANT: The result object may be held indefinitely by a server and may
+    be accessed by arbitrary threads. For that reason the result should not
+    hold on to any application resources and should be prepared to be invoked
+    from any thread.
+    """
+
+    headers = Attribute('A sequence of tuples of result headers, such as'
+                        '"Content-Type" and "Content-Length", etc.')
+
+    body = Attribute('An iterable that provides the body data of the'
+                     'response.')
+

Modified: Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/publish.py
===================================================================
--- Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/publish.py	2005-09-02 19:25:40 UTC (rev 38246)
+++ Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/publish.py	2005-09-02 19:50:56 UTC (rev 38247)
@@ -138,7 +138,7 @@
                             result = publication.callObject(request, object)
                             response = request.response
                             if result is not response:
-                                response.setBody(result)
+                                response.setResult(result)
 
                             publication.afterCall(request, object)
 
@@ -180,7 +180,6 @@
                     raise
 
         response = request.response
-        response.outputBody()
         if to_raise is not None:
             raise to_raise[0], to_raise[1], to_raise[2]
 

Modified: Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/xmlrpc.py
===================================================================
--- Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/xmlrpc.py	2005-09-02 19:25:40 UTC (rev 38246)
+++ Zope3/branches/stephan_and_jim-response-refactor/src/zope/publisher/xmlrpc.py	2005-09-02 19:50:56 UTC (rev 38247)
@@ -34,9 +34,9 @@
 
     _args = ()
 
-    def _createResponse(self, outstream):
+    def _createResponse(self):
         """Create a specific XML-RPC response object."""
-        return XMLRPCResponse(outstream)
+        return XMLRPCResponse()
 
     def processInputs(self):
         'See IPublisherRequest'
@@ -52,8 +52,7 @@
 
 class TestRequest(XMLRPCRequest):
 
-    def __init__(self, body_instream=None, outstream=None, environ=None,
-                 response=None, **kw):
+    def __init__(self, body_instream=None, environ=None, response=None, **kw):
 
         _testEnv =  {
             'SERVER_URL':         'http://127.0.0.1',
@@ -69,13 +68,9 @@
         if body_instream is None:
             body_instream = StringIO('')
 
-        if outstream is None:
-            outstream = StringIO()
+        super(TestRequest, self).__init__(body_instream, _testEnv, response)
 
-        super(TestRequest, self).__init__(
-            body_instream, outstream, _testEnv, response)
 
-
 class XMLRPCResponse(HTTPResponse):
     """XMLRPC response.
 



More information about the Zope3-Checkins mailing list