[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