Merge lp:~allenap/launchpad/cache-experiment into lp:launchpad
- cache-experiment
- Merge into devel
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 | ||||
Related bugs: |
|
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/
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_cachedpro
Tim Penhey (thumper) wrote : | # |
I think this interface is very good. It looks nice to use too.
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.
Thanks,
Rob
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.
I'll start a new branch to convert the rest of the tree now.
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
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, |
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