[Checkins] SVN: martian/trunk/src/martian/ Implement context association behavior using the new get default logic.

Martijn Faassen faassen at infrae.com
Tue Feb 24 17:32:20 EST 2009


Log message for revision 97225:
  Implement context association behavior using the new get default logic.
  Extend the get default logic so we can support this fairly cleanly.
  

Changed:
  U   martian/trunk/src/martian/__init__.py
  A   martian/trunk/src/martian/context.py
  A   martian/trunk/src/martian/context.txt
  U   martian/trunk/src/martian/directive.py
  U   martian/trunk/src/martian/directive.txt
  U   martian/trunk/src/martian/tests/test_all.py

-=-
Modified: martian/trunk/src/martian/__init__.py
===================================================================
--- martian/trunk/src/martian/__init__.py	2009-02-24 22:01:12 UTC (rev 97224)
+++ martian/trunk/src/martian/__init__.py	2009-02-24 22:32:19 UTC (rev 97225)
@@ -8,7 +8,8 @@
 from martian.directive import (ONCE, ONCE_NOBASE, ONCE_IFACE,
                                MULTIPLE, MULTIPLE_NOBASE, DICT)
 from martian.directive import CLASS, CLASS_OR_MODULE, MODULE
-from martian.directive import UNKNOWN
+from martian.directive import UNKNOWN, UnknownError
 from martian.directive import (
     validateText, validateInterface, validateClass, validateInterfaceOrClass)
 from martian.martiandirective import component, directive, priority, baseclass
+from martian.context import GetDefaultComponentFactory

Added: martian/trunk/src/martian/context.py
===================================================================
--- martian/trunk/src/martian/context.py	                        (rev 0)
+++ martian/trunk/src/martian/context.py	2009-02-24 22:32:19 UTC (rev 97225)
@@ -0,0 +1,48 @@
+from martian.directive import UnknownError
+from martian.util import scan_for_classes
+
+class GetDefaultComponentFactory(object):
+    def __init__(self, iface, component_name, directive_name):
+        """Create a get_default_component function.
+
+        iface - the iface that the component to be associated implements.
+                for example: IContext
+        component_name - the name of the type of thing we are looking for.
+                for example: context
+        directive_name - the name of the directive in use.
+                for example: grok.context.
+        """
+        self.iface = iface
+        self.component_name = component_name
+        self.directive_name = directive_name
+        
+    def __call__(self, component, module, **data):
+        """Determine module-level component.
+
+        Look for components in module.
+
+        iface determines the kind of module-level component to look for
+        (it will implement iface).
+        
+        If there is no module-level component, raise an error.
+        
+        If there is one module-level component, it is returned.
+
+        If there are more than one module-level component, raise
+        an error.
+        """
+        components = list(scan_for_classes(module, self.iface))
+        if len(components) == 0:
+            raise UnknownError(
+                "No module-level %s for %r, please use the '%s' "
+                "directive."
+                % (self.component_name, component, self.directive_name),
+                component)
+        elif len(components) == 1:
+            return components[0]
+        else:
+            raise UnknownError(
+                "Multiple possible %ss for %r, please use the '%s' "
+                "directive."
+                % (self.component_name, component, self.directive_name),
+                component)

Added: martian/trunk/src/martian/context.txt
===================================================================
--- martian/trunk/src/martian/context.txt	                        (rev 0)
+++ martian/trunk/src/martian/context.txt	2009-02-24 22:32:19 UTC (rev 97225)
@@ -0,0 +1,181 @@
+Context associating directives
+==============================
+
+Martian can help you implement directives that implicitly associate
+with another object or class in the modules. The most common example
+of this is the way Grok's ``context`` directive works.
+
+It has the following rules:
+
+* ``grok.context`` can be used on the class to establish the context
+  class for this class.
+
+* ``grok.context`` can be used on the module to establish the context
+  class for all classes in the module that require a context. Only
+  class-level ``grok.context`` use will override this.
+
+* If there is no ``grok.context`` for the class or module, the context
+  will be a class in the module that implements a special ``IContext``
+  interface.
+
+* If there are multiple possible impicit contexts, the context is
+  ambiguous. This is an error.
+
+* If there is no possible implicit context, the context cannot be
+  established. This is an error too.
+
+Let's implement a context directive with this behavior::
+
+  >>> import martian
+  >>> class context(martian.Directive):  
+  ...   scope = martian.CLASS_OR_MODULE
+  ...   store = martian.ONCE
+
+Let's use an explicit class context::
+
+  >>> class A(object):
+  ...   pass
+  >>> class explicitclasscontext(FakeModule):
+  ...    class B(object):
+  ...      context(A)
+  >>> from martiantest.fake import explicitclasscontext
+  >>> context.bind().get(explicitclasscontext.B)
+  <class 'A'>
+
+Let's now use the directive on the module-level, explicitly::
+
+  >>> class explicitmodulecontext(FakeModule):
+  ...   context(A)
+  ...   class B(object):
+  ...     pass
+  >>> from martiantest.fake import explicitmodulecontext
+  >>> context.bind().get(explicitmodulecontext.B)
+  <class 'martiantest.fake.explicitmodulecontext.A'>
+
+XXX why does this get this put into martiantest.fake.explicitmodule? A
+problem in FakeModule?
+
+Let's see a combination of the two, to check whether context on the class
+level overrides that on the module level::
+
+  >>> class D(object):
+  ...   pass
+  >>> class explicitcombo(FakeModule):
+  ...   context(A)
+  ...   class B(object):
+  ...     pass
+  ...   class C(object):
+  ...     context(D)
+  >>> from martiantest.fake import explicitcombo
+  >>> context.bind().get(explicitcombo.B)
+  <class 'martiantest.fake.explicitcombo.A'>
+  >>> context.bind().get(explicitcombo.C)
+  <class 'D'>
+
+So far so good. Now let's look at automatic association. Let's provide
+a ``get_default`` function that associates with any class that implements
+``IContext``:
+
+  >>> from zope.interface import Interface
+  >>> class IContext(Interface):
+  ...    pass
+  >>> get_default_context = martian.GetDefaultComponentFactory(
+  ...   IContext, 'context', 'context')
+
+We define a base class that will be automatically associated with::
+
+  >>> from zope.interface import implements
+
+  >>> class Context(object):
+  ...    implements(IContext)
+
+Let's experiment whether implicit context association works::
+
+  >>> class implicitcontext(FakeModule):
+  ...    class A(Context):
+  ...      pass
+  ...    class B(object):
+  ...      pass
+  >>> from martiantest.fake import implicitcontext
+  >>> context.bind(get_default=get_default_context).get(implicitcontext.B)
+  <class 'martiantest.fake.implicitcontext.A'>
+
+We now test the failure conditions.
+
+There is no implicit context to associate with::
+
+  >>> class noimplicitcontext(FakeModule):
+  ...    class B(object):
+  ...      pass
+  >>> from martiantest.fake import noimplicitcontext
+  >>> context.bind(get_default=get_default_context).get(noimplicitcontext.B)
+  Traceback (most recent call last):
+    ...
+  GrokError: No module-level context for <class 'martiantest.fake.noimplicitcontext.B'>, please use the 'context' directive.
+
+There are too many possible contexts::
+
+  >>> class ambiguouscontext(FakeModule):
+  ...   class A(Context):
+  ...     pass
+  ...   class B(Context):
+  ...     pass
+  ...   class C(object):
+  ...     pass
+  >>> from martiantest.fake import ambiguouscontext
+  >>> context.bind(get_default=get_default_context).get(ambiguouscontext.B)
+  Traceback (most recent call last):
+    ... 
+  GrokError: Multiple possible contexts for <class 'martiantest.fake.ambiguouscontext.B'>, please use the 'context' directive.
+
+Let's try this with inheritance, where an implicit context is provided
+by a base class defined in another module::
+
+  >>> class basemodule(FakeModule):
+  ...   class A(Context):
+  ...     pass
+  ...   class B(object):
+  ...     pass
+  >>> from martiantest.fake import basemodule
+  >>> class submodule(FakeModule):
+  ...   class C(basemodule.B):
+  ...     pass
+  >>> from martiantest.fake import submodule
+  >>> context.bind(get_default=get_default_context).get(submodule.C)
+  <class 'martiantest.fake.basemodule.A'>
+
+Let's try it again with an ambiguous context in this case, resolved because
+there is an unambiguous context for the base class ``B``::
+
+  >>> class basemodule2(FakeModule):
+  ...   class A(Context):
+  ...     pass
+  ...   class B(object):
+  ...     pass
+  >>> from martiantest.fake import basemodule2
+  >>> class submodule2(FakeModule):
+  ...   class Ambiguous1(Context):
+  ...     pass
+  ...   class Ambiguous2(Context):
+  ...     pass
+  ...   class C(basemodule2.B):
+  ...     pass
+  >>> from martiantest.fake import submodule2
+  >>> context.bind(get_default=get_default_context).get(submodule2.C)
+  <class 'martiantest.fake.basemodule2.A'>
+
+If the implicit context cannot be found in the base class either, the error
+will show up for the most specific class (``C``)::
+
+  >>> class basemodule3(FakeModule):
+  ...   class B(object):
+  ...     pass
+  >>> from martiantest.fake import basemodule3
+  >>> class submodule3(FakeModule):
+  ...   class C(basemodule3.B):
+  ...     pass
+  >>> from martiantest.fake import submodule3
+  >>> context.bind(get_default=get_default_context).get(submodule3.C)
+  Traceback (most recent call last):
+    ...
+  GrokError: No module-level context for <class 'martiantest.fake.submodule3.C'>, please use the 'context' directive.

Modified: martian/trunk/src/martian/directive.py
===================================================================
--- martian/trunk/src/martian/directive.py	2009-02-24 22:01:12 UTC (rev 97224)
+++ martian/trunk/src/martian/directive.py	2009-02-24 22:32:19 UTC (rev 97225)
@@ -124,6 +124,30 @@
 
 _USE_DEFAULT = object()
 
+class UnknownError(GrokError):
+    pass
+
+def _default(mro, get_default):
+    """Apply default rule to list of classes in mro.
+    """
+    error = None
+    for base in mro:
+        module_of_base = scan.resolve(base.__module__)
+        try:
+            result = get_default(base, module_of_base)
+        except UnknownError, e:
+            # store error if this is the first UnknownError we ran into
+            if error is None:
+                error = e
+            result = UNKNOWN
+        if result is not UNKNOWN:
+            return result
+    # if we haven't found a result, raise the first error we had as
+    # a GrokError
+    if error is not None:
+        raise GrokError(unicode(error), error.component)
+    return UNKNOWN
+
 class ClassScope(object):
     description = 'class'
 
@@ -137,12 +161,7 @@
         # We may be really dealing with an instance instead of a class.
         if not util.isclass(component):
             component = component.__class__
-        for base in inspect.getmro(component):
-            module_of_base = scan.resolve(base.__module__)
-            result = get_default(base, module_of_base)
-            if result is not UNKNOWN:
-                return result
-        return UNKNOWN
+        return _default(inspect.getmro(component), get_default)
 
 CLASS = ClassScope()
 
@@ -174,12 +193,7 @@
             if result is not _USE_DEFAULT:
                 return result
         # look up default rule for this class or its bases
-        for base in mro:
-            module_of_base = scan.resolve(base.__module__)
-            result = get_default(base, module_of_base)
-            if result is not UNKNOWN:
-                return result
-        return UNKNOWN
+        return _default(mro, get_default)
     
 CLASS_OR_MODULE = ClassOrModuleScope()
 

Modified: martian/trunk/src/martian/directive.txt
===================================================================
--- martian/trunk/src/martian/directive.txt	2009-02-24 22:01:12 UTC (rev 97224)
+++ martian/trunk/src/martian/directive.txt	2009-02-24 22:32:19 UTC (rev 97225)
@@ -683,6 +683,102 @@
   ...   inheritmodule3) is martian.UNKNOWN
   True
 
+Raising errors in a computed default
+------------------------------------
+
+Let's define a simple directive with a default rule that always raises
+a ``GrokError`` if the class name doesn't start with an upper case
+letter::
+
+  >>> class name(Directive):
+  ...     scope = CLASS
+  ...     store = ONCE
+
+  >>> from martian.error import GrokError
+  >>> def name_get_default(component, module, **data):
+  ...     if component.__name__[0].isupper():
+  ...          return component.__name__
+  ...     raise GrokError("Component %r has a name that doesn't start with upper case letter." % component, component)
+
+Let's test it::
+
+  >>> class A(object):
+  ...   pass
+  >>> class b(object):
+  ...   pass
+  >>> name.bind(get_default=name_get_default).get(A)
+  'A'
+  >>> name.bind(get_default=name_get_default).get(b)
+  Traceback (most recent call last):
+    ...
+  GrokError: Component <class 'b'> has a name that doesn't start with upper case letter.
+
+Instead of raising ``GrokError`` we can also raise ``UnknownError`` in
+a computed default. This has the same meaning as returning
+``UNKNOWN``, except that the error information is recorded and the
+default rule is tried again on the base class in the mro chain. If the
+default rule has the error raised or ``UNKNOWN`` value returned in
+each step of the chain, the first ``UnknownError`` that was raised is
+converted into a ``GrokError``.
+
+This makes it possible for the default logic to raise specific errors
+for the most specific class if the implicit rule failed to apply on
+the class and any of its bases. This is used for instance in the
+implementation of the ``get_default`` rule for ``grok.context``.
+
+Let's test this behavior with a rule that raises an ``UnknownError``
+if there is no ``foo`` attribute on the class::
+
+  >>> from martian import UnknownError
+  >>> def foo_get_default(component, module, **data):
+  ...   if not component.__dict__.has_key('foo'):
+  ...      raise UnknownError("Foo cannot be found for class %s" % component.__name__, component)
+  ...   return "Found for class %s" % component.__name__
+
+Let's try it on a simple class first::
+
+  >>> class Test:
+  ...   pass
+  >>> name.bind(get_default=foo_get_default).get(Test)
+  Traceback (most recent call last):
+    ...
+  GrokError: Foo cannot be found for class Test
+
+Let's try it on a new style class::
+
+  >>> class Test(object):
+  ...   pass
+  >>> name.bind(get_default=foo_get_default).get(Test)
+  Traceback (most recent call last):
+    ...
+  GrokError: Foo cannot be found for class Test
+
+Let's try it on a class where there is some inheritance::
+
+  >>> class Test1(object):
+  ...    pass
+  >>> class Test2(Test1):
+  ...    pass
+  >>> name.bind(get_default=foo_get_default).get(Test2)
+  Traceback (most recent call last):
+    ...
+  GrokError: Foo cannot be found for class Test2
+
+As you can see the error message will apply to the most specific
+class, ``Test2``, even though of course the error will also occur for
+the base class, ``Test1``.
+
+Let's now demonstrate an inheritance scenario where the error does
+not occur, because the get_default rule will succeed at some point
+during the inheritance chain::
+
+  >>> class Test1(object):
+  ...    foo = 1
+  >>> class Test2(Test1):
+  ...    pass
+  >>> name.bind(get_default=foo_get_default).get(Test2)
+  'Found for class Test1'
+
 Computed defaults for instances
 -------------------------------
 

Modified: martian/trunk/src/martian/tests/test_all.py
===================================================================
--- martian/trunk/src/martian/tests/test_all.py	2009-02-24 22:01:12 UTC (rev 97224)
+++ martian/trunk/src/martian/tests/test_all.py	2009-02-24 22:32:19 UTC (rev 97225)
@@ -34,5 +34,9 @@
         doctest.DocFileSuite('public_methods_from_class.txt',
                              package='martian.tests',
                              optionflags=optionflags),
+        doctest.DocFileSuite('context.txt',
+                             package='martian',
+                             globs=globs,
+                             optionflags=optionflags),
         ])
     return suite



More information about the Checkins mailing list