[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