[Zope3-checkins] SVN: Zope3/branches/zipimport-support/src/zope/configuration/ support loading ZCML from packages imported from ZIP files (including eggs)

Fred L. Drake, Jr. fdrake at gmail.com
Tue Nov 8 17:55:56 EST 2005


Log message for revision 39988:
  support loading ZCML from packages imported from ZIP files (including eggs)

Changed:
  U   Zope3/branches/zipimport-support/src/zope/configuration/config.py
  U   Zope3/branches/zipimport-support/src/zope/configuration/fields.py
  A   Zope3/branches/zipimport-support/src/zope/configuration/path.py
  A   Zope3/branches/zipimport-support/src/zope/configuration/path.txt
  U   Zope3/branches/zipimport-support/src/zope/configuration/tests/test_config.py
  A   Zope3/branches/zipimport-support/src/zope/configuration/tests/zippitysample.zip
  A   Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/
  A   Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/README.txt
  A   Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/
  A   Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/__init__.py
  A   Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/
  A   Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/__init__.py
  A   Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/configure.zcml
  A   Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/including.zcml
  A   Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/silliness.zcml.in
  U   Zope3/branches/zipimport-support/src/zope/configuration/xmlconfig.py

-=-
Modified: Zope3/branches/zipimport-support/src/zope/configuration/config.py
===================================================================
--- Zope3/branches/zipimport-support/src/zope/configuration/config.py	2005-11-08 22:35:44 UTC (rev 39987)
+++ Zope3/branches/zipimport-support/src/zope/configuration/config.py	2005-11-08 22:55:56 UTC (rev 39988)
@@ -34,6 +34,7 @@
 from zope.interface.interfaces import IInterface
 from zope.schema.interfaces import WrongType
 from zope.configuration import fields
+from zope.configuration.path import newReference
 
 
 zopens = 'http://namespaces.zope.org/zope'
@@ -244,26 +245,13 @@
         >>> c.path("y/../z") == d + os.path.normpath("/z")
         1
         """
-
-        filename = os.path.normpath(filename)
-        if os.path.isabs(filename):
-            return filename
-
-        # Got a relative path, combine with base path.
-        # If we have no basepath, compute the base path from the package
-        # path.
-
         basepath = getattr(self, 'basepath', '')
+        if basepath or os.path.isabs(filename):
+            package = getattr(self, 'package', None)
+        else:
+            package = self.package
+        return newReference(filename, package, basepath)
 
-        if not basepath:
-            if self.package is None:
-                basepath = os.getcwd()
-            else:
-                basepath = os.path.dirname(self.package.__file__)
-            self.basepath = basepath
-
-        return os.path.join(basepath, filename)
-
     def checkDuplicate(self, filename):
         """Check for duplicate imports of the same file.
 
@@ -305,7 +293,7 @@
         Return True if processing is needed and False otherwise. If
         the file needs to be processed, it will be marked as
         processed, assuming that the caller will procces the file if
-        it needs to be procssed.
+        it needs to be processed.
 
         >>> c = ConfigurationContext()
         >>> c.processFile('/foo.zcml')
@@ -392,17 +380,17 @@
     def hasFeature(self, feature):
         """Check whether a named feature has been provided.
 
-        Initially no features are provided
+        Initially no features are provided:
 
         >>> c = ConfigurationContext()
         >>> c.hasFeature('onlinehelp')
         False
 
-        You can declare that a feature is provided
+        You can declare that a feature is provided:
 
         >>> c.provideFeature('onlinehelp')
 
-        and it becomes available
+        and it becomes available:
 
         >>> c.hasFeature('onlinehelp')
         True

Modified: Zope3/branches/zipimport-support/src/zope/configuration/fields.py
===================================================================
--- Zope3/branches/zipimport-support/src/zope/configuration/fields.py	2005-11-08 22:35:44 UTC (rev 39987)
+++ Zope3/branches/zipimport-support/src/zope/configuration/fields.py	2005-11-08 22:55:56 UTC (rev 39988)
@@ -285,9 +285,6 @@
 
     def fromUnicode(self, u):
         u = u.strip()
-        if os.path.isabs(u):
-            return os.path.normpath(u)
-
         return self.context.path(u)
 
 

Added: Zope3/branches/zipimport-support/src/zope/configuration/path.py
===================================================================
--- Zope3/branches/zipimport-support/src/zope/configuration/path.py	2005-11-08 22:35:44 UTC (rev 39987)
+++ Zope3/branches/zipimport-support/src/zope/configuration/path.py	2005-11-08 22:55:56 UTC (rev 39988)
@@ -0,0 +1,121 @@
+"""Path reference to package-relative resources.
+"""
+__docformat__ = "reStructuredText"
+
+import errno
+import os
+import StringIO
+
+import zope.interface
+
+try:
+    import pkg_resources
+except ImportError:
+    pkg_resources = None
+
+
+class IResourceReference(zope.interface.Interface):
+
+    def open(mode="rb"):
+        """Open the referenced resource, returning a file-like object.
+
+        Only 'read' modes are supported.
+
+        """
+
+
+def newReference(path, package=None, basepath=None):
+
+    if os.path.isabs(path):
+        return PathReference(path)
+
+    # Got a relative path, combine with base path.
+    # If we have no basepath, compute the base path from the package
+    # path.
+
+    if not basepath:
+        if package is None:
+            basepath = os.getcwd()
+        else:
+            basepath = os.path.dirname(package.__file__)
+
+    p = os.path.join(basepath, path)
+    p = os.path.normpath(p)
+    p = os.path.abspath(p)
+
+    if package:
+        return PackagePathReference(p, package, path)
+    else:
+        return PathReference(p)
+
+
+class PathReference(str):
+
+    zope.interface.implements(IResourceReference)
+
+    def __add__(self, other):
+        path = str(self) + other
+        return self.__class__(path)
+
+    def open(self, mode="rb"):
+        return open(self, mode)
+
+
+class PackagePathReference(str):
+
+    zope.interface.implements(IResourceReference)
+
+    def __new__(cls, value, package, relpath):
+        assert package
+        self = str.__new__(cls, value)
+        self._package = package
+        self._relpath = relpath
+        return self
+
+    def __add__(self, other):
+        value = str(self) + other
+        relpath = self._relpath + other
+        return self.__class__(value, self._package, relpath)
+
+    def open_pkg_resources(self, mode="rb"):
+        try:
+            data = pkg_resources.resource_string(
+                self._package.__name__, self._relpath)
+        except IOError, e:
+            if len(e.args) == 1:
+                # zipimport raises IOError w/ insufficient arguments
+                raise IOError(errno.ENOENT, "file not found", self)
+            else:
+                raise
+        f = StringIO.StringIO(data)
+        f.name = self
+        f.mode = mode
+        return f
+
+    def open_path_or_loader(self, mode="rb"):
+        try:
+            loader = self._package.__loader__
+        except AttributeError:
+            for dir in self._package.__path__:
+                filename = os.path.join(dir, self._relpath)
+                if os.path.exists(filename):
+                    break
+            return open(filename, mode)
+        else:
+            dir = os.path.dirname(self._package.__file__)
+            filename = os.path.join(dir, self._relpath)
+            return loader.get_data(self._package.__name__)
+
+    if pkg_resources:
+        open = open_pkg_resources
+    else:
+        open = open_path_or_loader
+
+
+def openResource(path, mode="rb"):
+    if not mode.startswith("r"):
+        raise ValueError("`mode` must be a read-only mode")
+    if IResourceReference.providedBy(path):
+        return path.open(mode)
+    else:
+        return open(path, mode)


Property changes on: Zope3/branches/zipimport-support/src/zope/configuration/path.py
___________________________________________________________________
Name: svn:mime-type
   + text/x-python
Name: svn:eol-style
   + native

Added: Zope3/branches/zipimport-support/src/zope/configuration/path.txt
===================================================================
--- Zope3/branches/zipimport-support/src/zope/configuration/path.txt	2005-11-08 22:35:44 UTC (rev 39987)
+++ Zope3/branches/zipimport-support/src/zope/configuration/path.txt	2005-11-08 22:55:56 UTC (rev 39988)
@@ -0,0 +1,159 @@
+====================================
+Package-relative resource references
+====================================
+
+The `zope.configuration.path` module provides a way to refer to and
+open a file using a package-relative path.  This is especially useful
+for packages imported from ZIP files (including eggs).  Such files are
+considered resources, and may only be opened in read-only mode.
+
+Resource references have a dual personality:  They are both strings
+and objects with interesting non-string methods.  The string behavior
+is intended to help support compatibility for code that was written
+before this API existed, while new code can use the extended API for
+more flexibility.
+
+There are two functions which are used::
+
+  >>> from zope.configuration.path import newReference, openResource
+
+`newReference()` is used to construct a new path reference, and
+`openResource()` is used to open the resource as a file-like object.
+
+`newReference()` takes three arguments: a path, a package, and a base
+path.  Only the first is required; passing `None` for the `package`
+and `basepath` arguments is equivalent to omitting them.
+
+The idea of the resource references is that they can carry along
+additional information that allows them to retain package-relative
+information, so they are most interesting when the `package` argument
+to the constructor is non-`None`.  Let's take a look at what this
+provides::
+
+  >>> import os
+  >>> import zope.configuration
+
+  >>> ref = newReference("path.txt", package=zope.configuration)
+
+If we examine the reference as a string, we get a path that points
+into the package::
+
+  >>> directory = os.path.dirname(zope.configuration.__file__)
+  >>> ref == os.path.join(directory, "path.txt")
+  True
+
+The resource can be opened using the `openResource()` function (which
+also accepts simple strings)::
+
+  >>> f = openResource(ref)
+  >>> f.readline()
+  '====================================\n'
+  >>> f.close()
+
+While this looks little different from using a simple string to refer
+to the resource file, it provides more functionality if the resource
+being referenced is part of a package contained in a ZIP archive.
+Let's add a convenient ZIP file containing a Python package to the
+module search path::
+
+  >>> import sys
+
+  >>> here = os.path.normpath(os.path.dirname(__file__))
+  >>> zipfile = os.path.join(here, "tests", "zippitysample.zip")
+  >>> sys.path.append(zipfile)
+
+We can now import the package contained in the zipfile and load
+resources from it::
+
+  >>> import zippity.sample
+  >>> ref = newReference("configure.zcml", package=zippity.sample)
+
+  >>> f = openResource(ref)
+  >>> f.readline()
+  '<configure\n'
+  >>> f.close()
+
+Note that only read modes are supported::
+
+  >>> openResource(ref, "w")
+  Traceback (most recent call last):
+    ...
+  ValueError: `mode` must be a read-only mode
+
+  >>> openResource(ref, "w+")
+  Traceback (most recent call last):
+    ...
+  ValueError: `mode` must be a read-only mode
+
+  >>> openResource(ref, "a")
+  Traceback (most recent call last):
+    ...
+  ValueError: `mode` must be a read-only mode
+
+  >>> openResource(ref, "a+")
+  Traceback (most recent call last):
+    ...
+  ValueError: `mode` must be a read-only mode
+
+
+Use in ZCML
+-----------
+
+The ZCML loading machinery uses these functions to deal with
+references to files that should be loaded.  This allows including ZCML
+files from ZIP archives using <include/> directives.
+
+Our example ZIP file includes an example ZCML file; let's see how we
+can load it using <include/>::
+
+  >>> from StringIO import StringIO
+  >>> from zope.configuration import config, xmlconfig
+
+  >>> context = config.ConfigurationMachine()
+  >>> xmlconfig.registerCommonDirectives(context)
+
+  >>> f = StringIO('<include package="zippity.sample"/>')
+  >>> xmlconfig.processxmlfile(f, context, testing=True)
+  >>> f.close()
+
+We can now check that the "feature" provided by our sample
+configuration has been defined::
+
+  >>> context.hasFeature("sample")
+  True
+
+We can also include one ZCML file from another in the ZIP archive.
+We'll create a new context so the provided feature is no longer
+known::
+
+  >>> context = config.ConfigurationMachine()
+  >>> xmlconfig.registerCommonDirectives(context)
+
+  >>> context.hasFeature("sample")
+  False
+
+Loading an alternate configuration file that includes the first should
+cause the "sample" feature to be provided::
+
+  >>> f = StringIO('<include package="zippity.sample" file="including.zcml"/>')
+  >>> xmlconfig.processxmlfile(f, context, testing=True)
+  >>> f.close()
+
+  >>> context.hasFeature("sample")
+  True
+
+One oddball feature of the ZCML machinery is that if a ZCML file being
+loaded doesn't exist, but a file of the same name with '.in' appended
+does, that will be loaded instead.  In this example, the sample
+package provides a 'silliness.zcml.in', but we're going to request
+'silliness.zcml'::
+
+  >>> context = config.ConfigurationMachine()
+  >>> xmlconfig.registerCommonDirectives(context)
+
+  >>> f = StringIO('<include package="zippity.sample" file="silliness.zcml"/>')
+  >>> xmlconfig.processxmlfile(f, context, testing=True)
+  >>> f.close()
+
+  >>> context.hasFeature("silliness")
+  True


Property changes on: Zope3/branches/zipimport-support/src/zope/configuration/path.txt
___________________________________________________________________
Name: svn:mime-type
   + text/plain
Name: svn:eol-style
   + native

Modified: Zope3/branches/zipimport-support/src/zope/configuration/tests/test_config.py
===================================================================
--- Zope3/branches/zipimport-support/src/zope/configuration/tests/test_config.py	2005-11-08 22:35:44 UTC (rev 39987)
+++ Zope3/branches/zipimport-support/src/zope/configuration/tests/test_config.py	2005-11-08 22:55:56 UTC (rev 39988)
@@ -18,7 +18,7 @@
 
 import sys
 import unittest
-from zope.testing.doctestunit import DocTestSuite
+from zope.testing.doctestunit import DocTestSuite, DocFileSuite
 from zope.configuration.config import metans, ConfigurationMachine
 from zope.configuration import config
 
@@ -266,8 +266,18 @@
     ...        del sys.modules[name]
     """
 
+
+def sys_path_setUp(self):
+    self.__old_path = sys.path[:]
+
+def sys_path_tearDown(self):
+    sys.path[:] = self.__old_path
+
+
 def test_suite():
     return unittest.TestSuite((
+        DocFileSuite('../path.txt',
+                     setUp=sys_path_setUp, tearDown=sys_path_tearDown),
         DocTestSuite('zope.configuration.fields'),
         DocTestSuite('zope.configuration.config'),
         DocTestSuite(),

Added: Zope3/branches/zipimport-support/src/zope/configuration/tests/zippitysample.zip
===================================================================
(Binary files differ)


Property changes on: Zope3/branches/zipimport-support/src/zope/configuration/tests/zippitysample.zip
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/README.txt
===================================================================
--- Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/README.txt	2005-11-08 22:35:44 UTC (rev 39987)
+++ Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/README.txt	2005-11-08 22:55:56 UTC (rev 39988)
@@ -0,0 +1,2 @@
+This directory contains a sample package that is used to create the
+'zippitysample.zip' file used in the zope.configuration.path tests.


Property changes on: Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/README.txt
___________________________________________________________________
Name: svn:mime-type
   + text/plain
Name: svn:eol-style
   + native

Added: Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/__init__.py
===================================================================
--- Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/__init__.py	2005-11-08 22:35:44 UTC (rev 39987)
+++ Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/__init__.py	2005-11-08 22:55:56 UTC (rev 39988)
@@ -0,0 +1 @@
+# This directory is a Python package.


Property changes on: Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/__init__.py
___________________________________________________________________
Name: svn:mime-type
   + text/x-python
Name: svn:eol-style
   + native

Added: Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/__init__.py
===================================================================
--- Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/__init__.py	2005-11-08 22:35:44 UTC (rev 39987)
+++ Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/__init__.py	2005-11-08 22:55:56 UTC (rev 39988)
@@ -0,0 +1 @@
+# This directory is a Python package.


Property changes on: Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/__init__.py
___________________________________________________________________
Name: svn:mime-type
   + text/x-python
Name: svn:eol-style
   + native

Added: Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/configure.zcml
===================================================================
--- Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/configure.zcml	2005-11-08 22:35:44 UTC (rev 39987)
+++ Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/configure.zcml	2005-11-08 22:55:56 UTC (rev 39988)
@@ -0,0 +1,8 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:meta="http://namespaces.zope.org/meta"
+    >
+
+  <meta:provides feature="sample"/>
+
+</configure>


Property changes on: Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/configure.zcml
___________________________________________________________________
Name: svn:mime-type
   + text/xml
Name: svn:eol-style
   + native

Added: Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/including.zcml
===================================================================
--- Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/including.zcml	2005-11-08 22:35:44 UTC (rev 39987)
+++ Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/including.zcml	2005-11-08 22:55:56 UTC (rev 39988)
@@ -0,0 +1,8 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:meta="http://namespaces.zope.org/meta"
+    >
+
+  <include file="configure.zcml"/>
+
+</configure>


Property changes on: Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/including.zcml
___________________________________________________________________
Name: svn:mime-type
   + text/xml
Name: svn:eol-style
   + native

Added: Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/silliness.zcml.in
===================================================================
--- Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/silliness.zcml.in	2005-11-08 22:35:44 UTC (rev 39987)
+++ Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/silliness.zcml.in	2005-11-08 22:55:56 UTC (rev 39988)
@@ -0,0 +1,8 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:meta="http://namespaces.zope.org/meta"
+    >
+
+  <meta:provides feature="silliness"/>
+
+</configure>


Property changes on: Zope3/branches/zipimport-support/src/zope/configuration/tests/zipsource/zippity/sample/silliness.zcml.in
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: Zope3/branches/zipimport-support/src/zope/configuration/xmlconfig.py
===================================================================
--- Zope3/branches/zipimport-support/src/zope/configuration/xmlconfig.py	2005-11-08 22:35:44 UTC (rev 39987)
+++ Zope3/branches/zipimport-support/src/zope/configuration/xmlconfig.py	2005-11-08 22:55:56 UTC (rev 39988)
@@ -34,6 +34,7 @@
 from xml.sax import SAXParseException
 from zope import schema
 from zope.configuration.exceptions import ConfigurationError
+from zope.configuration.path import openResource
 from zope.configuration.zopeconfigure import IZopeConfigure, ZopeConfigure
 from zope.interface import Interface
 
@@ -391,16 +392,17 @@
 
     """
     try:
-        fp = open(filename)
-    except IOError, (code, msg):
+        fp = openResource(filename)
+    except IOError, e:
+        code, msg = e
         if code == errno.ENOENT:
             fn = filename + ".in"
-            if os.path.exists(fn):
-                fp = open(fn)
-            else:
-                raise
+            try:
+                fp = openResource(fn)
+            except IOError:
+                raise e
         else:
-            raise
+            raise e
     return fp
 
 class IInclude(Interface):



More information about the Zope3-Checkins mailing list