[Checkins] SVN: martian/branches/jw-philipp-using-ndir-directives/src/martian/ndir. Redesigned new-style directives to cope with multiple arguments,

Jan-Wijbrand Kolman janwijbrand at gmail.com
Fri May 2 15:36:19 EDT 2008


Log message for revision 86124:
  Redesigned new-style directives to cope with multiple arguments,
  storing custom values, and the baseclass() directive.
  
  Optional argument support has been removed (although it can easily
  be restored by overriding the 'factory' and 'validate' methods).

Changed:
  U   martian/branches/jw-philipp-using-ndir-directives/src/martian/ndir.py
  U   martian/branches/jw-philipp-using-ndir-directives/src/martian/ndir.txt

-=-
Modified: martian/branches/jw-philipp-using-ndir-directives/src/martian/ndir.py
===================================================================
--- martian/branches/jw-philipp-using-ndir-directives/src/martian/ndir.py	2008-05-02 19:15:44 UTC (rev 86123)
+++ martian/branches/jw-philipp-using-ndir-directives/src/martian/ndir.py	2008-05-02 19:36:19 UTC (rev 86124)
@@ -1,25 +1,53 @@
 import sys
+import inspect
 
 from zope.interface.interfaces import IInterface
 
 from martian import util
 from martian.error import GrokImportError
 
-# ONCE or MULTIPLE
-ONCE = object()
-MULTIPLE = object()
+class StoreOnce(object):
 
-# arguments
-SINGLE_ARG = object()
-NO_ARG = object()
-OPTIONAL_ARG = object()
+    def set(self, frame, directive, value):
+        dotted_name = (directive.__class__.__module__ + '.' +
+                       directive.__class__.__name__)
+        if dotted_name in frame.f_locals:
+            raise GrokImportError("The '%s' directive can only be called once per %s." %
+                                  (directive.name, directive.scope.description))
+        frame.f_locals[dotted_name] = value
 
+    def get(self, directive, component, default):
+        dotted_name = (directive.__class__.__module__ + '.' +
+                       directive.__class__.__name__)
+        return getattr(component, dotted_name, default)
+
+ONCE = StoreOnce()
+
+class StoreOnceGetFromThisClassOnly(StoreOnce):
+
+    def get(self, directive, component, default):
+        dotted_name = (directive.__class__.__module__ + '.' +
+                       directive.__class__.__name__)
+        return component.__dict__.get(dotted_name, default)
+
+class StoreMultipleTimes(StoreOnce):
+
+    def set(self, frame, directive, value):
+        dotted_name = (directive.__class__.__module__ + '.' +
+                       directive.__class__.__name__)
+        values = frame.f_locals.get(dotted_name, [])
+        values.append(value)
+        frame.f_locals[dotted_name] = values
+
+MULTIPLE = StoreMultipleTimes()
+
+
 _SENTINEL = object()
 _USE_DEFAULT = object()
 
 class ClassScope(object):
     description = 'class'
-    
+
     def check(self, frame):
         return (util.frame_is_class(frame) and
                 not is_fake_module(frame))
@@ -36,85 +64,89 @@
 CLASS_OR_MODULE = ClassOrModuleScope()
 
 class Directive(object):
-    def __init__(self, namespace, name, scope, times, default,
-                 validate=None, arg=SINGLE_ARG):
-        self.namespace = namespace
-        self.name = name
-        self.scope = scope
-        self.times = times
-        self.default = default
-        self.validate = validate
-        self.arg = arg
 
-    def __call__(self, value=_SENTINEL):            
-        name = self.namespaced_name()
+    default = None
 
-        if self.arg is NO_ARG:
-            if value is _SENTINEL:
-                value = True
-            else:
-                raise GrokImportError("%s accepts no arguments." % name)
-        elif self.arg is SINGLE_ARG:
-            if value is _SENTINEL:
-                raise GrokImportError("%s requires a single argument." % name)
-        elif self.arg is OPTIONAL_ARG:
-            if value is _SENTINEL:
-                value = _USE_DEFAULT
+    def __init__(self, *args, **kw):
+        self.name = self.__class__.__name__
 
-        if self.validate is not None:
-            self.validate(name, value)
-
         frame = sys._getframe(1)
         if not self.scope.check(frame):
-            raise GrokImportError("%s can only be used on %s level." %
-                                  (name, self.scope.description))
-        if self.times is ONCE:
-            if name in frame.f_locals:
-                raise GrokImportError("%s can only be called once per %s." %
-                                      (name, self.scope.description))
-            frame.f_locals[name] = value
-        elif self.times is MULTIPLE:
-            values = frame.f_locals.get(name, [])
-            values.append(value)
-            frame.f_locals[name] = values
-        else:
-            assert False, "Unknown value for times: %" % self.times
+            raise GrokImportError("The '%s' directive can only be used on "
+                                  "%s level." %
+                                  (self.name, self.scope.description))
 
-    def get(self, component, module=None):
-        name = self.namespaced_name()
-        value = getattr(component, name, _USE_DEFAULT)
-        if value is _USE_DEFAULT and module is not None:
-            value = getattr(module, name, _USE_DEFAULT)
-        if value is _USE_DEFAULT:
-            return self.get_default(component)
+        self.check_factory_signature(*args, **kw)
+        validate = getattr(self, 'validate', None)
+        if validate is not None:
+            validate(*args, **kw)
+        value = self.factory(*args, **kw)
+
+        self.store.set(frame, self, value)
+
+    # To get a correct error message, we construct a function that has
+    # the same signature as check_arguments(), but without "self".
+    def check_factory_signature(self, *arguments, **kw):
+        args, varargs, varkw, defaults = inspect.getargspec(
+            self.factory)
+        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 factory(self, value):
         return value
 
     def get_default(self, component):
-        if callable(self.default):
-            return self.default(component)
         return self.default
 
-    def namespaced_name(self):
-        return self.namespace + '.' + self.name
+    @classmethod
+    def get(cls, component, module=None):
+        # Create an instance of the directive without calling __init__
+        self = cls.__new__(cls)
 
-def validateText(name, value):
+        value = self.store.get(self, component, _USE_DEFAULT)
+        if value is _USE_DEFAULT and module is not None:
+            value = self.store.get(self, module, _USE_DEFAULT)
+        if value is _USE_DEFAULT:
+            value = self.get_default(component)
+        return value
+
+
+class MarkerDirective(Directive):
+    store = ONCE
+    default = False
+
+    def factory(self):
+        return True
+
+
+class baseclass(MarkerDirective):
+    scope = CLASS
+    store = StoreOnceGetFromThisClassOnly()
+
+
+def validateText(directive, value):
     if util.not_unicode_or_ascii(value):
-        raise GrokImportError("%s can only be called with unicode or ASCII." %
-                              name)
+        raise GrokImportError("The '%s' directive can only be called with "
+                              "unicode or ASCII." % directive.name)
 
-def validateInterfaceOrClass(name, value):
+def validateInterfaceOrClass(directive, value):
     if not (IInterface.providedBy(value) or util.isclass(value)):
-        raise GrokImportError("%s can only be called with a class or "
-                              "interface." %
-                              name)
+        raise GrokImportError("The '%s' directive can only be called with "
+                              "a class or an interface." % directive.name)
 
 
-def validateInterface(name, value):
+def validateInterface(directive, value):
     if not (IInterface.providedBy(value)):
-        raise GrokImportError("%s can only be called with an interface." %
-                              name)
+        raise GrokImportError("The '%s' directive can only be called with "
+                              "an interface." % directive.name)
 
-    
+
 # this here only for testing purposes, which is a bit unfortunate
 # but makes the tests a lot clearer for module-level directives
 # also unfortunate that fake_module needs to be defined directly

Modified: martian/branches/jw-philipp-using-ndir-directives/src/martian/ndir.txt
===================================================================
--- martian/branches/jw-philipp-using-ndir-directives/src/martian/ndir.txt	2008-05-02 19:15:44 UTC (rev 86123)
+++ martian/branches/jw-philipp-using-ndir-directives/src/martian/ndir.txt	2008-05-02 19:36:19 UTC (rev 86124)
@@ -13,14 +13,11 @@
 We define a simple directive that sets a description::
 
   >>> from martian.ndir import Directive, CLASS, ONCE
-  >>> description = Directive('martian', 'description', 
-  ...   CLASS, ONCE, u'')
+  >>> class description(Directive):
+  ...     scope = CLASS
+  ...     store = ONCE
+  ...     default = u''
 
-This directive is placed in a namespace (in this case, ``martian``);
-this is just a string and should be used to avoid conflicts between
-directives with the same name that could be defined by different
-packages.
-
 The name of the directive is ``description``. We specify that the
 directive can only be used in the scope of a class. We also specify it
 can only be used a single time. Finally we define the default in case
@@ -40,12 +37,12 @@
 Directives in different namespaces get stored differently. We'll
 define a similar directive in another namespace::
 
-  >>> description2 = Directive('different', 'description', 
-  ...   CLASS, ONCE, u'')
+  >>> class description2(description):
+  ...     pass
 
   >>> class Foo(object):
-  ...    description(u"Description1")
-  ...    description2(u"Description2")
+  ...     description(u"Description1")
+  ...     description2(u"Description2")
   >>> description.get(Foo)
   u'Description1'
   >>> description2.get(Foo)
@@ -55,17 +52,29 @@
 default value for that directive, this case the empty unicode string::
 
   >>> class Foo(object):
-  ...   pass
+  ...     pass
   >>> description.get(Foo)
   u''
 
+Subclasses of the original class will inherit the properties set by the
+directive:
+
+  >>> class Foo(object):
+  ...     description('This is a foo.')
+  ...
+  >>> class Bar(Foo):
+  ...     pass
+  ...
+  >>> description.get(Bar)
+  'This is a foo.'
+
 When we use the directive outside of class scope, we get an error
 message::
 
   >>> description('Description')
   Traceback (most recent call last):
     ...
-  GrokImportError: martian.description can only be used on class level.
+  GrokImportError: The 'description' directive can only be used on class level.
 
 In particular, we cannot use it in a module::
 
@@ -74,7 +83,7 @@
   ...   description("Description")
   Traceback (most recent call last):
     ...
-  GrokImportError: martian.description can only be used on class level.
+  GrokImportError: The 'description' directive can only be used on class level.
 
 We cannot use the directive twice in the class scope. If we do so, we
 get an error message as well::
@@ -84,7 +93,7 @@
   ...   description(u"Description2")
   Traceback (most recent call last):
     ...
-  GrokImportError: martian.description can only be called once per class.
+  GrokImportError: The 'description' directive can only be called once per class.
 
 We cannot call the directive with no argument either::
 
@@ -92,7 +101,7 @@
   ...   description()
   Traceback (most recent call last):
     ...
-  GrokImportError: martian.description requires a single argument.
+  TypeError: description takes exactly 1 argument (0 given)
 
 Class and module scope
 ----------------------
@@ -101,10 +110,15 @@
 scope both::
 
   >>> from martian.ndir import CLASS_OR_MODULE
-  >>> layer = Directive('martian', 'layer', CLASS_OR_MODULE, ONCE, None)
+  >>> class layer(Directive):
+  ...     scope = CLASS_OR_MODULE
+  ...     store = ONCE
 
-We can use it on a class::
+By default, the ``default`` property is None which is why we can omit
+specifying it here.
 
+We can use this directive now on a class::
+
   >>> class Foo(object):
   ...   layer('Test')
   >>> layer.get(Foo)
@@ -152,7 +166,9 @@
 in the same scope::
 
   >>> from martian.ndir import MULTIPLE
-  >>> multi = Directive('martian', 'multi', CLASS, MULTIPLE, None)
+  >>> class multi(Directive):
+  ...     scope = CLASS
+  ...     store = MULTIPLE
 
 We can now use the directive multiple times without any errors::
 
@@ -174,10 +190,11 @@
 lower-cased. Instead of passing a default value, we pass a function as the
 default argument::
 
-  >>> def default(component):
-  ...     return component.__name__.lower()
-  >>> name = Directive('martian', 'name', 
-  ...   CLASS, ONCE, default)
+  >>> class name(Directive):
+  ...     scope = CLASS
+  ...     store = ONCE
+  ...     def get_default(self, component):
+  ...         return component.__name__.lower()
 
   >>> class Foo(object):
   ...   name('bar')
@@ -195,15 +212,15 @@
 Another type of directive is a marker directive. This directive takes
 no arguments at all, but when used it marks the context::
 
-  >>> from martian.ndir import NO_ARG
-  >>> mark = Directive('martian', 'mark', CLASS, ONCE, False, 
-  ...                  arg=NO_ARG)
-  
+  >>> from martian.ndir import MarkerDirective
+  >>> class mark(MarkerDirective):
+  ...     scope = CLASS
+
   >>> class Foo(object):
   ...     mark()
 
 Class ``Foo`` is now marked::
-  
+
   >>> mark.get(Foo)
   True
 
@@ -220,68 +237,16 @@
   ...   mark("An argument")
   Traceback (most recent call last):
     ...
-  GrokImportError: martian.mark accepts no arguments.
+  TypeError: mark takes no arguments (1 given)
 
-The marker directive also works with calculated defaults::
 
-  >>> def func(component):
-  ...    return component.__name__
-  >>> mark = Directive('martian', 'mark', CLASS, ONCE, func, arg=NO_ARG)
-  >>> class Foo(object):
-  ...    pass
-  >>> mark.get(Foo)
-  'Foo'
-
-Optional arguments
-------------------
-
-We can also construct a directive that receives an optional argument::
-
-  >>> from martian.ndir import OPTIONAL_ARG
-  >>> optional = Directive('martian', 'optional', CLASS, ONCE, 'default',
-  ...                       arg=OPTIONAL_ARG)
-
-We can give it an argument::
-
-  >>> class Foo(object):
-  ...    optional("Hoi")
-  >>> optional.get(Foo)
-  'Hoi'
-
-We can also give it no argument. The default will then be used::
-
-  >>> class Foo(object):
-  ...    optional()
-  >>> optional.get(Foo)
-  'default'
-
-The default will also be used if the directive isn't used at all::
-
-  >>> class Foo(object):
-  ...   pass
-  >>> optional.get(Foo)
-  'default'
-
-The optional value directive also works with calculated defaults::
-
-  >>> optional = Directive('martian', 'optional', CLASS, ONCE, func,
-  ...                       arg=OPTIONAL_ARG)
-  >>> class Foo(object):
-  ...   optional()
-  >>> optional.get(Foo)
-  'Foo'
-  >>> class Foo(object):
-  ...   pass
-  >>> optional.get(Foo)
-  'Foo'
-
 Validation
 ----------
 
-A directive can be supplied with a validation function.  validation
-function checks whether the value passed in is allowed. The function
-should raise ``GrokImportError`` if the value cannot be validated,
-together with a description of why not. 
+A directive can be supplied with a validation method. The validation method
+checks whether the value passed in is allowed. It should raise
+``GrokImportError`` if the value cannot be validated, together with a
+description of why not.
 
 First we define our own validation function. A validation function
 takes two arguments:
@@ -292,17 +257,17 @@
 
 The name can be used to format the exception properly.
 
-We'll define a validation function that only expects integer numbers::
+We'll define a validation method that only expects integer numbers::
 
   >>> from martian.error import GrokImportError
-  >>> def validateInt(name, value):
-  ...    if type(value) is not int:
-  ...        raise GrokImportError("%s can only be called with an integer." % 
-  ...                              name)
+  >>> class number(Directive):
+  ...     scope = CLASS
+  ...     store = ONCE
+  ...     def validate(self, value):
+  ...         if type(value) is not int:
+  ...             raise GrokImportError("The '%s' directive can only be called with an integer." %
+  ...                                   self.name)
 
-We use it with a directive::
-
-  >>> number = Directive('martian', 'number', CLASS, ONCE, None, validateInt)
   >>> class Foo(object):
   ...    number(3)
 
@@ -310,7 +275,7 @@
   ...    number("This shouldn't work")
   Traceback (most recent call last):
     ...
-  GrokImportError: martian.number can only be called with an integer.
+  GrokImportError: The 'number' directive can only be called with an integer.
 
 Some built-in validation functions
 ----------------------------------
@@ -321,7 +286,11 @@
 is unicode or plain ascii::
 
   >>> from martian.ndir import validateText
-  >>> title = Directive('martian', 'title', CLASS, ONCE, u'', validateText)
+  >>> class title(Directive):
+  ...     scope = CLASS
+  ...     store = ONCE
+  ...     default = u''
+  ...     validate = validateText
 
 When we pass ascii text into the directive, there is no error::
 
@@ -333,14 +302,14 @@
   >>> class Foo(object):
   ...    title(u'Some unicode text')
 
-Let's now try it with something that's not text at all, such as a number. 
+Let's now try it with something that's not text at all, such as a number.
 This fails::
 
   >>> class Foo(object):
   ...    title(123)
   Traceback (most recent call last):
     ...
-  GrokImportError: martian.title can only be called with unicode or ASCII.
+  GrokImportError: The 'title' directive can only be called with unicode or ASCII.
 
 It's not allowed to call the direct with a non-ascii encoded string::
 
@@ -348,20 +317,22 @@
   ...   title(u'è'.encode('latin-1'))
   Traceback (most recent call last):
     ...
-  GrokImportError: martian.title can only be called with unicode or ASCII.
+  GrokImportError: The 'title' directive can only be called with unicode or ASCII.
 
  >>> class Foo(object):
  ...   title(u'è'.encode('UTF-8'))
  Traceback (most recent call last):
    ...
- GrokImportError: martian.title can only be called with unicode or ASCII.
+ GrokImportError: The 'title' directive can only be called with unicode or ASCII.
 
 The ``validateInterfaceOrClass`` function only accepts class or
 interface objects::
 
   >>> from martian.ndir import validateInterfaceOrClass
-  >>> klass = Directive('martian', 'klass', CLASS, ONCE, None,
-  ...                   validateInterfaceOrClass)
+  >>> class klass(Directive):
+  ...     scope = CLASS
+  ...     store = ONCE
+  ...     validate = validateInterfaceOrClass
 
 It works with interfaces and classes::
 
@@ -382,36 +353,73 @@
   ...   klass(Bar())
   Traceback (most recent call last):
     ...
-  GrokImportError: martian.klass can only be called with a class or interface.
+  GrokImportError: The 'klass' directive can only be called with a class or an interface.
 
   >>> class Foo(object):
   ...   klass(1)
   Traceback (most recent call last):
     ...
-  GrokImportError: martian.klass can only be called with a class or interface.
+  GrokImportError: The 'klass' directive can only be called with a class or an interface.
 
 The ``validateInterface`` validator only accepts an interface::
 
   >>> from martian.ndir import validateInterface
-  >>> iface = Directive('martian', 'iface', CLASS, ONCE, None,
-  ...                   validateInterface)
-  
+  >>> class iface(Directive):
+  ...     scope = CLASS
+  ...     store = ONCE
+  ...     validate = validateInterface
+
 Let's try it::
 
   >>> class Foo(object):
   ...    iface(IBar)
-  
+
 It won't work with classes or other things::
 
   >>> class Foo(object):
   ...   iface(Bar)
   Traceback (most recent call last):
     ...
-  GrokImportError: martian.iface can only be called with an interface.
+  GrokImportError: The 'iface' directive can only be called with an interface.
 
   >>> class Foo(object):
   ...   iface(1)
   Traceback (most recent call last):
     ...
-  GrokImportError: martian.iface can only be called with an interface.
+  GrokImportError: The 'iface' directive can only be called with an interface.
 
+Declaring base classes
+----------------------
+
+There's a special directive called 'baseclass' which lets you declare that a
+certain class is the base class for a series of other components.  This
+property should not be inherited by those components.  Consider the following
+base class:
+
+  >>> from martian.ndir import baseclass
+  >>> class MyBase(object):
+  ...     baseclass()
+
+As you would expect, the directive will correctly identify this class as a
+baseclass:
+
+  >>> baseclass.get(MyBase)
+  True
+
+But, if we create a subclass of this base class, the subclass won't inherit
+that property, unlike with a regular directive:
+
+  >>> class SubClass(MyBase):
+  ...     pass
+  ...
+  >>> baseclass.get(SubClass)
+  False
+
+Naturally, the directive will also report a false answer if the class doesn't
+inherit from a base class at all and hasn't been marked with the directive:
+
+  >>> class NoBase(object):
+  ...     pass
+  ...
+  >>> baseclass.get(NoBase)
+  False



More information about the Checkins mailing list