[Checkins] SVN: grok/trunk/martian/ Initial check in of Martian. The martian package abstracts out the grokking

Martijn Faassen faassen at infrae.com
Fri Apr 27 21:47:42 EDT 2007


Log message for revision 74875:
  Initial check in of Martian. The martian package abstracts out the grokking
  behavior from the Grok core into its own python package. We also
  take the opportunity to write better tests and better documentation,
  and to refactor things here and there.
  
  Note that martian is far from finished and not yet in use by the grok core.
  Lots of the code in here is taken from the grok core and at this point,
  barely tested. The two doctests, README.txt and scan.txt do pass, however.
  

Changed:
  A   grok/trunk/martian/
  A   grok/trunk/martian/buildout.cfg
  A   grok/trunk/martian/setup.py
  A   grok/trunk/martian/src/
  A   grok/trunk/martian/src/martian/
  A   grok/trunk/martian/src/martian/README.txt
  A   grok/trunk/martian/src/martian/__init__.py
  A   grok/trunk/martian/src/martian/components.py
  A   grok/trunk/martian/src/martian/core.py
  A   grok/trunk/martian/src/martian/directive.py
  A   grok/trunk/martian/src/martian/error.py
  A   grok/trunk/martian/src/martian/grokker.py
  A   grok/trunk/martian/src/martian/interfaces.py
  A   grok/trunk/martian/src/martian/scan.py
  A   grok/trunk/martian/src/martian/scan.txt
  A   grok/trunk/martian/src/martian/tests/
  A   grok/trunk/martian/src/martian/tests/__init__.py
  A   grok/trunk/martian/src/martian/tests/stoneage/
  U   grok/trunk/martian/src/martian/tests/stoneage/__init__.py
  U   grok/trunk/martian/src/martian/tests/stoneage/cave.py
  U   grok/trunk/martian/src/martian/tests/stoneage/hunt/mammoth.py
  A   grok/trunk/martian/src/martian/tests/test_all.py
  A   grok/trunk/martian/src/martian/util.py

-=-

Property changes on: grok/trunk/martian
___________________________________________________________________
Name: svn:ignore
   + develop-eggs
bin
parts
eggs
.installed.cfg


Added: grok/trunk/martian/buildout.cfg
===================================================================
--- grok/trunk/martian/buildout.cfg	2007-04-28 00:18:45 UTC (rev 74874)
+++ grok/trunk/martian/buildout.cfg	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1,12 @@
+[buildout]
+develop = . 
+parts = devpython test
+
+[devpython]
+recipe = zc.recipe.egg
+interpreter = devpython
+eggs = martian
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = martian

Added: grok/trunk/martian/setup.py
===================================================================
--- grok/trunk/martian/setup.py	2007-04-28 00:18:45 UTC (rev 74874)
+++ grok/trunk/martian/setup.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1,24 @@
+from setuptools import setup, find_packages
+
+setup(
+    name='martian',
+    version='0.1',
+    author='Grok project',
+    author_email='grok-dev at zope.org',
+    description="""\
+Martian is a library that allows the embedding of configuration
+information in Python code. Martian can then grok the system and
+do the appropriate configuration registrations. One example of a system
+that uses Martian is the system where it originated: Grok
+(http://grok.zope.org)
+""",
+    packages=find_packages('src'),
+    package_dir = {'': 'src'},
+    include_package_data = True,
+    zip_safe=False,
+    license='ZPL',
+    install_requires=[
+    'zope.interface',
+    'setuptools',
+    ],
+)


Property changes on: grok/trunk/martian/src
___________________________________________________________________
Name: svn:ignore
   + martian.egg-info


Added: grok/trunk/martian/src/martian/README.txt
===================================================================
--- grok/trunk/martian/src/martian/README.txt	2007-04-28 00:18:45 UTC (rev 74874)
+++ grok/trunk/martian/src/martian/README.txt	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1,271 @@
+Martian
+=======
+
+"There was so much to grok, so little to grok from." -- Stranger in a
+Strange Land, by Robert A. Heinlein
+
+Martian provides infrastructure for declarative configuration of
+Python code. Martian is especially useful for the construction of
+frameworks that need to provide a flexible plugin infrastructure. 
+
+Why is this package named ``martian``? In the novel "Stranger in a
+Strange Land", the verb *grok* is introduced:
+
+  Grok means to understand so thoroughly that the observer becomes a
+  part of the observed -- to merge, blend, intermarry, lose identity
+  in group experience.
+
+The ``martian`` package is a spin-off from the `Grok project`_. In
+Grok, as well as in the ``martian`` package, "grokking" stands for the
+process of deducing declarative configuration actions from Python
+code.
+
+In the novel, grokking is originally a concept that comes from the
+planet Mars. Martians *grok*. Since this package helps you grok code,
+it's a Martian.
+
+.. _`Grok project`: http://grok.zope.org
+
+Motivation
+----------
+
+What does all this mean? In order to explain this, let's first look at
+an example of a simple framework that can be *configured* with
+plugins. We will define a framework for handling files based on their
+extensions::
+
+  >>> class filehandler(FakeModule):
+  ...   import os
+  ...
+  ...   def handle_txt(filepath):
+  ...     return "Text file"
+  ...
+  ...   def handle_xml(filepath):
+  ...     return "XML file"
+  ...
+  ...   extension_handlers = { '.txt': handle_txt, '.xml': handle_xml }
+  ...
+  ...   def handle(filepath):
+  ...      name, ext = os.path.splitext(filepath)
+  ...      return extension_handlers[ext](filepath)
+
+Since normally we cannot create modules in a doctest, we will emulate
+Python modules using the ``FakeModule`` class. Whenever you see
+``FakeModule`` subclasses, imagine you're looking at a module
+definition. We also need to be able to import our fake module, so we
+also have a fake import statement that lets us do this::
+
+  >>> filehandler = fake_import(filehandler)
+
+Now let's try the ``handle_file`` function for a few file types::
+
+  >>> filehandler.handle('test.txt')
+  'Text file'
+  >>> filehandler.handle('test2.xml')
+  'XML file'
+
+File extensions that we do not recognize give us a KeyError::
+ 
+  >>> filehandler.handle('image.png')  
+  Traceback (most recent call last):
+  ...
+  KeyError: '.png'
+
+What about pluggability? We want to plug into this filehandler
+framework and provide the a handler for ``.png`` files. Since we are
+writing a plugin, we cannot change the ``filehandler`` module
+directly. Let's write an extension module instead::
+
+  >>> class pnghandler(FakeModule):
+  ...    def handle_png(filepath):
+  ...        return "PNG file"
+  ...
+  ...    filehandler.extension_handlers['.png'] = handle_png
+  >>> pnghandler = fake_import(pnghandler)
+
+PNG handling works now::
+
+  >>> filehandler.handle('image.png')
+  'PNG file'
+
+The action of registering something into a central registry is also
+called *configuration*. Larger frameworks often offer a lot of ways to
+configure them: ways to combine its own components along with
+components you provide to build a larger application. Using Python
+code to manually hook components into registries can get rather
+cumbersome and poses a maintenance risk. It is tempting to start doing
+fancy things in Python code such as conditional configuration, making
+the configuration state of a program hard to understand. Doing
+configuration at import time can also lead to unwanted side effects
+during import and ordering problems.
+
+Martian provides a framework that allows configuration to be expressed
+in declarative Python code. The idea is to make these declarations so
+minimal and easy to read that even complex and extensive configuration
+does not overly burden the programmer or the person reading the code.
+Configuration actions are also executed during a separate phase ("grok
+time"), not at import time.
+
+Martians that grok
+------------------
+
+What is a ``Martian``? It is an object that can grok other objects -
+execute configuration actions pertaining to the other
+object. Different kinds of Martians can grok different types of
+objects.
+
+Let's define a Martian to help us register the file type handler
+functions as seen in our previous example::
+
+  >>> import types
+  >>> from zope.interface import implements
+  >>> from martian import InstanceMartian
+  >>> class FileTypeMartian(InstanceMartian):
+  ...   component_class = types.FunctionType 
+  ...   def match(self, name, obj):
+  ...     return (super(FileTypeMartian, self).match(name, obj) and 
+  ...             name.startswith('handle_'))
+  ...
+  ...   def grok(self, name, obj, **kw):
+  ...       ext = name.split('_')[1]
+  ...       filehandler.extension_handlers['.' + ext] = obj
+
+
+This ``InstanceMartian`` allows us to grok instances of a particular
+type (such as functions). We need to define the type of object we're
+looking for with the ``component_class`` attribute. In addition, we've
+amended the ``match`` method to make sure we only match those
+instances that have a name that starts with ``handle_`.
+
+This martian will provide the IMartian interface::
+
+  >>> filetype_martian = FileTypeMartian()
+  >>> from martian.interfaces import IMartian
+  >>> IMartian.providedBy(filetype_martian)
+  True
+
+The martian will only match function objects that have a name
+that starts with ``'handle_'``::
+
+  >>> filetype_martian.match('handle_txt', filehandler.handle_txt)
+  True
+  >>> filetype_martian.match('handle_xml', filehandler.handle_xml)
+  True
+  >>> filetype_martian.match('handle_png', pnghandler.handle_png)
+  True
+
+It won't match ``handle``, as that does not start with ``handle_``::
+
+  >>> filetype_martian.match('handle', filehandler.handle)
+  False
+
+It also won't match non-function objects that happen to be prefixed
+with ``handle_``::
+
+  >>> class handle_foo(object):
+  ...   pass
+  >>> filetype_martian.match('handle_foo', handle_foo)
+  False
+  >>> filetype_martian.match('handle_foo', handle_foo())
+  False
+
+Now let's use the martian to grok a new handle function::
+
+  >>> def handle_jpg(filepath):
+  ...   return "JPG file"
+  >>> filetype_martian.match('handle_jpg', handle_jpg)
+  True
+  >>> filetype_martian.grok('handle_jpg', handle_jpg)
+
+Grokking will have have registered a handler for ``.jpg`` files (deducing
+the extension to register under from the function name)::
+ 
+  >>> sorted(filehandler.extension_handlers.keys())
+  ['.jpg', '.png', '.txt', '.xml']
+
+This means now our ``filehandler.handle`` function is now able to
+handle JPG files as well::
+  
+  >>> filehandler.handle('image2.jpg')
+  'JPG file'
+
+Grokking a module
+-----------------
+
+Let's now look at a special martian that can grok a Python
+module::
+
+  >>> from martian import ModuleMartian
+  >>> module_martian = ModuleMartian()
+  
+The idea is that the ``ModuleMartian`` groks any components in a
+module that it recognizes. A ModuleMartian does not work alone. It
+needs to be supplied with one or more martians that can grok
+components to be found in a module. Let's register the
+``filetype_martian`` with our ``module_martian``::
+
+  >>> module_martian.register(filetype_martian)
+
+We define a module that defines a lot of filetype handlers to be
+grokked::
+
+  >>> class lotsofhandlers(FakeModule):
+  ...   def handle_exe(filepath):
+  ...     return "EXE file"
+  ...
+  ...   def handle_ogg(filepath):
+  ...     return "OGG file"
+  ...
+  ...   def handle_svg(filepath):
+  ...     return "SVG file"
+  >>> lotsofhandlers = fake_import(lotsofhandlers)
+
+Our module martian matches this module::
+
+  >>> module_martian.match('lotsofhandlers', lotsofhandlers)
+  True
+
+Let's grok it::
+
+  >>> module_martian.grok('lotsofhandlers', lotsofhandlers)
+
+The new registrations are now available::
+
+  >>> sorted(filehandler.extension_handlers.keys())
+  ['.exe', '.jpg', '.ogg', '.png', '.svg', '.txt', '.xml']
+
+The system indeed recognizes them now::
+
+  >>> filehandler.handle('test.ogg')
+  'OGG file'
+  >>> filehandler.handle('test.svg')
+  'SVG file'
+  >>> filehandler.handle('test.exe')
+  'EXE file'
+
+As you can see, we can now define handlers without ever having to
+register them manually. We can now rewrite our original module and
+take out the manual registrations completely::
+
+  >>> class filehandler(FakeModule):
+  ...   import os
+  ...
+  ...   def handle_txt(filepath):
+  ...     return "Text file"
+  ...
+  ...   def handle_xml(filepath):
+  ...     return "XML file"
+  ...
+  ...   extension_handlers = {}
+  ...
+  ...   def handle(filepath):
+  ...      name, ext = os.path.splitext(filepath)
+  ...      return extension_handlers[ext](filepath)
+
+  >>> filehandler = fake_import(filehandler)
+ 
+Let's use martian to do the registrations for us::
+
+  >>> module_martian.grok('filehandler', filehandler)
+  >>> filehandler.handle('test.txt')
+  'Text file'

Added: grok/trunk/martian/src/martian/__init__.py
===================================================================
--- grok/trunk/martian/src/martian/__init__.py	2007-04-28 00:18:45 UTC (rev 74874)
+++ grok/trunk/martian/src/martian/__init__.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1,2 @@
+from core import ModuleMartian
+from components import GlobalMartian, ClassMartian, InstanceMartian

Copied: grok/trunk/martian/src/martian/components.py (from rev 74872, grok/trunk/src/grok/components.py)
===================================================================
--- grok/trunk/src/grok/components.py	2007-04-27 21:13:57 UTC (rev 74872)
+++ grok/trunk/martian/src/martian/components.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1,67 @@
+##############################################################################
+#
+# Copyright (c) 2006-2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+from zope.interface import implements
+
+from martian.interfaces import IMartian, IComponentMartian
+from martian import util
+
+NOT_DEFINED = object()
+
+class MartianBase(object):
+    implements(IMartian)
+    
+    priority = 0
+    continue_scanning = False
+
+    def match(self, name, obj):
+        raise NotImplementedError
+
+    def grok(self, name, obj, **kw):
+        raise NotImplementedError
+
+    
+class GlobalMartian(MartianBase):
+    """Martian that groks once per module.
+    """
+
+    def match(self, name, obj):
+        # we never match with any object
+        return False
+
+    def grok(self, name, obj, **kw):
+        raise NotImplementedError
+    
+
+class ComponentMartianBase(MartianBase):
+    implements(IComponentMartian)
+
+    component_class = NOT_DEFINED
+
+    def grok(self, name, obj, **kw):
+        raise NotImplementedError
+
+
+class ClassMartian(ComponentMartianBase):
+    """Martian that groks classes in a module.
+    """
+    def match(self, name, obj):
+        return util.check_subclass(obj, self.component_class)
+
+
+class InstanceMartian(ComponentMartianBase):
+    """Martian that groks instances in a module.
+    """
+    def match(self, name, obj):
+        return isinstance(obj, self.component_class)

Added: grok/trunk/martian/src/martian/core.py
===================================================================
--- grok/trunk/martian/src/martian/core.py	2007-04-28 00:18:45 UTC (rev 74874)
+++ grok/trunk/martian/src/martian/core.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1,80 @@
+import types
+
+from zope.interface import implements
+
+from martian.interfaces import INestedMartian
+from martian import components, util
+
+class ModuleMartian(components.MartianBase):
+    implements(INestedMartian)
+
+    def __init__(self):
+        self._martians = {}
+        
+    def register(self, martian):
+        # make sure martians are only registered once
+        key = martian.__class__
+        if key in self._martians:
+            return
+        self._martians[key] = martian
+
+    def match(self, name, module):
+        return isinstance(module, types.ModuleType)
+    
+    def grok(self, name, module, **kw):
+        scanned_results = self._scan(module)
+        # run through all martians registering found components in order
+        for martian in self._get_ordered_martians():
+            # if we run into a GlobalMartian, just do simple registration.
+            # this allows us to hook up martians that actually
+            # do not respond to anything in the module but for instance
+            # go to the filesystem to find templates
+            if isinstance(martian, components.GlobalMartian):
+                martian.grok(name, module, **kw)
+                continue
+
+            found_components = scanned_results.get(martian.component_class, [])
+
+            for name, component in found_components:
+                # this is a base class as it ends with Base, skip
+                if type(component) is type:
+                    if name.endswith('Base'):
+                        continue
+                    elif util.class_annotation_nobase(component,
+                                                      'grok.baseclass', False):
+                        continue
+                martian.grok(name, component, **kw)    
+
+    def _scan(self, module):
+        found_components = {}
+        for martian in self._martians.values():
+            if isinstance(martian, components.GlobalMartian):
+                continue
+            found_components[martian.component_class] = []
+
+        martians = self._get_ordered_martians()
+
+        for name in dir(module):
+            if name.startswith('__grok_'):
+                continue
+            obj = getattr(module, name)
+            if not util.defined_locally(obj, module.__name__):
+                continue
+            # XXX find way to get rid of this inner loop by doing hash table
+            # lookup?
+            for martian in martians:
+                if martian.match(name, obj):
+                    found_components[martian.component_class].append(
+                        (name, obj))
+                    if not martian.continue_scanning:
+                        break
+
+        return found_components
+
+    def _get_ordered_martians(self):
+        # sort martians by priority
+        martians = sorted(self._martians.values(),
+                          key=lambda martian: martian.priority)
+        # we want to handle high priority first
+        martians.reverse()
+        return martians

Copied: grok/trunk/martian/src/martian/directive.py (from rev 74872, grok/trunk/src/grok/directive.py)
===================================================================
--- grok/trunk/src/grok/directive.py	2007-04-27 21:13:57 UTC (rev 74872)
+++ grok/trunk/martian/src/martian/directive.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1,198 @@
+##############################################################################
+#
+# Copyright (c) 2006-2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Grok directives.
+"""
+
+import sys
+import inspect
+
+from zope import interface
+from zope.interface.interfaces import IInterface
+
+from martian import util
+from martian.error import GrokImportError
+
+def frame_is_module(frame):
+    return frame.f_locals is frame.f_globals
+
+def frame_is_class(frame):
+    return '__module__' in frame.f_locals    
+
+
+class IDirectiveContext(interface.Interface):
+    description = interface.Attribute("The correct place in which the "
+                                      "directive can be used.")
+
+    def matches(frame):
+        """returns whether the given frame is the correct place in
+        which the directive can be used.
+        """
+
+class ClassDirectiveContext(object):
+    interface.implements(IDirectiveContext)
+    
+    description = "class"
+
+    def matches(self, frame):
+        return frame_is_class(frame)
+
+    
+class ModuleDirectiveContext(object):
+    interface.implements(IDirectiveContext)
+    
+    description = "module"
+
+    def matches(self, frame):
+        return frame_is_module(frame)
+    
+
+class ClassOrModuleDirectiveContext(object):
+    interface.implements(IDirectiveContext)
+    
+    description = "class or module"
+
+    def matches(self, frame):
+        return frame_is_module(frame) or frame_is_class(frame)
+
+
+class Directive(object):
+    """
+    Directive sets a value into the context's locals as __<name>__
+    ('.' in the name are replaced with '_').
+    """
+
+    def __init__(self, name, directive_context):
+        self.name = name
+        self.local_name = '__%s__' % name.replace('.', '_')
+        self.directive_context = directive_context
+
+    def __call__(self, *args, **kw):
+        self.check_argument_signature(*args, **kw)
+        self.check_arguments(*args, **kw)
+
+        frame = sys._getframe(1)
+        self.check_directive_context(frame)
+
+        value = self.value_factory(*args, **kw)
+        return self.store(frame, value)
+
+    def check_arguments(self, *args, **kw):
+        raise NotImplementedError
+
+    # to get a correct error message, we construct a function that has
+    # the same signature as check_arguments(), but without "self".
+    def check_argument_signature(self, *arguments, **kw):
+        args, varargs, varkw, defaults = inspect.getargspec(
+            self.check_arguments)
+        argspec = inspect.formatargspec(args[1:], varargs, varkw, defaults)
+        exec("def signature_checker" + argspec + ": pass")
+        try:
+            signature_checker(*arguments, **kw)
+        except TypeError, e:
+            message = e.args[0]
+            message = message.replace("signature_checker()", self.name)
+            raise TypeError(message)
+
+    def check_directive_context(self, frame):
+        if not self.directive_context.matches(frame):
+            raise GrokImportError("%s can only be used on %s level."
+                                  % (self.name,
+                                     self.directive_context.description))
+
+    def value_factory(self, *args, **kw):
+        raise NotImplementedError
+
+    def store(self, frame, value):
+        raise NotImplementedError
+
+
+class OnceDirective(Directive):
+    def store(self, frame, value):
+        if self.local_name in frame.f_locals:
+            raise GrokImportError("%s can only be called once per %s."
+                                  % (self.name,
+                                     self.directive_context.description))
+        frame.f_locals[self.local_name] = value
+
+
+class MarkerDirective(OnceDirective):
+    """A directive without argument that places a marker.
+    """
+    def value_factory(self):
+        return True
+
+    def check_arguments(self):
+        pass
+
+class MultipleTimesDirective(Directive):
+    def store(self, frame, value):
+        values = frame.f_locals.get(self.local_name, [])
+        values.append(value)
+        frame.f_locals[self.local_name] = values
+
+
+class SingleValue(object):
+
+    # Even though the value_factory is called with (*args, **kw),
+    # we're safe since check_arguments would have bailed out with a
+    # TypeError if the number arguments we were called with was not
+    # what we expect here.
+    def value_factory(self, value):
+        return value
+
+
+class BaseTextDirective(object):
+    """
+    Base directive that only accepts unicode/ASCII values.
+    """
+
+    def check_arguments(self, value):
+        if util.not_unicode_or_ascii(value):
+            raise GrokImportError("You can only pass unicode or ASCII to "
+                                  "%s." % self.name)
+
+
+class SingleTextDirective(BaseTextDirective, SingleValue, OnceDirective):
+    """
+    Directive that accepts a single unicode/ASCII value, only once.
+    """
+
+
+class MultipleTextDirective(BaseTextDirective, SingleValue,
+                            MultipleTimesDirective):
+    """
+    Directive that accepts a single unicode/ASCII value, multiple times.
+    """
+
+
+class InterfaceOrClassDirective(SingleValue, OnceDirective):
+    """
+    Directive that only accepts classes or interface values.
+    """
+
+    def check_arguments(self, value):
+        if not (IInterface.providedBy(value) or util.isclass(value)):
+            raise GrokImportError("You can only pass classes or interfaces to "
+                                  "%s." % self.name)
+
+
+class InterfaceDirective(SingleValue, OnceDirective):
+    """
+    Directive that only accepts interface values.
+    """
+
+    def check_arguments(self, value):
+        if not (IInterface.providedBy(value)):
+            raise GrokImportError("You can only pass interfaces to "
+                                  "%s." % self.name)

Added: grok/trunk/martian/src/martian/error.py
===================================================================
--- grok/trunk/martian/src/martian/error.py	2007-04-28 00:18:45 UTC (rev 74874)
+++ grok/trunk/martian/src/martian/error.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1,21 @@
+##############################################################################
+#
+# Copyright (c) 2006-2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+class GrokError(Exception):
+    def __init__(self, message, component):
+        Exception.__init__(self, message)
+        self.component = component
+
+class GrokImportError(ImportError):
+    pass

Copied: grok/trunk/martian/src/martian/grokker.py (from rev 74872, grok/trunk/src/grok/grokker.py)
===================================================================
--- grok/trunk/src/grok/grokker.py	2007-04-27 21:13:57 UTC (rev 74872)
+++ grok/trunk/martian/src/martian/grokker.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1,138 @@
+##############################################################################
+#
+# Copyright (c) 2006-2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+import grok
+from grok import templatereg
+
+from martian import util, components
+from martian.error import GrokError
+
+class GrokkerRegistry(object):
+    def __init__(self):
+        self.clear()
+
+    def clear(self):
+        self._grokkers = {}
+        # register the meta grokkers manually as we can't grok those
+        self.registerGrokker(ClassGrokkerGrokker())
+        self.registerGrokker(InstanceGrokkerGrokker())
+        self.registerGrokker(ModuleGrokkerGrokker())
+
+    def registerGrokker(self, grokker):
+        # we're using a dictionary to make sure each type of grokker
+        # is registered only once (e.g. during meta-grok-time, and not again
+        # during grok-time).
+        key = grokker.__class__
+        if key in self._grokkers:
+            return
+        self._grokkers[key] = grokker
+
+    def _getGrokkersInOrder(self):
+        # sort grokkers by priority
+        grokkers = sorted(self._grokkers.values(),
+                          key=lambda grokker: grokker.priority)
+        # we want to handle high priority first
+        grokkers.reverse()
+        return grokkers
+
+    def scan(self, module_info):
+        components = {}
+        for grokker in self._grokkers.values():
+            if isinstance(grokker, components.ModuleGrokker):
+                continue
+            components[grokker.component_class] = []
+
+        grokkers = self._getGrokkersInOrder()
+        module = module_info.getModule()
+        for name in dir(module):
+            if name.startswith('__grok_'):
+                continue
+            obj = getattr(module, name)
+            if not util.defined_locally(obj, module_info.dotted_name):
+                continue
+            # XXX find way to get rid of this inner loop by doing hash table
+            # lookup?
+            for grokker in grokkers:
+                if grokker.match(obj):
+                    components[grokker.component_class].append((name, obj))
+                    if not grokker.continue_scanning:
+                        break
+
+        return components
+
+    def grok(self, module_info):
+        scanned_results = self.scan(module_info)
+
+        # XXX hardcoded in here which base classes are possible contexts
+        # this should be made extensible
+        possible_contexts = [obj for (name, obj) in
+                             (scanned_results.get(grok.Model, []) +
+                              scanned_results.get(grok.LocalUtility, []) +
+                              scanned_results.get(grok.Container, []))]
+        context = util.determine_module_context(module_info, possible_contexts)
+
+        templates = templatereg.TemplateRegistry()
+
+        # run through all grokkers registering found components in order
+        for grokker in self._getGrokkersInOrder():
+            # if we run into a ModuleGrokker, just do simple registration.
+            # this allows us to hook up grokkers to the process that actually
+            # do not respond to anything in the module but for instance
+            # to the filesystem to find templates
+            if isinstance(grokker, components.ModuleGrokker):
+                grokker.register(context, module_info, templates)
+                continue
+
+            components = scanned_results.get(grokker.component_class, [])
+
+            for name, component in components:
+                # this is a base class as it ends with Base, skip
+                if type(component) is type:
+                    if name.endswith('Base'):
+                        continue
+                    elif util.class_annotation_nobase(component,
+                                                      'grok.baseclass', False):
+                        continue
+                grokker.register(context,
+                                 name, component,
+                                 module_info, templates)
+
+        unassociated = list(templates.listUnassociated())
+        if unassociated:
+            raise GrokError("Found the following unassociated template(s) when "
+                            "grokking %r: %s.  Define view classes inheriting "
+                            "from grok.View to enable the template(s)."
+                            % (module_info.dotted_name,
+                               ', '.join(unassociated)), module_info)
+
+
+# deep meta mode here - we define grokkers for ClassGrokker,
+# InstanceGrokker, and ModuleGrokker. This allows the martian
+# package to grok itself
+
+class MetaGrokker(components.ClassGrokker):
+    def register(self, context, name, factory, module_info, templates):
+        grokkerRegistry.registerGrokker(factory())
+
+class ClassGrokkerGrokker(MetaGrokker):
+    component_class = components.ClassGrokker
+
+class InstanceGrokkerGrokker(MetaGrokker):
+    component_class = components.InstanceGrokker
+
+class ModuleGrokkerGrokker(MetaGrokker):
+    component_class = components.ModuleGrokker
+
+# the global grokker registry
+grokkerRegistry = GrokkerRegistry()

Copied: grok/trunk/martian/src/martian/interfaces.py (from rev 74872, grok/trunk/src/grok/interfaces.py)
===================================================================
--- grok/trunk/src/grok/interfaces.py	2007-04-27 21:13:57 UTC (rev 74872)
+++ grok/trunk/martian/src/martian/interfaces.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1,99 @@
+##############################################################################
+#
+# Copyright (c) 2006-2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+from zope.interface import Interface, Attribute
+
+class IMartian(Interface):
+    priority = Attribute('Integer indicating relative priority')
+    continue_scanning = Attribute('If true, continue scanning after match')
+
+    def match(name, obj):
+        """Returns True if this Martian can grok obj.
+
+        name - name of object (in module)
+        obj - object to potentially grok
+        """
+
+    def grok(name, obj, **kw):
+        """Grok obj.
+
+        name - name of object (in module)
+        obj - object to grok
+        **kw - optional parameters passed along the grokking process.
+        """
+
+class IComponentMartian(IMartian):
+    """A martian that groks components in a module.
+
+    Components may be instances or classes indicated by component_class.
+    """
+    component_class = Attribute('Class of the component to match')
+    
+class INestedMartian(IComponentMartian):
+    """A martian that contains other martians.
+    """
+    def register(martian):
+        """Register a martian to nest within this one.
+        """
+        
+class IModuleInfo(Interface):
+    def getModule():
+        """Get the module object this module info is representing.
+
+        In case of packages, gives back the module object for the package's
+        __init__.py
+        """
+
+    def isPackage():
+        """Returns True if this module is a package.
+        """
+
+    def getSubModuleInfos():
+        """Get module infos for any sub modules.
+
+        In a module, this will always return an empty list.
+        """
+
+    def getSubModuleInfo(name):
+        """Get sub module info for <name>.
+
+        In a package, give module info for sub-module.
+        Returns None if no such sub module is found. In a module,
+        always returns None.
+        """
+
+    def getResourcePath(name):
+        """Get the absolute path of a resource directory.
+
+        The resource directory will be 'relative' to this package or
+        module.
+
+        Case one: get the resource directory with name <name> from the same
+        directory as this module
+
+        Case two: get the resource directory with name <name> from the children
+        of this package
+        """
+
+    def getAnnotation(key, default):
+        """Given a key, get annotation object from module.
+
+        The annotation key is a dotted name. All dots are replaced
+        with underscores and the result is pre and post-fixed by
+        double underscore. For instance 'grok.name' will be translated
+        to '__grok_name__'.
+        
+        Uses default if no such annotation found.
+        """
+

Copied: grok/trunk/martian/src/martian/scan.py (from rev 74872, grok/trunk/src/grok/scan.py)
===================================================================
--- grok/trunk/src/grok/scan.py	2007-04-27 21:13:57 UTC (rev 74872)
+++ grok/trunk/martian/src/martian/scan.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1,153 @@
+##############################################################################
+#
+# Copyright (c) 2006-2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Scanning packages and modules
+"""
+
+import os
+
+from zope.interface import implements
+
+from martian.interfaces import IModuleInfo
+
+def is_package(path):
+    if not os.path.isdir(path):
+        return False
+    init_py = os.path.join(path, '__init__.py')
+    init_pyc = init_py + 'c'
+    # Check whether either __init__.py or __init__.pyc exist
+    return os.path.isfile(init_py) or os.path.isfile(init_pyc)
+
+
+class ModuleInfo(object):
+    implements(IModuleInfo)
+    
+    def __init__(self, path, dotted_name):
+        # Normalize .pyc files to .py
+        if path.endswith('c'):
+            path = path[:-1]
+        self.path = path
+        self.dotted_name = dotted_name
+
+        name_parts = dotted_name.split('.')
+        self.name = name_parts[-1]
+        if self.isPackage():
+            self.package_dotted_name = dotted_name
+        else:
+            self.package_dotted_name = '.'.join(name_parts[:-1])
+
+        self._module = None
+
+    def getResourcePath(self, name):
+        """Get the absolute path of a resource directory 'relative' to this
+        package or module.
+
+        Case one: get the resource directory with name <name> from the same
+        directory as this module
+
+        Case two: get the resource directory with name <name> from the children
+        of this package
+        """
+        return os.path.join(os.path.dirname(self.path), name)
+
+    def getSubModuleInfos(self):
+        if not self.isPackage():
+            return []
+        directory = os.path.dirname(self.path)
+        module_infos = []
+        seen = []
+        for entry in sorted(os.listdir(directory)):
+            entry_path = os.path.join(directory, entry)
+            name, ext = os.path.splitext(entry)
+            dotted_name = self.dotted_name + '.' + name
+
+            # Case one: modules
+            if (os.path.isfile(entry_path) and ext in ['.py', '.pyc']):
+                if name == '__init__':
+                    continue
+                # Avoid duplicates when both .py and .pyc exist
+                if name in seen:
+                    continue
+                seen.append(name)
+                module_infos.append(ModuleInfo(entry_path, dotted_name))
+            # Case two: packages
+            elif is_package(entry_path):
+                # We can blindly use __init__.py even if only
+                # __init__.pyc exists because we never actually use
+                # that filename.
+                module_infos.append(ModuleInfo(
+                    os.path.join(entry_path, '__init__.py'), dotted_name))
+        return module_infos
+
+    def getSubModuleInfo(self, name):
+        path = os.path.join(os.path.dirname(self.path), name)
+        if is_package(path):
+            return ModuleInfo(os.path.join(path, '__init__.py'),
+                              '%s.%s' % (self.package_dotted_name, name))
+        elif os.path.isfile(path + '.py') or os.path.isfile(path + '.pyc'):
+                return ModuleInfo(path + '.py',
+                                  '%s.%s' % (self.package_dotted_name, name))
+        else:
+            return None
+        
+
+    def getAnnotation(self, key, default):
+        key = key.replace('.', '_')
+        key = '__%s__' % key
+        module = self.getModule()
+        return getattr(module, key, default)
+
+    def getModule(self):
+        if self._module is None:
+            self._module = resolve(self.dotted_name)
+        return self._module
+
+    def isPackage(self):
+        return self.path.endswith('__init__.py')
+
+    def __repr__(self):
+        return "<ModuleInfo object for '%s'>" % self.dotted_name
+
+
+def module_info_from_dotted_name(dotted_name):
+    module = resolve(dotted_name)
+    return ModuleInfo(module.__file__, dotted_name)
+
+def module_info_from_module(module):
+    return ModuleInfo(module.__file__, module.__name__)
+
+
+# taken from zope.dottedname.resolve
+def resolve(name, module=None):
+    name = name.split('.')
+    if not name[0]:
+        if module is None:
+            raise ValueError("relative name without base module")
+        module = module.split('.')
+        name.pop(0)
+        while not name[0]:
+            module.pop()
+            name.pop(0)
+        name = module + name
+
+    used = name.pop(0)
+    found = __import__(used)
+    for n in name:
+        used += '.' + n
+        try:
+            found = getattr(found, n)
+        except AttributeError:
+            __import__(used)
+            found = getattr(found, n)
+
+    return found

Added: grok/trunk/martian/src/martian/scan.txt
===================================================================
--- grok/trunk/martian/src/martian/scan.txt	2007-04-28 00:18:45 UTC (rev 74874)
+++ grok/trunk/martian/src/martian/scan.txt	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1,164 @@
+Scanning modules
+================
+
+Martian can grok modules or packages. In order to grok packages, it
+needs to scan all modules in it. The martian.scan package provides an
+abstraction over packages and modules that helps with the scanning
+process.
+
+  >>> from martian.scan import module_info_from_dotted_name
+  
+We have provided a special test fixture package called stoneage that we are
+going to scan, in ``martian.tests.stoneage``.
+
+Modules
+-------
+
+The scanning module defines a class ``ModuleInfo`` that provides
+information about a module or a package. Let's take a look at the
+``cave`` module in the stone-age package::
+
+  >>> module_info = module_info_from_dotted_name('martian.tests.stoneage.cave')
+  
+We get a ``ModuleInfo`` object representing the ``cave module::
+
+  >>> module_info
+  <ModuleInfo object for 'martian.tests.stoneage.cave'>
+  
+``cave`` is a module, not a package.
+
+  >>> module_info.isPackage()
+  False
+
+We can retrieve the name of the module::
+
+  >>> module_info.name
+  'cave'
+
+We can also retrieve the dotted name of the module::
+
+  >>> module_info.dotted_name
+  'martian.tests.stoneage.cave'
+
+And the dotted name of the package the module is in::
+
+  >>> module_info.package_dotted_name
+  'martian.tests.stoneage'
+
+It is possible to get the actual module object that the ModuleInfo
+object stands for, in this case the package's ``cave.py``::
+
+  >>> module = module_info.getModule()
+  >>> module
+  <module 'martian.tests.stoneage.cave' from '...cave.py...'>
+
+We can store a module-level annotation in the module::
+
+  >>> module.__grok_foobar__ = 'GROK LOVE FOO'
+
+The ModuleInfo object allows us to retrieve the annotation again::
+
+  >>> module_info.getAnnotation('grok.foobar', None)
+  'GROK LOVE FOO'
+
+If a requested annotation does not exist, we get the default value::
+
+  >>> module_info.getAnnotation('grok.barfoo', 42)
+  42
+
+A module has no sub-modules in it (only packages have this)::
+
+  >>> module_info.getSubModuleInfos()
+  []
+
+Trying to retrieve any sub modules will give back None::
+
+  >>> print module_info.getSubModuleInfo('doesnotexist')
+  None
+
+Packages
+--------
+
+Now let's scan a package::
+
+  >>> module_info = module_info_from_dotted_name('martian.tests.stoneage')
+
+We will get a ModuleInfo instance representing the ``stoneage`` package::
+
+  >>> module_info
+  <ModuleInfo object for 'martian.tests.stoneage'>
+
+The object knows it is a package::
+
+  >>> module_info.isPackage()
+  True
+
+Like with the module, we can get the package's name::
+
+  >>> module_info.name
+  'stoneage'
+
+We can also get the package's dotted name back from it::
+
+  >>> module_info.dotted_name
+  'martian.tests.stoneage'
+
+It is also possible to get the dotted name of the nearest package the
+package resides in. This will always be itself::
+
+  >>> module_info.package_dotted_name
+  'martian.tests.stoneage'
+
+Now let's go into the package and a few sub modules that are in it::
+
+  >>> module_info.getSubModuleInfo('cave')
+  <ModuleInfo object for 'martian.tests.stoneage.cave'>
+
+  >>> module_info.getSubModuleInfo('hunt')
+  <ModuleInfo object for 'martian.tests.stoneage.hunt'>
+
+Trying to retrieve non-existing sub modules gives back None::
+
+  >>> print module_info.getSubModuleInfo('doesnotexist')
+  None
+
+It is possible to get the actual module object that the ModuleInfo
+object stands for, in this case the package's ``__init__.py``::
+
+  >>> module = module_info.getModule()
+  >>> module
+  <module 'martian.tests.stoneage' from '...__init__.py...'>
+
+A package has sub modules::
+
+  >>> sub_modules = module_info.getSubModuleInfos()
+  >>> sub_modules
+  [<ModuleInfo object for 'martian.tests.stoneage.cave'>,
+   <ModuleInfo object for 'martian.tests.stoneage.hunt'>,
+   <ModuleInfo object for 'martian.tests.stoneage.painting'>]
+
+Resource paths
+--------------
+
+Resources can be stored in a directory alongside a module (in their
+containing package).  We can get the path to such a resource directory
+using the ``getResourcePath`` method.
+
+For packages, a resource path will be a child of the package directory:
+
+  >>> import os.path
+  >>> expected_resource_path = os.path.join(os.path.dirname(
+  ...     module.__file__), 'stoneage-templates')
+  >>> resource_path = module_info.getResourcePath('stoneage-templates')
+  >>> resource_path == expected_resource_path
+  True
+
+For modules, a resource path will be a sibling of the module's file:
+
+  >>> cave_module_info = module_info_from_dotted_name(
+  ...    'martian.tests.stoneage.cave')
+  >>> expected_resource_path = os.path.join(os.path.dirname(
+  ...     cave_module_info.getModule().__file__), 'cave-templates')
+  >>> resource_path = cave_module_info.getResourcePath('cave-templates')
+  >>> resource_path == expected_resource_path
+  True

Added: grok/trunk/martian/src/martian/tests/__init__.py
===================================================================
--- grok/trunk/martian/src/martian/tests/__init__.py	2007-04-28 00:18:45 UTC (rev 74874)
+++ grok/trunk/martian/src/martian/tests/__init__.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1 @@
+#

Copied: grok/trunk/martian/src/martian/tests/stoneage (from rev 74872, grok/trunk/src/grok/tests/scan/stoneage)

Modified: grok/trunk/martian/src/martian/tests/stoneage/__init__.py
===================================================================
--- grok/trunk/src/grok/tests/scan/stoneage/__init__.py	2007-04-27 21:13:57 UTC (rev 74872)
+++ grok/trunk/martian/src/martian/tests/stoneage/__init__.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -1,3 +1,3 @@
-# this is a package
+# this package is a test fixture for the scan tests.
 
 

Modified: grok/trunk/martian/src/martian/tests/stoneage/cave.py
===================================================================
--- grok/trunk/src/grok/tests/scan/stoneage/cave.py	2007-04-27 21:13:57 UTC (rev 74872)
+++ grok/trunk/martian/src/martian/tests/stoneage/cave.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -1,15 +1,8 @@
-import grok
 
-class Cave(grok.Model):
+class Cave(object):
     pass
 
-class Index(grok.View):
+class Index(object):
     pass
 
-index = grok.PageTemplate("""\
-<html>
-<body>
-<h1>A comfy cave</h1>
-</body>
-</html>
-""")
+index = Index()

Modified: grok/trunk/martian/src/martian/tests/stoneage/hunt/mammoth.py
===================================================================
--- grok/trunk/src/grok/tests/scan/stoneage/hunt/mammoth.py	2007-04-27 21:13:57 UTC (rev 74872)
+++ grok/trunk/martian/src/martian/tests/stoneage/hunt/mammoth.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -1,7 +1,6 @@
-import grok
 
-class Mammoth(grok.Model):
+class Mammoth(object):
     pass
 
-class Index(grok.View):
+class Index(object):
     pass

Added: grok/trunk/martian/src/martian/tests/test_all.py
===================================================================
--- grok/trunk/martian/src/martian/tests/test_all.py	2007-04-28 00:18:45 UTC (rev 74874)
+++ grok/trunk/martian/src/martian/tests/test_all.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1,50 @@
+import unittest
+from zope.testing import doctest
+import new
+
+class FakeModule(object):
+    pass
+
+def fake_import(fake_module):
+    module = new.module(fake_module.__name__)
+    glob = {}
+    for name in dir(fake_module):
+        if name.startswith('__'):
+            continue
+        obj = getattr(fake_module, name)
+        glob[name] = obj
+        try:
+            obj = obj.im_func
+        except AttributeError:
+            pass
+        setattr(module, name, obj)
+        glob[name] = obj
+    # provide correct globals for functions
+    for name in dir(module):
+        if name.startswith('__'):
+            continue
+        obj = getattr(module, name)
+        try:
+            code = obj.func_code
+        except AttributeError:
+            continue
+        new_func = new.function(code, glob, name)
+        new_func.__module__ = module.__name__
+        setattr(module, name, new_func)
+        glob[name] = new_func
+    return module
+
+optionflags = doctest.NORMALIZE_WHITESPACE + doctest.ELLIPSIS
+
+globs = dict(FakeModule=FakeModule, fake_import=fake_import)
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTests([
+        doctest.DocFileSuite('../README.txt',
+                             globs=globs,
+                             optionflags=optionflags),
+        doctest.DocFileSuite('../scan.txt',
+                             optionflags=optionflags),
+        ])
+    return suite

Copied: grok/trunk/martian/src/martian/util.py (from rev 74872, grok/trunk/src/grok/util.py)
===================================================================
--- grok/trunk/src/grok/util.py	2007-04-27 21:13:57 UTC (rev 74872)
+++ grok/trunk/martian/src/martian/util.py	2007-04-28 01:47:41 UTC (rev 74875)
@@ -0,0 +1,138 @@
+##############################################################################
+#
+# Copyright (c) 2006-2007 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Martian utility functions.
+"""
+
+import re
+import types
+import sys
+import inspect
+
+from zope import interface
+
+from martian.error import GrokError, GrokImportError
+
+def not_unicode_or_ascii(value):
+    if isinstance(value, unicode):
+        return False
+    if not isinstance(value, str):
+        return True
+    return is_not_ascii(value)
+
+is_not_ascii = re.compile(eval(r'u"[\u0080-\uffff]"')).search
+
+def isclass(obj):
+    """We cannot use ``inspect.isclass`` because it will return True
+    for interfaces"""
+    return isinstance(obj, (types.ClassType, type))
+
+
+def check_subclass(obj, class_):
+    if not isclass(obj):
+        return False
+    return issubclass(obj, class_)
+
+
+def caller_module():
+    return sys._getframe(2).f_globals['__name__']
+
+
+def class_annotation(obj, name, default):
+    return getattr(obj, '__%s__' % name.replace('.', '_'), default)
+
+def class_annotation_nobase(obj, name, default):
+    """This will only look in the given class obj for the annotation.
+
+    It will not look in the inheritance chain.
+    """
+    return obj.__dict__.get('__%s__' % name.replace('.', '_'), default)
+    
+def class_annotation_list(obj, name, default):
+    """This will process annotations that are lists correctly in the face of
+    inheritance.
+    """
+    if class_annotation(obj, name, default) is default:
+        return default
+
+    result = []
+    for base in reversed(obj.mro()):
+        list = class_annotation(base, name, [])
+        if list not in result:
+            result.append(list)
+
+    result_flattened = []
+    for entry in result:
+        result_flattened.extend(entry)
+    return result_flattened
+
+def defined_locally(obj, dotted_name):
+    obj_module = getattr(obj, '__grok_module__', None)
+    if obj_module is None:
+        obj_module = getattr(obj, '__module__', None)
+    return obj_module == dotted_name
+
+
+AMBIGUOUS_CONTEXT = object()
+def check_context(component, context):
+    if context is None:
+        raise GrokError("No module-level context for %r, please use "
+                        "grok.context." % component, component)
+    elif context is AMBIGUOUS_CONTEXT:
+        raise GrokError("Multiple possible contexts for %r, please use "
+                        "grok.context." % component, component)
+
+
+def check_implements_one(class_):
+    check_implements_one_from_list(list(interface.implementedBy(class_)),
+                                   class_)
+
+def check_implements_one_from_list(list, class_):
+    if len(list) < 1:
+        raise GrokError("%r must implement at least one interface "
+                        "(use grok.implements to specify)."
+                        % class_, class_)
+    elif len(list) > 1:
+        raise GrokError("%r is implementing more than one interface "
+                        "(use grok.provides to specify which one to use)."
+                        % class_, class_)
+
+
+def determine_module_context(module_info, models):
+    if len(models) == 0:
+        context = None
+    elif len(models) == 1:
+        context = models[0]
+    else:
+        context = AMBIGUOUS_CONTEXT
+
+    module_context = module_info.getAnnotation('grok.context', None)
+    if module_context:
+        context = module_context
+
+    return context
+
+
+def determine_class_context(class_, module_context):
+    context = class_annotation(class_, 'grok.context', module_context)
+    check_context(class_, context)
+    return context
+
+
+def methods_from_class(class_):
+    # XXX Problem with zope.interface here that makes us special-case
+    # __provides__.
+    candidates = [getattr(class_, name) for name in dir(class_)
+                  if name != '__provides__' ]
+    methods = [c for c in candidates if inspect.ismethod(c)]
+    return methods



More information about the Checkins mailing list