[Checkins] SVN: martian/trunk/ * inheritance in combination with get_default

Martijn Faassen faassen at infrae.com
Tue Jan 27 14:44:06 EST 2009


Log message for revision 95258:
  * inheritance in combination with get_default
  
  * inheritance and old-style classes
  
  * directive storage on interfaces
  

Changed:
  U   martian/trunk/CHANGES.txt
  U   martian/trunk/src/martian/__init__.py
  U   martian/trunk/src/martian/directive.py
  U   martian/trunk/src/martian/directive.txt

-=-
Modified: martian/trunk/CHANGES.txt
===================================================================
--- martian/trunk/CHANGES.txt	2009-01-27 19:37:34 UTC (rev 95257)
+++ martian/trunk/CHANGES.txt	2009-01-27 19:44:06 UTC (rev 95258)
@@ -7,6 +7,9 @@
 Feature changes
 ---------------
 
+* Changes to better support various inheritance scenarios in combination with
+  directives. Details follow.
+
 * ``CLASS_OR_MODULE`` scope directives will be aware of inheritance of
   values that are defined in module-scope. Consider the following case::
 
@@ -23,6 +26,38 @@
   As before, ``Foo`` will have the value ``A`` configured for it. ``Bar``,
   since it inherits from ``Foo``, will inherit this value.
 
+* ``CLASS_OR_MODULE`` and ``CLASS`` scope directives will be aware of 
+  inheritance of computed default values. Consider the following case::
+
+    module a:
+      class Foo(object):
+         pass
+  
+    module b:
+      import a
+      class Bar(a.Foo):
+         pass
+
+    def get_default(component, module, **data):
+        if module.__name__ == 'a':
+           return "we have a default value for module a"
+        return martian.UNKNOWN
+
+  When we now do this::
+
+    some_directive.bind(get_default=get_default).get(b.Bar)
+
+  We will get the value "we have a default value for module a". This
+  is because when trying to compute the default value for ``Bar`` we
+  returned ``martian.UNKNOWN`` to indicate the value couldn't be found
+  yet. The system then looks at the base class and tries again, and in
+  this case it succeeds (as the module-name is ``a``).
+
+* ``martian.ONCE_IFACE`` storage option to allow the creation of
+  directives that store their value on ``zope.interface``
+  interfaces. This was originally in ``grokcore.view`` but was of
+  wider usefulness.
+
 Bugs fixed
 ----------
 

Modified: martian/trunk/src/martian/__init__.py
===================================================================
--- martian/trunk/src/martian/__init__.py	2009-01-27 19:37:34 UTC (rev 95257)
+++ martian/trunk/src/martian/__init__.py	2009-01-27 19:44:06 UTC (rev 95258)
@@ -5,8 +5,10 @@
 from martian.components import MethodGrokker
 from martian.util import scan_for_classes
 from martian.directive import Directive, MarkerDirective, MultipleTimesDirective
-from martian.directive import ONCE, ONCE_NOBASE, MULTIPLE, MULTIPLE_NOBASE, DICT
+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 (
     validateText, validateInterface, validateClass, validateInterfaceOrClass)
 from martian.martiandirective import component, directive, priority, baseclass

Modified: martian/trunk/src/martian/directive.py
===================================================================
--- martian/trunk/src/martian/directive.py	2009-01-27 19:37:34 UTC (rev 95257)
+++ martian/trunk/src/martian/directive.py	2009-01-27 19:44:06 UTC (rev 95258)
@@ -2,11 +2,14 @@
 import inspect
 
 from zope.interface.interfaces import IInterface
+from zope.interface.interface import TAGGED_DATA
 
 from martian import util
 from martian.error import GrokImportError, GrokError
 from martian import scan
 
+UNKNOWN = object()
+
 class StoreOnce(object):
 
     def set(self, locals_, directive, value):
@@ -65,7 +68,7 @@
 MULTIPLE_NOBASE = StoreMultipleTimesNoBase()
 
 class StoreDict(StoreOnce):
-
+    
     def get(self, directive, component, default):
         if getattr(component, directive.dotted_name(), default) is default:
             return default
@@ -92,6 +95,33 @@
 
 DICT = StoreDict()
 
+class TaggedValueStoreOnce(StoreOnce):
+    """Stores the directive value in a interface tagged value.
+    """
+
+    def get(self, directive, component, default):
+        return component.queryTaggedValue(directive.dotted_name(), default)
+
+    def set(self, locals_, directive, value):
+        if directive.dotted_name() in locals_:
+            raise GrokImportError(
+                "The '%s' directive can only be called once per %s." %
+                (directive.name, directive.scope.description))
+        # Make use of the implementation details of interface tagged
+        # values.  Instead of being able to call "setTaggedValue()"
+        # on an interface object, we only have access to the "locals"
+        # of the interface object.  We inject whatever setTaggedValue()
+        # would've injected.
+        taggeddata = locals_.setdefault(TAGGED_DATA, {})
+        taggeddata[directive.dotted_name()] = value
+
+    def setattr(self, context, directive, value):
+        context.setTaggedValue(directive.dotted_name(), value)
+
+# for now, use scope = martian.CLASS to create directives that can
+# work on interfaces (or martian.CLASS_OR_MODULE)
+ONCE_IFACE = TaggedValueStoreOnce()
+
 _USE_DEFAULT = object()
 
 class ClassScope(object):
@@ -100,9 +130,20 @@
     def check(self, frame):
         return util.frame_is_class(frame) and not is_fake_module(frame)
 
-    def get(self, directive, component, module, default):
-        return directive.store.get(directive, component, default)
-    
+    def get(self, directive, component, module, get_default):
+        result = directive.store.get(directive, component, _USE_DEFAULT)
+        if result is not _USE_DEFAULT:
+            return result
+        # 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
+
 CLASS = ClassScope()
 
 class ClassOrModuleScope(object):
@@ -111,18 +152,30 @@
     def check(self, frame):
         return util.frame_is_class(frame) or util.frame_is_module(frame)
 
-    def get(self, directive, component, module, default):
-        value = directive.store.get(directive, component, default)
-        if value is default:
-            value = directive.store.get(directive, module, default)
-        if value is default:
-            mro = component.__mro__
-            if len(mro) > 1:
-                base = mro[1]
-                module_of_base = scan.resolve(base.__module__)
-                value = self.get(directive, base, module_of_base, default)
-        return value
-
+    def get(self, directive, component, module, get_default):
+        # look up class-level directive on this class or its bases
+        # we don't need to loop through the __mro__ here as Python will
+        # do it for us
+        result = directive.store.get(directive, component, _USE_DEFAULT)
+        if result is not _USE_DEFAULT:
+            return result
+        # now we need to loop through the mro, potentially twice
+        mro = inspect.getmro(component)
+        # look up module-level directive for this class or its bases
+        for base in mro:
+            module_of_base = scan.resolve(base.__module__)
+            result = directive.store.get(directive, module_of_base,
+                                         _USE_DEFAULT)
+            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
+    
 CLASS_OR_MODULE = ClassOrModuleScope()
 
 class ModuleScope(object):
@@ -131,8 +184,11 @@
     def check(self, frame):
         return util.frame_is_module(frame) or is_fake_module(frame)
 
-    def get(self, directive, component, module, default):
-        return directive.store.get(directive, module, default)
+    def get(self, directive, component, module, get_default):
+        result = directive.store.get(directive, module, _USE_DEFAULT)
+        if result is not _USE_DEFAULT:
+            return result
+        return get_default(component, module)
 
 MODULE = ModuleScope()
 
@@ -207,11 +263,10 @@
 
     def get(self, component=None, module=None, **data):
         directive = self.directive
-        value = directive.scope.get(directive, component, module,
-                                    default=_USE_DEFAULT)
-        if value is _USE_DEFAULT:
-            value = self.get_default(component, module, **data)
-        return value
+        def get_default(component, module):
+            return self.get_default(component, module, **data)
+        return directive.scope.get(directive, component, module,
+                                   get_default=get_default)
 
 class MultipleTimesDirective(Directive):
     store = MULTIPLE

Modified: martian/trunk/src/martian/directive.txt
===================================================================
--- martian/trunk/src/martian/directive.txt	2009-01-27 19:37:34 UTC (rev 95257)
+++ martian/trunk/src/martian/directive.txt	2009-01-27 19:44:06 UTC (rev 95258)
@@ -241,6 +241,20 @@
   >>> layer.bind().get(inheritmodule5.OverrideFoo, inheritmodule5)
   'AnotherTest'
 
+Inheritance with module scope also works for old-style classes::
+
+  >>> class oldstyle1(FakeModule):
+  ...    layer('one')
+  ...    class Base:
+  ...       pass
+  >>> from martiantest.fake import oldstyle1
+  >>> class oldstyle2(FakeModule):
+  ...    class Sub(oldstyle1.Base):
+  ...       pass
+  >>> from martiantest.fake import oldstyle2
+  >>> layer.bind().get(oldstyle2.Sub)
+  'one'
+
 Using a directive multiple times
 --------------------------------
 
@@ -410,12 +424,42 @@
   >>> print sorted(d.items())
   [(1, 'One'), (2, 'Two')]
 
-Calculated defaults
--------------------
+Directives on an interface
+--------------------------
 
+When you use ``zope.interface.Interface`` to define a new interface using
+the class statement, in fact a special interface instance is created, not a 
+class. To let the directive store a value on an interface, we need to
+use a special storage (``martian.ONCE_IFACE``)::
+
+  >>> from martian import ONCE_IFACE
+  >>> class skin(Directive):
+  ...    scope = CLASS
+  ...    store = ONCE_IFACE
+  
+Note that we still indicate the ``CLASS`` scope for this kind of
+directive. At some point we may introduce a special scope for
+directives on interfaces.
+
+Let's try the directive. We shouldn't get an error::
+
+  >>> class once_iface(FakeModule):
+  ...   from zope.interface import Interface
+  ...   class TestIface(Interface):
+  ...     skin('Foo')
+  >>> from martiantest.fake import once_iface
+
+We can now retrieve the value::
+
+  >>> skin.bind().get(once_iface.TestIface)
+  'Foo'
+
+Computed defaults
+-----------------
+
 Often instead of just supplying the system with a default, we want to
-calculate the default in some way. We define the ``name`` directive,
-which if not present, will calculate its value from the name of class,
+compute the default in some way. We define the ``name`` directive,
+which if not present, will compute its value from the name of class,
 lower-cased. Instead of passing a default value, we pass a function as the
 default argument::
 
@@ -434,18 +478,89 @@
   >>> bound_name.get(Foo)
   'bar'
 
-  >>> class Foo(object):
+  >>> class Foo2(object):
   ...   pass
-  >>> bound_name.get(Foo)
+  >>> bound_name.get(Foo2)
+  'foo2'
+
+Let's test this with a deeper inheritance hierarchy. Explicit
+directives should always trump computed defaults::
+
+  >>> class Subclass(Foo):
+  ...   pass
+  >>> bound_name.get(Subclass)
+  'bar'
+
+Now let's look at a hierarchy in which the explicit rule should
+apply::
+
+  >>> def default_name_lowercase2(component, module, **data):
+  ...     return component.__name__.lower()
+  ...
+  >>> bound_name2 = name.bind(get_default=default_name_lowercase2)
+  >>> class Alpha(object):
+  ...    pass
+  >>> class Beta(Alpha):
+  ...    pass
+  >>> bound_name2.get(Alpha)
+  'alpha'
+  >>> bound_name2.get(Beta)
+  'beta'
+
+We will now define a default rule that only triggers for particular
+components in the inheritance chain, but returns ``UNKNOWN``
+otherwise. This can be useful if the default rule is dependent on
+outside information. In Grok for instance, the default rule for
+``grok.context`` will look for a class that implements ``IContext`` in
+the same module, and ``grok.templatedir`` will look for a directory
+with templates with a name based on the name of the module with
+``_templates`` appended.
+
+This rule returns a value only if the module name includes the number
+``1``, and will return ``UNKNOWN`` otherwise::
+
+  >>> import martian
+  >>> def default_name_lowercase3(component, module, **data):
+  ...     if '1' in module.__name__:
+  ...         return component.__name__.lower()
+  ...     return martian.UNKNOWN
+
+  >>> bound_name3 = name.bind(get_default=default_name_lowercase3)
+
+This won't trigger for this module, as it doesn't have the character
+``1`` in it::
+
+  >>> class testmodule(FakeModule):
+  ...    class Foo(object):
+  ...       pass
+  >>> from martiantest.fake import testmodule
+  >>> bound_name3.get(testmodule.Foo, testmodule) is martian.UNKNOWN
+  True
+
+Now we define a module which does have ``1`` in it, so the rule should 
+be triggered::
+
+  >>> class testmodule1(FakeModule):
+  ...    class Foo(object):
+  ...       pass
+  >>> from martiantest.fake import testmodule1
+  >>> bound_name3.get(testmodule1.Foo, testmodule1)
   'foo'
 
-Calculated defaults in combination with inheritance
----------------------------------------------------
+This also works with inheritance::
 
-Calculated defaults should not kick in when we can actually find a
-value due to inheritance of a module-level directive. Let's set up a
-situation that has this inheritance::
+  >>> class testmodule2(FakeModule):
+  ...   class Bar(testmodule1.Foo):
+  ...      pass
+  >>> from martiantest.fake import testmodule2
+  >>> bound_name3.get(testmodule2.Bar, testmodule2)
+  'foo'
 
+Module-level explicit directives always trump computed defaults as
+well.  The ``layer`` directive is ``CLASS_OR_MODULE`` scope. Let's set
+up a hierarchy of modules and classes using ``layer`` to demonstrate
+this::
+
   >>> class inheritmodule1(FakeModule):
   ...   layer('Test')
   ...   class Foo(object):
@@ -456,19 +571,22 @@
   ...      pass
   >>> from martiantest.fake import inheritmodule2
 
-We define a way to compute the default::
+We define a way to compute the default in which we compute a string
+based on the module name and the class name, so we can later check
+whether the right module and class was passed to compute the default::
 
-  >>> def our_get_default(component, module, **data):
-  ...   return "the computed default"
+  >>> def immediate_get_default(component, module, **data):
+  ...   return "computed: %s %s" % (module.__name__, component.__name__)
 
-We don't expect the default rule to kick in as we can find an explicitly
-set value::
+We don't expect the default rule to kick in as we can find an
+explicitly set value::
 
-  >>> layer.bind(get_default=our_get_default).get(inheritmodule2.Bar, inheritmodule2)
+  >>> layer.bind(get_default=immediate_get_default).get(inheritmodule2.Bar, 
+  ...   inheritmodule2)
   'Test'
 
-Let's now consider a case where we have inheritance without explicit use
-of the directive::
+Let's now consider a case where we have inheritance without explicit
+use of the ``layer`` directive::
 
   >>> class inheritmodule1(FakeModule):
   ...   class Foo(object):
@@ -479,9 +597,121 @@
   ...      pass
   >>> from martiantest.fake import inheritmodule2
 
-  >>> layer.bind(get_default=our_get_default).get(inheritmodule2.Bar, inheritmodule2)
-  'the computed default'
+We expect to receive the computed default for ``Bar``, as
+``immediate_get_default`` immediately returns a result for any
+component::
 
+  >>> layer.bind(get_default=immediate_get_default).get(inheritmodule2.Bar,
+  ...   inheritmodule2)
+  'computed: martiantest.fake.inheritmodule2 Bar'
+
+Let's try the default rule that triggers upon seeing ``1`` in the
+module name again, this time for the ``CLASS_OR_MODULE`` scope
+directive ``layer``::
+
+  >>> def picky_get_default(component, module, **data):
+  ...     if '1' in module.__name__:
+  ...         return "computed: %s %s" % (module.__name__, component.__name__)
+  ...     return martian.UNKNOWN
+
+Since only the ``Foo`` class is in a module with the character ``1``
+in it (``inheritmodule1``), we will get the result for ``Foo`` (and
+its module)::
+
+  >>> layer.bind(get_default=picky_get_default).get(inheritmodule2.Bar,
+  ...   inheritmodule2)
+  'computed: martiantest.fake.inheritmodule1 Foo'
+
+We will get the same result if we ask ``Foo`` directly::
+
+  >>> layer.bind(get_default=picky_get_default).get(inheritmodule1.Foo,
+  ...   inheritmodule1)
+  'computed: martiantest.fake.inheritmodule1 Foo'
+
+If we have a hierarchy that never has a module with the character
+``1`` in it, we will receive ``UNKNOWN`` (and the grokkker that uses
+this directive should raise an error)::
+
+  >>> class inheritmodule3(FakeModule):
+  ...   class Foo(object):
+  ...      pass
+  >>> from martiantest.fake import inheritmodule3
+  >>> layer.bind(get_default=picky_get_default).get(inheritmodule3.Foo,
+  ...   inheritmodule3) is martian.UNKNOWN
+  True
+
+Computed defaults for instances
+-------------------------------
+
+In some cases directives are used to retrieve values from instances instead of
+from their classes::
+
+  >>> class name(Directive):
+  ...     scope = CLASS
+  ...     store = ONCE
+  >>> class instancemodule(FakeModule):
+  ...   class Hoi(object):
+  ...     name('Test')
+  ...   class NoDirectiveOnThis(object):
+  ...     pass
+  ...   hoi = Hoi()
+  ...   no_directive_on_this = NoDirectiveOnThis()
+  >>> from martiantest.fake import instancemodule
+
+Let's try to use the directive::
+
+  >>> name.bind().get(instancemodule.hoi, instancemodule)
+  'Test'
+
+If no directive was used on the class, we will get ``None``, the
+default default value::
+
+  >>> (name.bind().get(instancemodule.no_directive_on_this, instancemodule) is 
+  ...  None)
+  True
+
+Let's try it with a computed value now::
+
+  >>> def get_default(component, module, **data):
+  ...   return "The default"
+
+  >>> name.bind(get_default=get_default).get(instancemodule.no_directive_on_this)
+  'The default'
+
+Computed default for old-style classes
+--------------------------------------
+
+We should also test old-style classes with ``CLASS`` scope directives in
+combination with computed defaults::
+
+  >>> class layer2(Directive):
+  ...     scope = CLASS
+  ...     store = ONCE
+
+  >>> class oldstyle1(FakeModule):
+  ...    class Base:
+  ...       pass
+  >>> from martiantest.fake import oldstyle1
+  >>> class oldstyle2(FakeModule):
+  ...    class Sub(oldstyle1.Base):
+  ...       pass
+  >>> from martiantest.fake import oldstyle2
+
+  >>> def get_default(component, module, **data):
+  ...   if '1' in module.__name__:
+  ...     return 'we found it'
+  ...   return martian.UNKNOWN    
+  >>> layer2.bind(get_default=get_default).get(oldstyle2.Sub)
+  'we found it'
+
+And let's try it with a ``CLASS_OR_MODULE`` scope directive too::
+
+  >>> class layer3(Directive):
+  ...     scope = CLASS_OR_MODULE
+  ...     store = ONCE
+  >>> layer3.bind(get_default=get_default).get(oldstyle2.Sub)
+  'we found it'
+
 A marker directive
 ------------------
 



More information about the Checkins mailing list