diff options
| author | Chris McDonough <chrism@plope.com> | 2012-02-22 20:02:03 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2012-02-22 20:02:03 -0500 |
| commit | 6b3cca0d548c0c3bcec62902f5b261df4e7c1d1e (patch) | |
| tree | 453a3aaeba0a22472aeefe58af3d720abeeae00f | |
| parent | 08f805a360185c648bfeccbbe568a97f0f036a0c (diff) | |
| download | pyramid-6b3cca0d548c0c3bcec62902f5b261df4e7c1d1e.tar.gz pyramid-6b3cca0d548c0c3bcec62902f5b261df4e7c1d1e.tar.bz2 pyramid-6b3cca0d548c0c3bcec62902f5b261df4e7c1d1e.zip | |
- New APIs: ``pyramid.response.FileResponse`` and
``pyramid.response.FileIter``, for usage in views that must serve files
"manually".
| -rw-r--r-- | CHANGES.txt | 4 | ||||
| -rw-r--r-- | TODO.txt | 3 | ||||
| -rw-r--r-- | docs/api/response.rst | 5 | ||||
| -rw-r--r-- | docs/whatsnew-1.3.rst | 4 | ||||
| -rw-r--r-- | pyramid/response.py | 74 | ||||
| -rw-r--r-- | pyramid/static.py | 53 | ||||
| -rw-r--r-- | pyramid/tests/test_response.py | 27 | ||||
| -rw-r--r-- | pyramid/tests/test_static.py | 29 |
8 files changed, 117 insertions, 82 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 39bf59210..ecac7a68f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -36,6 +36,10 @@ Features be preferred over using ``pyramid.view.view_config`` with ``context=HTTPForbidden`` as was previously recommended. +- New APIs: ``pyramid.response.FileResponse`` and + ``pyramid.response.FileIter``, for usage in views that must serve files + "manually". + Backwards Incompatibilities --------------------------- @@ -4,9 +4,6 @@ Pyramid TODOs Nice-to-Have ------------ -- Expose _FileIter and _FileResponse somehow fbo of - manual-static-view-creators. - - Add docs about upgrading between Pyramid versions (e.g. how to see deprecation warnings). diff --git a/docs/api/response.rst b/docs/api/response.rst index 8020b629a..52978a126 100644 --- a/docs/api/response.rst +++ b/docs/api/response.rst @@ -9,6 +9,11 @@ :members: :inherited-members: +.. autoclass:: FileResponse + :members: + +.. autoclass:: FileIter + Functions ~~~~~~~~~ diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index 101caed94..daa1ffdec 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -354,6 +354,10 @@ Minor Feature Additions can be used to replace the respective default values of ``request.application_url`` partially. +- New APIs: :class:`pyramid.response.FileResponse` and + :class:`pyramid.response.FileIter`, for usage in views that must serve + files "manually". + Backwards Incompatibilities --------------------------- diff --git a/pyramid/response.py b/pyramid/response.py index b0c965296..d2b6fc8a7 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -1,13 +1,87 @@ +import mimetypes +from os.path import ( + getmtime, + getsize, + ) + import venusian from webob import Response as _Response from zope.interface import implementer from pyramid.interfaces import IResponse +_BLOCK_SIZE = 4096 * 64 # 256K + @implementer(IResponse) class Response(_Response): pass +class FileResponse(Response): + """ + A Response object that can be used to serve a static file from disk + simply. + + ``path`` is a file path on disk. + + ``request`` must be a Pyramid :term:`request` object if passed. Note + that a request *must* be passed if the response is meant to attempt to + use the ``wsgi.file_wrapper`` feature of the web server that you're using + to serve your Pyramid application. + + ``cache_max_age`` if passed, is the number of seconds that should be used + to HTTP cache this response. + """ + def __init__(self, path, request=None, cache_max_age=None): + super(FileResponse, self).__init__(conditional_response=True) + self.last_modified = getmtime(path) + content_type, content_encoding = mimetypes.guess_type(path, + strict=False) + if content_type is None: + content_type = 'application/octet-stream' + self.content_type = content_type + self.content_encoding = content_encoding + content_length = getsize(path) + f = open(path, 'rb') + app_iter = None + if request is not None: + environ = request.environ + if 'wsgi.file_wrapper' in environ: + app_iter = environ['wsgi.file_wrapper'](f, _BLOCK_SIZE) + if app_iter is None: + app_iter = FileIter(f, _BLOCK_SIZE) + self.app_iter = app_iter + # assignment of content_length must come after assignment of app_iter + self.content_length = content_length + if cache_max_age is not None: + self.cache_expires = cache_max_age + +class FileIter(object): + """ A fixed-block-size iterator for use as a WSGI app_iter. + + ``file`` is a Python file pointer (or at least an object with a ``read`` + method that takes a size hint). + + ``block_size`` is an optional block size for iteration. + """ + def __init__(self, file, block_size=_BLOCK_SIZE): + self.file = file + self.block_size = block_size + + def __iter__(self): + return self + + def next(self): + val = self.file.read(self.block_size) + if not val: + raise StopIteration + return val + + __next__ = next # py3 + + def close(self): + self.file.close() + + class response_adapter(object): """ Decorator activated via a :term:`scan` which treats the function being decorated as a :term:`response adapter` for the set of types or diff --git a/pyramid/static.py b/pyramid/static.py index e91485fad..fbe60b1dd 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -6,8 +6,6 @@ from os.path import ( normcase, normpath, join, - getmtime, - getsize, isdir, exists, ) @@ -30,7 +28,7 @@ from pyramid.httpexceptions import ( ) from pyramid.path import caller_package -from pyramid.response import Response +from pyramid.response import FileResponse from pyramid.traversal import traversal_path_info slash = text_('/') @@ -47,53 +45,6 @@ def init_mimetypes(mimetypes): # has been applied on the Python 2 trunk). init_mimetypes(mimetypes) -_BLOCK_SIZE = 4096 * 64 # 256K - -class _FileResponse(Response): - """ - Serves a static filelike object. - """ - def __init__(self, path, cache_max_age, request): - super(_FileResponse, self).__init__(conditional_response=True) - self.last_modified = getmtime(path) - content_type, content_encoding = mimetypes.guess_type(path, - strict=False) - if content_type is None: - content_type = 'application/octet-stream' - self.content_type = content_type - self.content_encoding = content_encoding - content_length = getsize(path) - f = open(path, 'rb') - environ = request.environ - if 'wsgi.file_wrapper' in environ: - app_iter = environ['wsgi.file_wrapper'](f, _BLOCK_SIZE) - else: - app_iter = _FileIter(f, _BLOCK_SIZE) - self.app_iter = app_iter - # assignment of content_length must come after assignment of app_iter - self.content_length = content_length - if cache_max_age is not None: - self.cache_expires = cache_max_age - -class _FileIter(object): - def __init__(self, file, block_size): - self.file = file - self.block_size = block_size - - def __iter__(self): - return self - - def next(self): - val = self.file.read(self.block_size) - if not val: - raise StopIteration - return val - - __next__ = next # py3 - - def close(self): - self.file.close() - class static_view(object): """ An instance of this class is a callable which can act as a :app:`Pyramid` :term:`view callable`; this view will serve @@ -187,7 +138,7 @@ class static_view(object): if not exists(filepath): return HTTPNotFound(request.url) - return _FileResponse(filepath ,self.cache_max_age, request) + return FileResponse(filepath, request, self.cache_max_age) def add_slash_redirect(self, request): url = request.path_url + '/' diff --git a/pyramid/tests/test_response.py b/pyramid/tests/test_response.py index 24931ec4b..af6eb3532 100644 --- a/pyramid/tests/test_response.py +++ b/pyramid/tests/test_response.py @@ -1,3 +1,4 @@ +import io import unittest from pyramid import testing @@ -16,6 +17,32 @@ class TestResponse(unittest.TestCase): inst = self._getTargetClass()() self.assertTrue(IResponse.providedBy(inst)) +class TestFileIter(unittest.TestCase): + def _makeOne(self, file, block_size): + from pyramid.response import FileIter + return FileIter(file, block_size) + + def test___iter__(self): + f = io.BytesIO(b'abc') + inst = self._makeOne(f, 1) + self.assertEqual(inst.__iter__(), inst) + + def test_iteration(self): + data = b'abcdef' + f = io.BytesIO(b'abcdef') + inst = self._makeOne(f, 1) + r = b'' + for x in inst: + self.assertEqual(len(x), 1) + r+=x + self.assertEqual(r, data) + + def test_close(self): + f = io.BytesIO(b'abc') + inst = self._makeOne(f, 1) + inst.close() + self.assertTrue(f.closed) + class Dummy(object): pass diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 02cd49430..70932143e 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -1,6 +1,5 @@ import datetime import unittest -import io # 5 years from now (more or less) fiveyrsfuture = datetime.datetime.utcnow() + datetime.timedelta(5*365) @@ -114,7 +113,7 @@ class Test_static_view_use_subpath_False(unittest.TestCase): self.assertTrue(b'<html>static</html>' in response.body) def test_resource_is_file_with_wsgi_file_wrapper(self): - from pyramid.static import _BLOCK_SIZE + from pyramid.response import _BLOCK_SIZE inst = self._makeOne('pyramid.tests:fixtures/static') request = self._makeRequest({'PATH_INFO':'/index.html'}) class _Wrapper(object): @@ -386,32 +385,6 @@ class Test_patch_mimetypes(unittest.TestCase): result = self._callFUT(module) self.assertEqual(result, False) -class Test_FileIter(unittest.TestCase): - def _makeOne(self, file, block_size): - from pyramid.static import _FileIter - return _FileIter(file, block_size) - - def test___iter__(self): - f = io.BytesIO(b'abc') - inst = self._makeOne(f, 1) - self.assertEqual(inst.__iter__(), inst) - - def test_iteration(self): - data = b'abcdef' - f = io.BytesIO(b'abcdef') - inst = self._makeOne(f, 1) - r = b'' - for x in inst: - self.assertEqual(len(x), 1) - r+=x - self.assertEqual(r, data) - - def test_close(self): - f = io.BytesIO(b'abc') - inst = self._makeOne(f, 1) - inst.close() - self.assertTrue(f.closed) - class DummyContext: pass |
