[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