[Checkins] SVN: martian/trunk/ Merged the 'philikon-decl-dir-rules'
branch:
Philipp von Weitershausen
philikon at philikon.de
Wed May 14 11:57:20 EDT 2008
Log message for revision 86732:
Merged the 'philikon-decl-dir-rules' branch:
* Refactored the ``martian.Directive`` base class yet again to allow
more declarative (rather than imperative) usage in grokkers.
Directives themselves no longer have a ``get()`` method nor a
default value factory (``get_default()``). Instead you will have to
"bind" the directive first which is typically done in a grokker.
* Extended the ``ClassGrokker`` baseclass with a standard ``grok()``
method that allows you to simply declare a set of directives that
are used on the grokked classes. Then you just have to implement an
``execute()`` method that will receive the data from those
directives as keyword arguments. This simplifies the implementation
of class grokkers a lot.
Changed:
U martian/trunk/CHANGES.txt
U martian/trunk/src/martian/README.txt
U martian/trunk/src/martian/components.py
U martian/trunk/src/martian/directive.py
U martian/trunk/src/martian/directive.txt
U martian/trunk/src/martian/tests/test_all.py
U martian/trunk/src/martian/util.py
-=-
Modified: martian/trunk/CHANGES.txt
===================================================================
--- martian/trunk/CHANGES.txt 2008-05-14 15:52:50 UTC (rev 86731)
+++ martian/trunk/CHANGES.txt 2008-05-14 15:57:19 UTC (rev 86732)
@@ -4,8 +4,22 @@
0.9.6 (unreleased)
==================
-* ...
+Feature changes
+---------------
+* Refactored the ``martian.Directive`` base class yet again to allow
+ more declarative (rather than imperative) usage in grokkers.
+ Directives themselves no longer have a ``get()`` method nor a
+ default value factory (``get_default()``). Instead you will have to
+ "bind" the directive first which is typically done in a grokker.
+
+* Extended the ``ClassGrokker`` baseclass with a standard ``grok()``
+ method that allows you to simply declare a set of directives that
+ are used on the grokked classes. Then you just have to implement an
+ ``execute()`` method that will receive the data from those
+ directives as keyword arguments. This simplifies the implementation
+ of class grokkers a lot.
+
0.9.5 (2008-05-04)
==================
Modified: martian/trunk/src/martian/README.txt
===================================================================
--- martian/trunk/src/martian/README.txt 2008-05-14 15:52:50 UTC (rev 86731)
+++ martian/trunk/src/martian/README.txt 2008-05-14 15:57:19 UTC (rev 86732)
@@ -424,34 +424,49 @@
ClassGrokker
------------
-Besides instances we can also grok classes. Let's define an application
-where we register classes representing animals::
+Besides instances we can also grok classes. Let's define an
+application where we register classes representing animals. Animals
+can be given names using the ``name`` directive::
+ >>> from martian.directive import Directive, CLASS, ONCE
+
>>> class animal(FakeModule):
+ ... class name(Directive):
+ ... scope = CLASS
+ ... store = ONCE
+ ...
... class Animal(object):
- ... name = None
... def __repr__(self):
- ... return '<Animal %s>' % self.name
+ ... return '<Animal %s>' % animal.name.bind().get(self)
+ ...
... all_animals = {}
... def create_animal(name):
... return all_animals[name]()
>>> animal = fake_import(animal)
-Let's define a grokker that can grok an ``Animal``::
+Let's define a grokker that can grok an ``Animal``. We could either
+implement the ``grok`` method as with ``InstanceGrokkers``, or we can
+rely on the implementation that the baseclass already provides. In
+the latter case, we just have to declare what directives the grokker
+may want to use on the class and the implement the ``execute`` method::
>>> from martian import ClassGrokker
>>> class AnimalGrokker(ClassGrokker):
... component_class = animal.Animal
- ... def grok(self, name, obj, **kw):
- ... animal.all_animals[obj.name] = obj
+ ... directives = [
+ ... animal.name.bind()
+ ... ]
+ ... def execute(self, class_, name, **kw):
+ ... animal.all_animals[name] = class_
... return True
Let's test our grokker::
+ >>> class Snake(animal.Animal):
+ ... animal.name('snake')
+ ...
>>> animal_grokker = AnimalGrokker()
- >>> class Snake(animal.Animal):
- ... name = 'snake'
- >>> animal_grokker.grok('snake', Snake)
+ >>> animal_grokker.grok('Snake', Snake)
True
>>> animal.all_animals.keys()
['snake']
@@ -461,6 +476,80 @@
>>> animal.create_animal('snake')
<Animal snake>
+Note that we can supply a different default value for the directive
+default when binding the directive to the grokker:
+
+ >>> class AnimalGrokker(AnimalGrokker):
+ ... directives = [
+ ... animal.name.bind(default='generic animal')
+ ... ]
+ ...
+ >>> class Generic(animal.Animal):
+ ... pass
+ ...
+ >>> animal_grokker = AnimalGrokker()
+ >>> animal_grokker.grok('Generic', Generic)
+ True
+ >>> sorted(animal.all_animals.keys())
+ ['generic animal', 'snake']
+
+Moreover, we can also supply a default factory that may want to
+determine a dynamic default value based on the class that's being
+grokked. For instance, let's say the default name of an animal should
+the class name converted to lowercase letters::
+
+ >>> def default_animal_name(class_, module, **data):
+ ... return class_.__name__.lower()
+ ...
+ >>> class AnimalGrokker(AnimalGrokker):
+ ... directives = [
+ ... animal.name.bind(get_default=default_animal_name)
+ ... ]
+ ...
+ >>> class Mouse(animal.Animal):
+ ... pass
+ ...
+ >>> animal_grokker = AnimalGrokker()
+ >>> animal_grokker.grok('Mouse', Mouse)
+ True
+ >>> sorted(animal.all_animals.keys())
+ ['generic animal', 'mouse', 'snake']
+
+Note that these default value factories will also get the data from
+all directives that are in front of them in the grokker's directive
+list. For instance, consider the following directive:
+
+ >>> class zoologicalname(animal.name):
+ ... pass
+ ...
+
+with the following default rule that takes the regular name as the
+default zoological name::
+
+ >>> def default_zoological_name(class_, module, name, **data):
+ ... return name
+ ...
+ >>> class ZooAnimalGrokker(ClassGrokker):
+ ... component_class = animal.Animal
+ ... directives = [
+ ... animal.name.bind(get_default=default_animal_name),
+ ... zoologicalname.bind(get_default=default_zoological_name)
+ ... ]
+ ... def execute(self, class_, name, zoologicalname, **kw):
+ ... print zoologicalname
+ ... return True
+ ...
+ >>> class Hippopotamus(animal.Animal):
+ ... pass
+ ... # No need to use animal.name(), we'll take the class name as default.
+ ... # The zoological name is the same as well.
+ ...
+
+ >>> zoo_animal_grokker = ZooAnimalGrokker()
+ >>> zoo_animal_grokker.grok('Hippopotamus', Hippopotamus)
+ hippopotamus
+ True
+
MultiClassGrokker
-----------------
@@ -470,13 +559,13 @@
>>> class animals(FakeModule):
... class Elephant(animal.Animal):
- ... name = 'elephant'
+ ... animal.name('elephant')
... class Tiger(animal.Animal):
- ... name = 'tiger'
+ ... animal.name('tiger')
... class Lion(animal.Animal):
- ... name = 'lion'
+ ... animal.name('lion')
... class Chair(object):
- ... name = 'chair'
+ ... animal.name('chair')
>>> animals = fake_import(animals)
First we need to wrap our ``AnimalGrokker`` into a ``MultiClassGrokker``::
@@ -494,7 +583,7 @@
The animals (but not anything else) should have become available::
>>> sorted(animal.all_animals.keys())
- ['elephant', 'lion', 'snake', 'tiger']
+ ['elephant', 'generic animal', 'lion', 'mouse', 'snake', 'tiger']
We can create animals using their name now::
@@ -524,7 +613,7 @@
Let's try it with some individual objects::
>>> class Whale(animal.Animal):
- ... name = 'whale'
+ ... animal.name('whale')
>>> multi.grok('Whale', Whale)
True
>>> 'whale' in animal.all_animals
@@ -599,7 +688,7 @@
>>> class mix(FakeModule):
... # grokked by AnimalGrokker
... class Whale(animal.Animal):
- ... name = 'whale'
+ ... animal.name('whale')
... # not grokked
... my_whale = Whale()
... # grokked by ColorGrokker
@@ -613,7 +702,7 @@
... rocknroll = RockMusic('rock n roll')
... # grokked by AnimalGrokker
... class Dragon(animal.Animal):
- ... name = 'dragon'
+ ... animal.name('dragon')
... # not grokked
... class Chair(object):
... pass
@@ -625,7 +714,7 @@
... pass
... # grokked by AnimalGrokker
... class SpermWhale(Whale):
- ... name = 'sperm whale'
+ ... animal.name('sperm whale')
... # not grokked
... another = object()
>>> mix = fake_import(mix)
Modified: martian/trunk/src/martian/components.py
===================================================================
--- martian/trunk/src/martian/components.py 2008-05-14 15:52:50 UTC (rev 86731)
+++ martian/trunk/src/martian/components.py 2008-05-14 15:57:19 UTC (rev 86732)
@@ -48,8 +48,22 @@
class ClassGrokker(ComponentGrokkerBase):
"""Grokker that groks classes in a module.
"""
- pass
+ # Use a tuple instead of a list here to make it immutable, just to be safe
+ directives = ()
+ def grok(self, name, class_, module_info=None, **kw):
+ module = None
+ if module_info is not None:
+ module = module_info.getModule()
+
+ # Populate the data dict with information from the directives:
+ for directive in self.directives:
+ kw[directive.name] = directive.get(class_, module, **kw)
+ return self.execute(class_, **kw)
+
+ def execute(self, class_, **data):
+ raise NotImplementedError
+
class InstanceGrokker(ComponentGrokkerBase):
"""Grokker that groks instances in a module.
"""
Modified: martian/trunk/src/martian/directive.py
===================================================================
--- martian/trunk/src/martian/directive.py 2008-05-14 15:52:50 UTC (rev 86731)
+++ martian/trunk/src/martian/directive.py 2008-05-14 15:57:19 UTC (rev 86732)
@@ -4,7 +4,7 @@
from zope.interface.interfaces import IInterface
from martian import util
-from martian.error import GrokImportError
+from martian.error import GrokImportError, GrokError
class StoreOnce(object):
@@ -82,7 +82,6 @@
DICT = StoreDict()
-_SENTINEL = object()
_USE_DEFAULT = object()
class ClassScope(object):
@@ -91,6 +90,9 @@
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)
+
CLASS = ClassScope()
class ClassOrModuleScope(object):
@@ -99,6 +101,12 @@
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)
+ return value
+
CLASS_OR_MODULE = ClassOrModuleScope()
class ModuleScope(object):
@@ -107,6 +115,9 @@
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)
+
MODULE = ModuleScope()
class Directive(object):
@@ -149,31 +160,43 @@
def factory(self, value):
return value
- def get_default(self, component):
- return self.default
-
@classmethod
def dotted_name(cls):
return cls.__module__ + '.' + cls.__name__
@classmethod
- def get(cls, component, module=None):
- # Create an instance of the directive without calling __init__
- self = cls.__new__(cls)
+ def set(cls, component, value):
+ cls.store.setattr(component, cls, 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)
+ @classmethod
+ def bind(cls, default=None, get_default=None, name=None):
+ return BoundDirective(cls, default, get_default, name)
- return value
- @classmethod
- def set(cls, component, value):
- cls.store.setattr(component, cls, value)
+class BoundDirective(object):
+ def __init__(self, directive, default=None, get_default=None, name=None):
+ self.directive = directive
+ self.default = default
+ if name is None:
+ name = directive.__name__
+ self.name = name
+ if get_default is not None:
+ self.get_default = get_default
+ def get_default(self, component, module=None, **data):
+ if self.default is not None:
+ return self.default
+ return self.directive.default
+
+ 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
+
class MultipleTimesDirective(Directive):
store = MULTIPLE
default = []
Modified: martian/trunk/src/martian/directive.txt
===================================================================
--- martian/trunk/src/martian/directive.txt 2008-05-14 15:52:50 UTC (rev 86731)
+++ martian/trunk/src/martian/directive.txt 2008-05-14 15:57:19 UTC (rev 86732)
@@ -1,5 +1,5 @@
-Directives New Style
-====================
+Directives
+==========
When grokking a class, the grokking procedure can be informed by
directives, on a class, or a module. If a directive is absent, the
@@ -28,10 +28,11 @@
>>> class Foo(object):
... description(u"This is a description")
-After setting it, we can use the ``get`` method on the directive to
-retrieve it from the class again::
+After setting the description, we bind the directive and obtain a
+bound directive object. This object has means for retrieving the data
+set by the directive, in particular the ``get`` method::
- >>> description.get(Foo)
+ >>> description.bind().get(Foo)
u'This is a description'
Directives in different namespaces get stored differently. We'll
@@ -43,9 +44,9 @@
>>> class Foo(object):
... description(u"Description1")
... description2(u"Description2")
- >>> description.get(Foo)
+ >>> description.bind().get(Foo)
u'Description1'
- >>> description2.get(Foo)
+ >>> description2.bind().get(Foo)
u'Description2'
If we check the value of a class without the directive, we see the
@@ -53,14 +54,14 @@
>>> class Foo(object):
... pass
- >>> description.get(Foo)
+ >>> description.bind().get(Foo)
u''
In certain cases we need to set a value on a component as if the directive was
actually used::
>>> description.set(Foo, u'value as set')
- >>> description.get(Foo)
+ >>> description.bind().get(Foo)
u'value as set'
Subclasses of the original class will inherit the properties set by the
@@ -72,7 +73,7 @@
>>> class Bar(Foo):
... pass
...
- >>> description.get(Bar)
+ >>> description.bind().get(Bar)
'This is a foo.'
When we use the directive outside of class scope, we get an error
@@ -124,18 +125,24 @@
By default, the ``default`` property is None which is why we can omit
specifying it here.
+This directive has been declared ``CLASS_OR_MODULE``, so you will
+always have to pass a module to the directive. Since we don't have a
+module yet we'll simply create a dummy, empty, fallback module::
+
+ >>> dummy = object()
+
We can use this directive now on a class::
>>> class Foo(object):
... layer('Test')
- >>> layer.get(Foo)
+ >>> layer.bind().get(Foo, dummy)
'Test'
The defaulting to ``None`` works::
>>> class Foo(object):
... pass
- >>> layer.get(Foo) is None
+ >>> layer.bind().get(Foo, dummy) is None
True
We can also use it in a module::
@@ -144,13 +151,13 @@
... layer('Test2')
... class Foo(object):
... pass
- >>> test_module = fake_import(testmodule)
+ >>> testmodule = fake_import(testmodule)
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)
+ >>> layer.bind().get(testmodule.Foo, testmodule)
'Test2'
Let's look at a module where the directive is not used::
@@ -163,9 +170,15 @@
In this case, the value cannot be found so the system falls back on
the default, ``None``::
- >>> layer.get(testmodule.Foo, testmodule) is None
+ >>> layer.bind().get(testmodule.Foo, testmodule) is None
True
+Let's now look at this using a directive with CLASS scope only::
+
+ >>> class layer2(Directive):
+ ... scope = CLASS
+ ... store = ONCE
+
Using a directive multiple times
--------------------------------
@@ -184,14 +197,14 @@
We can now retrieve the value and we'll get a list::
- >>> multi.get(Foo)
+ >>> multi.bind().get(Foo)
[u'Once', u'Twice']
The default value for a MultipleTimesDirective is an empty list::
>>> class Bar(object):
... pass
- >>> multi.get(Bar)
+ >>> multi.bind().get(Bar)
[]
Whenever the directive is used on a sub class of a component, the values set by
@@ -200,7 +213,7 @@
>>> class Qux(Foo):
... multi(u'Triple')
...
- >>> multi.get(Qux)
+ >>> multi.bind().get(Qux)
[u'Once', u'Twice', u'Triple']
@@ -226,7 +239,7 @@
We can now retrieve the value and we'll get a to the items::
- >>> d = multi.get(Bar)
+ >>> d = multi.bind().get(Bar)
>>> print sorted(d.items())
[(u'once', u'Once'), (u'twice', u'Twice')]
@@ -276,7 +289,7 @@
... multi(3, 'DDD')
... multi(4, 'EEE')
- >>> d = multi.get(Fropple)
+ >>> d = multi.bind().get(Fropple)
>>> print sorted(d.items())
[(1, 'CCC'), (2, 'BBB'), (3, 'DDD'), (4, 'EEE')]
@@ -295,7 +308,7 @@
... multi('Two')
...
>>> module_with_directive = fake_import(module_with_directive)
- >>> print multi.get(module_with_directive)
+ >>> print multi.bind().get(module=module_with_directive)
['One', 'Two']
>>> from martian import MODULE
@@ -313,7 +326,7 @@
... multi(2, 'Two')
...
>>> module_with_directive = fake_import(module_with_directive)
- >>> d = multi.get(module_with_directive)
+ >>> d = multi.bind().get(module=module_with_directive)
>>> print sorted(d.items())
[(1, 'One'), (2, 'Two')]
@@ -329,17 +342,21 @@
>>> class name(Directive):
... scope = CLASS
... store = ONCE
- ... def get_default(self, component):
- ... return component.__name__.lower()
+ ...
+ >>> def default_name_lowercase(component, module, **data):
+ ... return component.__name__.lower()
+ ...
+ >>> bound_name = name.bind(get_default=default_name_lowercase)
>>> class Foo(object):
... name('bar')
- >>> name.get(Foo)
+
+ >>> bound_name.get(Foo)
'bar'
>>> class Foo(object):
... pass
- >>> name.get(Foo)
+ >>> bound_name.get(Foo)
'foo'
A marker directive
@@ -357,14 +374,14 @@
Class ``Foo`` is now marked::
- >>> mark.get(Foo)
+ >>> mark.bind().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)
+ >>> mark.bind().get(Bar)
False
If we pass in an argument, we get an error::
@@ -539,7 +556,7 @@
As you would expect, the directive will correctly identify this class as a
baseclass:
- >>> baseclass.get(MyBase)
+ >>> baseclass.bind().get(MyBase)
True
But, if we create a subclass of this base class, the subclass won't inherit
@@ -548,7 +565,7 @@
>>> class SubClass(MyBase):
... pass
...
- >>> baseclass.get(SubClass)
+ >>> baseclass.bind().get(SubClass)
False
Naturally, the directive will also report a false answer if the class doesn't
@@ -557,5 +574,5 @@
>>> class NoBase(object):
... pass
...
- >>> baseclass.get(NoBase)
+ >>> baseclass.bind().get(NoBase)
False
Modified: martian/trunk/src/martian/tests/test_all.py
===================================================================
--- martian/trunk/src/martian/tests/test_all.py 2008-05-14 15:52:50 UTC (rev 86731)
+++ martian/trunk/src/martian/tests/test_all.py 2008-05-14 15:57:19 UTC (rev 86732)
@@ -16,7 +16,7 @@
module = new.module(fake_module.__name__)
glob = {}
for name in dir(fake_module):
- if name.startswith('__'):
+ if name.startswith('__') and '.' not in name:
continue
obj = getattr(fake_module, name)
glob[name] = obj
Modified: martian/trunk/src/martian/util.py
===================================================================
--- martian/trunk/src/martian/util.py 2008-05-14 15:52:50 UTC (rev 86731)
+++ martian/trunk/src/martian/util.py 2008-05-14 15:57:19 UTC (rev 86732)
@@ -49,7 +49,7 @@
return sys._getframe(2).f_globals['__name__']
def is_baseclass(name, component):
- return (isclass(component) and martian.baseclass.get(component))
+ return (isclass(component) and martian.baseclass.bind().get(component))
def defined_locally(obj, dotted_name):
obj_module = getattr(obj, '__grok_module__', None)
More information about the Checkins
mailing list