[Checkins] SVN: martian/trunk/src/martian/ndir. Expand the options for directives. Note that the argument checking

Martijn Faassen faassen at infrae.com
Wed Jan 30 15:25:18 EST 2008


Log message for revision 83322:
  Expand the options for directives. Note that the argument checking
  is too limited to support some multi-argument directives, so this code
  will need to be revised.
  

Changed:
  U   martian/trunk/src/martian/ndir.py
  U   martian/trunk/src/martian/ndir.txt

-=-
Modified: martian/trunk/src/martian/ndir.py
===================================================================
--- martian/trunk/src/martian/ndir.py	2008-01-30 19:33:54 UTC (rev 83321)
+++ martian/trunk/src/martian/ndir.py	2008-01-30 20:25:18 UTC (rev 83322)
@@ -1,5 +1,7 @@
 import sys
 
+from zope.interface.interfaces import IInterface
+
 from martian import util
 from martian.error import GrokImportError
 
@@ -9,6 +11,13 @@
 ONCE = object()
 MULTIPLE = object()
 
+# arguments
+SINGLE_ARG = object()
+NO_ARG = object()
+OPTIONAL_ARG = object()
+
+_SENTINEL = object()
+
 class ClassScope(object):
     description = 'class'
     
@@ -28,16 +37,35 @@
 CLASS_OR_MODULE = ClassOrModuleScope()
 
 class Directive(object):
-    def __init__(self, namespace, name, scope, times, default):
+    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()
 
-    def __call__(self, value):
+        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 = self.default
+
+        if self.validate is not None:
+            self.validate(name, value)
+
         frame = sys._getframe(1)
-        name = self.namespaced_name()
         if not self.scope.check(frame):
             raise GrokImportError("%s can only be used on %s level." %
                                   (name, self.scope.description))
@@ -67,7 +95,24 @@
     def namespaced_name(self):
         return self.namespace + '.' + self.name
 
+def validateText(name, value):
+    if util.not_unicode_or_ascii(value):
+        raise GrokImportError("%s can only be called with unicode or ASCII." %
+                              name)
 
+def validateInterfaceOrClass(name, value):
+    if not (IInterface.providedBy(value) or util.isclass(value)):
+        raise GrokImportError("%s can only be called with a class or "
+                              "interface." %
+                              name)
+
+
+def validateInterface(name, value):
+    if not (IInterface.providedBy(value)):
+        raise GrokImportError("%s can only be called with an interface." %
+                              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/trunk/src/martian/ndir.txt
===================================================================
--- martian/trunk/src/martian/ndir.txt	2008-01-30 19:33:54 UTC (rev 83321)
+++ martian/trunk/src/martian/ndir.txt	2008-01-30 20:25:18 UTC (rev 83322)
@@ -10,7 +10,7 @@
 A simple directive
 ------------------
 
-Let's define a simple directive that sets a description::
+We define a simple directive that sets a description::
 
   >>> from martian.ndir import Directive, CLASS, ONCE
   >>> description = Directive('martian', 'description', 
@@ -26,7 +26,7 @@
 can only be used a single time. Finally we define the default in case
 the directive is absent (the empty string).
 
-Let's look at the directive in action::
+Now we look at the directive in action::
 
   >>> class Foo(object):
   ...    description(u"This is a description")
@@ -37,8 +37,8 @@
   >>> description.get(Foo)
   u'This is a description'
 
-Let's see that directives in different namespaces indeed get stored
-differently. We'll define a similar directive in another namespace::
+Directives in different namespaces get stored differently. We'll
+define a similar directive in another namespace::
 
   >>> description2 = Directive('different', 'description', 
   ...   CLASS, ONCE, u'')
@@ -51,15 +51,16 @@
   >>> description2.get(Foo)
   u'Description2'
 
-Let's check the defaulting behavior. If we check the value of a class
-without the directive, we expect to see the default for that directive::
+If we check the value of a class without the directive, we see the
+default value for that directive, this case the empty unicode string::
 
   >>> class Foo(object):
   ...   pass
   >>> description.get(Foo)
   u''
 
-When we use the directive outside of class scope, we expect an error message::
+When we use the directive outside of class scope, we get an error
+message::
 
   >>> description('Description')
   Traceback (most recent call last):
@@ -76,7 +77,7 @@
   GrokImportError: martian.description can only be used on class level.
 
 We cannot use the directive twice in the class scope. If we do so, we
-expect an error message as well::
+get an error message as well::
 
   >>> class Foo(object):
   ...   description(u"Description1")
@@ -85,11 +86,19 @@
     ...
   GrokImportError: martian.description can only be called once per class.
 
+We cannot call the directive with no argument either::
+
+  >>> class Foo(object):
+  ...   description()
+  Traceback (most recent call last):
+    ...
+  GrokImportError: martian.description requires a single argument.
+
 Class and module scope
 ----------------------
 
-Let's now define a ``layer`` directive that can be used in class and
-module scope both::
+We define a ``layer`` directive that can be used in class and module
+scope both::
 
   >>> from martian.ndir import CLASS_OR_MODULE
   >>> layer = Directive('martian', 'layer', CLASS_OR_MODULE, ONCE, None)
@@ -116,21 +125,22 @@
   ...       pass
   >>> test_module = fake_import(testmodule)
 
-When we now try to access ``layer`` on ``Foo``, we expect to find the
-module-level default which we just set. We pass the module as the second
-argument to the ``get`` method to have it fall back on this::
+When we now try to access ``layer`` on ``Foo``, we find the
+module-level default which we just set. We pass the module as the
+second argument to the ``get`` method to have it fall back on this::
 
   >>> layer.get(testmodule.Foo, testmodule)
   'Test2'
 
-Let's try again in a module where the directive is not used::
+Let's look at a module where the directive is not used::
 
   >>> class testmodule(FakeModule):
   ...   class Foo(object):
   ...      pass
   >>> testmodule = fake_import(testmodule)
 
-We expect the value to be the default, ``None``::
+In this case, the value cannot be found so the system falls back on
+the default, ``None``::
 
   >>> layer.get(testmodule.Foo, testmodule) is None
   True
@@ -144,7 +154,7 @@
   >>> from martian.ndir import MULTIPLE
   >>> multi = Directive('martian', 'multi', CLASS, MULTIPLE, None)
 
-Let's try it::
+We can now use the directive multiple times without any errors::
 
   >>> class Foo(object):
   ...   multi(u"Once")
@@ -154,3 +164,206 @@
 
   >>> multi.get(Foo)
   [u'Once', u'Twice']
+
+A marker directive
+------------------
+
+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)
+  
+  >>> class Foo(object):
+  ...     mark()
+
+Class ``Foo`` is now marked::
+  
+  >>> mark.get(Foo)
+  True
+
+When we have a class that isn't marked, we get the default value, ``False``::
+
+  >>> class Bar(object):
+  ...    pass
+  >>> mark.get(Bar)
+  False
+
+If we pass in an argument, we get an error::
+
+  >>> class Bar(object):
+  ...   mark("An argument")
+  Traceback (most recent call last):
+    ...
+  GrokImportError: martian.mark accepts no arguments.
+
+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'
+
+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. 
+
+First we define our own validation function. A validation function
+takes two arguments:
+
+* the name of the directive we're validating for
+
+* the value we need to validate
+
+The name can be used to format the exception properly.
+
+We'll define a validation function 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)
+
+We use it with a directive::
+
+  >>> number = Directive('martian', 'number', CLASS, ONCE, None, validateInt)
+  >>> class Foo(object):
+  ...    number(3)
+
+  >>> class Foo(object):
+  ...    number("This shouldn't work")
+  Traceback (most recent call last):
+    ...
+  GrokImportError: martian.number can only be called with an integer.
+
+Some built-in validation functions
+----------------------------------
+
+Let's look at some built-in validation functions.
+
+The ``validateText`` function determines whether a string
+is unicode or plain ascii::
+
+  >>> from martian.ndir import validateText
+  >>> title = Directive('martian', 'title', CLASS, ONCE, u'', validateText)
+
+When we pass ascii text into the directive, there is no error::
+
+  >>> class Foo(object):
+  ...    title('Some ascii text')
+
+We can also pass in a unicode string without error::
+
+  >>> 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. 
+This fails::
+
+  >>> class Foo(object):
+  ...    title(123)
+  Traceback (most recent call last):
+    ...
+  GrokImportError: martian.title can only be called with unicode or ASCII.
+
+It's not allowed to call the direct with a non-ascii encoded string::
+
+  >>> class Foo(object):
+  ...   title(u'è'.encode('latin-1'))
+  Traceback (most recent call last):
+    ...
+  GrokImportError: martian.title 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.
+
+The ``validateInterfaceOrClass`` function only accepts class or
+interface objects::
+
+  >>> from martian.ndir import validateInterfaceOrClass
+  >>> klass = Directive('martian', 'klass', CLASS, ONCE, None,
+  ...                   validateInterfaceOrClass)
+
+It works with interfaces and classes::
+
+  >>> class Bar(object):
+  ...    pass
+  >>> class Foo(object):
+  ...    klass(Bar)
+
+  >>> from zope.interface import Interface
+  >>> class IBar(Interface):
+  ...    pass
+  >>> class Foo(object):
+  ...    klass(IBar)
+
+It won't work with other things::
+
+  >>> class Foo(object):
+  ...   klass(Bar())
+  Traceback (most recent call last):
+    ...
+  GrokImportError: martian.klass can only be called with a class or interface.
+
+  >>> class Foo(object):
+  ...   klass(1)
+  Traceback (most recent call last):
+    ...
+  GrokImportError: martian.klass can only be called with a class or interface.
+
+The ``validateInterface`` validator only accepts an interface::
+
+  >>> from martian.ndir import validateInterface
+  >>> iface = Directive('martian', 'iface', CLASS, ONCE, None,
+  ...                   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.
+
+  >>> class Foo(object):
+  ...   iface(1)
+  Traceback (most recent call last):
+    ...
+  GrokImportError: martian.iface can only be called with an interface.



More information about the Checkins mailing list