diff --git a/CHANGES.rst b/CHANGES.rst index f02becbed1..5170e82aeb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,9 +21,14 @@ https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst - Fix ``OFS.Image.File.__str__`` for ``Pdata`` contents (`#711 `_) +- Add ``wsgi.file_wrapper`` implementation + https://www.python.org/dev/peps/pep-0333/#optional-platform-specific-file-handling + (`#719 `_) + - Set ``REMOTE_USER`` in wsgi environ using Zope user authentication (`#713 `_) + - Improve documentation for Zope's error logging services. Backwards incompatible changes diff --git a/src/ZPublisher/WSGIPublisher.py b/src/ZPublisher/WSGIPublisher.py index af0cbddc4d..7bde076370 100644 --- a/src/ZPublisher/WSGIPublisher.py +++ b/src/ZPublisher/WSGIPublisher.py @@ -351,7 +351,10 @@ def publish_module(environ, start_response, if isinstance(response.body, _FILE_TYPES) or \ IUnboundStreamIterator.providedBy(response.body): - result = response.body + if 'wsgi.file_wrapper' in environ: + result = environ['wsgi.file_wrapper'](response.body) + else: + result = response.body else: # If somebody used response.write, that data will be in the # response.stdout BytesIO, so we put that before the body. diff --git a/src/ZPublisher/tests/test_WSGIPublisher.py b/src/ZPublisher/tests/test_WSGIPublisher.py index 9c45fc8f8d..76d57741cb 100644 --- a/src/ZPublisher/tests/test_WSGIPublisher.py +++ b/src/ZPublisher/tests/test_WSGIPublisher.py @@ -445,6 +445,73 @@ def __next__(self): app_iter = self._callFUT(environ, start_response, _publish) self.assertTrue(app_iter is body) + def test_stream_file_wrapper(self): + from ZPublisher.Iterators import IStreamIterator + from zope.interface import implementer + from ZPublisher.HTTPResponse import WSGIResponse + + @implementer(IStreamIterator) + class TestStreamIterator(object): + data = "hello" * 20 + + def __len__(self): + return len(self.data) + + class Wrapper(object): + def __init__(self, file): + self.file = file + + _response = WSGIResponse() + _response.setHeader('Content-Type', 'text/plain') + body = _response.body = TestStreamIterator() + environ = self._makeEnviron(**{'wsgi.file_wrapper': Wrapper}) + start_response = DummyCallable() + _publish = DummyCallable() + _publish._result = _response + app_iter = self._callFUT(environ, start_response, _publish) + self.assertTrue(app_iter.file is body) + self.assertTrue(isinstance(app_iter, Wrapper)) + self.assertEqual( + int(_response.headers['content-length']), len(body)) + self.assertTrue( + _response.headers['content-type'].startswith('text/plain')) + self.assertEqual(_response.status, 200) + + def test_unboundstream_file_wrapper(self): + from ZPublisher.Iterators import IUnboundStreamIterator + from zope.interface import implementer + from ZPublisher.HTTPResponse import WSGIResponse + + @implementer(IUnboundStreamIterator) + class TestUnboundStreamIterator(object): + data = "hello" + + def __len__(self): + return len(self.data) + + class Wrapper(object): + def __init__(self, file): + self.file = file + + _response = WSGIResponse() + _response.setStatus(200) + # UnboundStream needs Content-Length header + _response.setHeader('Content-Length', '5') + _response.setHeader('Content-Type', 'text/plain') + body = _response.body = TestUnboundStreamIterator() + environ = self._makeEnviron(**{'wsgi.file_wrapper': Wrapper}) + start_response = DummyCallable() + _publish = DummyCallable() + _publish._result = _response + app_iter = self._callFUT(environ, start_response, _publish) + self.assertTrue(app_iter.file is body) + self.assertTrue(isinstance(app_iter, Wrapper)) + self.assertEqual( + int(_response.headers['content-length']), len(body)) + self.assertTrue( + _response.headers['content-type'].startswith('text/plain')) + self.assertEqual(_response.status, 200) + def test_request_closed(self): environ = self._makeEnviron() start_response = DummyCallable()