Merge lp:~allenap/launchpad/cache-experiment into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 11495
Proposed branch: lp:~allenap/launchpad/cache-experiment
Merge into: lp:launchpad
Diff against target: 922 lines (+455/-104)
12 files modified
lib/canonical/configure.zcml (+1/-10)
lib/canonical/database/sqlbase.py (+50/-37)
lib/lp/registry/doc/personlocation.txt (+4/-3)
lib/lp/registry/doc/teammembership.txt (+2/-2)
lib/lp/registry/model/distribution.py (+7/-12)
lib/lp/registry/model/person.py (+32/-31)
lib/lp/registry/tests/test_distribution.py (+7/-7)
lib/lp/services/configure.zcml (+19/-0)
lib/lp/services/doc/propertycache.txt (+153/-0)
lib/lp/services/propertycache.py (+174/-0)
lib/lp/services/tests/test_doc.py (+4/-0)
lib/lp/soyuz/model/archive.py (+2/-2)
To merge this branch: bzr merge lp:~allenap/launchpad/cache-experiment
Reviewer Review Type Date Requested Status
Tim Penhey (community) Approve
Henning Eggers (community) code Approve
Review via email: mp+33223@code.launchpad.net

Commit message

New module propertycache to replace cachedproperty. It provides a cleaner interface to inspect and modify the cache.

Description of the change

This is a proposed replacement for the cachedproperty module. It provides a cleaner interface for inspecting and modifying the cache, and even makes cache implementations pluggable (via adaption). In use it's pretty simple and unsurprising.

See the documentation at the top of lib/lp/services/propertycache.py for a concise demonstration of how it works.

I haven't converted all uses of the existing cachedproperty yet, only those places which actually manipulate the cache (i.e. call sites for cache_property clear_property and clear_cachedproperties). I'll do that in a follow-up branch if this one is accepted.

To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) wrote :

Hi Gavin,
this is a great improvement to the API for dealing with cached properties. I like it very much. Thank your for submitting it and being open to my comments.

As this is a parallel implementation, I don't see a problem with landing it. Still, I ask you to present this on the dev mailing list to see if people see problems with it, like an after-imp discussion since you didn't have a pre-imp about this.

Cheers,
Henning

review: Approve (code)
Revision history for this message
Tim Penhey (thumper) wrote :

I think this interface is very good. It looks nice to use too.

review: Approve
Revision history for this message
Robert Collins (lifeless) wrote :

Thanks for doing this Gavin.

Please convert everything across so we don't have two implementations
half-migrated.

As the author of the extended cachedproperty API you are replacing,
I'd like to say that you read my mind about further enchancements - a
dict like object with a trivial clear-the-cache facility is perfect.
Thanks.

I don't think a post-implementation discussion is needed - we're all
on the reviewers list after all.

However, I think it would be awesome for you to:
 - announce it on the list with some quick examples so people know how
to use it.
 - update dev.lp.net/Performance, which I think mentions cachedproperty.

Thanks,
Rob

Revision history for this message
Gavin Panella (allenap) wrote :

Tim, Rob: thank you for your responses :)

I was worried that I was going to be treading on your toes Rob, so I'm glad you're happy with the result.

dev.lp.net/Performance talks about memoised data, but not specifically about cachedproperty, so I think that's okay.

I'll start a new branch to convert the rest of the tree now.

Revision history for this message
Henning Eggers (henninge) wrote :

> I don't think a post-implementation discussion is needed - we're all
> on the reviewers list after all.

I think there is too much noise on that list to serve that purpose. But your and Tim's comments here show that Gavin's mail to the list has served its purpose to include more developers in this change. Thank you! ;)

Henning

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/configure.zcml'
2--- lib/canonical/configure.zcml 2010-08-17 15:05:44 +0000
3+++ lib/canonical/configure.zcml 2010-08-23 16:19:43 +0000
4@@ -15,16 +15,7 @@
5 <include package="canonical.launchpad" file="permissions.zcml" />
6 <include package="canonical.launchpad.webapp" file="meta.zcml" />
7 <include package="lazr.restful" file="meta.zcml" />
8- <include package="lp.services.database" />
9- <include package="lp.services.inlinehelp" file="meta.zcml" />
10- <include package="lp.services.openid" />
11- <include package="lp.services.job" />
12- <include package="lp.services.memcache" />
13- <include package="lp.services.profile" />
14- <include package="lp.services.features" />
15- <include package="lp.services.scripts" />
16- <include package="lp.services.worlddata" />
17- <include package="lp.services.salesforce" />
18+ <include package="lp.services" />
19 <include package="lazr.uri" />
20 <include package="canonical.librarian" />
21
22
23=== modified file 'lib/canonical/database/sqlbase.py'
24--- lib/canonical/database/sqlbase.py 2010-08-16 00:40:47 +0000
25+++ lib/canonical/database/sqlbase.py 2010-08-23 16:19:43 +0000
26@@ -2,39 +2,6 @@
27 # GNU Affero General Public License version 3 (see the file LICENSE).
28
29 __metaclass__ = type
30-
31-import warnings
32-from datetime import datetime
33-import re
34-from textwrap import dedent
35-import psycopg2
36-from psycopg2.extensions import (
37- ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED,
38- ISOLATION_LEVEL_SERIALIZABLE)
39-import pytz
40-import storm
41-from storm.databases.postgres import compile as postgres_compile
42-from storm.expr import State
43-from storm.expr import compile as storm_compile
44-from storm.locals import Storm, Store
45-from storm.zope.interfaces import IZStorm
46-
47-from sqlobject.sqlbuilder import sqlrepr
48-import transaction
49-
50-from twisted.python.util import mergeFunctionMetadata
51-
52-from zope.component import getUtility
53-from zope.interface import implements
54-from zope.security.proxy import removeSecurityProxy
55-
56-from lazr.restful.interfaces import IRepresentationCache
57-
58-from canonical.cachedproperty import clear_cachedproperties
59-from canonical.config import config, dbconfig
60-from canonical.database.interfaces import ISQLBase
61-
62-
63 __all__ = [
64 'alreadyInstalledMsg',
65 'begin',
66@@ -63,7 +30,49 @@
67 'SQLBase',
68 'sqlvalues',
69 'StupidCache',
70- 'ZopelessTransactionManager',]
71+ 'ZopelessTransactionManager',
72+]
73+
74+
75+from datetime import datetime
76+import re
77+from textwrap import dedent
78+import warnings
79+
80+from lazr.restful.interfaces import IRepresentationCache
81+import psycopg2
82+from psycopg2.extensions import (
83+ ISOLATION_LEVEL_AUTOCOMMIT,
84+ ISOLATION_LEVEL_READ_COMMITTED,
85+ ISOLATION_LEVEL_SERIALIZABLE,
86+ )
87+import pytz
88+from sqlobject.sqlbuilder import sqlrepr
89+import storm
90+from storm.databases.postgres import compile as postgres_compile
91+from storm.expr import (
92+ compile as storm_compile,
93+ State,
94+ )
95+from storm.locals import (
96+ Store,
97+ Storm,
98+ )
99+from storm.zope.interfaces import IZStorm
100+import transaction
101+from twisted.python.util import mergeFunctionMetadata
102+from zope.component import getUtility
103+from zope.interface import implements
104+from zope.security.proxy import removeSecurityProxy
105+
106+from canonical.cachedproperty import clear_cachedproperties
107+from canonical.config import (
108+ config,
109+ dbconfig,
110+ )
111+from canonical.database.interfaces import ISQLBase
112+from lp.services.propertycache import IPropertyCacheManager
113+
114
115 # Default we want for scripts, and the PostgreSQL default. Note psycopg1 will
116 # use SERIALIZABLE unless we override, but psycopg2 will not.
117@@ -259,10 +268,14 @@
118
119 def __storm_invalidated__(self):
120 """Flush cached properties."""
121- # Note this is not directly tested; but the entire test suite blows up
122- # awesomely if its broken : its entirely unclear where tests for this
123- # should be -- RBC 20100816.
124+ # XXX: RobertCollins 2010-08-16 bug=622648: Note this is not directly
125+ # tested, but the entire test suite blows up awesomely if it's broken.
126+ # It's entirely unclear where tests for this should be.
127+
128+ # While canonical.cachedproperty and lp.services.propertycache are
129+ # both in use, we must clear the caches for both.
130 clear_cachedproperties(self)
131+ IPropertyCacheManager(self).clear()
132
133
134 alreadyInstalledMsg = ("A ZopelessTransactionManager with these settings is "
135
136=== modified file 'lib/lp/registry/doc/personlocation.txt'
137--- lib/lp/registry/doc/personlocation.txt 2009-11-15 01:05:49 +0000
138+++ lib/lp/registry/doc/personlocation.txt 2010-08-23 16:19:43 +0000
139@@ -119,10 +119,11 @@
140 have been pre-cached so that we don't hit the database everytime we
141 access a person's .location property.
142
143- >>> from zope.security.proxy import removeSecurityProxy
144+ >>> from lp.services.propertycache import IPropertyCache
145 >>> for mapped in guadamen.getMappedParticipants():
146- ... mapped = removeSecurityProxy(mapped)
147- ... if not verifyObject(IPersonLocation, mapped._location):
148+ ... cache = IPropertyCache(mapped)
149+ ... if ("location" not in cache or
150+ ... not verifyObject(IPersonLocation, cache.location)):
151 ... print 'No cached location on %s' % mapped.name
152
153 The mapped_participants methods takes a optional argument to limit the
154
155=== modified file 'lib/lp/registry/doc/teammembership.txt'
156--- lib/lp/registry/doc/teammembership.txt 2010-08-20 01:29:08 +0000
157+++ lib/lp/registry/doc/teammembership.txt 2010-08-23 16:19:43 +0000
158@@ -1008,8 +1008,8 @@
159 >>> from canonical.launchpad.interfaces import IMasterObject
160 >>> IMasterObject(bad_user.account).status = AccountStatus.SUSPENDED
161 >>> IMasterObject(bad_user.preferredemail).status = EmailAddressStatus.OLD
162- >>> from canonical.cachedproperty import clear_property
163- >>> clear_property(removeSecurityProxy(bad_user), '_preferredemail_cached')
164+ >>> from lp.services.propertycache import IPropertyCache
165+ >>> del IPropertyCache(removeSecurityProxy(bad_user)).preferredemail
166 >>> transaction.commit()
167
168 >>> [m.displayname for m in t3.allmembers]
169
170=== modified file 'lib/lp/registry/model/distribution.py'
171--- lib/lp/registry/model/distribution.py 2010-08-22 19:26:46 +0000
172+++ lib/lp/registry/model/distribution.py 2010-08-23 16:19:43 +0000
173@@ -33,10 +33,6 @@
174 implements,
175 )
176
177-from canonical.cachedproperty import (
178- cachedproperty,
179- clear_property,
180- )
181 from canonical.database.constants import UTC_NOW
182 from canonical.database.datetimecol import UtcDateTimeCol
183 from canonical.database.enumcol import EnumCol
184@@ -63,17 +59,11 @@
185 ILaunchpadCelebrities,
186 )
187 from canonical.launchpad.interfaces.lpstorm import IStore
188-from lp.registry.model.announcement import MakesAnnouncements
189-from lp.soyuz.model.archive import Archive
190-from lp.soyuz.model.binarypackagename import BinaryPackageName
191-from lp.soyuz.model.binarypackagerelease import (
192- BinaryPackageRelease)
193 from canonical.launchpad.validators.name import (
194 sanitize_name,
195 valid_name,
196 )
197 from canonical.launchpad.webapp.url import urlparse
198-from canonical.lazr.utils import safe_hasattr
199 from lp.answers.interfaces.faqtarget import IFAQTarget
200 from lp.answers.interfaces.questioncollection import (
201 QUESTION_STATUS_DEFAULT_SEARCH,
202@@ -159,6 +149,10 @@
203 from lp.registry.model.structuralsubscription import (
204 StructuralSubscriptionTargetMixin,
205 )
206+from lp.services.propertycache import (
207+ cachedproperty,
208+ IPropertyCache,
209+ )
210 from lp.soyuz.interfaces.archive import (
211 ArchivePurpose,
212 ArchiveStatus,
213@@ -524,7 +518,7 @@
214 return (2, self.name)
215 return (3, self.name)
216
217- @cachedproperty('_cached_series')
218+ @cachedproperty
219 def series(self):
220 """See `IDistribution`."""
221 ret = Store.of(self).find(
222@@ -1760,7 +1754,8 @@
223
224 # May wish to add this to the series rather than clearing the cache --
225 # RBC 20100816.
226- clear_property(self, '_cached_series')
227+ del IPropertyCache(self).series
228+
229 return series
230
231 @property
232
233=== modified file 'lib/lp/registry/model/person.py'
234--- lib/lp/registry/model/person.py 2010-08-22 17:13:25 +0000
235+++ lib/lp/registry/model/person.py 2010-08-23 16:19:43 +0000
236@@ -91,11 +91,6 @@
237 removeSecurityProxy,
238 )
239
240-from canonical.cachedproperty import (
241- cache_property,
242- cachedproperty,
243- clear_property,
244- )
245 from canonical.config import config
246 from canonical.database import postgresql
247 from canonical.database.constants import UTC_NOW
248@@ -266,6 +261,10 @@
249 TeamMembershipSet,
250 TeamParticipation,
251 )
252+from lp.services.propertycache import (
253+ cachedproperty,
254+ IPropertyCache,
255+ )
256 from lp.services.salesforce.interfaces import (
257 ISalesforceVoucherProxy,
258 REDEEMABLE_VOUCHER_STATUSES,
259@@ -499,7 +498,7 @@
260
261 personal_standing_reason = StringCol(default=None)
262
263- @cachedproperty('_languages_cache')
264+ @cachedproperty
265 def languages(self):
266 """See `IPerson`."""
267 results = Store.of(self).find(
268@@ -513,19 +512,19 @@
269
270 :raises AttributeError: If the cache doesn't exist.
271 """
272- return self._languages_cache
273+ return IPropertyCache(self).languages
274
275 def setLanguagesCache(self, languages):
276 """Set this person's cached languages.
277
278 Order them by name if necessary.
279 """
280- cache_property(self, '_languages_cache', sorted(
281- languages, key=attrgetter('englishname')))
282+ IPropertyCache(self).languages = sorted(
283+ languages, key=attrgetter('englishname'))
284
285 def deleteLanguagesCache(self):
286 """Delete this person's cached languages, if it exists."""
287- clear_property(self, '_languages_cache')
288+ del IPropertyCache(self).languages
289
290 def addLanguage(self, language):
291 """See `IPerson`."""
292@@ -592,7 +591,7 @@
293 Or(OAuthRequestToken.date_expires == None,
294 OAuthRequestToken.date_expires > UTC_NOW))
295
296- @cachedproperty('_location')
297+ @cachedproperty
298 def location(self):
299 """See `IObjectWithLocation`."""
300 return PersonLocation.selectOneBy(person=self)
301@@ -628,7 +627,8 @@
302 """See `ISetLocation`."""
303 assert not self.is_team, 'Cannot edit team location.'
304 if self.location is None:
305- self._location = PersonLocation(person=self, visible=visible)
306+ IPropertyCache(self).location = PersonLocation(
307+ person=self, visible=visible)
308 else:
309 self.location.visible = visible
310
311@@ -646,7 +646,7 @@
312 self.location.last_modified_by = user
313 self.location.date_last_modified = UTC_NOW
314 else:
315- self._location = PersonLocation(
316+ IPropertyCache(self).location = PersonLocation(
317 person=self, time_zone=time_zone, latitude=latitude,
318 longitude=longitude, last_modified_by=user)
319
320@@ -1148,7 +1148,7 @@
321 result = result.order_by(KarmaCategory.title)
322 return [karma_cache for (karma_cache, category) in result]
323
324- @cachedproperty('_karma_cached')
325+ @cachedproperty
326 def karma(self):
327 """See `IPerson`."""
328 # May also be loaded from _all_members
329@@ -1169,7 +1169,7 @@
330
331 return self.is_valid_person
332
333- @cachedproperty('_is_valid_person_cached')
334+ @cachedproperty
335 def is_valid_person(self):
336 """See `IPerson`."""
337 if self.is_team:
338@@ -1663,6 +1663,7 @@
339
340 def prepopulate_person(row):
341 result = row[0]
342+ cache = IPropertyCache(result)
343 index = 1
344 #-- karma caching
345 if need_karma:
346@@ -1672,27 +1673,27 @@
347 karma_total = 0
348 else:
349 karma_total = karma.karma_total
350- cache_property(result, '_karma_cached', karma_total)
351+ cache.karma = karma_total
352 #-- ubuntu code of conduct signer status caching.
353 if need_ubuntu_coc:
354 signed = row[index]
355 index += 1
356- cache_property(result, '_is_ubuntu_coc_signer_cached', signed)
357+ cache.is_ubuntu_coc_signer = signed
358 #-- location caching
359 if need_location:
360 location = row[index]
361 index += 1
362- cache_property(result, '_location', location)
363+ cache.location = location
364 #-- archive caching
365 if need_archive:
366 archive = row[index]
367 index += 1
368- cache_property(result, '_archive_cached', archive)
369+ cache.archive = archive
370 #-- preferred email caching
371 if need_preferred_email:
372 email = row[index]
373 index += 1
374- cache_property(result, '_preferredemail_cached', email)
375+ cache.preferredemail = email
376 #-- validity caching
377 if need_validity:
378 # valid if:
379@@ -1702,7 +1703,7 @@
380 # -- preferred email found
381 and result.preferredemail is not None)
382 index += 1
383- cache_property(result, '_is_valid_person_cached', valid)
384+ cache.is_valid_person = valid
385 return result
386 return DecoratedResultSet(raw_result,
387 result_decorator=prepopulate_person)
388@@ -1730,7 +1731,7 @@
389 result = self._getMembersWithPreferredEmails()
390 person_list = []
391 for person, email in result:
392- cache_property(person, '_preferredemail_cached', email)
393+ IPropertyCache(person).preferredemail = email
394 person_list.append(person)
395 return person_list
396
397@@ -1865,7 +1866,7 @@
398 # fetches the rows when they're needed.
399 locations = self._getMappedParticipantsLocations(limit=limit)
400 for location in locations:
401- location.person._location = location
402+ IPropertyCache(location.person).location = location
403 participants = set(location.person for location in locations)
404 # Cache the ValidPersonCache query for all mapped participants.
405 if len(participants) > 0:
406@@ -2007,7 +2008,7 @@
407 self.account_status = AccountStatus.DEACTIVATED
408 self.account_status_comment = comment
409 IMasterObject(self.preferredemail).status = EmailAddressStatus.NEW
410- clear_property(self, '_preferredemail_cached')
411+ del IPropertyCache(self).preferredemail
412 base_new_name = self.name + '-deactivatedaccount'
413 self.name = self._ensureNewName(base_new_name)
414
415@@ -2397,7 +2398,7 @@
416 if email_address is not None:
417 email_address.status = EmailAddressStatus.VALIDATED
418 email_address.syncUpdate()
419- clear_property(self, '_preferredemail_cached')
420+ del IPropertyCache(self).preferredemail
421
422 def setPreferredEmail(self, email):
423 """See `IPerson`."""
424@@ -2434,9 +2435,9 @@
425 IMasterObject(email).syncUpdate()
426
427 # Now we update our cache of the preferredemail.
428- cache_property(self, '_preferredemail_cached', email)
429+ IPropertyCache(self).preferredemail = email
430
431- @cachedproperty('_preferredemail_cached')
432+ @cachedproperty
433 def preferredemail(self):
434 """See `IPerson`."""
435 emails = self._getEmailsByStatus(EmailAddressStatus.PREFERRED)
436@@ -2601,7 +2602,7 @@
437 distribution.main_archive, self)
438 return permissions.count() > 0
439
440- @cachedproperty('_is_ubuntu_coc_signer_cached')
441+ @cachedproperty
442 def is_ubuntu_coc_signer(self):
443 """See `IPerson`."""
444 # Also assigned to by self._all_members.
445@@ -2631,7 +2632,7 @@
446 sCoC_util = getUtility(ISignedCodeOfConductSet)
447 return sCoC_util.searchByUser(self.id, active=False)
448
449- @cachedproperty('_archive_cached')
450+ @cachedproperty
451 def archive(self):
452 """See `IPerson`."""
453 return getUtility(IArchiveSet).getPPAOwnedByPerson(self)
454@@ -2955,8 +2956,8 @@
455 # Populate the previously empty 'preferredemail' cached
456 # property, so the Person record is up-to-date.
457 if master_email.status == EmailAddressStatus.PREFERRED:
458- cache_property(account_person, '_preferredemail_cached',
459- master_email)
460+ cache = IPropertyCache(account_person)
461+ cache.preferredemail = master_email
462 return account_person
463 # There is no associated `Person` to the email `Account`.
464 # This is probably because the account was created externally
465
466=== modified file 'lib/lp/registry/tests/test_distribution.py'
467--- lib/lp/registry/tests/test_distribution.py 2010-08-22 19:26:46 +0000
468+++ lib/lp/registry/tests/test_distribution.py 2010-08-23 16:19:43 +0000
469@@ -18,6 +18,7 @@
470 from lp.registry.tests.test_distroseries import (
471 TestDistroSeriesCurrentSourceReleases,
472 )
473+from lp.services.propertycache import IPropertyCache
474 from lp.soyuz.interfaces.distributionsourcepackagerelease import (
475 IDistributionSourcePackageRelease,
476 )
477@@ -83,27 +84,26 @@
478 distribution = removeSecurityProxy(
479 self.factory.makeDistribution('foo'))
480
481+ cache = IPropertyCache(distribution)
482+
483 # Not yet cached.
484- missing = object()
485- cached_series = getattr(distribution, '_cached_series', missing)
486- self.assertEqual(missing, cached_series)
487+ self.assertNotIn("series", cache)
488
489 # Now cached.
490 series = distribution.series
491- self.assertTrue(series is distribution._cached_series)
492+ self.assertIs(series, cache.series)
493
494 # Cache cleared.
495 distribution.newSeries(
496 name='bar', displayname='Bar', title='Bar', summary='',
497 description='', version='1', parent_series=None,
498 owner=self.factory.makePerson())
499- cached_series = getattr(distribution, '_cached_series', missing)
500- self.assertEqual(missing, cached_series)
501+ self.assertNotIn("series", cache)
502
503 # New cached value.
504 series = distribution.series
505 self.assertEqual(1, len(series))
506- self.assertTrue(series is distribution._cached_series)
507+ self.assertIs(series, cache.series)
508
509
510 class SeriesByStatusTests(TestCaseWithFactory):
511
512=== added file 'lib/lp/services/configure.zcml'
513--- lib/lp/services/configure.zcml 1970-01-01 00:00:00 +0000
514+++ lib/lp/services/configure.zcml 2010-08-23 16:19:43 +0000
515@@ -0,0 +1,19 @@
516+<!-- Copyright 2010 Canonical Ltd. This software is licensed under the
517+ GNU Affero General Public License version 3 (see the file LICENSE).
518+-->
519+
520+<configure xmlns="http://namespaces.zope.org/zope">
521+ <adapter factory=".propertycache.get_default_cache"/>
522+ <adapter factory=".propertycache.PropertyCacheManager"/>
523+ <adapter factory=".propertycache.DefaultPropertyCacheManager"/>
524+ <include package=".database" />
525+ <include package=".features" />
526+ <include package=".inlinehelp" file="meta.zcml" />
527+ <include package=".job" />
528+ <include package=".memcache" />
529+ <include package=".openid" />
530+ <include package=".profile" />
531+ <include package=".salesforce" />
532+ <include package=".scripts" />
533+ <include package=".worlddata" />
534+</configure>
535
536=== added file 'lib/lp/services/doc/propertycache.txt'
537--- lib/lp/services/doc/propertycache.txt 1970-01-01 00:00:00 +0000
538+++ lib/lp/services/doc/propertycache.txt 2010-08-23 16:19:43 +0000
539@@ -0,0 +1,153 @@
540+Cached Properties in propertycache
541+==================================
542+
543+ >>> from lp.services.propertycache import (
544+ ... cachedproperty,
545+ ... IPropertyCache,
546+ ... IPropertyCacheManager,
547+ ... )
548+
549+Cached properties are for situations where a property is computed once
550+and then returned each time it is asked for.
551+
552+ >>> from itertools import count
553+ >>> counter = count(1)
554+
555+ >>> class Foo:
556+ ... @cachedproperty
557+ ... def bar(self):
558+ ... return next(counter)
559+
560+ >>> foo = Foo()
561+
562+The property cache can be obtained via adaption.
563+
564+ >>> cache = IPropertyCache(foo)
565+
566+Initially it is empty. Caches can be iterated over to reveal the names
567+of the values cached within.
568+
569+ >>> list(cache)
570+ []
571+
572+After accessing a cached property the cache is no longer empty.
573+
574+ >>> foo.bar
575+ 1
576+ >>> list(cache)
577+ ['bar']
578+ >>> cache.bar
579+ 1
580+
581+Attempting to access an unknown name from the cache is an error.
582+
583+ >>> cache.baz
584+ Traceback (most recent call last):
585+ ...
586+ AttributeError: 'DefaultPropertyCache' object has no attribute 'baz'
587+
588+Values in the cache can be deleted.
589+
590+ >>> del cache.bar
591+ >>> list(cache)
592+ []
593+
594+Accessing the cached property causes its populate function to be
595+called again.
596+
597+ >>> foo.bar
598+ 2
599+ >>> cache.bar
600+ 2
601+
602+Values in the cache can be set and updated.
603+
604+ >>> cache.bar = 456
605+ >>> foo.bar
606+ 456
607+
608+Caches respond to membership tests.
609+
610+ >>> "bar" in cache
611+ True
612+
613+ >>> del cache.bar
614+
615+ >>> "bar" in cache
616+ False
617+
618+It is safe to delete names from the cache even if there is no value
619+cached.
620+
621+ >>> del cache.bar
622+ >>> del cache.bar
623+
624+A cache manager can be used to empty the cache.
625+
626+ >>> manager = IPropertyCacheManager(cache)
627+
628+ >>> cache.bar = 123
629+ >>> cache.baz = 456
630+ >>> sorted(cache)
631+ ['bar', 'baz']
632+
633+ >>> manager.clear()
634+ >>> list(cache)
635+ []
636+
637+A cache manager can be obtained by adaption from non-cache objects
638+too.
639+
640+ >>> manager = IPropertyCacheManager(foo)
641+ >>> manager.cache is cache
642+ True
643+
644+
645+The cachedproperty decorator
646+----------------------------
647+
648+A cached property can be declared with or without an explicit name. If
649+not provided it will be derived from the decorated object. This name
650+is the name under which values will be cached.
651+
652+ >>> class Foo:
653+ ... @cachedproperty("a_in_cache")
654+ ... def a(self):
655+ ... return 1234
656+ ... @cachedproperty
657+ ... def b(self):
658+ ... return 5678
659+
660+ >>> foo = Foo()
661+
662+`a` was declared with an explicit name of "a_in_cache" so it is known
663+as "a_in_cache" in the cache.
664+
665+ >>> from lp.services.propertycache import CachedProperty
666+
667+ >>> isinstance(Foo.a, CachedProperty)
668+ True
669+ >>> Foo.a.name
670+ 'a_in_cache'
671+ >>> Foo.a.populate
672+ <function a at 0x...>
673+
674+ >>> foo.a
675+ 1234
676+ >>> IPropertyCache(foo).a_in_cache
677+ 1234
678+
679+`b` was defined without an explicit name so it is known as "b" in the
680+cache too.
681+
682+ >>> isinstance(Foo.b, CachedProperty)
683+ True
684+ >>> Foo.b.name
685+ 'b'
686+ >>> Foo.b.populate
687+ <function b at 0x...>
688+
689+ >>> foo.b
690+ 5678
691+ >>> IPropertyCache(foo).b
692+ 5678
693
694=== added file 'lib/lp/services/propertycache.py'
695--- lib/lp/services/propertycache.py 1970-01-01 00:00:00 +0000
696+++ lib/lp/services/propertycache.py 2010-08-23 16:19:43 +0000
697@@ -0,0 +1,174 @@
698+# Copyright 2010 Canonical Ltd. This software is licensed under the
699+# GNU Affero General Public License version 3 (see the file LICENSE).
700+
701+"""
702+Cached properties for situations where a property is computed once and then
703+returned each time it is asked for.
704+
705+See `doc/propertycache.txt` for documentation.
706+"""
707+
708+__metaclass__ = type
709+__all__ = [
710+ 'IPropertyCache',
711+ 'IPropertyCacheManager',
712+ 'cachedproperty',
713+ ]
714+
715+from functools import partial
716+
717+from zope.component import (
718+ adapter,
719+ adapts,
720+ )
721+from zope.interface import (
722+ implementer,
723+ implements,
724+ Interface,
725+ )
726+from zope.schema import Object
727+from zope.security.proxy import removeSecurityProxy
728+
729+
730+class IPropertyCache(Interface):
731+
732+ def __getattr__(name):
733+ """Return the cached value corresponding to `name`.
734+
735+ Raise `AttributeError` if no value is cached.
736+ """
737+
738+ def __setattr__(name, value):
739+ """Cache `value` for `name`."""
740+
741+ def __delattr__(name):
742+ """Delete value for `name`.
743+
744+ If no value is cached for `name` this is a no-op.
745+ """
746+
747+ def __contains__(name):
748+ """Whether or not `name` is cached."""
749+
750+ def __iter__():
751+ """Iterate over the cached names."""
752+
753+
754+class IPropertyCacheManager(Interface):
755+
756+ cache = Object(IPropertyCache)
757+
758+ def clear():
759+ """Empty the cache."""
760+
761+
762+class DefaultPropertyCache:
763+ """A simple cache."""
764+
765+ implements(IPropertyCache)
766+
767+ # __getattr__ -- well, __getattribute__ -- and __setattr__ are inherited
768+ # from object.
769+
770+ def __delattr__(self, name):
771+ """See `IPropertyCache`."""
772+ self.__dict__.pop(name, None)
773+
774+ def __contains__(self, name):
775+ """See `IPropertyCache`."""
776+ return name in self.__dict__
777+
778+ def __iter__(self):
779+ """See `IPropertyCache`."""
780+ return iter(self.__dict__)
781+
782+
783+@adapter(Interface)
784+@implementer(IPropertyCache)
785+def get_default_cache(target):
786+ """Adapter to obtain a `DefaultPropertyCache` for any object."""
787+ naked_target = removeSecurityProxy(target)
788+ try:
789+ return naked_target._property_cache
790+ except AttributeError:
791+ naked_target._property_cache = DefaultPropertyCache()
792+ return naked_target._property_cache
793+
794+
795+class PropertyCacheManager:
796+ """A simple `IPropertyCacheManager`.
797+
798+ Should work for any `IPropertyCache` instance.
799+ """
800+
801+ implements(IPropertyCacheManager)
802+ adapts(Interface)
803+
804+ def __init__(self, target):
805+ self.cache = IPropertyCache(target)
806+
807+ def clear(self):
808+ """See `IPropertyCacheManager`."""
809+ for name in list(self.cache):
810+ delattr(self.cache, name)
811+
812+
813+class DefaultPropertyCacheManager:
814+ """A `IPropertyCacheManager` specifically for `DefaultPropertyCache`.
815+
816+ The implementation of `clear` is more efficient.
817+ """
818+
819+ implements(IPropertyCacheManager)
820+ adapts(DefaultPropertyCache)
821+
822+ def __init__(self, cache):
823+ self.cache = cache
824+
825+ def clear(self):
826+ self.cache.__dict__.clear()
827+
828+
829+class CachedProperty:
830+ """Cached property descriptor.
831+
832+ Provides only the `__get__` part of the descriptor protocol. Setting and
833+ clearing cached values should be done explicitly via `IPropertyCache`
834+ instances.
835+ """
836+
837+ def __init__(self, populate, name):
838+ """Initialize this instance.
839+
840+ `populate` is a callable responsible for providing the value when this
841+ property has not yet been cached.
842+
843+ `name` is the name under which this property will cache itself.
844+ """
845+ self.populate = populate
846+ self.name = name
847+
848+ def __get__(self, instance, cls):
849+ if instance is None:
850+ return self
851+ cache = IPropertyCache(instance)
852+ try:
853+ return getattr(cache, self.name)
854+ except AttributeError:
855+ value = self.populate(instance)
856+ setattr(cache, self.name, value)
857+ return value
858+
859+
860+def cachedproperty(name_or_function):
861+ """Decorator to create a cached property.
862+
863+ See `doc/propertycache.txt` for usage.
864+ """
865+ if isinstance(name_or_function, basestring):
866+ name = name_or_function
867+ return partial(CachedProperty, name=name)
868+ else:
869+ name = name_or_function.__name__
870+ populate = name_or_function
871+ return CachedProperty(name=name, populate=populate)
872
873=== modified file 'lib/lp/services/tests/test_doc.py'
874--- lib/lp/services/tests/test_doc.py 2010-08-20 20:31:18 +0000
875+++ lib/lp/services/tests/test_doc.py 2010-08-23 16:19:43 +0000
876@@ -9,6 +9,7 @@
877
878 from canonical.launchpad.testing.systemdocs import LayeredDocFileSuite
879 from canonical.testing import BaseLayer
880+from canonical.testing import LaunchpadZopelessLayer
881 from lp.services.testing import build_test_suite
882
883
884@@ -19,6 +20,9 @@
885 'limitedlist.txt': LayeredDocFileSuite(
886 '../doc/limitedlist.txt',
887 layer=BaseLayer),
888+ 'propertycache.txt': LayeredDocFileSuite(
889+ '../doc/propertycache.txt',
890+ layer=LaunchpadZopelessLayer),
891 }
892
893
894
895=== modified file 'lib/lp/soyuz/model/archive.py'
896--- lib/lp/soyuz/model/archive.py 2010-08-20 20:31:18 +0000
897+++ lib/lp/soyuz/model/archive.py 2010-08-23 16:19:43 +0000
898@@ -38,7 +38,6 @@
899 implements,
900 )
901
902-from canonical.cachedproperty import clear_property
903 from canonical.config import config
904 from canonical.database.constants import UTC_NOW
905 from canonical.database.datetimecol import UtcDateTimeCol
906@@ -89,6 +88,7 @@
907 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
908 from lp.registry.model.teammembership import TeamParticipation
909 from lp.services.job.interfaces.job import JobStatus
910+from lp.services.propertycache import IPropertyCache
911 from lp.soyuz.adapters.archivedependencies import expand_dependencies
912 from lp.soyuz.adapters.packagelocation import PackageLocation
913 from lp.soyuz.interfaces.archive import (
914@@ -1834,7 +1834,7 @@
915 signing_key = owner.archive.signing_key
916 else:
917 # owner.archive is a cached property and we've just cached it.
918- clear_property(owner, '_archive_cached')
919+ del IPropertyCache(owner).archive
920
921 new_archive = Archive(
922 owner=owner, distribution=distribution, name=name,