[Zope3-checkins] CVS: Zope3/src/zope/app/apidoc/classmodule - __init__.py:1.2

Stephan Richter srichter at cosmos.phy.tufts.edu
Sun Mar 28 18:39:49 EST 2004


Update of /cvs-repository/Zope3/src/zope/app/apidoc/classmodule
In directory cvs.zope.org:/tmp/cvs-serv22176/src/zope/app/apidoc/classmodule

Modified Files:
	__init__.py 
Log Message:


Added tests.



Added interfaces for objects representing modules and classes.



Implemented class registry. It is efficiently filled when the class module
hierarchy is built using sys.modules for module lookup, unless it has not been
imported.



All class and module introspection methods moved into the representation
objects.




=== Zope3/src/zope/app/apidoc/classmodule/__init__.py 1.1 => 1.2 ===
--- Zope3/src/zope/app/apidoc/classmodule/__init__.py:1.1	Thu Feb 19 15:46:40 2004
+++ Zope3/src/zope/app/apidoc/classmodule/__init__.py	Sun Mar 28 18:39:48 2004
@@ -18,13 +18,386 @@
 
 $Id$
 """
+import os
+import sys
+import inspect
+from types import ClassType, TypeType
+
+import zope
+from zope.security.checker import getCheckerForInstancesOf
+from zope.interface import Interface, Attribute, implements, implementedBy
 from zope.app import zapi
-from zope.interface import implements
+from zope.app.container.interfaces import IReadContainer
+
+from zope.app.location.interfaces import ILocation
 from zope.app.apidoc.interfaces import IDocumentationModule
+from zope.app.apidoc.utilities import ReadContainerBase
+from zope.app.apidoc.utilities import getPythonPath
+from zope.app.apidoc.utilities import getPublicAttributes
+from zope.app.apidoc.utilities import getInterfaceForAttribute
+
+# Ignore these files, since they are not necessary or cannot be imported
+# correctly.
+IGNORE_FILES = ('tests', 'tests.py', 'ftests', 'ftests.py', 'CVS', 'gadfly',
+                'setup.py', 'introspection.py')
+
+class IModuleDocumentation(IReadContainer):
+    """Representation of a Python module for documentation.
+
+    The items of the container are sub-modules and classes.
+    """
+    def getDocString():
+        """Return the doc string of the module."""
+
+    def getFileName():
+        """Return the file name of the module."""
+
+    def getPath():
+        """Return the Python path of the module."""
+
+
+class IClassDocumentation(Interface):
+    """Representation of a class or type for documentation."""
+
+    def getDocString():
+        """Return the doc string of the class."""
+
+    def getPath():
+        """Return the Python path of the class."""
+
+    def getBases():
+        """Return the base classes of the class."""
+
+    def getInterfaces():
+        """Return the interfaces the class implements."""
+
+    def getAttributes():
+        """Return a list of 3-tuple attribute information.
+
+        The first entry of the 3-tuple is the name of the attribute, the
+        second is the attribute object itself. The third entry is the
+        interface in which the attribute is defined.
+
+        Note that only public attributes are returned, meaning only attributes
+        that do not start with an '_'-character.
+        """
+
+    def getMethods():
+        """Return a list of 3-tuple method information.
+
+        The first entry of the 3-tuple is the name of the method, the
+        second is the method object itself. The third entry is the
+        interface in which the method is defined.
+
+        Note that only public methods are returned, meaning only methods
+        that do not start with an '_'-character.
+        """
+
+    def getSecurityChecker():
+        """Return the security checker for this class.
+
+        Since 99% of the time we are dealing with name-based security
+        checkers, we can look up the get/set permission required for a
+        particular class attribute/method.
+        """
+
+
+class Module(ReadContainerBase):
+    """This class represents a Python module.
+
+    The module can be easily setup by simply passing the parent module, the
+    module name (not the entire Python path) and the Python module instance
+    itself::
+
+      >>> import zope.app.apidoc
+      >>> module = Module(None, 'apidoc', zope.app.apidoc)
+
+    We can now get some of the common module attributes via accessor methods::
+
+      >>> module.getDocString()[:24]
+      'Zope 3 API Documentation'
+
+      >>> module.getFileName()[-32:]
+      'src/zope/app/apidoc/__init__.pyc'
+
+      >>> module.getPath()
+      'zope.app.apidoc'
+      
+    The setup for creating the sub module and class tree is automatically
+    called during initialization, so that the sub-objects are available as
+    soon as you have the object::
 
+      >>> keys = module.keys()
+      >>> keys.sort()
+      >>> keys[:4]
+      ['APIDocumentation', 'browser', 'classmodule', 'ifacemodule']
 
-class ClassModule(object):
-    """Represent the Documentation of any possible class."""
+      >>> print module['browser'].getPath()
+      zope.app.apidoc.browser
+
+    Now, the 'get(key, default=None)' is actually much smarter than you might
+    originally suspect, since it can actually get to more objects than it
+    promises. If a key is not found in the module's children, it tries to
+    import the key as a module relative to this module.
+
+    For example, while 'tests' directories are not added to the module and
+    classes hierarchy (since they do not provide or implement any API), we can
+    still get to them::
+
+      >>> print module['tests'].getPath()
+      zope.app.apidoc.tests
+
+      >>> module['tests'].keys()
+      ['Root']
+    """
+    implements(ILocation, IModuleDocumentation)
+
+    def __init__(self, parent, name, module, setup=True):
+        """Initialize object."""
+        self.__parent__ = parent
+        self.__name__ = name
+        self.__module = module
+        self.__children = {}
+        if setup:
+            self.__setup()
+
+    def __setup(self):
+        """Setup the module sub-tree."""
+        # Detect packages
+        if hasattr(self.__module, '__file__') and \
+               self.__module.__file__.endswith('__init__.pyc'):
+            dir = os.path.split(self.__module.__file__)[0]
+            for file in os.listdir(dir):
+                if file in IGNORE_FILES:
+                    continue
+                path = os.path.join(dir, file)
+
+                if os.path.isdir(path) and '__init__.py' in os.listdir(path):
+                    fullname = self.__module.__name__ + '.' + file
+                    module = safe_import(fullname)
+                    if module is not None:
+                        self.__children[file] = Module(self, file, module)
+
+                elif os.path.isfile(path) and file.endswith('.py') and \
+                         not file.startswith('__init__'):
+                    name = file[:-3]
+                    fullname = self.__module.__name__ + '.' + name
+                    module = safe_import(fullname)
+                    if module is not None:
+                        self.__children[name] = Module(self, name, module)
+
+        # Setup classes in module, if any are available.
+        for name in self.__module.__dict__.keys():
+            attr = getattr(self.__module, name)
+            # We do not want to register duplicates or non-"classes"
+            if type(attr) in (ClassType, TypeType) and \
+                   attr.__module__ == self.__module.__name__:
+                self.__children[attr.__name__] = Class(self, name, attr)
+                
+
+    def getDocString(self):
+        """See IModule."""
+        return self.__module.__doc__
+
+    def getFileName(self):
+        """See IModule."""
+        return self.__module.__file__
+
+    def getPath(self):
+        """See IModule."""
+        return self.__module.__name__
+
+    def get(self, key, default=None):
+        """See zope.app.container.interfaces.IReadContainer."""
+        obj = self.__children.get(key, default)
+        if obj is not default:
+            return obj
+
+        # We are actually able to find much more than we promise
+        if self.getPath():
+            path = self.getPath() + '.' + key
+        else:
+            path = key
+        obj = safe_import(path)
+        if obj is not None:
+            return Module(self, key, obj)
+
+        return default
+
+    def items(self):
+        """See zope.app.container.interfaces.IReadContainer."""
+        return self.__children.items()
+
+
+class Class(object):
+    """This class represents a class declared in the module.
+
+    Setting up a class for documentation is easy. You only need to provide an
+    object providing 'IModule' as a parent, the name and the klass itself::
+
+      >>> import zope.app.apidoc
+      >>> module = Module(None, 'apidoc', zope.app.apidoc)
+      >>> klass = Class(module, 'APIDocumentation',
+      ...               zope.app.apidoc.APIDocumentation)
+
+    This class provides data about the class in an accessible format. The
+    Python path and doc string are easily retrieved using::
+
+      >>> klass.getPath()
+      'zope.app.apidoc.APIDocumentation'
+
+      >>> klass.getDocString()[:41]
+      'Represent the complete API Documentation.'
+
+    A list of base classes can also be retrieved. The list only includes
+    direct bases, so if we have class 'Blah', which extends 'Bar', which
+    extends 'Foo', then the bases of 'Blah' is just 'Bar'. In our example this
+    looks like this::
+
+      >>> klass.getBases()
+      (<class 'zope.app.apidoc.utilities.ReadContainerBase'>,)
+
+    For a more detailed analysis, you can also retrieve the public attributes
+    and methods of this class::
+
+      >>> klass.getAttributes()
+      []
+
+      >>> klass.getMethods()[0]
+      ('get', <unbound method APIDocumentation.get>, None)
+    
+    """
+    implements(ILocation, IClassDocumentation)
+
+    def __init__(self, module, name, klass):
+        self.__parent__ = module
+        self.__name__ = name
+        self.__klass = klass
+
+        # Setup interfaces that are implemented by this class. 
+        self.__interfaces = list(implementedBy(klass))
+        all_ifaces = {}
+        for iface in self.__interfaces:
+            all_ifaces[getPythonPath(iface)] = iface
+            for base in [base for base in iface.__bases__]:
+                all_ifaces[getPythonPath(base)] = base
+        self.__all_ifaces = all_ifaces.values()
+
+        # Register the class with the global class registry.
+        global classRegistry
+        classRegistry[self.getPath()] = klass
+
+    def getPath(self):
+        """See IClassDocumentation."""
+        return self.__parent__.getPath() + '.' + self.__name__
+
+    def getDocString(self):
+        """See IClassDocumentation."""
+        return self.__klass.__doc__
+
+    def getBases(self):
+        """See IClassDocumentation."""
+        return self.__klass.__bases__
+
+    def getInterfaces(self):
+        """See IClassDocumentation."""
+        return self.__interfaces
+
+    def getAttributes(self):
+        """See IClassDocumentation.
+
+        Here a detailed example::
+
+          >>> import pprint
+
+          >>> class ModuleStub(object):
+          ...      def getPath(self): return ''
+
+          >>> class IBlah(Interface):
+          ...      foo = Attribute('Foo')
+
+          >>> class Blah(object):
+          ...      implements(IBlah)
+          ...      foo = 'f'
+          ...      bar = 'b'
+          ...      _blah = 'l'
+
+          >>> klass = Class(ModuleStub(), 'Blah', Blah)
+
+          >>> attrs = klass.getAttributes()
+          >>> attrs.sort()
+          >>> pprint.pprint(attrs)
+          [('bar', 'b', None),
+           ('foo', 'f', <InterfaceClass zope.app.apidoc.classmodule.IBlah>)]
+        """
+        return [
+            (name, getattr(self.__klass, name), 
+             getInterfaceForAttribute(name, self.__all_ifaces, asPath=False))
+            
+            for name in getPublicAttributes(self.__klass)
+            if not inspect.ismethod(getattr(self.__klass, name))]
+
+    def getMethods(self):
+        """See IClassDocumentation.
+
+        Here a detailed example::
+
+          >>> import pprint
+
+          >>> class ModuleStub(object):
+          ...      def getPath(self): return ''
+
+          >>> class IBlah(Interface):
+          ...      def foo(): pass
+
+          >>> class Blah(object):
+          ...      implements(IBlah)
+          ...      def foo(self): pass
+          ...      def bar(self): pass
+          ...      def _blah(self): pass
+
+          >>> klass = Class(ModuleStub(), 'Blah', Blah)
+
+          >>> methods = klass.getMethods()
+          >>> methods.sort()
+          >>> pprint.pprint(methods)
+          [('bar', <unbound method Blah.bar>, None),
+           ('foo',
+            <unbound method Blah.foo>,
+            <InterfaceClass zope.app.apidoc.classmodule.IBlah>)]
+        """
+        return [
+            (name, getattr(self.__klass, name), 
+             getInterfaceForAttribute(name, self.__all_ifaces, asPath=False))
+
+            for name in getPublicAttributes(self.__klass)
+            if inspect.ismethod(getattr(self.__klass, name))]
+
+    def getSecurityChecker(self):
+        """See IClassDocumentation."""
+        return getCheckerForInstancesOf(self.__klass)
+
+
+class ClassModule(Module):
+    """Represent the Documentation of any possible class.
+
+    This object extends a module, since it can be seen as some sort of root
+    module. However, its sementacs are obviously a bit different::
+
+      >>> cm = ClassModule()
+
+      >>> cm.getDocString()
+      'Zope 3 root.'
+      >>> cm.getFileName()
+      ''
+      >>> cm.getPath()
+      ''
+
+      >>> names = cm.keys()
+      >>> names.sort()
+      >>> names == cm.rootModules
+      True
+    """
 
     implements(IDocumentationModule)
 
@@ -33,13 +406,14 @@
 
     # See zope.app.apidoc.interfaces.IDocumentationModule
     description = """
-    Since there is no registry for implemented classes for Zope 3, the user
-    experience of this module is somewhat different than the others. Two
-    alternative navigation means are provided. In the menu of the module you
-    will see an input field in which you can type the dotted name of the
-    desired class; for example 'zope.app.location.LocationProxy'. Once you
-    press enter or click on "Show" you can see the documentation for the
-    class.
+    This module allows you to get an overview of the modules and classes
+    defined in the Zope 3 framework and its supporting packages. There are
+    two methods to navigate through the modules to find the classes you are
+    interested in.
+
+    The first method is to type in aome part of the Python path of the class
+    and the module will look in the class registry for matches. The menu will
+    then return with a list of these matches.
 
     The second method is to click on the "Browse Zope Source" link. In the
     main window, you will see a directory listing with the root Zope 3
@@ -53,5 +427,122 @@
     permissions required to access it. 
     """
 
-    rootModules = ['ZConfig', 'persistence', 'transaction', 'zdaemon',
-                   'zodb', 'zope']
+    rootModules = ['ZConfig', 'ZODB', 'persistence', 'transaction', 'zdaemon',
+                   'zope']
+
+    def __init__(self):
+        """Initialize object."""
+        super(ClassModule, self).__init__(None, '', None, False)
+        self.__setup()
+        
+    def __setup(self):
+        """Setup module and class tree."""
+        for name in self.rootModules:
+            self._Module__children[name] = Module(self, name, safe_import(name))
+
+    def getDocString(self):
+        """See Module class."""
+        return 'Zope 3 root.'
+
+    def getFileName(self):
+        """See Module class."""
+        return ''
+
+    def getPath(self):
+        """See Module class."""
+        return ''
+
+
+class ClassRegistry(dict):
+    """A simple registry for classes.
+
+    This little registry allows us to quickly query a complete list of classes
+    that are defined and used by Zope 3. The prime feature of the class is the
+    'getClassesThatImplement(iface)' method that returns all classes that
+    implement the passed interface.
+
+    Here is the registry in action::
+
+      >>> reg = ClassRegistry()
+
+      >>> class IA(Interface):
+      ...      pass
+      >>> class IB(IA):
+      ...      pass
+      >>> class IC(Interface):
+      ...      pass
+      >>> class ID(Interface):
+      ...      pass
+
+      >>> class A:
+      ...    implements(IA)
+      >>> reg['A'] = A
+      >>> class B:
+      ...    implements(IB)
+      >>> reg['B'] = B
+      >>> class B2:
+      ...    implements(IB)
+      >>> reg['B2'] = B2
+      >>> class C:
+      ...    implements(IC)
+      >>> reg['C'] = C
+
+      >>> names = reg.keys()
+      >>> names.sort()
+      >>> names
+      ['A', 'B', 'B2', 'C']
+
+      >>> reg['A'] is A
+      True
+
+      >>> [n for n, k in reg.getClassesThatImplement(IA)]
+      ['A', 'B', 'B2']
+      >>> [n for n, k in reg.getClassesThatImplement(IB)]
+      ['B', 'B2']
+      >>> [n for n, k in reg.getClassesThatImplement(IC)]
+      ['C']
+      >>> [n for n, k in reg.getClassesThatImplement(ID)]
+      []
+    """
+
+    def getClassesThatImplement(self, iface):
+        """Return the all class items that implement iface.
+
+        Methods returns a 2-tuple of the form (path, class).
+        """
+        return [(path, klass) for path, klass in self.items()
+                if iface.implementedBy(klass)]
+
+# Initialize the registry
+classRegistry = ClassRegistry()
+
+
+def safe_import(path, default=None):
+    """Import a given path as efficiently as possible and without failure.
+
+    First we try to find the path in 'sys.modules', since this lookup is much
+    more efficient than importing it. If it was not found, we go back and try
+    to import the path. If that also fails, we return the 'default' value.
+
+    Here are some examples::
+
+      >>> 'zope.app' in sys.modules
+      True
+      >>> safe_import('zope.app') is sys.modules['zope.app']
+      True
+
+      >>> 'shelve' in sys.modules
+      False
+      >>> safe_import('shelve').__name__
+      'shelve'
+
+      >>> safe_import('foo') is None
+      True
+    """
+    module = sys.modules.get(path, default)
+    if module is default:
+        try:
+            module = __import__(path, {}, {}, ('*',))
+        except ImportError:
+            return default
+    return module




More information about the Zope3-Checkins mailing list