[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