[Checkins] SVN: grok/trunk/ Merge from jspaans-traversableattrs branch, implementing grok.traversable directive

Jasper Spaans jspaans at thehealthagency.com
Fri May 2 06:04:41 EDT 2008


Log message for revision 86074:
  Merge from jspaans-traversableattrs branch, implementing grok.traversable directive

Changed:
  U   grok/trunk/CHANGES.txt
  U   grok/trunk/doc/grok_overview.rst
  U   grok/trunk/doc/reference/directives.rst
  U   grok/trunk/src/grok/__init__.py
  U   grok/trunk/src/grok/components.py
  U   grok/trunk/src/grok/directive.py
  A   grok/trunk/src/grok/ftests/traversal/traversableattr.py
  A   grok/trunk/src/grok/tests/directive/multipleasdict.py
  U   grok/trunk/src/grok/util.py

-=-
Modified: grok/trunk/CHANGES.txt
===================================================================
--- grok/trunk/CHANGES.txt	2008-05-02 10:00:28 UTC (rev 86073)
+++ grok/trunk/CHANGES.txt	2008-05-02 10:04:41 UTC (rev 86074)
@@ -7,6 +7,9 @@
 Feature changes
 ---------------
 
+* Added grok.traversable directive for easy traversal to attributes and
+  methods.
+
 * grok.require() can refer to subclasses of grok.Permission directly, instead
   of their id. This, for one, avoids making typos in permission ids. Permission
   components *do* still need the grok.name() directive for defining the

Modified: grok/trunk/doc/grok_overview.rst
===================================================================
--- grok/trunk/doc/grok_overview.rst	2008-05-02 10:00:28 UTC (rev 86073)
+++ grok/trunk/doc/grok_overview.rst	2008-05-02 10:04:41 UTC (rev 86074)
@@ -1,4 +1,4 @@
-Grok Developer's Notes 
+Grok Developer's Notes
 ======================
 
 This document is a developer's overview of Grok. It is not intended to
@@ -72,9 +72,9 @@
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Models in Grok are automatically supplied with a ``__parent__`` and a
-``__name__`` attribute. 
+``__name__`` attribute.
 
-* ``__parent__`` points to object this object is in. If the object is in 
+* ``__parent__`` points to object this object is in. If the object is in
   a container, this is the container.
 
 * ``__name__`` is the name this object has in a URL (and in its
@@ -138,6 +138,27 @@
 object asked for. Grok then falls back on default behavior, which in
 this case would mean a ``404 Not Found`` error.
 
+Traversable attributes
+~~~~~~~~~~~~~~~~~~~~~~
+
+In some cases, you want to traverse to attributes or methods of your
+``grok.Model``. This can be done easily using the ``grok.traversable``
+directive::
+
+  class Mammoth(grok.Model):
+      grok.traversable('trunk')
+
+      trunk = Trunk()
+
+  class MammothView(grok.View):
+      grok.context(Mammoth)
+
+      def render(self):
+          return "I'm a mammoth!"
+
+Now, if traversing to http://localhost/mammoth/trunk , a Trunk()
+object will be exposed at that URL.
+
 Views
 -----
 
@@ -146,10 +167,10 @@
 is a view? A view is a class that represents a model in some way. It
 creates a user interface of some sort (typically HTML) for a model. A
 single model can have more than one view. It looks like this::
- 
+
   class Index(grok.View):
       grok.context(Application)
-     
+
       def render(self):
           return "This is the application"
 
@@ -170,7 +191,7 @@
 
 What happens when you go to this URL is that Grok instantiates the
 ``Index`` class, creating a ``Index`` instance. View instances have
-a number of attributes by default: 
+a number of attributes by default:
 
   * ``context``, the model instance that the view is presenting.
 
@@ -188,7 +209,7 @@
 
   class Edit(grok.View):
       grok.context(Application)
-    
+
       def render(self):
           return "This is the edit screen for the application"
 
@@ -203,7 +224,7 @@
   class SomeImpossiblyLongClassName(grok.View):
       grok.context(Application)
       grok.name('edit')
- 
+
       def render(self):
           return "This is the edit screen for the application"
 
@@ -314,7 +335,7 @@
 ~~~~~~~~~~~~~~~~~~
 
 Views have a special method called ``url()`` that can be used to
-create URLs to objects. The ``url`` method takes zero, one or two 
+create URLs to objects. The ``url`` method takes zero, one or two
 arguments and an additional optional keyword argument 'data' that
 is converted into a CGI query string appended to the URL::
 
@@ -328,12 +349,12 @@
 * self.url(object, u"name") - URL to the provided object, with
   		   ``/name`` appended, to point to a view or subobject
   		   of the provided object.
-                   
-* self.url(object, u"name", data={'name':'Peter', 'age':28}) 
+
+* self.url(object, u"name", data={'name':'Peter', 'age':28})
             - URL to the provided object, with ``/name`` appended
               with '?name=Peter&age=28' at the end.
-                   
-* self.url(data={'name':u'Andr\xe9', 'age:int':28}) - URL to the provided 
+
+* self.url(data={'name':u'Andr\xe9', 'age:int':28}) - URL to the provided
                    object with '?name=Andre%C3%A9'&age%3Aint=28'.
 
 From the view, this is accessed through ``self.url()``. From the
@@ -368,7 +389,7 @@
 ``context``, which should normally be used).
 
 ``__name__`` is the name of the view in the URL.
- 
+
 The ``@@`` thing
 ~~~~~~~~~~~~~~~~
 
@@ -487,7 +508,7 @@
         def size(self):
             total = 0
             for obj in self.values():
-                total += obj.size() 
+                total += obj.size()
             return total
 
 For simple cases this is fine, but for larger applications this can
@@ -531,36 +552,36 @@
           elif isintance(self.context, Container):
                total = 0
                for obj in self.context.values():
-                   total += Sized(obj).size() 
+                   total += Sized(obj).size()
                return total
-               
+
 Instead, we can create a smart ``sized`` factory that does this
 switch-on-type behavior instead, keeping our adapters clean::
 
   class DocumentSized(object):
       def __init__(self, context):
           self.context = context
- 
+
       def sized(self):
           return len(self.context.text.encode('UTF-8'))
 
   class ImageSized(object):
       def __init__(self, context):
           self.context = context
- 
+
       def sized(self):
           return len(self.context.data)
 
   class ContainerSized(object):
       def __init__(self, context):
           self.context = context
- 
+
       def sized(self):
           total = 0
           for obj in self.context.values():
-              total += sized(obj).size() 
+              total += sized(obj).size()
           return total
-   
+
   def sized(context):
       if isinstance(context, Document):
           return DocumentedSized(context)
@@ -568,7 +589,7 @@
           return ImageSized(context)
       elif isinstance(context, Container):
           return ContainerSized(context)
-       
+
 We can now call ``sized`` for a content object and get an object back
 that implements the "sized API"::
 
@@ -595,21 +616,21 @@
   class DocumentSized(grok.Adapter):
       grok.context(Document)
       grok.provides(ISized)
- 
+
       def sized(self):
           return len(self.context.text.encode('UTF-8'))
 
   class ImageSized(grok.Adapter):
       grok.context(Image)
       grok.provides(ISized)
- 
+
       def sized(self):
           return len(self.context.data)
 
   class ContainerSized(grok.Adapter):
       grok.context(Container)
       grok.provides(ISized)
- 
+
       def sized(self):
           total = 0
           for obj in self.context.values():
@@ -646,7 +667,7 @@
 
   class Cow(object):
       grok.implements(IAnimal)
- 
+
       def __init__(self, name):
           self.name = name
 
@@ -700,7 +721,7 @@
       grok.name('somename')
       grok.context(SomeClass)
       grok.provides(ISomeInterface)
- 
+
 Actually all adapters are named: by default the name of an adapter is
 the empty string.
 
@@ -709,8 +730,8 @@
 ``zope.component`` package, in particular ``getAdapter``::
 
   from zope import component
-  
-  my_adapter = component.getAdapter(some_object, ISomeInterface, 
+
+  my_adapter = component.getAdapter(some_object, ISomeInterface,
                                    name='somename')
 
 ``getAdapter`` can also be used to look up unnamed adapters, as an
@@ -731,7 +752,7 @@
   class MyMultiAdapter(grok.MultiAdapter):
       grok.adapts(SomeClass, AnotherClass)
       grok.provides(ISomeInterface)
-   
+
       def __init__(some_instance, another_instance):
           self.some_interface = some_instance
           self.another_instance = another_instance
@@ -763,7 +784,7 @@
 A view in Grok is in fact a named multi adapter, providing the base
 interface (``Interface``). This means that a view in Grok can be
 looked up in code by the following call::
- 
+
   from zope.interface import Interface
 
   view = component.getMultiAdapter((object, request), Interface, name="index")
@@ -917,7 +938,7 @@
 
 Fired when an object was copied. It is a specialization of
 ``IObjectCreatedEvent`` that is fired by the system if you use the
-``zope.copypastemove`` functionality. 
+``zope.copypastemove`` functionality.
 
 Besides the ``object`` attribute it shares with
 ``IObjectCreattedEvent``, it has also has the ``original`` attribute,
@@ -951,7 +972,7 @@
 This subclassing from ``ObjectEvent`` is not required; if your event
 isn't about an object, you can choose to design your event class
 entirely yourself. See ``zope.sendmail`` for the construction of mail sending
-events for an example. 
+events for an example.
 
 Interfaces for events
 ~~~~~~~~~~~~~~~~~~~~~
@@ -990,7 +1011,7 @@
 
 * The system can inspect the interfaces a particular object provides,
   and treat them as an abstract form of classes for registration
-  purposes. 
+  purposes.
 
 Interfaces make it possible to use a generic framework's pluggability
 points with confidence: you can clearly see what you are supposed to
@@ -1060,7 +1081,7 @@
 
   class NonSubclassObjectEvent(object):
       grok.implements(IObjectEvent)
- 
+
       def __init__(self, object):
            self.object = object
 
@@ -1093,7 +1114,7 @@
       grok.provides(ISortedKeys)
 
       def sortedKeys(self):
-          return sorted(self.context.keys())     
+          return sorted(self.context.keys())
 
 Interfaces and views
 ~~~~~~~~~~~~~~~~~~~~
@@ -1105,7 +1126,7 @@
 
   class Keys(grok.View):
      grok.context(IContainer)
-     
+
      def render(self):
          return ', '.join(ISortedKeysAdapter(self.context).sortedKeys())
 
@@ -1119,7 +1140,7 @@
 
   class Layout(grok.View):
       grok.context(Interface)
- 
+
 with a template ``layout.pt`` associated to it.
 
 You can then use these macros in any page template anywhere by
@@ -1139,7 +1160,7 @@
 
   from zope.interface import Interface
   from zope import schema
-  
+
   class ISpecies(Interface):
       name = schema.TextLine(u"Animal species name")
       scientific_name = schema.TextLine(u"Scientific name")
@@ -1160,7 +1181,7 @@
 
   class Species(grok.Form):
       form_fields = grok.Fields(ISpecies)
-  
+
       @grok.action(u"Save form")
       def handle_save(self, **data):
           print data['name']
@@ -1203,7 +1224,7 @@
       grok.context(SpeciesContainer)
 
       form_fields = grok.Fields(ISpecies)
-      
+
       @grok.action(u"Add species")
       def add_species(self, **data):
           # create a species instance
@@ -1230,13 +1251,13 @@
 
   class Edit(grok.EditForm):
      grok.context(Species)
-    
+
      form_fields = grok.Fields(ISpecies)
 
      @grok.action(u"Edit species")
      def edit_species(self, **data):
           self.applyData(species, **data)
-     
+
 Forms are self-submitting, so this will show the edit form again. If
 you want to display another page, you can redirect the browser as we
 showed for the add form previously.
@@ -1252,7 +1273,7 @@
 
   class Display(grok.DisplayForm):
      grok.context(Species)
-    
+
      form_fields = grok.Fields(ISpecies)
 
 The user can now go to ``myspecies/display`` to look at the species.
@@ -1289,12 +1310,12 @@
         <!-- the title of the field -->
         <span i18n:translate="" tal:content="widget/label">label</span>
       </label>
-     
+
       <!-- render the HTML widget -->
       <div class="widget" tal:content="structure widget">
         <input type="text" />
       </div>
-      
+
       <!-- render any field specific validation error from a previous
            form submit next to the field -->
       <div class="error" tal:condition="widget/error">
@@ -1315,10 +1336,9 @@
     <tal:block content="widget/label" />
     <input tal:replace="structure widget" />
   </tal:block>
-   
+
   <!-- render all the action submit buttons -->
   <span class="actionButtons" tal:condition="view/availableActions">
     <input tal:repeat="action view/actions"
            tal:replace="structure action/render" />
   </span>
-

Modified: grok/trunk/doc/reference/directives.rst
===================================================================
--- grok/trunk/doc/reference/directives.rst	2008-05-02 10:00:28 UTC (rev 86073)
+++ grok/trunk/doc/reference/directives.rst	2008-05-02 10:04:41 UTC (rev 86074)
@@ -239,7 +239,7 @@
 
     class ModelBase(grok.Model):
         pass
- 
+
     class ViewBase(grok.View):
         def render(self):
             return "hello world"
@@ -463,7 +463,7 @@
 
     class ViewPainting(grok.Permission):
 	grok.name('grok.ViewPainting')
-	
+
   .. seealso::
 
     :class:`grok.Permission` component, :func:`@grok.require` decorator
@@ -535,3 +535,33 @@
 	grok.context(IMammoth)
 
 	name = index.Field()
+
+
+:func:`grok.traversable`
+========================
+
+A class level directive used to mark attributes or methods as traversable. An
+optional `name` argument can be used to give the attribute a different name in
+the URL.
+
+.. function:: grok.traversable(attr, name=None)
+
+  **Example**
+
+  .. code-block:: python
+
+
+      class Foo(grok.Model):
+          grok.traversable('bar')
+          grok.traversable('foo')
+          grok.traversable(attr='bar', name='namedbar')
+
+          def __init__(self, name):
+              self.name = name
+
+          foo = Bar('foo')
+          def bar(self):
+              return Bar('bar')
+
+The result is that you can now access http://localhost/foo/bar,
+http://localhost/foo/foo and http://localhost/foo/namedbar.
\ No newline at end of file

Modified: grok/trunk/src/grok/__init__.py
===================================================================
--- grok/trunk/src/grok/__init__.py	2008-05-02 10:00:28 UTC (rev 86073)
+++ grok/trunk/src/grok/__init__.py	2008-05-02 10:04:41 UTC (rev 86074)
@@ -48,7 +48,7 @@
     context, name, title, provides, baseclass, global_utility, direct, order)
 from grok.directive import (
     template, templatedir, local_utility, permissions, require, site,
-    layer, viewletmanager, view)
+    layer, viewletmanager, view, traversable)
 from grokcore.component.decorators import subscribe, adapter, implementer
 from martian.error import GrokError, GrokImportError
 

Modified: grok/trunk/src/grok/components.py
===================================================================
--- grok/trunk/src/grok/components.py	2008-05-02 10:00:28 UTC (rev 86073)
+++ grok/trunk/src/grok/components.py	2008-05-02 10:04:41 UTC (rev 86074)
@@ -436,6 +436,14 @@
         if subob is not None:
             return util.safely_locate_maybe(subob, self.context, name)
 
+        traversable_dict = getattr(self.context, '__grok_traversable__', None)
+        if traversable_dict:
+            if name in traversable_dict:
+                subob = getattr(self.context, traversable_dict[name])
+                if callable(subob):
+                    subob = subob()
+                return util.safely_locate_maybe(subob, self.context, name)
+
         # XXX Special logic here to deal with containers.  It would be
         # good if we wouldn't have to do this here. One solution is to
         # rip this out and make you subclass ContainerTraverser if you

Modified: grok/trunk/src/grok/directive.py
===================================================================
--- grok/trunk/src/grok/directive.py	2008-05-02 10:00:28 UTC (rev 86073)
+++ grok/trunk/src/grok/directive.py	2008-05-02 10:04:41 UTC (rev 86074)
@@ -17,7 +17,7 @@
 from zope.interface.interfaces import IInterface
 
 from martian.error import GrokImportError
-from martian.directive import (OnceDirective,
+from martian.directive import (Directive, OnceDirective,
                                MultipleTimesDirective, BaseTextDirective,
                                SingleValue, SingleTextDirective,
                                MultipleTextDirective,
@@ -56,8 +56,25 @@
         self.name_in_container = name_in_container
 
 
+class MultipleTimesAsDictDirective(Directive):
+    def store(self, frame, value):
+        values = frame.f_locals.get(self.local_name, {})
+        values[value[1]] = value[0]
+        frame.f_locals[self.local_name] = values
+
+
+class TraversableDirective(MultipleTimesAsDictDirective):
+    def check_argument_signature(self, attr, name=None):
+        pass
+    def check_arguments(self, attr, name=None):
+        pass
+    def value_factory(self, attr, name=None):
+        if name is None:
+            name = attr
+        return (attr, name)
+
+
 class RequireDirective(SingleValue, MultipleTimesDirective):
-
     def check_arguments(self, value):
         if util.check_subclass(value, components.Permission):
             return
@@ -100,3 +117,4 @@
                                            ClassOrModuleDirectiveContext())
 view = InterfaceOrClassDirective('grok.view',
                                  ClassOrModuleDirectiveContext())
+traversable = TraversableDirective('grok.traversable', ClassDirectiveContext())

Copied: grok/trunk/src/grok/ftests/traversal/traversableattr.py (from rev 86064, grok/branches/jspaans-traversableattrs/src/grok/ftests/traversal/traversableattr.py)
===================================================================
--- grok/trunk/src/grok/ftests/traversal/traversableattr.py	                        (rev 0)
+++ grok/trunk/src/grok/ftests/traversal/traversableattr.py	2008-05-02 10:04:41 UTC (rev 86074)
@@ -0,0 +1,67 @@
+"""
+Models can expose attributes using the grok.traversable directive.
+
+  >>> getRootFolder()["traversefoo"] = Foo('foo')
+
+  >>> from zope.testbrowser.testing import Browser
+  >>> browser = Browser()
+  >>> browser.handleErrors = False
+
+As always, we can access a model with a view::
+  >>> browser.open("http://localhost/traversefoo/")
+  >>> print browser.contents
+  foo
+
+'foo' is an exposed attribute, so it should be accessible::
+  >>> browser.open("http://localhost/traversefoo/foo")
+  >>> print browser.contents
+  foo
+
+'bar' is an exposed method, and should also be accessible::
+  >>> browser.open("http://localhost/traversefoo/bar")
+  >>> print browser.contents
+  bar
+
+'bar' is also exposed under the name 'namedbar', and can also be accessed::
+  >>> browser.open("http://localhost/traversefoo/namedbar")
+  >>> print browser.contents
+  bar
+
+Finally, attributes which are not exposed, should not be visible:
+  >>> browser.open("http://localhost/traversefoo/z")
+  Traceback (most recent call last):
+  ...
+  NotFound: ...
+
+"""
+import grok
+
+class Bar(grok.Model):
+    def __init__(self, name):
+        self.name = name
+
+class BarIndex(grok.View):
+    grok.context(Bar)
+    grok.name('index')
+
+    def render(self):
+        return self.context.name
+
+class Foo(grok.Model):
+    grok.traversable('bar')
+    grok.traversable('foo')
+    grok.traversable(attr='bar', name='namedbar')
+
+    def __init__(self, name):
+        self.name = name
+
+    foo = Bar('foo')
+    def bar(self):
+        return Bar('bar')
+    z = "i'm not called"
+
+class FooIndex(grok.View):
+    grok.context(Foo)
+    grok.name('index')
+    def render(self):
+        return self.context.name

Copied: grok/trunk/src/grok/tests/directive/multipleasdict.py (from rev 86064, grok/branches/jspaans-traversableattrs/src/grok/tests/directive/multipleasdict.py)
===================================================================
--- grok/trunk/src/grok/tests/directive/multipleasdict.py	                        (rev 0)
+++ grok/trunk/src/grok/tests/directive/multipleasdict.py	2008-05-02 10:04:41 UTC (rev 86074)
@@ -0,0 +1,27 @@
+"""
+The MultipleTimesAsDictDirective is used by grok.traversable so multiple
+attributes can be mentioned.
+
+  >>> from martian import scan
+  >>> from grok.tests.directive import multipleasdict
+  >>> module_info = scan.module_info_from_module(multipleasdict)
+
+  >>> g = Club.__grok_traversable__
+  >>> isinstance(g, dict)
+  True
+  >>> g['demo']
+  'demo'
+  >>> g['attr']
+  'attr'
+  >>> g['asdf']
+  'attr'
+"""
+import grok
+from zope import interface
+
+class Club(grok.Model):
+    grok.traversable('asdf', name='attr')
+    grok.traversable('attr')
+    grok.traversable('attr', name='asdf')
+    grok.traversable('demo')
+    demo = 'something'

Modified: grok/trunk/src/grok/util.py
===================================================================
--- grok/trunk/src/grok/util.py	2008-05-02 10:00:28 UTC (rev 86073)
+++ grok/trunk/src/grok/util.py	2008-05-02 10:04:41 UTC (rev 86074)
@@ -89,7 +89,7 @@
             if isinstance(v, unicode):
                 data[k] = v.encode('utf-8')
             if isinstance(v, (list, set, tuple)):
-                data[k] = [isinstance(item, unicode) and item.encode('utf-8') 
+                data[k] = [isinstance(item, unicode) and item.encode('utf-8')
                 or item for item in v]
         url += '?' + urllib.urlencode(data, doseq=True)
     return url



More information about the Checkins mailing list