Merge lp:~barry/lazr.restful/387487-subordinate into lp:lazr.restful

Proposed by Barry Warsaw
Status: Merged
Approved by: Barry Warsaw
Approved revision: 46
Merge reported by: Barry Warsaw
Merged at revision: not available
Proposed branch: lp:~barry/lazr.restful/387487-subordinate
Merge into: lp:lazr.restful
Diff against target: None lines
To merge this branch: bzr merge lp:~barry/lazr.restful/387487-subordinate
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+9263@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Barry Warsaw (barry) wrote :

This fixes lazr.restful default navigation so that subentries which implement the IObject interface can be found. For Launchpad, this will allow ~team/mailman_list to be traversed, since mailing lists only live under the ~team.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Barry.

Thanks for undertaking this effort. Since your cover letter was sparce, and the inline documentation is too, I am puzzled by the complexity of the issue described in the pub and simplicity of your solution.

> === modified file 'src/lazr/restful/publisher.py'
> --- src/lazr/restful/publisher.py 2009-03-27 04:25:34 +0000
> +++ src/lazr/restful/publisher.py 2009-07-24 21:16:28 +0000
...

> def traverseName(self, request, ob, name):
> """See `zope.publisher.interfaces.IPublication`.
> @@ -66,6 +66,12 @@
> elif IBytes.providedBy(field):
> return self._traverseToByteStorage(
> request, entry, field, name)
> + elif IObject.providedBy(field):
> + sub_entry = getattr(entry, name, None)
> + if sub_entry is None:
> + raise NotFound(ob, name, request)
> + else:
> + return sub_entry
> elif field is not None:
> return EntryField(entry, field, name)
> else:

This appears to be an easy fix for a hard problem. The problem stated in
the bug is that the suborinate object and field conflict in the name space,
but there is no conflict detection here. Are there any IObject
fields that we expect to be a field? I am sure you thought about this
since you decided this is the right solution.

> === added file 'src/lazr/restful/tests/test_navigation.py'
> --- src/lazr/restful/tests/test_navigation.py 1970-01-01 00:00:00 +0000
> +++ src/lazr/restful/tests/test_navigation.py 2009-07-24 21:16:28 +0000

...

> +class Child:
> + implements(IChildEntry)
> + schema = IChild
> +

I see trailing whitespace.

...

> +class FakePublication:
> + """A fake superclass publication."""
> + def traverseName(self, request, ob, name):
> + return 'no such name: ' + name

I am confused by this. I do not see why this is need below. Are
you testing that the FakePublication.traverseName() is never reached?

> +class NavigationPublication(WebServicePublicationMixin, FakePublication):
> + pass
> +
> +
> +class NavigationTestCase(unittest.TestCase):
> +
> + def test_toplevel_navigation(self):
> + # Test that publication can reach sub-entries.
> + publication = NavigationPublication()
> + obj = publication.traverseName(FakeRequest(), Parent(), 'child')
> + self.assertEqual(obj.one, 'one')
> +
> + def test_toplevel_navigation_without_subentry(self):
> + # Test that publication raises NotFound when subentry attribute
> + # returns None.
> + parent = Parent()
> + parent.child = None
> + publication = NavigationPublication()
> + self.assertRaises(
> + NotFound, publication.traverseName,
> + FakeRequest(), parent, 'child')

review: Needs Fixing (code)
Revision history for this message
Barry Warsaw (barry) wrote :
Download full text (4.2 KiB)

On Jul 25, 2009, at 03:22 AM, Curtis Hovey wrote:

>Thanks for undertaking this effort. Since your cover letter was sparce, and the inline documentation is too, I am puzzled by the complexity of the issue described in the pub and simplicity of your solution.

Hi Curtis, thanks for the review.

My understanding is that the core problem described in the bug report is
caused by a lack of navigation in the publisher. I hit the same problem in
Launchpad trying to navigate from a team to a mailing list. Leonard has not
verified that the bug reported here is the same one I encountered.

Unfortunately, this is a situation where the solution came before the test.
While trying to figure out what was going wrong in Launchpad, Leonard and I
came up with the simple solution you see below. The problem was in crafting a
test for the change. It was a rats nest of lots of additional code to test
the change at the end-user visible layer. Trying to add a subordinate object
to the test scenario laid out in the doctests is far from easy. You have to
add the object, a schema, an entry interface, adapters, etc.

The test that I added might be too narrow, but it does tests the specific code
added. As you observed in irc, there really aren't any direct tests of
navigation that we could find.

>> === modified file 'src/lazr/restful/publisher.py'
>> --- src/lazr/restful/publisher.py 2009-03-27 04:25:34 +0000
>> +++ src/lazr/restful/publisher.py 2009-07-24 21:16:28 +0000
>...
>
>> def traverseName(self, request, ob, name):
>> """See `zope.publisher.interfaces.IPublication`.
>> @@ -66,6 +66,12 @@
>> elif IBytes.providedBy(field):
>> return self._traverseToByteStorage(
>> request, entry, field, name)
>> + elif IObject.providedBy(field):
>> + sub_entry = getattr(entry, name, None)
>> + if sub_entry is None:
>> + raise NotFound(ob, name, request)
>> + else:
>> + return sub_entry
>> elif field is not None:
>> return EntryField(entry, field, name)
>> else:
>
>This appears to be an easy fix for a hard problem. The problem stated in
>the bug is that the suborinate object and field conflict in the name space,
>but there is no conflict detection here. Are there any IObject
>fields that we expect to be a field? I am sure you thought about this
>since you decided this is the right solution.

How would the subordinate object and field conflict? The attribute would have
to be accessed via a single name in the parent object. Maybe I misunderstand
what you mean by "conflict".

>> === added file 'src/lazr/restful/tests/test_navigation.py'
>> --- src/lazr/restful/tests/test_navigation.py 1970-01-01 00:00:00 +0000
>> +++ src/lazr/restful/tests/test_navigation.py 2009-07-24 21:16:28 +0000
>
>...
>
>> +class Child:
>> + implements(IChildEntry)
>> + schema = IChild
>> +
>
>I see trailing whitespace.

Fixed.

>
>...
>
>> +class FakePublication:
>> + """A fake superclass publication."""
>> + def traverseName(self, request, ...

Read more...

45. By Barry Warsaw

reviewer comments

Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Barry.

Thanks for the explanation of the complication. I think, given my new understanding. Thie code is fine to land. Your test solution is the right way to this problem. There may be some tests to that should also be unittest instead of contrived documentation.

review: Approve (code)
Revision history for this message
Curtis Hovey (sinzui) wrote :

Barry.

This branch needs to be merged into the launchpad branch and the official branch:
lp:~launchpad-pqm/lazr.restful/trunk and lp:lazr.restful. Please update
src/lazr/restful/NEWS.txt with your change when you merge your official branch.

46. By Barry Warsaw

Merge upstream r45. Add NEWS.txt entry.

Revision history for this message
Barry Warsaw (barry) wrote :

> Barry.
>
> This branch needs to be merged into the launchpad branch and the official
> branch:
> lp:~launchpad-pqm/lazr.restful/trunk and lp:lazr.restful. Please update
> src/lazr/restful/NEWS.txt with your change when you merge your official
> branch.

Hi Curtis. I merged this into lp:lazr.restful but my current email situation will not allow me to email pqm so I can't request a merge into there. Could you do that for me?

Also, what's the ETA for running Launchpad from released lazr.restful egg?

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/restful/publisher.py'
2--- src/lazr/restful/publisher.py 2009-03-27 04:25:34 +0000
3+++ src/lazr/restful/publisher.py 2009-07-24 21:16:28 +0000
4@@ -20,26 +20,26 @@
5 from zope.interface import alsoProvides, implementer, implements
6 from zope.publisher.interfaces import NotFound
7 from zope.publisher.interfaces.browser import IBrowserRequest
8-from zope.schema.interfaces import IBytes
9+from zope.schema.interfaces import IBytes, IObject
10 from zope.security.checker import ProxyFactory
11
12 from lazr.uri import URI
13
14+from lazr.restful import (
15+ CollectionResource, EntryField, EntryFieldResource, EntryResource,
16+ ScopedCollection)
17 from lazr.restful.interfaces import (
18- IByteStorage, ICollection, IEntry, IEntryField, IHTTPResource,
19- IWebBrowserInitiatedRequest, IWebServiceClientRequest,
20- IWebServiceConfiguration, ICollectionField)
21-from lazr.restful import (
22- CollectionResource, EntryField, EntryFieldResource,
23- EntryResource, ScopedCollection)
24+ IByteStorage, ICollection, ICollectionField, IEntry, IEntryField,
25+ IHTTPResource, IWebBrowserInitiatedRequest, IWebServiceClientRequest,
26+ IWebServiceConfiguration)
27
28
29 class WebServicePublicationMixin:
30 """A mixin for webservice publication.
31
32 This should usually be mixed-in with ZopePublication, or Browser,
33- or HTTPPublication"""
34-
35+ or HTTPPublication.
36+ """
37
38 def traverseName(self, request, ob, name):
39 """See `zope.publisher.interfaces.IPublication`.
40@@ -66,6 +66,12 @@
41 elif IBytes.providedBy(field):
42 return self._traverseToByteStorage(
43 request, entry, field, name)
44+ elif IObject.providedBy(field):
45+ sub_entry = getattr(entry, name, None)
46+ if sub_entry is None:
47+ raise NotFound(ob, name, request)
48+ else:
49+ return sub_entry
50 elif field is not None:
51 return EntryField(entry, field, name)
52 else:
53
54=== added file 'src/lazr/restful/tests/test_navigation.py'
55--- src/lazr/restful/tests/test_navigation.py 1970-01-01 00:00:00 +0000
56+++ src/lazr/restful/tests/test_navigation.py 2009-07-24 21:16:28 +0000
57@@ -0,0 +1,92 @@
58+# Copyright 2008 Canonical Ltd. All rights reserved.
59+
60+"""Tests of lazr.restful navigation."""
61+
62+__metaclass__ = type
63+
64+import unittest
65+
66+from zope.interface import Interface, implements
67+from zope.publisher.interfaces import NotFound
68+from zope.schema import Text, Object
69+
70+from lazr.restful.interfaces import IEntry
71+from lazr.restful.publisher import WebServicePublicationMixin
72+
73+
74+class IChild(Interface):
75+ one = Text(title=u'One')
76+ two = Text(title=u'Two')
77+
78+
79+class IChildEntry(IChild, IEntry):
80+ pass
81+
82+
83+class IParent(Interface):
84+ three = Text(title=u'Three')
85+ child = Object(schema=IChild)
86+
87+
88+class IParentEntry(IParent, IEntry):
89+ pass
90+
91+
92+class Child:
93+ implements(IChildEntry)
94+ schema = IChild
95+
96+ one = u'one'
97+ two = u'two'
98+
99+
100+class Parent:
101+ implements(IParentEntry)
102+ schema = IParent
103+
104+ three = u'three'
105+ child = Child()
106+
107+ @property
108+ def context(self):
109+ return self
110+
111+
112+class FakeRequest:
113+ """A fake request satisfying `traverseName()`."""
114+
115+ def getTraversalStack(self):
116+ return ()
117+
118+
119+class FakePublication:
120+ """A fake superclass publication."""
121+ def traverseName(self, request, ob, name):
122+ return 'no such name: ' + name
123+
124+
125+class NavigationPublication(WebServicePublicationMixin, FakePublication):
126+ pass
127+
128+
129+class NavigationTestCase(unittest.TestCase):
130+
131+ def test_toplevel_navigation(self):
132+ # Test that publication can reach sub-entries.
133+ publication = NavigationPublication()
134+ obj = publication.traverseName(FakeRequest(), Parent(), 'child')
135+ self.assertEqual(obj.one, 'one')
136+
137+ def test_toplevel_navigation_without_subentry(self):
138+ # Test that publication raises NotFound when subentry attribute
139+ # returns None.
140+ parent = Parent()
141+ parent.child = None
142+ publication = NavigationPublication()
143+ self.assertRaises(
144+ NotFound, publication.traverseName,
145+ FakeRequest(), parent, 'child')
146+
147+
148+def additional_tests():
149+ return unittest.TestLoader().loadTestsFromName(__name__)

Subscribers

People subscribed via source and target branches