diff options
| author | Chris McDonough <chrism@plope.com> | 2012-02-22 21:30:15 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2012-02-22 21:30:15 -0500 |
| commit | fae09cd18a64efb32150dd0587995ed96cfc9523 (patch) | |
| tree | f64ce3e285078253554dfc0f2bf1aa8e2c32da76 | |
| parent | 08f805a360185c648bfeccbbe568a97f0f036a0c (diff) | |
| parent | 662d6ea96a79cdd9943a9e3c871fe07282ee671c (diff) | |
| download | pyramid-fae09cd18a64efb32150dd0587995ed96cfc9523.tar.gz pyramid-fae09cd18a64efb32150dd0587995ed96cfc9523.tar.bz2 pyramid-fae09cd18a64efb32150dd0587995ed96cfc9523.zip | |
Merge branch '1.3-branch'
| -rw-r--r-- | CHANGES.txt | 8 | ||||
| -rw-r--r-- | TODO.txt | 3 | ||||
| -rw-r--r-- | docs/api/response.rst | 5 | ||||
| -rw-r--r-- | docs/conf.py | 2 | ||||
| -rw-r--r-- | docs/narr/assets.rst | 15 | ||||
| -rw-r--r-- | docs/whatsnew-1.3.rst | 4 | ||||
| -rw-r--r-- | pyramid/response.py | 83 | ||||
| -rw-r--r-- | pyramid/static.py | 53 | ||||
| -rw-r--r-- | pyramid/tests/test_response.py | 47 | ||||
| -rw-r--r-- | pyramid/tests/test_static.py | 29 | ||||
| -rw-r--r-- | setup.py | 2 |
11 files changed, 159 insertions, 92 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 39bf59210..bcedb19a1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ -Next release -============ +1.3a9 (2012-02-22) +================== Features -------- @@ -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/conf.py b/docs/conf.py index 3e38226c1..79d9946ad 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -80,7 +80,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.3a8' +version = '1.3a9' # The full version, including alpha/beta/rc tags. release = version diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index 2cc870619..22b38c929 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -370,19 +370,22 @@ do so, do things "by hand". First define the view callable. :linenos: import os - from pyramid.response import Response + from pyramid.response import FileResponse def favicon_view(request): here = os.path.dirname(__file__) - icon = open(os.path.join(here, 'static', 'favicon.ico'), 'rb') - return Response(content_type='image/x-icon', app_iter=icon) + icon = os.path.join(here, 'static', 'favicon.ico') + return FileResponse(icon, request=request) The above bit of code within ``favicon_view`` computes "here", which is a path relative to the Python file in which the function is defined. It then uses the Python ``open`` function to obtain a file handle to a file within -"here" named ``static``, and returns a response using the open the file -handle as the response's ``app_iter``. It makes sure to set the right -content_type too. +"here" named ``static``, and returns a :class:`pyramid.response.FileResponse` +using the file path as the response's ``path`` argument and the request as +the response's ``request`` argument. :class:`pyramid.response.FileResponse` +will serve the file as quickly as possible when it's used this way. It makes +sure to set the right content length and content_type too based on the file +extension of the file you pass. You might register such a view via configuration as a view callable that should be called as the result of a traversal: 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..2642d7769 100644 --- a/pyramid/response.py +++ b/pyramid/response.py @@ -1,13 +1,96 @@ +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. + + ``content_type``, if passed, is the content_type of the response. + + ``content_encoding``, if passed is the content_encoding of the response. + It's generally safe to leave this set to ``None`` if you're serving a + binary file. This argument will be ignored if you don't also pass + ``content-type``. + """ + def __init__(self, path, request=None, cache_max_age=None, + content_type=None, content_encoding=None): + super(FileResponse, self).__init__(conditional_response=True) + self.last_modified = getmtime(path) + if content_type is None: + 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..c5df1fc05 100644 --- a/pyramid/tests/test_response.py +++ b/pyramid/tests/test_response.py @@ -1,3 +1,5 @@ +import io +import os import unittest from pyramid import testing @@ -16,6 +18,51 @@ class TestResponse(unittest.TestCase): inst = self._getTargetClass()() self.assertTrue(IResponse.providedBy(inst)) +class TestFileResponse(unittest.TestCase): + def _makeOne(self, file, **kw): + from pyramid.response import FileResponse + return FileResponse(file, **kw) + + def _getPath(self): + here = os.path.dirname(__file__) + return os.path.join(here, 'fixtures', 'minimal.txt') + + def test_with_content_type(self): + path = self._getPath() + r = self._makeOne(path, content_type='image/jpeg') + self.assertEqual(r.content_type, 'image/jpeg') + + def test_without_content_type(self): + path = self._getPath() + r = self._makeOne(path) + self.assertEqual(r.content_type, 'text/plain') + +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 @@ -56,7 +56,7 @@ if not PY3: ]) setup(name='pyramid', - version='1.3a8', + version='1.3a9', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, |
