[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