The key used in the original implementation is not unique, as it doesn't include the context. Also, the key uses a class attribute (_baseclassname) which is not necessarily defined in subclasses, so should not be used there.
The diff below addresses these two issues and also fixes one test that was using a FakeRequest which didn't provide the 'annotations' attribute, so the new code was crashing when attempting to cache stuff.
=== modified file 'lib/canonical/launchpad/doc/menus.txt'
--- lib/canonical/launchpad/doc/menus.txt 2009-06-11 16:02:25 +0000
+++ lib/canonical/launchpad/doc/menus.txt 2009-08-05 15:39:02 +0000
@@ -339,7 +339,6 @@
# We need to use a real launchpad test request so the view adapter
# lookups will work. That request also needs to implement
# IParticipation so that the login machinery will work.
-
>>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
>>> from zope.security.interfaces import IParticipation
>>> class InteractiveTestRequest(LaunchpadTestRequest):
@@ -354,7 +353,6 @@
<a class="menu-link-None sprite icon" title="summary">text--></a>
# Clean up our special login.
-
>>> login(ANONYMOUS)
@@ -436,6 +434,7 @@
... self.query = query
... self.url1 = url1 # returned from getURL(1)
... self.method = 'GET'
+ ... self.annotations = {}
...
... def getURL(self, level=0):
... assert 0 <= level <=1, 'level must be 0 or 1'
@@ -470,9 +469,9 @@
The following request is for a url that is different from all of the
facet menu links for 'sesamestreet'. So, all links will be linked.
=== modified file 'lib/canonical/launchpad/webapp/menu.py'
--- lib/canonical/launchpad/webapp/menu.py 2009-08-04 20:07:33 +0000
+++ lib/canonical/launchpad/webapp/menu.py 2009-08-05 16:42:49 +0000
@@ -237,7 +237,7 @@
# would be no reference to the Link object, which would allow it
# to be garbage collected during the course of the request.
cache = request.annotations.setdefault(MENU_ANNOTATION_KEY, {})
- key = (self._baseclassname, name)
+ key = (self.__class__, self.context, name)
link = cache.get(key)
if link is None: link = self._buildLink(name)
+ def test_cache_key_is_unique(self):
+ # The cache key must include the link name, the context of the link
+ # and the class where the link is defined.
+ login(ANONYMOUS)
+ context = object()
+ menu = TestMenu(context)
+ link = menu._get_link('test_link')
+ cache = get_current_browser_request().annotations.get(
+ MENU_ANNOTATION_KEY)
+ self.assertEquals(len(cache.keys()), 1)
+ self.assertContentEqual(
+ cache.keys()[0], (menu.__class__, context, 'test_link'))
+
The key used in the original implementation is not unique, as it doesn't include the context. Also, the key uses a class attribute (_baseclassname) which is not necessarily defined in subclasses, so should not be used there.
The diff below addresses these two issues and also fixes one test that was using a FakeRequest which didn't provide the 'annotations' attribute, so the new code was crashing when attempting to cache stuff.
=== modified file 'lib/canonical/ launchpad/ doc/menus. txt' launchpad/ doc/menus. txt 2009-06-11 16:02:25 +0000 launchpad/ doc/menus. txt 2009-08-05 15:39:02 +0000 launchpad. webapp. servers import LaunchpadTestRe quest interfaces import IParticipation Request( LaunchpadTestRe quest): menu-link- None sprite icon" title=" summary" >text-- ></a>
--- lib/canonical/
+++ lib/canonical/
@@ -339,7 +339,6 @@
# We need to use a real launchpad test request so the view adapter
# lookups will work. That request also needs to implement
# IParticipation so that the login machinery will work.
-
>>> from canonical.
>>> from zope.security.
>>> class InteractiveTest
@@ -354,7 +353,6 @@
<a class="
# Clean up our special login.
-
>>> login(ANONYMOUS)
@@ -436,6 +434,7 @@
... self.query = query
... self.url1 = url1 # returned from getURL(1)
... self.method = 'GET'
+ ... self.annotations = {}
...
... def getURL(self, level=0):
... assert 0 <= level <=1, 'level must be 0 or 1'
@@ -470,9 +469,9 @@
The following request is for a url that is different from all of the
facet menu links for 'sesamestreet'. So, all links will be linked.
- >>> request = URI('http:// launchpad. dev/sesamestree t/+xxx') launchpad. dev/sesamestree t/+xxx') iterlinks( request) : iterlinks( request_ uri): launchpad. dev/sesamestree t/+foo True launchpad. dev/sesamestree t/+bar True
+ >>> request_uri = URI('http://
>>> facets = Facets(street)
- >>> for link in facets.
+ >>> for link in facets.
... print link.url, link.linked
http://
http://
@@ -480,19 +479,19 @@
This next request is for +foo, but with a query string. So, both links
will still be linked.
- >>> request = FakeRequest( launchpad. dev/sesamestree t/+foo', query="123") launchpad. dev/sesamestree t/+foo? a=123') iterlinks( request_ uri): launchpad. dev/sesamestree t/+foo True launchpad. dev/sesamestree t/+bar True
- ... 'http://
+ >>> request_uri = URI(
+ ... 'http://
>>> facets = Facets(street)
- >>> for link in facets.iterlinks():
+ >>> for link in facets.
... print link.url, link.linked
http://
http://
Next, we'll use the link +bar. The +bar link will not be linked.
- >>> request = URI('http:// launchpad. dev/sesamestree t/+bar') launchpad. dev/sesamestree t/+bar') iterlinks( request) : iterlinks( request_ uri): launchpad. dev/sesamestree t/+foo True launchpad. dev/sesamestree t/+bar False
+ >>> request_uri = URI('http://
>>> facets = Facets(street)
- >>> for link in facets.
+ >>> for link in facets.
... print link.url, link.linked
http://
http://
@@ -500,9 +499,9 @@
Now, we use +bar with a trailing slash. The +bar link will still be not
linked.
- >>> request = URI('http:// launchpad. dev/sesamestree t/+bar/ ') launchpad. dev/sesamestree t/+bar/ ') iterlinks( request) : iterlinks( request_ uri): launchpad. dev/sesamestree t/+foo True launchpad. dev/sesamestree t/+bar False
+ >>> request_uri = URI('http://
>>> facets = Facets(street)
- >>> for link in facets.
+ >>> for link in facets.
... print link.url, link.linked
http://
http://
@@ -527,9 +526,9 @@
... 'More explanation about Baz of %s' % self.context.name)
... return Link(target, text, summary)
- >>> request = URI('http:// launchpad. dev/sesamestree t/+bar') launchpad. dev/sesamestree t/+bar') Facets( street) iterlinks( request) : iterlinks( request_ uri): launchpad. dev/sesamestree t True launchpad. dev/sesamestree t/+bar False url(street) launchpad. dev/sesamestree t'
+ >>> request_uri = URI('http://
>>> facets = BlankTargetTest
- >>> for link in facets.
+ >>> for link in facets.
... print link.url, link.linked
http://
http://
@@ -588,10 +587,10 @@
>>> canonical_
u'http://
- >>> request = URI('http:// launchpad. dev/sesamestree t') launchpad. dev/sesamestree t')
+ >>> request_uri = URI('http://
>>> facets = AbsoluteUrlTarg etTestFacets( street) iterlinks( request) : iterlinks( request_ uri): launchpad. dev/sesamestree t False example. com/barbarbar True
- >>> for link in facets.
+ >>> for link in facets.
... print link.url, link.linked
http://
ftp://barlink.
=== modified file 'lib/canonical/ launchpad/ webapp/ menu.py' launchpad/ webapp/ menu.py 2009-08-04 20:07:33 +0000 launchpad/ webapp/ menu.py 2009-08-05 16:42:49 +0000 annotations. setdefault( MENU_ANNOTATION _KEY, {}) baseclassname, name)
link = self._buildLink (name)
--- lib/canonical/
+++ lib/canonical/
@@ -237,7 +237,7 @@
# would be no reference to the Link object, which would allow it
# to be garbage collected during the course of the request.
cache = request.
- key = (self._
+ key = (self.__class__, self.context, name)
link = cache.get(key)
if link is None:
=== modified file 'lib/canonical/ launchpad/ webapp/ tests/test_ menu.py' launchpad/ webapp/ tests/test_ menu.py 2009-08-03 20:38:34 +0000 launchpad/ webapp/ tests/test_ menu.py 2009-08-05 16:52:42 +0000
--- lib/canonical/
+++ lib/canonical/
@@ -16,7 +16,6 @@
class TestMenu(MenuBase):
links = ['test_link']
- _baseclassname = 'TestMenu'
times_called = 0
def test_link(self): annotations. get(MENU_ ANNOTATION_ KEY)
self. assertEquals( [link], cache.values())
@@ -65,6 +64,19 @@
cache = request.
+ def test_cache_ key_is_ unique( self): link('test_ link') browser_ request( ).annotations. get( _KEY) ls(len( cache.keys( )), 1) entEqual(
+ # The cache key must include the link name, the context of the link
+ # and the class where the link is defined.
+ login(ANONYMOUS)
+ context = object()
+ menu = TestMenu(context)
+ link = menu._get_
+ cache = get_current_
+ MENU_ANNOTATION
+ self.assertEqua
+ self.assertCont
+ cache.keys()[0], (menu.__class__, context, 'test_link'))
+
def test_suite(): TestLoader( ).loadTestsFrom Name(__ name__)
return unittest.