[Checkins] SVN: zope.file/trunk/s well, might not be perfect,
but tests pass with Blob and IResult
Gary Poster
gary at zope.com
Wed Apr 18 01:34:45 EDT 2007
Log message for revision 74233:
well, might not be perfect, but tests pass with Blob and IResult
Changed:
U zope.file/trunk/setup.py
U zope.file/trunk/src/zope/file/configure.zcml
U zope.file/trunk/src/zope/file/contenttype.txt
U zope.file/trunk/src/zope/file/download.py
U zope.file/trunk/src/zope/file/download.txt
U zope.file/trunk/src/zope/file/file.py
U zope.file/trunk/src/zope/file/ftests.py
U zope.file/trunk/src/zope/file/interfaces.py
-=-
Modified: zope.file/trunk/setup.py
===================================================================
--- zope.file/trunk/setup.py 2007-04-18 04:18:46 UTC (rev 74232)
+++ zope.file/trunk/setup.py 2007-04-18 05:34:45 UTC (rev 74233)
@@ -2,7 +2,7 @@
setup(
name="zope.file",
- version="0.2dev",
+ version="0.3dev",
packages=find_packages('src'),
package_dir={'':'src'},
namespace_packages=['zope'],
Modified: zope.file/trunk/src/zope/file/configure.zcml
===================================================================
--- zope.file/trunk/src/zope/file/configure.zcml 2007-04-18 04:18:46 UTC (rev 74232)
+++ zope.file/trunk/src/zope/file/configure.zcml 2007-04-18 05:34:45 UTC (rev 74233)
@@ -8,7 +8,8 @@
<class class=".file.File">
<require
permission="zope.View"
- interface=".interfaces.IFile"
+ interface="zope.mimetype.interfaces.IContentTypeAware"
+ attributes="size"
/>
<require
permission="zope.ManageContent"
@@ -16,7 +17,7 @@
/>
<require
permission="zope.ManageContent"
- attributes="openDetached"
+ attributes="openDetached open"
/>
<implements
interface="
@@ -40,14 +41,6 @@
/>
</class>
-
- <class class=".file.Writer">
- <require
- permission="zope.ManageContent"
- attributes="write close"
- />
- </class>
-
<!-- Subscriber to update the mimeType field on content-type
changes. -->
<subscriber
Modified: zope.file/trunk/src/zope/file/contenttype.txt
===================================================================
--- zope.file/trunk/src/zope/file/contenttype.txt 2007-04-18 04:18:46 UTC (rev 74232)
+++ zope.file/trunk/src/zope/file/contenttype.txt 2007-04-18 05:34:45 UTC (rev 74233)
@@ -58,6 +58,7 @@
Let's now set the encoding value to an old favorite, Latin-1::
>>> ctrl.value = ["iso-8859-1"]
+ >>> browser.handleErrors = False
>>> browser.getControl("Save").click()
We now see the updated value in the form, and can check the value in
Modified: zope.file/trunk/src/zope/file/download.py
===================================================================
--- zope.file/trunk/src/zope/file/download.py 2007-04-18 04:18:46 UTC (rev 74232)
+++ zope.file/trunk/src/zope/file/download.py 2007-04-18 05:34:45 UTC (rev 74233)
@@ -20,45 +20,40 @@
import zope.interface
import zope.mimetype.interfaces
import zope.publisher.browser
-import zope.publisher.http
-from zope.proxy import removeAllProxies
+import zope.publisher.interfaces.http
+import zope.security.proxy
-
class Download(zope.publisher.browser.BrowserView):
def __call__(self):
- return DownloadResult(self.context, contentDisposition="attachment")
+ for k, v in getHeaders(self.context, contentDisposition="attachment"):
+ self.request.response.setHeader(k, v)
+ return DownloadResult(self.context)
class Inline(zope.publisher.browser.BrowserView):
def __call__(self):
- return DownloadResult(self.context, contentDisposition="inline")
+ for k, v in getHeaders(self.context, contentDisposition="inline"):
+ self.request.response.setHeader(k, v)
+ return DownloadResult(self.context)
class Display(zope.publisher.browser.BrowserView):
def __call__(self):
+ for k, v in getHeaders(self.context):
+ self.request.response.setHeader(k, v)
return DownloadResult(self.context)
-
-class DownloadResult(object):
- """Result object for a download request."""
-
- zope.interface.implements(
- zope.publisher.http.IResult)
-
- def getFile(self, context):
- return removeAllProxies(context.openDetached())
-
- def __init__(self, context, contentType=None, downloadName=None,
- contentDisposition=None, contentLength=None):
+def getHeaders(context, contentType=None, downloadName=None,
+ contentDisposition=None, contentLength=None):
if not contentType:
cti = zope.mimetype.interfaces.IContentInfo(context, None)
if cti is not None:
contentType = cti.contentType
contentType = contentType or "application/octet-stream"
- self.headers = ("Content-Type", contentType),
+ headers = ("Content-Type", contentType),
downloadName = downloadName or context.__name__
if contentDisposition:
@@ -66,14 +61,25 @@
contentDisposition += (
'; filename="%s"' % downloadName.encode("utf-8")
)
- self.headers += ("Content-Disposition", contentDisposition),
+ headers += ("Content-Disposition", contentDisposition),
if contentLength is None:
contentLength = context.size
- self.headers += ("Content-Length", str(contentLength)),
- self.body = bodyIterator(self.getFile(context))
+ headers += ("Content-Length", str(contentLength)),
+ return headers
+class DownloadResult(object):
+ """Result object for a download request."""
+ zope.interface.implements(zope.publisher.interfaces.http.IResult)
+
+ def __init__(self, context):
+ self._iter = bodyIterator(
+ zope.security.proxy.removeSecurityProxy(context.openDetached()))
+
+ def __iter__(self):
+ return self._iter
+
CHUNK_SIZE = 64 * 1024
Modified: zope.file/trunk/src/zope/file/download.txt
===================================================================
--- zope.file/trunk/src/zope/file/download.txt 2007-04-18 04:18:46 UTC (rev 74232)
+++ zope.file/trunk/src/zope/file/download.txt 2007-04-18 05:34:45 UTC (rev 74233)
@@ -9,8 +9,9 @@
The download support is provided by two distinct objects: A view that
provides the download support using the information in the content
object, and a result object that can be used to implement a file
-download by other views. The result object can be used to override
-the content-type or the filename suggested to the browser.
+download by other views. The view can override the content-type or the
+filename suggested to the browser using the standard IResponse.setHeader
+method.
Note that result objects are intended to be used once and then
discarded.
@@ -24,17 +25,18 @@
Headers
-------
-Now, let's create a download result for this file::
+Now, let's get the headers for this file. We use a utility function called
+``getHeaders``::
- >>> from zope.file.download import DownloadResult
- >>> result = DownloadResult(f, contentDisposition='attachment')
+ >>> from zope.file.download import getHeaders
+ >>> headers = getHeaders(f, contentDisposition='attachment')
Since there's no suggested download filename on the file, the
Content-Disposition header doesn't specify one, but does indicate that
the response body be treated as a file to save rather than to apply
the default handler for the content type.
- >>> sorted(result.headers)
+ >>> sorted(headers)
[('Content-Disposition', 'attachment'),
('Content-Length', '0'),
('Content-Type', 'application/octet-stream')]
@@ -43,22 +45,21 @@
Note that a default content type of 'application/octet-stream' is
used.
-If the file object specifies a content type, that's used in the result
+If the file object specifies a content type, that's used in the headers
by default::
>>> f.mimeType = "text/plain"
- >>> result = DownloadResult(f, contentDisposition='attachment')
- >>> sorted(result.headers)
+ >>> headers = getHeaders(f, contentDisposition='attachment')
+ >>> sorted(headers)
[('Content-Disposition', 'attachment'),
('Content-Length', '0'),
('Content-Type', 'text/plain')]
-Alternatively, a content type can be specified to the result
-constructor::
+Alternatively, a content type can be specified to ``getHeaders``::
- >>> result = DownloadResult(f, contentType="text/xml",
- ... contentDisposition='attachment')
- >>> sorted(result.headers)
+ >>> headers = getHeaders(f, contentType="text/xml",
+ ... contentDisposition='attachment')
+ >>> sorted(headers)
[('Content-Disposition', 'attachment'),
('Content-Length', '0'),
('Content-Type', 'text/xml')]
@@ -66,27 +67,27 @@
The filename provided to the browser can be controlled similarly. If
the content object provides one, it will be used by default::
- >>> result = DownloadResult(f, contentDisposition='attachment')
- >>> sorted(result.headers)
+ >>> headers = getHeaders(f, contentDisposition='attachment')
+ >>> sorted(headers)
[('Content-Disposition', 'attachment'),
('Content-Length', '0'),
('Content-Type', 'text/plain')]
-Providing an alternate name to the result constructor overrides the
-download name from the file::
+Providing an alternate name to ``getHeaders`` overrides the download
+name from the file::
- >>> result = DownloadResult(f, downloadName="foo.txt",
- ... contentDisposition='attachment')
- >>> sorted(result.headers)
+ >>> headers = getHeaders(f, downloadName="foo.txt",
+ ... contentDisposition='attachment')
+ >>> sorted(headers)
[('Content-Disposition', 'attachment; filename="foo.txt"'),
('Content-Length', '0'),
('Content-Type', 'text/plain')]
The default Content-Disposition header can be overridden by providing
-an argument to the DownloadResult constructor::
+an argument to ``getHeaders``::
- >>> result = DownloadResult(f, contentDisposition="inline")
- >>> sorted(result.headers)
+ >>> headers = getHeaders(f, contentDisposition="inline")
+ >>> sorted(headers)
[('Content-Disposition', 'inline'),
('Content-Length', '0'),
('Content-Type', 'text/plain')]
@@ -94,8 +95,8 @@
If the `contentDisposition` argument is not provided, none will be
included in the headers::
- >>> result = DownloadResult(f)
- >>> sorted(result.headers)
+ >>> headers = getHeaders(f)
+ >>> sorted(headers)
[('Content-Length', '0'),
('Content-Type', 'text/plain')]
@@ -103,9 +104,12 @@
Body
----
-Since there's no data in this file, there are no body chunks:
+We use DownloadResult to deliver the content to the browser. Since
+there's no data in this file, there are no body chunks:
- >>> list(result.body)
+ >>> from zope.file.download import DownloadResult
+ >>> result = DownloadResult(f)
+ >>> list(result)
[]
We still need to see how non-empty files are handled. Let's write
@@ -114,39 +118,42 @@
>>> w = f.open("wb")
>>> w.write("some text")
>>> w.flush()
+ >>> w.close()
Now we can create a result object and see if we get the data we
expect::
>>> result = DownloadResult(f)
- >>> L = list(result.body)
+ >>> L = list(result)
>>> "".join(L)
'some text'
If the body content is really large, the iterator may provide more
than one chunk of data::
+ >>> w = f.open("wb")
>>> w.write("*" * 1024 * 1024)
>>> w.flush()
+ >>> w.close()
>>> result = DownloadResult(f)
- >>> L = list(result.body)
+ >>> L = list(result)
>>> len(L) > 1
True
Once iteration over the body has completed, further iteration will not
yield additional data::
- >>> list(result.body)
+ >>> list(result)
[]
The Download View
-----------------
-Now that we've seen the result object, let's take a look at the basic
-download view that uses it. We'll need to add a file object where we
-can get to it using a browser::
+Now that we've seen the ``getHeaders`` function and the result object,
+let's take a look at the basic download view that uses them. We'll need
+to add a file object where we can get to it using a browser::
>>> f = File()
>>> f.mimeType = "text/plain"
Modified: zope.file/trunk/src/zope/file/file.py
===================================================================
--- zope.file/trunk/src/zope/file/file.py 2007-04-18 04:18:46 UTC (rev 74232)
+++ zope.file/trunk/src/zope/file/file.py 2007-04-18 05:34:45 UTC (rev 74233)
@@ -52,11 +52,7 @@
fp.close()
def open(self, mode="r"):
- if mode.startswith("r"):
- return Reader(self, mode)
- if mode.startswith("w"):
- return Writer(self, mode)
- raise ValueError("unsupported `mode` value")
+ return self._data.open(mode)
def openDetached(self):
return self._data.openDetached()
@@ -71,91 +67,3 @@
reader.close()
return size
-
-class Accessor(object):
- """Base class for the reader and writer."""
-
- _closed = False
- _sio = None
- _write = False
- mode = None
-
- # XXX Accessor objects need to have an __parent__ to support the
- # security machinery, but they aren't ILocation instances since
- # they aren't addressable via URL.
- #
- # There needs to be an interface for this in Zope 3, but that's a
- # large task since it affects lots of Z3 code. __parent__ should
- # be defined by an interface from which ILocation is derived.
-
- def __init__(self, file, mode):
- self.__parent__ = file
- self.mode = mode
- self._stream = self.__parent__._data.open(mode)
-
- def close(self):
- if not self._closed:
- self._close()
- self._closed = True
-
- def __getstate__(self):
- """Make sure the accessors can't be stored in ZODB."""
- cls = self.__class__
- raise TypeError("%s.%s instance is not picklable"
- % (cls.__module__, cls.__name__))
-
- def _get_stream(self):
- return self._stream
-
- def _close(self):
- pass
-
-
-class Reader(Accessor):
-
- zope.interface.implements(
- zope.file.interfaces.IFileReader)
-
- _data = File._data
-
- def read(self, size=-1):
- if self._closed:
- raise ValueError("I/O operation on closed file")
- return self._get_stream().read(size)
-
- def seek(self, offset, whence=0):
- if self._closed:
- raise ValueError("I/O operation on closed file")
- if whence not in (0, 1, 2):
- raise ValueError("illegal value for `whence`")
- self._get_stream().seek(offset, whence)
-
- def tell(self):
- if self._closed:
- raise ValueError("I/O operation on closed file")
- return int(self._get_stream().tell())
-
- def _close(self):
- self._get_stream().close()
-
-
-class Writer(Accessor):
-
- zope.interface.implements(
- zope.file.interfaces.IFileWriter)
-
- _write = True
-
- def flush(self):
- if self._closed:
- raise ValueError("I/O operation on closed file")
- self._get_stream().flush()
-
- def write(self, data):
- if self._closed:
- raise ValueError("I/O operation on closed file")
- self._get_stream().write(data)
-
- def _close(self):
- self._get_stream().close()
-
Modified: zope.file/trunk/src/zope/file/ftests.py
===================================================================
--- zope.file/trunk/src/zope/file/ftests.py 2007-04-18 04:18:46 UTC (rev 74232)
+++ zope.file/trunk/src/zope/file/ftests.py 2007-04-18 05:34:45 UTC (rev 74233)
@@ -18,16 +18,94 @@
import os.path
import unittest
+import shutil
+import tempfile
+
+import transaction
+from ZODB.DB import DB
+from ZODB.DemoStorage import DemoStorage
+from ZODB.Blobs.BlobStorage import BlobStorage
+from zope.testing import doctest
import zope.app.testing.functional
+from zope.app.component.hooks import setSite
here = os.path.dirname(os.path.realpath(__file__))
-ZopeFileLayer = zope.app.testing.functional.ZCMLLayer(
+class FunctionalBlobTestSetup(zope.app.testing.functional.FunctionalTestSetup):
+
+ temp_dir_name = None
+
+ def setUp(self):
+ """Prepares for a functional test case."""
+ # Tear down the old demo storage (if any) and create a fresh one
+ transaction.abort()
+ self.db.close()
+ storage = DemoStorage("Demo Storage", self.base_storage)
+ # make a dir
+ temp_dir_name = self.temp_dir_name = tempfile.mkdtemp()
+ # wrap storage with BlobStorage
+ storage = BlobStorage(temp_dir_name, storage)
+ self.db = self.app.db = DB(storage)
+ self.connection = None
+
+ def tearDown(self):
+ """Cleans up after a functional test case."""
+ transaction.abort()
+ if self.connection:
+ self.connection.close()
+ self.connection = None
+ self.db.close()
+ # del dir named '__blob_test__%s' % self.name
+ if self.temp_dir_name is not None:
+ shutil.rmtree(self.temp_dir_name, True)
+ self.temp_dir_name = None
+ setSite(None)
+
+class ZCMLLayer(zope.app.testing.functional.ZCMLLayer):
+
+ def setUp(self):
+ self.setup = FunctionalBlobTestSetup(self.config_file)
+
+def FunctionalBlobDocFileSuite(*paths, **kw):
+ globs = kw.setdefault('globs', {})
+ globs['http'] = zope.app.testing.functional.HTTPCaller()
+ globs['getRootFolder'] = zope.app.testing.functional.getRootFolder
+ globs['sync'] = zope.app.testing.functional.sync
+
+ kw['package'] = doctest._normalize_module(kw.get('package'))
+
+ kwsetUp = kw.get('setUp')
+ def setUp(test):
+ FunctionalBlobTestSetup().setUp()
+
+ if kwsetUp is not None:
+ kwsetUp(test)
+ kw['setUp'] = setUp
+
+ kwtearDown = kw.get('tearDown')
+ def tearDown(test):
+ if kwtearDown is not None:
+ kwtearDown(test)
+ FunctionalBlobTestSetup().tearDown()
+ kw['tearDown'] = tearDown
+
+ if 'optionflags' not in kw:
+ old = doctest.set_unittest_reportflags(0)
+ doctest.set_unittest_reportflags(old)
+ kw['optionflags'] = (old
+ | doctest.ELLIPSIS
+ | doctest.REPORT_NDIFF
+ | doctest.NORMALIZE_WHITESPACE)
+
+ suite = doctest.DocFileSuite(*paths, **kw)
+ suite.layer = zope.app.testing.functional.Functional
+ return suite
+
+ZopeFileLayer = ZCMLLayer(
os.path.join(here, "ftesting.zcml"), __name__, "ZopeFileLayer")
-
def fromDocFile(path):
- suite = zope.app.testing.functional.FunctionalDocFileSuite(path)
+ suite = FunctionalBlobDocFileSuite(path)
suite.layer = ZopeFileLayer
return suite
Modified: zope.file/trunk/src/zope/file/interfaces.py
===================================================================
--- zope.file/trunk/src/zope/file/interfaces.py 2007-04-18 04:18:46 UTC (rev 74232)
+++ zope.file/trunk/src/zope/file/interfaces.py 2007-04-18 05:34:45 UTC (rev 74233)
@@ -34,31 +34,21 @@
'wb' (write); and 'w+', 'w+b', 'wb+', 'r+', 'r+b', and 'rb+' (both).
Other values cause `ValueError` to be raised.
- If the file is opened in read mode, an `IFileReader` is
- returned; if opened in write mode, an `IFileWriter` is
+ If the file is opened in read mode, an object with an API (but
+ not necessarily interface) of `IFileReader` is returned; if
+ opened in write mode, an object with an API of `IFileWriter` is
returned; if in read/write, an object that implements both is
returned.
All readers and writers operate in 'binary' mode.
"""
- def open():
- """Return an object providing access to the file data.
-
- Allowed values for `mode` are 'r' and 'rb' (read); 'w' and
- 'wb' (write); and 'w+', 'w+b', 'wb+', 'r+', 'r+b', and 'rb+' (both).
- Other values cause `ValueError` to be raised.
-
- If the file is opened in read mode, an `IFileReader` is
- returned; if opened in write mode, an `IFileWriter` is
- returned; if in read/write, an object that implements both is
- returned.
-
- All readers and writers operate in 'binary' mode.
-
+ def openDetached():
+ """Return file data disconnected from database connection.
+
+ Read access only.
"""
-
size = zope.schema.Int(
title=_("Size"),
description=_("Size in bytes"),
@@ -67,6 +57,9 @@
)
+# The remaining interfaces below serve only to document the kind of APIs
+# to be expected, as described in IFile.open above.
+
class IFileAccessor(zope.interface.Interface):
"""Base accessor for `IFileReader` and `IFileWriter`."""
More information about the Checkins
mailing list