Chris McDonough
2012-02-23 fae09cd18a64efb32150dd0587995ed96cfc9523
Merge branch '1.3-branch'
11 files modified
251 ■■■■■ changed files
CHANGES.txt 8 ●●●● patch | view | raw | blame | history
TODO.txt 3 ●●●●● patch | view | raw | blame | history
docs/api/response.rst 5 ●●●●● patch | view | raw | blame | history
docs/conf.py 2 ●●● patch | view | raw | blame | history
docs/narr/assets.rst 15 ●●●●● patch | view | raw | blame | history
docs/whatsnew-1.3.rst 4 ●●●● patch | view | raw | blame | history
pyramid/response.py 83 ●●●●● patch | view | raw | blame | history
pyramid/static.py 53 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_response.py 47 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_static.py 29 ●●●●● patch | view | raw | blame | history
setup.py 2 ●●● patch | view | raw | blame | history
CHANGES.txt
@@ -1,5 +1,5 @@
Next release
============
1.3a9 (2012-02-22)
==================
Features
--------
@@ -36,6 +36,10 @@
  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
---------------------------
TODO.txt
@@ -4,9 +4,6 @@
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).
docs/api/response.rst
@@ -9,6 +9,11 @@
   :members:
   :inherited-members:
.. autoclass:: FileResponse
   :members:
.. autoclass:: FileIter
Functions
~~~~~~~~~
docs/conf.py
@@ -80,7 +80,7 @@
# 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
docs/narr/assets.rst
@@ -370,19 +370,22 @@
   :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:
docs/whatsnew-1.3.rst
@@ -354,6 +354,10 @@
  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
---------------------------
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
pyramid/static.py
@@ -6,8 +6,6 @@
    normcase,
    normpath,
    join,
    getmtime,
    getsize,
    isdir,
    exists,
    )
@@ -30,7 +28,7 @@
    )
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_('/')
@@ -46,53 +44,6 @@
# 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
@@ -187,7 +138,7 @@
            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 + '/'
pyramid/tests/test_response.py
@@ -1,3 +1,5 @@
import io
import os
import unittest
from pyramid import testing
@@ -16,6 +18,51 @@
        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
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 @@
        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):
@@ -385,32 +384,6 @@
        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.py
@@ -56,7 +56,7 @@
        ])
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,