Skip to content
Browse files

- New APIs: ``pyramid.response.FileResponse`` and

  ``pyramid.response.FileIter``, for usage in views that must serve files
  "manually".
  • Loading branch information...
1 parent 08f805a commit 6b3cca0d548c0c3bcec62902f5b261df4e7c1d1e @mcdonc mcdonc committed Feb 22, 2012
Showing with 117 additions and 82 deletions.
  1. +4 −0 CHANGES.txt
  2. +0 −3 TODO.txt
  3. +5 −0 docs/api/response.rst
  4. +4 −0 docs/whatsnew-1.3.rst
  5. +74 −0 pyramid/response.py
  6. +2 −51 pyramid/static.py
  7. +27 −0 pyramid/tests/test_response.py
  8. +1 −28 pyramid/tests/test_static.py
View
4 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
---------------------------
View
3 TODO.txt
@@ -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).
View
5 docs/api/response.rst
@@ -9,6 +9,11 @@
:members:
:inherited-members:
+.. autoclass:: FileResponse
+ :members:
+
+.. autoclass:: FileIter
+
Functions
~~~~~~~~~
View
4 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
---------------------------
View
74 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
View
53 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_('/')
@@ -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 @@ def __call__(self, context, request):
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 + '/'
View
27 pyramid/tests/test_response.py
@@ -1,3 +1,4 @@
+import io
import unittest
from pyramid import testing
@@ -16,6 +17,32 @@ def test_provides_IResponse(self):
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
View
29 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 @@ def test_resource_is_file(self):
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 DummyMimetypes(object):
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

0 comments on commit 6b3cca0

Please sign in to comment.
Something went wrong with that request. Please try again.