← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/launchpad/cache-experiment into lp:launchpad/devel

 

Gavin Panella has proposed merging lp:~allenap/launchpad/cache-experiment into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


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.

-- 
https://code.launchpad.net/~allenap/launchpad/cache-experiment/+merge/33223
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/cache-experiment into lp:launchpad/devel.
=== modified file 'lib/canonical/configure.zcml'
--- lib/canonical/configure.zcml	2010-08-17 15:05:44 +0000
+++ lib/canonical/configure.zcml	2010-08-20 14:47:44 +0000
@@ -15,16 +15,7 @@
     <include package="canonical.launchpad" file="permissions.zcml" />
     <include package="canonical.launchpad.webapp" file="meta.zcml" />
     <include package="lazr.restful" file="meta.zcml" />
-    <include package="lp.services.database" />
-    <include package="lp.services.inlinehelp" file="meta.zcml" />
-    <include package="lp.services.openid" />
-    <include package="lp.services.job" />
-    <include package="lp.services.memcache" />
-    <include package="lp.services.profile" />
-    <include package="lp.services.features" />
-    <include package="lp.services.scripts" />
-    <include package="lp.services.worlddata" />
-    <include package="lp.services.salesforce" />
+    <include package="lp.services" />
     <include package="lazr.uri" />
     <include package="canonical.librarian" />
 

=== modified file 'lib/canonical/database/sqlbase.py'
--- lib/canonical/database/sqlbase.py	2010-08-16 00:40:47 +0000
+++ lib/canonical/database/sqlbase.py	2010-08-20 14:47:44 +0000
@@ -34,6 +34,8 @@
 from canonical.config import config, dbconfig
 from canonical.database.interfaces import ISQLBase
 
+from lp.services.propertycache import IPropertyCacheManager
+
 
 __all__ = [
     'alreadyInstalledMsg',
@@ -263,6 +265,7 @@
         # awesomely if its broken : its entirely unclear where tests for this
         # should be -- RBC 20100816.
         clear_cachedproperties(self)
+        IPropertyCacheManager(self).clear()
 
 
 alreadyInstalledMsg = ("A ZopelessTransactionManager with these settings is "

=== modified file 'lib/lp/registry/doc/personlocation.txt'
--- lib/lp/registry/doc/personlocation.txt	2009-11-15 01:05:49 +0000
+++ lib/lp/registry/doc/personlocation.txt	2010-08-20 14:47:44 +0000
@@ -119,10 +119,11 @@
 have been pre-cached so that we don't hit the database everytime we
 access a person's .location property.
 
-    >>> from zope.security.proxy import removeSecurityProxy
+    >>> from lp.services.propertycache import IPropertyCache
     >>> for mapped in guadamen.getMappedParticipants():
-    ...     mapped = removeSecurityProxy(mapped)
-    ...     if not verifyObject(IPersonLocation, mapped._location):
+    ...     cache = IPropertyCache(mapped)
+    ...     if ("location" not in cache or
+    ...         not verifyObject(IPersonLocation, cache.location)):
     ...         print 'No cached location on %s' % mapped.name
 
 The mapped_participants methods takes a optional argument to limit the

=== modified file 'lib/lp/registry/doc/teammembership.txt'
--- lib/lp/registry/doc/teammembership.txt	2010-08-17 11:50:43 +0000
+++ lib/lp/registry/doc/teammembership.txt	2010-08-20 14:47:44 +0000
@@ -1008,8 +1008,8 @@
     >>> from canonical.launchpad.interfaces import IMasterObject
     >>> IMasterObject(bad_user.account).status = AccountStatus.SUSPENDED
     >>> IMasterObject(bad_user.preferredemail).status = EmailAddressStatus.OLD
-    >>> from canonical.cachedproperty import clear_property
-    >>> clear_property(removeSecurityProxy(bad_user), '_preferredemail_cached')
+    >>> from lp.services.propertycache import IPropertyCache
+    >>> del IPropertyCache(removeSecurityProxy(bad_user)).preferredemail
     >>> transaction.commit()
 
     >>> [m.displayname for m in t3.allmembers]

=== modified file 'lib/lp/registry/model/distribution.py'
--- lib/lp/registry/model/distribution.py	2010-08-16 01:58:24 +0000
+++ lib/lp/registry/model/distribution.py	2010-08-20 14:47:44 +0000
@@ -19,7 +19,6 @@
 from zope.interface import alsoProvides, implements
 
 from lp.archivepublisher.debversion import Version
-from canonical.cachedproperty import cachedproperty, clear_property
 from canonical.database.constants import UTC_NOW
 
 from canonical.database.datetimecol import UtcDateTimeCol
@@ -30,7 +29,6 @@
     DecoratedResultSet)
 from canonical.launchpad.components.storm_operators import FTQ, Match, RANK
 from canonical.launchpad.interfaces.lpstorm import IStore
-from canonical.lazr.utils import safe_hasattr
 from lp.registry.model.announcement import MakesAnnouncements
 from lp.soyuz.model.archive import Archive
 from lp.soyuz.model.binarypackagename import BinaryPackageName
@@ -119,6 +117,7 @@
 from lp.answers.interfaces.questioncollection import (
     QUESTION_STATUS_DEFAULT_SEARCH)
 from lp.answers.interfaces.questiontarget import IQuestionTarget
+from lp.services.propertycache import IPropertyCache, cachedproperty
 
 
 class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
@@ -381,7 +380,7 @@
             return (2, self.name)
         return (3, self.name)
 
-    @cachedproperty('_cached_series')
+    @cachedproperty
     def series(self):
         """See `IDistribution`."""
         ret = Store.of(self).find(
@@ -1619,7 +1618,8 @@
 
         # May wish to add this to the series rather than clearing the cache --
         # RBC 20100816.
-        clear_property(self, '_cached_series')
+        del IPropertyCache(self).series
+
         return series
 
     @property

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2010-08-18 22:02:34 +0000
+++ lib/lp/registry/model/person.py	2010-08-20 14:47:44 +0000
@@ -61,9 +61,6 @@
 from canonical.database.sqlbase import (
     cursor, quote, quote_like, sqlvalues, SQLBase)
 
-from canonical.cachedproperty import (cachedproperty, cache_property,
-    clear_property)
-
 from canonical.lazr.utils import get_current_browser_request
 
 from canonical.launchpad.components.decoratedresultset import (
@@ -165,6 +162,8 @@
 from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy
 from lp.registry.interfaces.person import validate_public_person
 
+from lp.services.propertycache import IPropertyCache, cachedproperty
+
 
 class JoinTeamEvent:
     """See `IJoinTeamEvent`."""
@@ -380,7 +379,7 @@
 
     personal_standing_reason = StringCol(default=None)
 
-    @cachedproperty('_languages_cache')
+    @cachedproperty
     def languages(self):
         """See `IPerson`."""
         results = Store.of(self).find(
@@ -394,19 +393,19 @@
 
         :raises AttributeError: If the cache doesn't exist.
         """
-        return self._languages_cache
+        return IPropertyCache(self).languages
 
     def setLanguagesCache(self, languages):
         """Set this person's cached languages.
 
         Order them by name if necessary.
         """
-        cache_property(self, '_languages_cache', sorted(
-            languages, key=attrgetter('englishname')))
+        IPropertyCache(self).languages = sorted(
+            languages, key=attrgetter('englishname'))
 
     def deleteLanguagesCache(self):
         """Delete this person's cached languages, if it exists."""
-        clear_property(self, '_languages_cache')
+        del IPropertyCache(self).languages
 
     def addLanguage(self, language):
         """See `IPerson`."""
@@ -473,7 +472,7 @@
             Or(OAuthRequestToken.date_expires == None,
                OAuthRequestToken.date_expires > UTC_NOW))
 
-    @cachedproperty('_location')
+    @cachedproperty
     def location(self):
         """See `IObjectWithLocation`."""
         return PersonLocation.selectOneBy(person=self)
@@ -509,7 +508,8 @@
         """See `ISetLocation`."""
         assert not self.is_team, 'Cannot edit team location.'
         if self.location is None:
-            self._location = PersonLocation(person=self, visible=visible)
+            IPropertyCache(self).location = PersonLocation(
+                person=self, visible=visible)
         else:
             self.location.visible = visible
 
@@ -527,7 +527,7 @@
             self.location.last_modified_by = user
             self.location.date_last_modified = UTC_NOW
         else:
-            self._location = PersonLocation(
+            IPropertyCache(self).location = PersonLocation(
                 person=self, time_zone=time_zone, latitude=latitude,
                 longitude=longitude, last_modified_by=user)
 
@@ -1029,7 +1029,7 @@
         result = result.order_by(KarmaCategory.title)
         return [karma_cache for (karma_cache, category) in result]
 
-    @cachedproperty('_karma_cached')
+    @cachedproperty
     def karma(self):
         """See `IPerson`."""
         # May also be loaded from _all_members
@@ -1050,7 +1050,7 @@
 
         return self.is_valid_person
 
-    @cachedproperty('_is_valid_person_cached')
+    @cachedproperty
     def is_valid_person(self):
         """See `IPerson`."""
         if self.is_team:
@@ -1543,6 +1543,7 @@
 
         def prepopulate_person(row):
             result = row[0]
+            cache = IPropertyCache(result)
             index = 1
             #-- karma caching
             if need_karma:
@@ -1552,27 +1553,27 @@
                     karma_total = 0
                 else:
                     karma_total = karma.karma_total
-                cache_property(result, '_karma_cached', karma_total)
+                cache.karma = karma_total
             #-- ubuntu code of conduct signer status caching.
             if need_ubuntu_coc:
                 signed = row[index]
                 index += 1
-                cache_property(result, '_is_ubuntu_coc_signer_cached', signed)
+                cache.is_ubuntu_coc_signer = signed
             #-- location caching
             if need_location:
                 location = row[index]
                 index += 1
-                cache_property(result, '_location', location)
+                cache.location = location
             #-- archive caching
             if need_archive:
                 archive = row[index]
                 index += 1
-                cache_property(result, '_archive_cached', archive)
+                cache.archive = archive
             #-- preferred email caching
             if need_preferred_email:
                 email = row[index]
                 index += 1
-                cache_property(result, '_preferredemail_cached', email)
+                cache.preferredemail = email
             #-- validity caching
             if need_validity:
                 # valid if:
@@ -1582,7 +1583,7 @@
                     # -- preferred email found
                     and result.preferredemail is not None)
                 index += 1
-                cache_property(result, '_is_valid_person_cached', valid)
+                cache.is_valid_person = valid
             return result
         return DecoratedResultSet(raw_result,
             result_decorator=prepopulate_person)
@@ -1610,7 +1611,7 @@
         result = self._getMembersWithPreferredEmails()
         person_list = []
         for person, email in result:
-            cache_property(person, '_preferredemail_cached', email)
+            IPropertyCache(person).preferredemail = email
             person_list.append(person)
         return person_list
 
@@ -1742,7 +1743,7 @@
         # fetches the rows when they're needed.
         locations = self._getMappedParticipantsLocations(limit=limit)
         for location in locations:
-            location.person._location = location
+            IPropertyCache(location.person).location = location
         participants = set(location.person for location in locations)
         # Cache the ValidPersonCache query for all mapped participants.
         if len(participants) > 0:
@@ -1884,7 +1885,7 @@
         self.account_status = AccountStatus.DEACTIVATED
         self.account_status_comment = comment
         IMasterObject(self.preferredemail).status = EmailAddressStatus.NEW
-        clear_property(self, '_preferredemail_cached')
+        del IPropertyCache(self).preferredemail
         base_new_name = self.name + '-deactivatedaccount'
         self.name = self._ensureNewName(base_new_name)
 
@@ -2218,7 +2219,7 @@
         if email_address is not None:
             email_address.status = EmailAddressStatus.VALIDATED
             email_address.syncUpdate()
-        clear_property(self, '_preferredemail_cached')
+        del IPropertyCache(self).preferredemail
 
     def setPreferredEmail(self, email):
         """See `IPerson`."""
@@ -2255,9 +2256,9 @@
         IMasterObject(email).syncUpdate()
 
         # Now we update our cache of the preferredemail.
-        cache_property(self, '_preferredemail_cached', email)
+        IPropertyCache(self).preferredemail = email
 
-    @cachedproperty('_preferredemail_cached')
+    @cachedproperty
     def preferredemail(self):
         """See `IPerson`."""
         emails = self._getEmailsByStatus(EmailAddressStatus.PREFERRED)
@@ -2422,7 +2423,7 @@
             distribution.main_archive, self)
         return permissions.count() > 0
 
-    @cachedproperty('_is_ubuntu_coc_signer_cached')
+    @cachedproperty
     def is_ubuntu_coc_signer(self):
         """See `IPerson`."""
         # Also assigned to by self._all_members.
@@ -2452,7 +2453,7 @@
         sCoC_util = getUtility(ISignedCodeOfConductSet)
         return sCoC_util.searchByUser(self.id, active=False)
 
-    @cachedproperty('_archive_cached')
+    @cachedproperty
     def archive(self):
         """See `IPerson`."""
         return getUtility(IArchiveSet).getPPAOwnedByPerson(self)
@@ -2776,8 +2777,8 @@
                 # Populate the previously empty 'preferredemail' cached
                 # property, so the Person record is up-to-date.
                 if master_email.status == EmailAddressStatus.PREFERRED:
-                    cache_property(account_person, '_preferredemail_cached',
-                        master_email)
+                    cache = IPropertyCache(account_person)
+                    cache.preferredemail = master_email
                 return account_person
             # There is no associated `Person` to the email `Account`.
             # This is probably because the account was created externally

=== modified file 'lib/lp/registry/tests/test_distribution.py'
--- lib/lp/registry/tests/test_distribution.py	2010-08-10 19:14:56 +0000
+++ lib/lp/registry/tests/test_distribution.py	2010-08-20 14:47:44 +0000
@@ -19,6 +19,7 @@
 from lp.testing import TestCaseWithFactory
 from canonical.testing.layers import (
     DatabaseFunctionalLayer, LaunchpadFunctionalLayer)
+from lp.services.propertycache import IPropertyCache
 
 
 class TestDistribution(TestCaseWithFactory):
@@ -80,27 +81,26 @@
         distribution = removeSecurityProxy(
             self.factory.makeDistribution('foo'))
 
+        cache = IPropertyCache(distribution)
+
         # Not yet cached.
-        missing = object()
-        cached_series = getattr(distribution, '_cached_series', missing)
-        self.assertEqual(missing, cached_series)
+        self.assertNotIn("series", cache)
 
         # Now cached.
         series = distribution.series
-        self.assertTrue(series is distribution._cached_series)
+        self.assertIs(series, cache.series)
 
         # Cache cleared.
         distribution.newSeries(
             name='bar', displayname='Bar', title='Bar', summary='',
             description='', version='1', parent_series=None,
             owner=self.factory.makePerson())
-        cached_series = getattr(distribution, '_cached_series', missing)
-        self.assertEqual(missing, cached_series)
+        self.assertNotIn("series", cache)
 
         # New cached value.
         series = distribution.series
         self.assertEqual(1, len(series))
-        self.assertTrue(series is distribution._cached_series)
+        self.assertIs(series, cache.series)
 
 
 class SeriesByStatusTests(TestCaseWithFactory):

=== added file 'lib/lp/services/configure.zcml'
--- lib/lp/services/configure.zcml	1970-01-01 00:00:00 +0000
+++ lib/lp/services/configure.zcml	2010-08-20 14:47:44 +0000
@@ -0,0 +1,19 @@
+<!-- Copyright 2010 Canonical Ltd.  This software is licensed under the
+     GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
+<configure xmlns="http://namespaces.zope.org/zope";>
+  <adapter factory=".propertycache.get_default_cache"/>
+  <adapter factory=".propertycache.PropertyCacheManager"/>
+  <adapter factory=".propertycache.DefaultPropertyCacheManager"/>
+  <include package=".database" />
+  <include package=".features" />
+  <include package=".inlinehelp" file="meta.zcml" />
+  <include package=".job" />
+  <include package=".memcache" />
+  <include package=".openid" />
+  <include package=".profile" />
+  <include package=".salesforce" />
+  <include package=".scripts" />
+  <include package=".worlddata" />
+</configure>

=== added file 'lib/lp/services/propertycache.py'
--- lib/lp/services/propertycache.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/propertycache.py	2010-08-20 14:47:44 +0000
@@ -0,0 +1,294 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Cached properties for situations where a property is computed once and
+then returned each time it is asked for.
+
+    >>> from itertools import count
+    >>> counter = count(1)
+
+    >>> class Foo:
+    ...     @cachedproperty
+    ...     def bar(self):
+    ...         return next(counter)
+
+    >>> foo = Foo()
+
+The property cache can be obtained via adaption.
+
+    >>> cache = IPropertyCache(foo)
+
+Initially it is empty. Caches can be iterated over to reveal the names of the
+values cached within.
+
+    >>> list(cache)
+    []
+
+After accessing a cached property the cache is no longer empty.
+
+    >>> foo.bar
+    1
+    >>> list(cache)
+    ['bar']
+    >>> cache.bar
+    1
+
+Attempting to access an unknown name from the cache is an error.
+
+    >>> cache.baz
+    Traceback (most recent call last):
+    ...
+    AttributeError: 'DefaultPropertyCache' object has no attribute 'baz'
+
+Values in the cache can be deleted.
+
+    >>> del cache.bar
+    >>> list(cache)
+    []
+
+Accessing the cached property causes its populate function to be called again.
+
+    >>> foo.bar
+    2
+    >>> cache.bar
+    2
+
+Values in the cache can be set and updated.
+
+    >>> cache.bar = 456
+    >>> foo.bar
+    456
+
+Caches respond to membership tests.
+
+    >>> "bar" in cache
+    True
+
+    >>> del cache.bar
+
+    >>> "bar" in cache
+    False
+
+It is safe to delete names from the cache even if there is no value cached.
+
+    >>> del cache.bar
+    >>> del cache.bar
+
+A cache manager can be used to empty the cache.
+
+    >>> manager = IPropertyCacheManager(cache)
+
+    >>> cache.bar = 123
+    >>> cache.baz = 456
+    >>> sorted(cache)
+    ['bar', 'baz']
+
+    >>> manager.clear()
+    >>> list(cache)
+    []
+
+A cache manager can be obtained by adaption from non-cache objects too.
+
+    >>> manager = IPropertyCacheManager(foo)
+    >>> manager.cache is cache
+    True
+
+"""
+
+__metaclass__ = type
+__all__ = [
+    'IPropertyCache',
+    'IPropertyCacheManager',
+    'cachedproperty',
+    ]
+
+from functools import partial
+
+from zope.component import adapter, adapts
+from zope.interface import Interface, implementer, implements
+from zope.schema import Object
+from zope.security.proxy import removeSecurityProxy
+
+
+class IPropertyCache(Interface):
+
+    def __getattr__(name):
+        """Return the cached value corresponding to `name`.
+
+        Raise `AttributeError` if no value is cached.
+        """
+
+    def __setattr__(name, value):
+        """Cache `value` for `name`."""
+
+    def __delattr__(name):
+        """Delete value for `name`.
+
+        If no value is cached for `name` this is a no-op.
+        """
+
+    def __contains__(name):
+        """Whether or not `name` is cached."""
+
+    def __iter__():
+        """Iterate over the cached names."""
+
+
+class IPropertyCacheManager(Interface):
+
+    cache = Object(IPropertyCache)
+
+    def clear():
+        """Empty the cache."""
+
+
+class DefaultPropertyCache:
+    """A simple cache."""
+
+    implements(IPropertyCache)
+
+    def __delattr__(self, name):
+        """See `IPropertyCache`."""
+        self.__dict__.pop(name, None)
+
+    def __contains__(self, name):
+        """See `IPropertyCache`."""
+        return name in self.__dict__
+
+    def __iter__(self):
+        """See `IPropertyCache`."""
+        return iter(self.__dict__)
+
+
+@adapter(Interface)
+@implementer(IPropertyCache)
+def get_default_cache(target):
+    """Adapter to obtain a `DefaultPropertyCache` for any object."""
+    naked_target = removeSecurityProxy(target)
+    try:
+        return naked_target._property_cache
+    except AttributeError:
+        naked_target._property_cache = DefaultPropertyCache()
+        return naked_target._property_cache
+
+
+class PropertyCacheManager:
+    """A simple `IPropertyCacheManager`.
+
+    Should work for any `IPropertyCache` instance.
+    """
+
+    implements(IPropertyCacheManager)
+    adapts(Interface)
+
+    def __init__(self, target):
+        self.cache = IPropertyCache(target)
+
+    def clear(self):
+        """See `IPropertyCacheManager`."""
+        for name in list(self.cache):
+            delattr(self.cache, name)
+
+
+class DefaultPropertyCacheManager:
+    """A `IPropertyCacheManager` specifically for `DefaultPropertyCache`.
+
+    The implementation of `clear` is more efficient.
+    """
+
+    implements(IPropertyCacheManager)
+    adapts(DefaultPropertyCache)
+
+    def __init__(self, cache):
+        self.cache = cache
+
+    def clear(self):
+        self.cache.__dict__.clear()
+
+
+class CachedProperty:
+    """Cached property descriptor.
+
+    Provides only the `__get__` part of the descriptor protocol. Setting and
+    clearing cached values should be done explicitly via `IPropertyCache`
+    instances.
+    """
+
+    def __init__(self, populate, name):
+        """Initialize this instance.
+
+        `populate` is a callable responsible for providing the value when this
+        property has not yet been cached.
+
+        `name` is the name under which this property will cache itself.
+        """
+        self.populate = populate
+        self.name = name
+
+    def __get__(self, instance, cls):
+        if instance is None:
+            return self
+        cache = IPropertyCache(instance)
+        try:
+            return getattr(cache, self.name)
+        except AttributeError:
+            value = self.populate(instance)
+            setattr(cache, self.name, value)
+            return value
+
+
+def cachedproperty(name_or_function):
+    """Decorator to create a cached property.
+
+    A cached property can be declared with or without an explicit name. If not
+    provided it will be derived from the decorated object. This name is the
+    name under which values will be cached.
+
+        >>> class Foo:
+        ...     @cachedproperty("a_in_cache")
+        ...     def a(self):
+        ...         return 1234
+        ...     @cachedproperty
+        ...     def b(self):
+        ...         return 5678
+
+        >>> foo = Foo()
+
+    `a` was declared with an explicit name of "a_in_cache" so it is known as
+    "a_in_cache" in the cache.
+
+        >>> isinstance(Foo.a, CachedProperty)
+        True
+        >>> Foo.a.name
+        'a_in_cache'
+        >>> Foo.a.populate
+        <function a at 0x...>
+
+        >>> foo.a
+        1234
+        >>> IPropertyCache(foo).a_in_cache
+        1234
+
+    `b` was defined without an explicit name so it is known as "b" in the
+    cache too.
+
+        >>> isinstance(Foo.b, CachedProperty)
+        True
+        >>> Foo.b.name
+        'b'
+        >>> Foo.b.populate
+        <function b at 0x...>
+
+        >>> foo.b
+        5678
+        >>> IPropertyCache(foo).b
+        5678
+
+    """
+    if isinstance(name_or_function, basestring):
+        name = name_or_function
+        return partial(CachedProperty, name=name)
+    else:
+        name = name_or_function.__name__
+        populate = name_or_function
+        return CachedProperty(name=name, populate=populate)

=== added file 'lib/lp/services/tests/test_propertycache.py'
--- lib/lp/services/tests/test_propertycache.py	1970-01-01 00:00:00 +0000
+++ lib/lp/services/tests/test_propertycache.py	2010-08-20 14:47:44 +0000
@@ -0,0 +1,16 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for lp.services.propertycache."""
+
+__metaclass__ = type
+
+from canonical.testing import LaunchpadZopelessLayer
+from lp.services import propertycache
+
+
+def test_suite():
+    from doctest import DocTestSuite, ELLIPSIS
+    suite = DocTestSuite(propertycache, optionflags=ELLIPSIS)
+    suite.layer = LaunchpadZopelessLayer
+    return suite

=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py	2010-08-16 08:23:59 +0000
+++ lib/lp/soyuz/model/archive.py	2010-08-20 14:47:44 +0000
@@ -25,7 +25,6 @@
 from lp.app.errors import NotFoundError
 from lp.archivepublisher.debversion import Version
 from lp.archiveuploader.utils import re_issource, re_isadeb
-from canonical.cachedproperty import clear_property
 from canonical.config import config
 from canonical.database.constants import UTC_NOW
 from canonical.database.datetimecol import UtcDateTimeCol
@@ -103,6 +102,7 @@
 from canonical.launchpad.webapp.url import urlappend
 from canonical.launchpad.validators.name import valid_name
 from lp.registry.interfaces.person import validate_person
+from lp.services.propertycache import IPropertyCache
 
 
 class Archive(SQLBase):
@@ -1759,7 +1759,7 @@
                 signing_key = owner.archive.signing_key
             else:
                 # owner.archive is a cached property and we've just cached it.
-                clear_property(owner, '_archive_cached')
+                del IPropertyCache(owner).archive
 
         new_archive = Archive(
             owner=owner, distribution=distribution, name=name,