summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2012-02-22 21:30:15 -0500
committerChris McDonough <chrism@plope.com>2012-02-22 21:30:15 -0500
commitfae09cd18a64efb32150dd0587995ed96cfc9523 (patch)
treef64ce3e285078253554dfc0f2bf1aa8e2c32da76
parent08f805a360185c648bfeccbbe568a97f0f036a0c (diff)
parent662d6ea96a79cdd9943a9e3c871fe07282ee671c (diff)
downloadpyramid-fae09cd18a64efb32150dd0587995ed96cfc9523.tar.gz
pyramid-fae09cd18a64efb32150dd0587995ed96cfc9523.tar.bz2
pyramid-fae09cd18a64efb32150dd0587995ed96cfc9523.zip
Merge branch '1.3-branch'
-rw-r--r--CHANGES.txt8
-rw-r--r--TODO.txt3
-rw-r--r--docs/api/response.rst5
-rw-r--r--docs/conf.py2
-rw-r--r--docs/narr/assets.rst15
-rw-r--r--docs/whatsnew-1.3.rst4
-rw-r--r--pyramid/response.py83
-rw-r--r--pyramid/static.py53
-rw-r--r--pyramid/tests/test_response.py47
-rw-r--r--pyramid/tests/test_static.py29
-rw-r--r--setup.py2
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
---------------------------
diff --git a/TODO.txt b/TODO.txt
index a58260d1f..5d96f7c7d 100644
--- a/TODO.txt
+++ b/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).
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
diff --git a/setup.py b/setup.py
index 7a5e80c67..05abb437a 100644
--- a/setup.py
+++ b/setup.py
@@ -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,