Merge branch '1.3-branch'
| | |
| | | Next release |
| | | ============ |
| | | 1.3a9 (2012-02-22) |
| | | ================== |
| | | |
| | | 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 |
| | | --------------------------- |
| | | |
| | |
| | | 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). |
| | | |
| | |
| | | :members: |
| | | :inherited-members: |
| | | |
| | | .. autoclass:: FileResponse |
| | | :members: |
| | | |
| | | .. autoclass:: FileIter |
| | | |
| | | Functions |
| | | ~~~~~~~~~ |
| | | |
| | |
| | | # 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 |
| | |
| | | :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: |
| | |
| | | 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 |
| | | --------------------------- |
| | | |
| | |
| | | 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 |
| | |
| | | normcase, |
| | | normpath, |
| | | join, |
| | | getmtime, |
| | | getsize, |
| | | isdir, |
| | | exists, |
| | | ) |
| | |
| | | ) |
| | | |
| | | 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_('/') |
| | |
| | | # that seems to effect Python 2.6, Python 2.6.1, and 2.6.2 (a fix |
| | | # 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 |
| | |
| | | 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 + '/' |
| | |
| | | import io |
| | | import os |
| | | import unittest |
| | | from pyramid import testing |
| | | |
| | |
| | | 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 |
| | | |
| | |
| | | import datetime |
| | | import unittest |
| | | import io |
| | | |
| | | # 5 years from now (more or less) |
| | | fiveyrsfuture = datetime.datetime.utcnow() + datetime.timedelta(5*365) |
| | |
| | | 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): |
| | |
| | | module = DummyMimetypes() |
| | | 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 |
| | |
| | | ]) |
| | | |
| | | 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, |