summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--paste/fileapp.py50
-rw-r--r--paste/httpexceptions.py134
-rwxr-xr-xpaste/util/httpserver.py62
-rw-r--r--paste/wsgilib.py9
-rw-r--r--tests/test_exceptions/test_httpexceptions.py8
-rw-r--r--tests/test_fileapp.py71
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)
+