diff options
author | cce <devnull@localhost> | 2005-12-23 21:36:37 +0000 |
---|---|---|
committer | cce <devnull@localhost> | 2005-12-23 21:36:37 +0000 |
commit | 370ba732f785cc211e850664ecfea6a62fea30b4 (patch) | |
tree | bbc6b3281820448e64ada2ae5c45e39a0170e15a | |
parent | db53428a989dca0526d5f6a4bad5c91424424ea1 (diff) | |
download | paste-370ba732f785cc211e850664ecfea6a62fea30b4.tar.gz |
- got rid of unnecessary trailing spaces in httpexceptions
- made error messages us \r\n rather than just \n in httpexceptions
to comply with various browsers
- added tests to check FileApp
- added support for handling 100 Continue in httpserver
- fixingup dumpenviron in wsgilib to dump message body
- misc changes to fileapp (mostly documentation)
-rw-r--r-- | paste/fileapp.py | 50 | ||||
-rw-r--r-- | paste/httpexceptions.py | 134 | ||||
-rwxr-xr-x | paste/util/httpserver.py | 62 | ||||
-rw-r--r-- | paste/wsgilib.py | 9 | ||||
-rw-r--r-- | tests/test_exceptions/test_httpexceptions.py | 8 | ||||
-rw-r--r-- | tests/test_fileapp.py | 71 |
6 files changed, 238 insertions, 96 deletions
diff --git a/paste/fileapp.py b/paste/fileapp.py index 4d2d860..118ba50 100644 --- a/paste/fileapp.py +++ b/paste/fileapp.py @@ -6,7 +6,9 @@ This module handles sending static content such as in-memory data or files. At this time it has cache helpers and understands the if-modified-since request header. """ + #@@: this still needs Range support for large files + import os, time import mimetypes import httpexceptions @@ -84,6 +86,7 @@ class DataApp(object): implementation does not support the enumeration of private fields + ``no_cache`` if True, this argument specifies that the response, as a whole, may be cashed; this implementation does not support the @@ -113,12 +116,18 @@ class DataApp(object): ``extensions`` gives additional cache-control extensionsn, such as items like, community="UCI" (14.9.6) - As recommended by RFC 2616, if ``max_age`` is provided (or - implicitly set by specifying ``no-cache``, then the ``Expires`` - header is also calculated for HTTP/1.0 clients. This is done + As recommended by RFC 2616, if ``max_age`` is provided, then + then the ``Expires`` header is also calculated for HTTP/1.0 + clients and proxies. For ``no-cache`` and for ``private`` + cases, we either do not want the response cached or do not want + any response accidently returned to other users; so to prevent + this case, we set the ``Expires`` header to the time of the + request, signifying to HTTP/1.0 transports that the content + isn't to be cached. If you are using SSL, your communication + is already "private", so to work with HTTP/1.0 browsers, + consider specifying your cache as public as the distinction + between public and private is moot for this case. """ - assert not has_header(self.headers,'cache-control') - assert not has_header(self.headers,'expires') assert isinstance(max_age,(type(None),int)) assert isinstance(s_maxage,(type(None),int)) result = [] @@ -146,7 +155,8 @@ class DataApp(object): for (k,v) in extensions.items(): assert '"' not in v result.append('%s="%s"' % (k.replace("_","-"),v)) - self.headers.append(('cache-control',", ".join(result))) + replace_header(self.headers,'cache-control',", ".join(result)) + return self def set_content(self, content): self.last_modified = time.time() @@ -154,6 +164,7 @@ class DataApp(object): replace_header(self.headers,'content-length', str(len(content))) replace_header(self.headers,'last-modified', formatdate(self.last_modified)) + return self def __call__(self, environ, start_response): if self.expires is not None: @@ -165,17 +176,17 @@ class DataApp(object): try: client_clock = mktime_tz(parsedate_tz(checkmod)) except TypeError: - return HTTPBadRequest( - "Bad Timestamp\n" - "Client program did not provide an appropriate " - "timestamp for its If-Modified-Since header." + return HTTPBadRequest(detail=( + "Client program provided an ill-formed timestamp for\r\n" + "its If-Modified-Since header:\r\n" + " %s\r\n") % checkmod ).wsgi_application(environ, start_response) if client_clock > time.time(): - return HTTPBadRequest(( - "Clock Time In Future\n" - "According to this server, the time provided in " - "the If-Modified-Since header (%s) is in the future.\n" - "Please check your system clock.") % checkmod + return HTTPBadRequest(detail=( + "Please check your system clock.\r\n" + "According to this server, the time provided in the\r\n" + "If-Modified-Since header is in the future:\r\n" + " %s\r\n") % checkmod ).wsgi_application(environ, start_response) elif client_clock <= self.last_modified: # the client has a recent copy @@ -190,10 +201,13 @@ class FileApp(DataApp): Returns an application that will send the file at the given filename. Adds a mime type based on ``mimetypes.guess_type()``. See DataApp for the arguments beyond ``filename``. + + """ def __init__(self, filename, headers=None, **kwargs): self.filename = filename + self.last_size = None content_type, content_encoding = mimetypes.guess_type(self.filename) if content_type and 'content_type' not in kwargs: kwargs['content_type'] = content_type @@ -203,9 +217,11 @@ class FileApp(DataApp): def update(self): stat = os.stat(self.filename) - if stat.st_mtime == self.last_modified: + if (stat.st_mtime == self.last_modified and + stat.st_size == self.last_size): return - if stat.st_size < CACHE_SIZE: + self.last_size = stat.st_size + if stat.st_size < CACHE_SIZE: fh = open(self.filename,"rb") self.set_content(fh.read()) fh.close() diff --git a/paste/httpexceptions.py b/paste/httpexceptions.py index 33507a9..8e46224 100644 --- a/paste/httpexceptions.py +++ b/paste/httpexceptions.py @@ -18,7 +18,7 @@ can call ``start_response`` more then once only under two conditions: subsequent invocations of ``start_response`` have a valid ``exc_info`` argument obtained from ``sys.exc_info()``. The WSGI specification then requires the server or gateway to handle the case where content has been -sent and then an exception was encountered. +sent and then an exception was encountered. Exceptions in the 5xx range and those raised after ``start_response`` has been called are treated as serious errors and the ``exc_info`` is @@ -74,25 +74,27 @@ from wsgilib import catch_errors_app from response import has_header, header_value from util.quoting import strip_html, html_quote +SERVER_NAME = 'WSGI Server' + class HTTPException(Exception): - """ + """ Base class for all HTTP exceptions - This encapsulates an HTTP response that interrupts normal application + This encapsulates an HTTP response that interrupts normal application flow; but one which is not necessarly an error condition. For example, codes in the 300's are exceptions in that they interrupt - normal processing; however, they are not considered errors. - + normal processing; however, they are not considered errors. + This class is complicated by 4 factors: 1. The content given to the exception may either be plain-text or - as html-text. - + as html-text. + 2. The template may want to have string-substitutions taken from the current ``environ`` or values from incoming headers. This is especially troublesome due to case sensitivity. - - 3. The final output may either be text/plain or text/html + + 3. The final output may either be text/plain or text/html mime-type as requested by the client application. 4. Each exception has a default explanation, but those who @@ -100,36 +102,36 @@ class HTTPException(Exception): Attributes: - ``code`` + ``code`` the HTTP status code for the exception - ``title`` + ``title`` remainder of the status line (stuff after the code) - ``explanation`` + ``explanation`` a plain-text explanation of the error message that is - not subject to environment or header substitutions; + not subject to environment or header substitutions; it is accessable in the template via %(explanation)s - - ``detail`` - a plain-text message customization that is not subject - to environment or header substutions; accessable in + + ``detail`` + a plain-text message customization that is not subject + to environment or header substutions; accessable in the template via %(detail)s - ``template`` - a content fragment (in HTML) used for environment and + ``template`` + a content fragment (in HTML) used for environment and header substution; the default template includes both - the explanation and further detail provided in the + the explanation and further detail provided in the message - ``required_headers`` + ``required_headers`` a sequence of headers which are required for proper construction of the exception Parameters: ``detail`` a plain-text override of the default ``detail`` - ``headers`` a list of (k,v) header pairs + ``headers`` a list of (k,v) header pairs ``comment`` a plain-text additional information which is usually stripped/hidden for end-users @@ -146,9 +148,8 @@ class HTTPException(Exception): explanation = '' detail = '' comment = '' - template = "%(explanation)s\n<br/>%(detail)s\n<!-- %(comment)s -->" + template = "%(explanation)s\r\n<br/>%(detail)s\r\n<!-- %(comment)s -->" required_headers = () - server_name = 'WSGI server' def __init__(self, detail=None, headers=None, comment=None): assert self.code, "Do not directly instantiate abstract exceptions." @@ -189,21 +190,21 @@ class HTTPException(Exception): """ text/plain representation of the exception """ noop = lambda _: _ body = self.make_body(environ, strip_html(self.template), noop) - return ('%s %s\n%s\n' % (self.code, self.title, body)) + return ('%s %s\r\n%s\r\n' % (self.code, self.title, body)) def html(self, environ): """ text/html representation of the exception """ body = self.make_body(environ, self.template, html_quote) - return ('<html><head><title>%(title)s</title></head>\n' - '<body>\n' - '<h1>%(title)s</h1>\n' - '<p>%(body)s</p>\n' - '<hr noshade>\n' - '<div align="right">%(server)s</div>\n' - '</body></html>\n' + return ('<html><head><title>%(title)s</title></head>\r\n' + '<body>\r\n' + '<h1>%(title)s</h1>\r\n' + '<p>%(body)s</p>\r\n' + '<hr noshade>\r\n' + '<div align="right">%(server)s</div>\r\n' + '</body></html>\r\n' % {'title': self.title, 'code': self.code, - 'server': self.server_name, + 'server': SERVER_NAME, 'body': body}) def wsgi_application(self, environ, start_response, exc_info=None): @@ -233,8 +234,8 @@ class HTTPException(Exception): self.title, self.code) class HTTPError(HTTPException): - """ - This is an exception which indicates that an error has occured, + """ + This is an exception which indicates that an error has occured, and that any work in progress should not be committed. These are typically results in the 400's and 500's. """ @@ -247,24 +248,24 @@ class HTTPError(HTTPException): # required MAY be carried out by the user agent without interaction with # the user if and only if the method used in the second request is GET or # HEAD. A client SHOULD detect infinite redirection loops, since such -# loops generate network traffic for each redirection. +# loops generate network traffic for each redirection. # class HTTPRedirection(HTTPException): - """ + """ This is an abstract base class for 3xx redirection. It indicates that further action needs to be taken by the user agent in order to fulfill the request. It does not necessarly signal an error condition. """ - + class _HTTPMove(HTTPRedirection): - """ + """ Base class for redirections which require a Location field. Since a 'Location' header is a required attribute of 301, 302, 303, 305 and 307 (but not 304), this base class provides the mechanics to - make this easy. While this has the same parameters as HTTPException, + make this easy. While this has the same parameters as HTTPException, if a location is not provided in the headers; it is assumed that the detail _is_ the location (this for backward compatibility, otherwise we'd add a new attribute). @@ -272,9 +273,9 @@ class _HTTPMove(HTTPRedirection): required_headers = ('location',) explanation = 'The resource has been moved to' template = ( - '%(explanation)s <a href="%(location)s">%(location)s</a>;\n' - 'you should be redirected automatically.\n' - '%(detail)s\n<!-- %(comment)s -->') + '%(explanation)s <a href="%(location)s">%(location)s</a>;\r\n' + 'you should be redirected automatically.\r\n' + '%(detail)s\r\n<!-- %(comment)s -->') def __init__(self, detail=None, headers=None, comment=None): assert isinstance(headers, (type(None), list)) @@ -340,11 +341,11 @@ class HTTPTemporaryRedirect(_HTTPMove): # server SHOULD include an entity containing an explanation of the error # situation, and whether it is a temporary or permanent condition. These # status codes are applicable to any request method. User agents SHOULD -# display any included entity to the user. -# +# display any included entity to the user. +# class HTTPClientError(HTTPError): - """ + """ This is an error condition in which the client is presumed to be in-error. This is an expected problem, and thus is not considered a bug. A server-side traceback is not warranted. Unless specialized, @@ -352,7 +353,8 @@ class HTTPClientError(HTTPError): """ code = 400 title = 'Bad Request' - explanation = 'The server could not understand your request.' + explanation = ('The server could not comply with the request since\r\n' + 'it is either malformed or otherwise incorrect.\r\n') HTTPBadRequest = HTTPClientError @@ -361,10 +363,10 @@ class HTTPUnauthorized(HTTPClientError): code = 401 title = 'Unauthorized' explanation = ( - 'This server could not verify that you are authorized to\n' - 'access the document you requested. Either you supplied the\n' - 'wrong credentials (e.g., bad password), or your browser\n' - 'does not understand how to supply the credentials required.\n') + 'This server could not verify that you are authorized to\r\n' + 'access the document you requested. Either you supplied the\r\n' + 'wrong credentials (e.g., bad password), or your browser\r\n' + 'does not understand how to supply the credentials required.\r\n') class HTTPForbidden(HTTPClientError): code = 403 @@ -382,15 +384,15 @@ class HTTPMethodNotAllowed(HTTPClientError): title = 'Method Not Allowed' # override template since we need an environment variable template = ('The method %(REQUEST_METHOD)s is not allowed for ' - 'this resource.\n%(detail)s') + 'this resource.\r\n%(detail)s') class HTTPNotAcceptable(HTTPClientError): code = 406 title = 'Not Acceptable' # override template since we need an environment variable template = ('The resource could not be generated that was ' - 'acceptable to your browser (content\nof type ' - '%(HTTP_ACCEPT)s).\n%(detail)s') + 'acceptable to your browser (content\r\nof type ' + '%(HTTP_ACCEPT)s).\r\n%(detail)s') class HTTPConflict(HTTPClientError): code = 409 @@ -429,7 +431,7 @@ class HTTPUnsupportedMediaType(HTTPClientError): title = 'Unsupported Media Type' # override template since we need an environment variable template = ('The request media type %(CONTENT_TYPE)s is not ' - 'supported by this server.\n%(detail)s') + 'supported by this server.\r\n%(detail)s') class HTTPRequestRangeNotSatisfiable(HTTPClientError): code = 416 @@ -443,7 +445,7 @@ class HTTPExpectationFailed(HTTPClientError): # # 5xx Server Error -# +# # Response status codes beginning with the digit "5" indicate cases in # which the server is aware that it has erred or is incapable of # performing the request. Except when responding to a HEAD request, the @@ -454,7 +456,7 @@ class HTTPExpectationFailed(HTTPClientError): # class HTTPServerError(HTTPError): - """ + """ This is an error condition in which the server is presumed to be in-error. This is usually unexpected, and thus requires a traceback; ideally, opening a support ticket for the customer. Unless specialized, @@ -462,16 +464,18 @@ class HTTPServerError(HTTPError): """ code = 500 title = 'Internal Server Error' - explanation = ('An internal server error occurred.') + explanation = ( + 'The server has either erred or is incapable of performing\r\n' + 'the requested operation.\r\n') HTTPInternalServerError = HTTPServerError - + class HTTPNotImplemented(HTTPServerError): code = 501 title = 'Not Implemented' # override template since we need an environment variable template = ('The request method %(REQUEST_METHOD)s is not implemented ' - 'for this server.\n%(detail)s') + 'for this server.\r\n%(detail)s') class HTTPBadGateway(HTTPServerError): code = 502 @@ -515,12 +519,12 @@ def get_exception(code): class HTTPExceptionHandler: """ This middleware catches any exceptions (which are subclasses of - ``HTTPException``) and turns them into proper HTTP responses. - + ``HTTPException``) and turns them into proper HTTP responses. + Attributes: - ``warning_level`` - This attribute determines for what exceptions a stack + ``warning_level`` + This attribute determines for what exceptions a stack trace is kept for lower level reporting; by default, it only keeps stack trace for 5xx, HTTPServerError exceptions. To keep a stack trace for 4xx, HTTPClientError exceptions, @@ -541,7 +545,7 @@ class HTTPExceptionHandler: def __call__(self, environ, start_response): environ['paste.httpexceptions'] = self - environ.setdefault('paste.expected_exceptions', + environ.setdefault('paste.expected_exceptions', []).append(HTTPException) return catch_errors_app( self.application, environ, start_response, diff --git a/paste/util/httpserver.py b/paste/util/httpserver.py index 9a62a54..c054ce0 100755 --- a/paste/util/httpserver.py +++ b/paste/util/httpserver.py @@ -9,12 +9,64 @@ This is a minimalistic WSGI server using Python's built-in BaseHTTPServer; if pyOpenSSL is installed, it also provides SSL capabilities. """ +# @@: add in protection against HTTP/1.0 clients who claim to +# be 1.1 but do not send a Content-Length + +# @@: add support for chunked encoding, this is not a 1.1 server +# till this is completed. + + import BaseHTTPServer, SocketServer import urlparse, sys, socket __all__ = ['WSGIHandlerMixin','WSGIServer','WSGIHandler', 'serve'] __version__ = "0.2" +class ContinueHook(object): + """ + When a client request includes a 'Expect: 100-continue' header, then + it is the responsibility of the server to send 100 Continue when it + is ready for the content body. This allows authentication, access + levels, and other exceptions to be detected *before* bandwith is + spent on the request body. + + This is a rfile wrapper that implements this functionality by + sending 100 Continue to the client immediately after the user + requests the content via a read() operation on the rfile stream. + After this response is sent, it becomes a pass-through object. + """ + + def __init__(self, rfile, write): + self._ContinueFile_rfile = rfile + self._ContinueFile_write = write + for attr in ('close','closed','fileno','flush', + 'mode','bufsize','softspace'): + if hasattr(rfile,attr): + setattr(self,attr,getattr(rfile,attr)) + for attr in ('read','readline','readlines'): + if hasattr(rfile,attr): + setattr(self,attr,getattr(self,'_ContinueFile_' + attr)) + + def _ContinueFile_send(self): + self._ContinueFile_write("HTTP/1.1 100 Continue\r\n\r\n") + rfile = self._ContinueFile_rfile + for attr in ('read','readline','readlines'): + if hasattr(rfile,attr): + setattr(self,attr,getattr(rfile,attr)) + + def _ContinueFile_read(self, size=-1): + self._ContinueFile_send() + return self._ContinueFile_rfile.readline(size) + + def _ContinueFile_readline(self, size=-1): + self._ContinueFile_send() + return self._ContinueFile_rfile.readline(size) + + def _ContinueFile_readlines(self, sizehint=0): + self._ContinueFile_send() + return self._ContinueFile_rfile.readlines(sizehint) + + class WSGIHandlerMixin: """ WSGI mix-in for HTTPRequestHandler @@ -92,10 +144,15 @@ class WSGIHandlerMixin: (_,_,path,query,fragment) = urlparse.urlsplit(self.path) (server_name, server_port) = self.server.server_address + rfile = self.rfile + if 'HTTP/1.1' == self.protocol_version and \ + '100-continue' == self.headers.get('Expect',''): + rfile = ContinueHook(rfile, self.wfile.write) + self.wsgi_environ = { 'wsgi.version': (1,0) ,'wsgi.url_scheme': 'http' - ,'wsgi.input': self.rfile + ,'wsgi.input': rfile ,'wsgi.errors': sys.stderr ,'wsgi.multithread': True ,'wsgi.multiprocess': False @@ -166,7 +223,8 @@ class WSGIHandler(WSGIHandlerMixin, BaseHTTPServer.BaseHTTPRequestHandler): A WSGI handler that overrides POST, GET and HEAD to delegate requests to the server's ``wsgi_application``. """ - do_POST = do_GET = do_HEAD = WSGIHandlerMixin.wsgi_execute + do_POST = do_GET = do_HEAD = do_DELETE = do_PUT = do_TRACE = \ + WSGIHandlerMixin.wsgi_execute # # SSL Functionality diff --git a/paste/wsgilib.py b/paste/wsgilib.py index 8b0cf67..4c02a02 100644 --- a/paste/wsgilib.py +++ b/paste/wsgilib.py @@ -34,7 +34,7 @@ class add_close: An an iterable that iterates over app_iter, then calls close_func. """ - + def __init__(self, app_iterable, close_func): self.app_iterable = app_iterable self.app_iter = iter(app_iterable) @@ -259,7 +259,7 @@ def interactive(*args, **kw): interactive.proxy = 'raw_interactive' def dump_environ(environ,start_response): - """ + """ Application which simply dumps the current environment variables out as a plain text response. """ @@ -269,6 +269,11 @@ def dump_environ(environ,start_response): for k in keys: v = str(environ[k]).replace("\n","\n ") output.append("%s: %s\n" % (k,v)) + output.append("\n") + content_length = environ.get("CONTENT_LENGTH",'') + if content_length: + output.append(environ['wsgi.input'].read(int(content_length))) + output.append("\n") output = "".join(output) headers = [('Content-Type', 'text/plain'), ('Content-Length', len(output))] diff --git a/tests/test_exceptions/test_httpexceptions.py b/tests/test_exceptions/test_httpexceptions.py index 1c24a4a..60095ce 100644 --- a/tests/test_exceptions/test_httpexceptions.py +++ b/tests/test_exceptions/test_httpexceptions.py @@ -38,14 +38,14 @@ def test_template(): e.template = 'A %(ping)s and <b>%(pong)s</b> message.' assert str(e).startswith("500 Internal Server Error") assert e.plain({'ping': 'fun', 'pong': 'happy'}) == ( - '500 Internal Server Error\n' - 'A fun and happy message.\n') + '500 Internal Server Error\r\n' + 'A fun and happy message.\r\n') assert '<p>A fun and <b>happy</b> message.</p>' in \ e.html({'ping': 'fun', 'pong': 'happy'}) def test_iterator_application(): - """ - This tests to see that an iterator's exceptions are caught by + """ + This tests to see that an iterator's exceptions are caught by HTTPExceptionHandler """ def basic_found(environ, start_response): diff --git a/tests/test_fileapp.py b/tests/test_fileapp.py index ffb7143..8d8c1b3 100644 --- a/tests/test_fileapp.py +++ b/tests/test_fileapp.py @@ -3,6 +3,8 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php from paste.fileapp import * from paste.fixture import * +from rfc822 import parsedate_tz, mktime_tz +import time def test_data(): harness = TestApp(DataApp('mycontent')) @@ -14,10 +16,28 @@ def test_data(): assert "<Response 200 OK 'bingles'>" == repr(harness.get("/")) def test_cache(): - app = DataApp('mycontent') - app.cache() - harness = TestApp(app) - res = harness.get("/") + def build(*args,**kwargs): + app = DataApp("SomeContent") + app.cache(*args,**kwargs) + return TestApp(app).get("/") + res = build() + assert 'public' == res.header('cache-control') + assert not res.header('expires',None) + res = build(private=True) + assert 'private' == res.header('cache-control') + assert mktime_tz(parsedate_tz(res.header('expires'))) < time.time() + res = build(no_cache=True) + assert 'no-cache' == res.header('cache-control') + assert mktime_tz(parsedate_tz(res.header('expires'))) < time.time() + res = build(max_age=60,s_maxage=30) + assert 'public, max-age=60, s-maxage=30' == res.header('cache-control') + expires = mktime_tz(parsedate_tz(res.header('expires'))) + assert expires > time.time()+58 and expires < time.time()+61 + res = build(private=True, max_age=60, no_transform=True, no_store=True) + reshead = res.header('cache-control') + assert 'private, no-store, no-transform, max-age=60' == reshead + expires = mktime_tz(parsedate_tz(res.header('expires'))) + assert mktime_tz(parsedate_tz(res.header('expires'))) < time.time() def test_modified(): harness = TestApp(DataApp('mycontent')) @@ -28,8 +48,47 @@ def test_modified(): assert "<Response 304 Not Modified ''>" == repr(res) res = harness.get("/",status=400, headers={'if-modified-since': 'garbage'}) - assert 400 == res.status and "Bad Timestamp" in res.body + assert 400 == res.status and "ill-formed timestamp" in res.body res = harness.get("/",status=400, headers={'if-modified-since': 'Thu, 22 Dec 2030 01:01:01 GMT'}) - assert 400 == res.status and "Clock Time In Future" in res.body + assert 400 == res.status and "check your system clock" in res.body + +def test_file(): + import random, string, os + tempfile = "test_fileapp.%s.txt" % (random.random()) + content = string.letters * 20 + file = open(tempfile,"w") + file.write(content) + file.close() + try: + from paste import fileapp + app = fileapp.FileApp(tempfile) + res = TestApp(app).get("/") + assert len(content) == int(res.header('content-length')) + assert 'text/plain' == res.header('content-type') + assert content == res.body + assert [content] == app.content # this is cashed + lastmod = res.header('last-modified') + print "updating", tempfile + file = open(tempfile,"a+") + file.write("0123456789") + file.close() + res = TestApp(app).get("/") + assert len(content)+10 == int(res.header('content-length')) + assert 'text/plain' == res.header('content-type') + assert content + "0123456789" == res.body + assert app.content # we are still cached + file = open(tempfile,"a+") + file.write("X" * fileapp.CACHE_SIZE) # exceed the cashe size + file.close() + res = TestApp(app).get("/") + newsize = fileapp.CACHE_SIZE + len(content)+10 + assert newsize == int(res.header('content-length')) + assert newsize == len(res.body) + assert res.body.startswith(content) and res.body.endswith('X') + assert not app.content # we are no longer cached + finally: + import os + os.unlink(tempfile) + |