Merge lp:~barry/lazr.restful/387487-subordinate into lp:lazr.restful
- 387487-subordinate
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Curtis Hovey (community) | code | Approve | |
Review via email: mp+9263@code.launchpad.net |
Commit message
Description of the change
Barry Warsaw (barry) wrote : | # |
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/
> --- src/lazr/
> +++ src/lazr/
...
> def traverseName(self, request, ob, name):
> """See `zope.publisher
> @@ -66,6 +66,12 @@
> elif IBytes.
> return self._traverseT
> request, entry, field, name)
> + elif IObject.
> + 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/
> --- src/lazr/
> +++ src/lazr/
...
> +class Child:
> + implements(
> + 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
> +class NavigationPubli
> + pass
> +
> +
> +class NavigationTestC
> +
> + def test_toplevel_
> + # Test that publication can reach sub-entries.
> + publication = NavigationPubli
> + obj = publication.
> + self.assertEqua
> +
> + def test_toplevel_
> + # Test that publication raises NotFound when subentry attribute
> + # returns None.
> + parent = Parent()
> + parent.child = None
> + publication = NavigationPubli
> + self.assertRaises(
> + NotFound, publication.
> + FakeRequest(), parent, 'child')
Barry Warsaw (barry) wrote : | # |
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/
>> --- src/lazr/
>> +++ src/lazr/
>...
>
>> def traverseName(self, request, ob, name):
>> """See `zope.publisher
>> @@ -66,6 +66,12 @@
>> elif IBytes.
>> return self._traverseT
>> request, entry, field, name)
>> + elif IObject.
>> + 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/
>> --- src/lazr/
>> +++ src/lazr/
>
>...
>
>> +class Child:
>> + implements(
>> + schema = IChild
>> +
>
>I see trailing whitespace.
Fixed.
>
>...
>
>> +class FakePublication:
>> + """A fake superclass publication."""
>> + def traverseName(self, request, ...
- 45. By Barry Warsaw
-
reviewer comments
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.
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/
- 46. By Barry Warsaw
-
Merge upstream r45. Add NEWS.txt entry.
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/
> 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
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 | 20 | from zope.interface import alsoProvides, implementer, implements | 20 | from zope.interface import alsoProvides, implementer, implements |
6 | 21 | from zope.publisher.interfaces import NotFound | 21 | from zope.publisher.interfaces import NotFound |
7 | 22 | from zope.publisher.interfaces.browser import IBrowserRequest | 22 | from zope.publisher.interfaces.browser import IBrowserRequest |
9 | 23 | from zope.schema.interfaces import IBytes | 23 | from zope.schema.interfaces import IBytes, IObject |
10 | 24 | from zope.security.checker import ProxyFactory | 24 | from zope.security.checker import ProxyFactory |
11 | 25 | 25 | ||
12 | 26 | from lazr.uri import URI | 26 | from lazr.uri import URI |
13 | 27 | 27 | ||
14 | 28 | from lazr.restful import ( | ||
15 | 29 | CollectionResource, EntryField, EntryFieldResource, EntryResource, | ||
16 | 30 | ScopedCollection) | ||
17 | 28 | from lazr.restful.interfaces import ( | 31 | from lazr.restful.interfaces import ( |
24 | 29 | IByteStorage, ICollection, IEntry, IEntryField, IHTTPResource, | 32 | IByteStorage, ICollection, ICollectionField, IEntry, IEntryField, |
25 | 30 | IWebBrowserInitiatedRequest, IWebServiceClientRequest, | 33 | IHTTPResource, IWebBrowserInitiatedRequest, IWebServiceClientRequest, |
26 | 31 | IWebServiceConfiguration, ICollectionField) | 34 | IWebServiceConfiguration) |
21 | 32 | from lazr.restful import ( | ||
22 | 33 | CollectionResource, EntryField, EntryFieldResource, | ||
23 | 34 | EntryResource, ScopedCollection) | ||
27 | 35 | 35 | ||
28 | 36 | 36 | ||
29 | 37 | class WebServicePublicationMixin: | 37 | class WebServicePublicationMixin: |
30 | 38 | """A mixin for webservice publication. | 38 | """A mixin for webservice publication. |
31 | 39 | 39 | ||
32 | 40 | This should usually be mixed-in with ZopePublication, or Browser, | 40 | This should usually be mixed-in with ZopePublication, or Browser, |
35 | 41 | or HTTPPublication""" | 41 | or HTTPPublication. |
36 | 42 | 42 | """ | |
37 | 43 | 43 | ||
38 | 44 | def traverseName(self, request, ob, name): | 44 | def traverseName(self, request, ob, name): |
39 | 45 | """See `zope.publisher.interfaces.IPublication`. | 45 | """See `zope.publisher.interfaces.IPublication`. |
40 | @@ -66,6 +66,12 @@ | |||
41 | 66 | elif IBytes.providedBy(field): | 66 | elif IBytes.providedBy(field): |
42 | 67 | return self._traverseToByteStorage( | 67 | return self._traverseToByteStorage( |
43 | 68 | request, entry, field, name) | 68 | request, entry, field, name) |
44 | 69 | elif IObject.providedBy(field): | ||
45 | 70 | sub_entry = getattr(entry, name, None) | ||
46 | 71 | if sub_entry is None: | ||
47 | 72 | raise NotFound(ob, name, request) | ||
48 | 73 | else: | ||
49 | 74 | return sub_entry | ||
50 | 69 | elif field is not None: | 75 | elif field is not None: |
51 | 70 | return EntryField(entry, field, name) | 76 | return EntryField(entry, field, name) |
52 | 71 | else: | 77 | else: |
53 | 72 | 78 | ||
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 | 1 | # Copyright 2008 Canonical Ltd. All rights reserved. | ||
59 | 2 | |||
60 | 3 | """Tests of lazr.restful navigation.""" | ||
61 | 4 | |||
62 | 5 | __metaclass__ = type | ||
63 | 6 | |||
64 | 7 | import unittest | ||
65 | 8 | |||
66 | 9 | from zope.interface import Interface, implements | ||
67 | 10 | from zope.publisher.interfaces import NotFound | ||
68 | 11 | from zope.schema import Text, Object | ||
69 | 12 | |||
70 | 13 | from lazr.restful.interfaces import IEntry | ||
71 | 14 | from lazr.restful.publisher import WebServicePublicationMixin | ||
72 | 15 | |||
73 | 16 | |||
74 | 17 | class IChild(Interface): | ||
75 | 18 | one = Text(title=u'One') | ||
76 | 19 | two = Text(title=u'Two') | ||
77 | 20 | |||
78 | 21 | |||
79 | 22 | class IChildEntry(IChild, IEntry): | ||
80 | 23 | pass | ||
81 | 24 | |||
82 | 25 | |||
83 | 26 | class IParent(Interface): | ||
84 | 27 | three = Text(title=u'Three') | ||
85 | 28 | child = Object(schema=IChild) | ||
86 | 29 | |||
87 | 30 | |||
88 | 31 | class IParentEntry(IParent, IEntry): | ||
89 | 32 | pass | ||
90 | 33 | |||
91 | 34 | |||
92 | 35 | class Child: | ||
93 | 36 | implements(IChildEntry) | ||
94 | 37 | schema = IChild | ||
95 | 38 | |||
96 | 39 | one = u'one' | ||
97 | 40 | two = u'two' | ||
98 | 41 | |||
99 | 42 | |||
100 | 43 | class Parent: | ||
101 | 44 | implements(IParentEntry) | ||
102 | 45 | schema = IParent | ||
103 | 46 | |||
104 | 47 | three = u'three' | ||
105 | 48 | child = Child() | ||
106 | 49 | |||
107 | 50 | @property | ||
108 | 51 | def context(self): | ||
109 | 52 | return self | ||
110 | 53 | |||
111 | 54 | |||
112 | 55 | class FakeRequest: | ||
113 | 56 | """A fake request satisfying `traverseName()`.""" | ||
114 | 57 | |||
115 | 58 | def getTraversalStack(self): | ||
116 | 59 | return () | ||
117 | 60 | |||
118 | 61 | |||
119 | 62 | class FakePublication: | ||
120 | 63 | """A fake superclass publication.""" | ||
121 | 64 | def traverseName(self, request, ob, name): | ||
122 | 65 | return 'no such name: ' + name | ||
123 | 66 | |||
124 | 67 | |||
125 | 68 | class NavigationPublication(WebServicePublicationMixin, FakePublication): | ||
126 | 69 | pass | ||
127 | 70 | |||
128 | 71 | |||
129 | 72 | class NavigationTestCase(unittest.TestCase): | ||
130 | 73 | |||
131 | 74 | def test_toplevel_navigation(self): | ||
132 | 75 | # Test that publication can reach sub-entries. | ||
133 | 76 | publication = NavigationPublication() | ||
134 | 77 | obj = publication.traverseName(FakeRequest(), Parent(), 'child') | ||
135 | 78 | self.assertEqual(obj.one, 'one') | ||
136 | 79 | |||
137 | 80 | def test_toplevel_navigation_without_subentry(self): | ||
138 | 81 | # Test that publication raises NotFound when subentry attribute | ||
139 | 82 | # returns None. | ||
140 | 83 | parent = Parent() | ||
141 | 84 | parent.child = None | ||
142 | 85 | publication = NavigationPublication() | ||
143 | 86 | self.assertRaises( | ||
144 | 87 | NotFound, publication.traverseName, | ||
145 | 88 | FakeRequest(), parent, 'child') | ||
146 | 89 | |||
147 | 90 | |||
148 | 91 | def additional_tests(): | ||
149 | 92 | return unittest.TestLoader().loadTestsFromName(__name__) |
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.