Permalink
Browse files

Support the WSGI ``wsgi.file_wrapper`` protocol as per

  • Loading branch information...
1 parent e95e781 commit 4b6e73772c8ddbd8effc710c421e108648cac1e5 @mcdonc mcdonc committed Jan 16, 2012
View
@@ -1,3 +1,57 @@
+Next release
+------------
+
+Features
+~~~~~~~~
+
+- Support the WSGI ``wsgi.file_wrapper`` protocol as per
+ http://www.python.org/dev/peps/pep-0333/#optional-platform-specific-file-handling.
+ Here's a usage example::
+
+ import os
+
+ here = os.path.dirname(os.path.abspath(__file__))
+
+ def myapp(environ, start_response):
+ f = open(os.path.join(here, 'myphoto.jpg'), 'rb')
+ headers = [('Content-Type', 'image/jpeg')]
+ start_response(
+ '200 OK',
+ headers
+ )
+ return environ['wsgi.file_wrapper'](f, 32768)
+
+ The signature of the file wrapper constructor is ``(filelike_object,
+ block_size)``. Both arguments must be passed as positional (not keyword)
+ arguments. The result of creating a file wrapper should be **returned** as
+ the ``app_iter`` from a WSGI application.
+
+ The object passed as ``filelike_object`` to the wrapper must be a file-like
+ object which supports *at least* the ``read()`` method, and the ``read()``
+ method must support an optional size hint argument. It *should* support
+ the ``seek()`` and ``tell()`` methods. If it does not, normal iteration
+ over the filelike object using the provided block_size is used (and copying
+ is done, negating any benefit of the file wrapper). It *should* support a
+ ``close()`` method.
+
+ The specified ``block_size`` argument to the file wrapper constructor will
+ be used only when the ``filelike_object`` doesn't support ``seek`` and/or
+ ``tell`` methods. Waitress needs to use normal iteration to serve the file
+ in this degenerate case (as per the WSGI spec), and this block size will be
+ used as the iteration chunk size. The ``block_size`` argument is optional;
+ if it is not passed, a default value``32768`` is used.
+
+ Waitress will set a ``Content-Length`` header on the behalf of an
+ application when a file wrapper with a sufficiently filelike object is used
+ if the application hasn't already set one.
+
+ The machinery which handles a file wrapper currently doesn't do anything
+ particularly special using fancy system calls (it doesn't use ``sendfile``
+ for example); using it currently just prevents the system from needing to
+ copy data to a temporary buffer in order to send it to the client. No
+ copying of data is done when a WSGI app returns a file wrapper that wraps a
+ sufficiently filelike object. It may do something fancier in the future.
+
0.7 (2012-01-11)
----------------
View
@@ -61,3 +61,5 @@ Differences from ``zope.server``
Content-Length header.
- Dont hang a thread up trying to send data to slow clients.
+
+- Supports ``wsgi.file_wrapper`` protocol.
View
@@ -0,0 +1,52 @@
+Support for ``wsgi.file_wrapper``
+---------------------------------
+
+Waitress supports the `WSGI file_wrapper protocol
+<http://www.python.org/dev/peps/pep-0333/#optional-platform-specific-file-handling>`_
+. Here's a usage example::
+
+ import os
+
+ here = os.path.dirname(os.path.abspath(__file__))
+
+ def myapp(environ, start_response):
+ f = open(os.path.join(here, 'myphoto.jpg'), 'rb')
+ headers = [('Content-Type', 'image/jpeg')]
+ start_response(
+ '200 OK',
+ headers
+ )
+ return environ['wsgi.file_wrapper'](f, 32768)
+
+The file wrapper constructor is accessed via
+``environ['wsgi.file_wrapper']``. The signature of the file wrapper
+constructor is ``(filelike_object, block_size)``. Both arguments must be
+passed as positional (not keyword) arguments. The result of creating a file
+wrapper should be **returned** as the ``app_iter`` from a WSGI application.
+
+The object passed as ``filelike_object`` to the wrapper must be a file-like
+object which supports *at least* the ``read()`` method, and the ``read()``
+method must support an optional size hint argument and the ``read()`` method
+*must* return **bytes** objects (never unicode). It *should* support the
+``seek()`` and ``tell()`` methods. If it does not, normal iteration over the
+filelike object using the provided block_size is used (and copying is done,
+negating any benefit of the file wrapper). It *should* support a ``close()``
+method.
+
+The specified ``block_size`` argument to the file wrapper constructor will be
+used only when the ``filelike_object`` doesn't support ``seek`` and/or
+``tell`` methods. Waitress needs to use normal iteration to serve the file
+in this degenerate case (as per the WSGI pec), and this block size will be
+used as the iteration chunk size. The ``block_size`` argument is optional;
+if it is not passed, a default value``32768`` is used.
+
+Waitress will set a ``Content-Length`` header on the behalf of an application
+when a file wrapper with a sufficiently filelike object is used if the
+application hasn't already set one.
+
+The machinery which handles a file wrapper currently doesn't do anything
+particularly special using fancy system calls (it doesn't use ``sendfile``
+for example); using it currently just prevents the system from needing to
+copy data to a temporary buffer in order to send it to the client. No
+copying of data is done when a WSGI app returns a file wrapper that wraps a
+sufficiently filelike object. It may do something fancier in the future.
View
@@ -184,6 +184,7 @@ Extended Documentation
differences.rst
api.rst
arguments.rst
+ filewrapper.rst
glossary.rst
Change History
View
@@ -15,8 +15,6 @@
"""
from io import BytesIO
-from waitress.compat import thread
-
# copy_bytes controls the size of temp. strings for shuffling data around.
COPY_BYTES = 1 << 18 # 256K
@@ -106,7 +104,12 @@ def prune(self):
def getfile(self):
return self.file
-
+ def _close(self):
+ # named _close because ReadOnlyFileBasedBuffer is used as
+ # wsgi file.wrapper, and its protocol reserves "close"
+ if hasattr(self.file, 'close'):
+ self.file.close()
+ self.remain = 0
class TempfileBasedBuffer(FileBasedBuffer):
@@ -131,7 +134,31 @@ def __init__(self, from_buffer=None):
def newfile(self):
return BytesIO()
+class ReadOnlyFileBasedBuffer(FileBasedBuffer):
+ # used as wsgi.file_wrapper
+ def __init__(self, file, block_size=32768):
+ self.file = file
+ self.block_size = block_size # for __iter__
+
+ def prepare(self):
+ if ( hasattr(self.file, 'seek') and
+ hasattr(self.file, 'tell') ):
+ start_pos = self.file.tell()
+ self.file.seek(0, 2)
+ end_pos = self.file.tell()
+ self.file.seek(start_pos)
+ self.remain = end_pos - start_pos
+ return True
+ elif hasattr(self.file, 'close'):
+ # called by task if self.filelike has no seek/tell
+ self.close = self.file.close
+ return False
+
+ def __iter__(self): # called by task if self.filelike has no seek/tell
+ return iter(lambda: self.file.read(self.block_size), b'')
+ def append(self, s):
+ raise NotImplementedError
class OverflowableBuffer(object):
"""
@@ -150,7 +177,6 @@ class OverflowableBuffer(object):
def __init__(self, overflow):
# overflow is the maximum to be stored in a StringIO buffer.
self.overflow = overflow
- self.lock = thread.allocate_lock() # API
def __len__(self):
buf = self.buf
@@ -240,3 +266,9 @@ def getfile(self):
if buf is None:
buf = self._create_buffer()
return buf.getfile()
+
+ def _close(self):
+ buf = self.buf
+ if buf is not None:
+ buf._close()
+
Oops, something went wrong.

0 comments on commit 4b6e737

Please sign in to comment.