← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/snapshot-modifying-helper-use-bugs into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/snapshot-modifying-helper-use-bugs into lp:launchpad.

Commit message:
Convert the rest of lp.bugs to notify_modified.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snapshot-modifying-helper-use-bugs/+merge/361613
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snapshot-modifying-helper-use-bugs into lp:launchpad.
=== modified file 'lib/lp/bugs/browser/bug.py'
--- lib/lp/bugs/browser/bug.py	2018-07-16 00:46:56 +0000
+++ lib/lp/bugs/browser/bug.py	2019-01-10 11:51:47 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """IBug related view classes."""
@@ -131,6 +131,7 @@
     ILaunchBag,
     )
 from lp.services.webapp.publisher import RedirectionView
+from lp.services.webapp.snapshot import notify_modified
 
 
 class BugNavigation(Navigation):
@@ -795,11 +796,9 @@
         # We handle duplicate changes by hand instead of leaving it to
         # the usual machinery because we must use bug.markAsDuplicate().
         bug = self.context.bug
-        bug_before_modification = Snapshot(bug, providing=providedBy(bug))
-        duplicateof = data.pop('duplicateof')
-        bug.markAsDuplicate(duplicateof)
-        notify(
-            ObjectModifiedEvent(bug, bug_before_modification, 'duplicateof'))
+        with notify_modified(bug, ['duplicateof']):
+            duplicateof = data.pop('duplicateof')
+            bug.markAsDuplicate(duplicateof)
         # Apply other changes.
         self.updateBugFromData(data)
         return self._duplicate_action_result()
@@ -812,10 +811,8 @@
     def remove_action(self, action, data):
         """Update the bug."""
         bug = self.context.bug
-        bug_before_modification = Snapshot(bug, providing=providedBy(bug))
-        bug.markAsDuplicate(None)
-        notify(
-            ObjectModifiedEvent(bug, bug_before_modification, 'duplicateof'))
+        with notify_modified(bug, ['duplicateof']):
+            bug.markAsDuplicate(None)
         return self._duplicate_action_result()
 
     def _duplicate_action_result(self):

=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py	2018-11-12 16:04:10 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py	2019-01-10 11:51:47 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -10,8 +10,6 @@
 import re
 import urllib
 
-from lazr.lifecycle.event import ObjectModifiedEvent
-from lazr.lifecycle.snapshot import Snapshot
 from lazr.restful.interfaces import IJSONRequestCache
 from pytz import UTC
 import simplejson
@@ -29,9 +27,7 @@
     getMultiAdapter,
     getUtility,
     )
-from zope.event import notify
 from zope.formlib.interfaces import ConversionError
-from zope.interface import providedBy
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.enums import InformationType
@@ -76,6 +72,7 @@
     ILaunchpadRoot,
     )
 from lp.services.webapp.servers import LaunchpadTestRequest
+from lp.services.webapp.snapshot import notify_modified
 from lp.soyuz.interfaces.component import IComponentSet
 from lp.testing import (
     ANONYMOUS,
@@ -1739,11 +1736,8 @@
     layer = DatabaseFunctionalLayer
 
     def setAttribute(self, obj, attribute, value):
-        obj_before_modification = Snapshot(obj, providing=providedBy(obj))
-        setattr(removeSecurityProxy(obj), attribute, value)
-        notify(ObjectModifiedEvent(
-            obj, obj_before_modification, [attribute],
-            self.factory.makePerson()))
+        with notify_modified(obj, [attribute], user=self.factory.makePerson()):
+            setattr(removeSecurityProxy(obj), attribute, value)
 
     def test_escapes_assignee(self):
         with celebrity_logged_in('admin'):

=== modified file 'lib/lp/bugs/mail/tests/test_bug_duplicate_notifications.py'
--- lib/lp/bugs/mail/tests/test_bug_duplicate_notifications.py	2013-11-29 12:51:58 +0000
+++ lib/lp/bugs/mail/tests/test_bug_duplicate_notifications.py	2019-01-10 11:51:47 +0000
@@ -1,18 +1,16 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for notification strings of duplicate Bugs."""
 
-from lazr.lifecycle.event import ObjectModifiedEvent
-from lazr.lifecycle.snapshot import Snapshot
 import transaction
 from zope.component import getUtility
-from zope.event import notify
-from zope.interface import providedBy
 
 from lp.bugs.interfaces.bugtask import BugTaskStatus
-from lp.services.mail import stub
+from lp.bugs.model.bugnotification import BugNotification
+from lp.bugs.scripts.bugnotification import construct_email_notifications
 from lp.services.webapp.interfaces import ILaunchBag
+from lp.services.webapp.snapshot import notify_modified
 from lp.testing import TestCaseWithFactory
 from lp.testing.layers import DatabaseFunctionalLayer
 
@@ -32,9 +30,6 @@
         self.master_bug = self.factory.makeBug(target=self.product)
         self.dup_bug = self.factory.makeBug(target=self.product)
         self.master_bug_task = self.master_bug.getBugTask(self.product)
-        self.master_bug_task_before_modification = Snapshot(
-            self.master_bug_task,
-            providing=providedBy(self.master_bug_task))
         self.person_subscribed_email = 'person@xxxxxxxxxxx'
         self.person_subscribed = self.factory.makePerson(
             name='subscribed', displayname='Person',
@@ -47,14 +42,14 @@
     def test_dup_subscriber_change_notification_message(self):
         """Duplicate bug number in the reason (email footer) for
            duplicate subscribers when a master bug is modified."""
-        self.assertEqual(len(stub.test_emails), 0, 'emails in queue')
-        self.master_bug_task.transitionToStatus(
-            BugTaskStatus.CONFIRMED, self.user)
-        notify(ObjectModifiedEvent(
-            self.master_bug_task, self.master_bug_task_before_modification,
-            ['status'], user=self.user))
+        with notify_modified(self.master_bug_task, ['status'], user=self.user):
+            self.master_bug_task.transitionToStatus(
+                BugTaskStatus.CONFIRMED, self.user)
         transaction.commit()
-        self.assertEqual(len(stub.test_emails), 2, 'email not sent')
+        latest_notification = BugNotification.selectFirst(orderBy='-id')
+        notifications, omitted, messages = construct_email_notifications(
+            [latest_notification])
+        self.assertEqual(
+            len(notifications), 1, 'email notification not created')
         rationale = 'duplicate bug report (%i)' % self.dup_bug.id
-        msg = stub.test_emails[-1][2]
-        self.assertIn(rationale, msg)
+        self.assertIn(rationale, str(messages[-1]))

=== modified file 'lib/lp/bugs/mail/tests/test_bug_task_assignment.py'
--- lib/lp/bugs/mail/tests/test_bug_task_assignment.py	2015-07-21 09:04:01 +0000
+++ lib/lp/bugs/mail/tests/test_bug_task_assignment.py	2019-01-10 11:51:47 +0000
@@ -1,19 +1,16 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for Bug task assignment-related email tests."""
 
-from lazr.lifecycle.event import ObjectModifiedEvent
-from lazr.lifecycle.snapshot import Snapshot
 import transaction
 from zope.component import getUtility
-from zope.event import notify
-from zope.interface import providedBy
 
 from lp.bugs.model.bugnotification import BugNotification
 from lp.bugs.scripts.bugnotification import construct_email_notifications
 from lp.services.mail import stub
 from lp.services.webapp.interfaces import ILaunchBag
+from lp.services.webapp.snapshot import notify_modified
 from lp.testing import TestCaseWithFactory
 from lp.testing.layers import DatabaseFunctionalLayer
 
@@ -32,8 +29,6 @@
                                                 name='rebirth')
         self.bug = self.factory.makeBug(target=self.product)
         self.bug_task = self.bug.getBugTask(self.product)
-        self.bug_task_before_modification = Snapshot(self.bug_task,
-            providing=providedBy(self.bug_task))
         self.person_assigned_email = 'stever@xxxxxxxxxxx'
         self.person_assigned = self.factory.makePerson(
             name='assigned', displayname='Steve Rogers',
@@ -53,10 +48,8 @@
         """Test notification string when a person is assigned a task by
            someone else."""
         self.assertEqual(len(stub.test_emails), 0, 'emails in queue')
-        self.bug_task.transitionToAssignee(self.person_assigned)
-        notify(ObjectModifiedEvent(
-            self.bug_task, self.bug_task_before_modification,
-            ['assignee'], user=self.user))
+        with notify_modified(self.bug_task, ['assignee'], user=self.user):
+            self.bug_task.transitionToAssignee(self.person_assigned)
         transaction.commit()
         self.assertEqual(len(stub.test_emails), 1, 'email not sent')
         rationale = (
@@ -69,10 +62,8 @@
         """Test notification string when a person is assigned a task by
            themselves."""
         stub.test_emails = []
-        self.bug_task.transitionToAssignee(self.user)
-        notify(ObjectModifiedEvent(
-            self.bug_task, self.bug_task_before_modification,
-            edited_fields=['assignee']))
+        with notify_modified(self.bug_task, ['assignee']):
+            self.bug_task.transitionToAssignee(self.user)
         transaction.commit()
         self.assertEqual(1, len(stub.test_emails))
         rationale = (
@@ -86,10 +77,8 @@
         """Test that a new recipient being assigned a bug task does send
            a NEW message."""
         self.assertEqual(len(stub.test_emails), 0, 'emails in queue')
-        self.bug_task.transitionToAssignee(self.person_assigned)
-        notify(ObjectModifiedEvent(
-            self.bug_task, self.bug_task_before_modification,
-            ['assignee'], user=self.user))
+        with notify_modified(self.bug_task, ['assignee'], user=self.user):
+            self.bug_task.transitionToAssignee(self.person_assigned)
         transaction.commit()
         self.assertEqual(len(stub.test_emails), 1, 'email not sent')
         new_message = '[NEW]'
@@ -100,10 +89,8 @@
     def test_assignee_new_subscriber(self):
         """Build a list of people who will receive emails about the bug
         task changes and ensure the assignee is not one."""
-        self.bug_task.transitionToAssignee(self.person_assigned)
-        notify(ObjectModifiedEvent(
-            self.bug_task, self.bug_task_before_modification,
-            ['assignee'], user=self.user))
+        with notify_modified(self.bug_task, ['assignee'], user=self.user):
+            self.bug_task.transitionToAssignee(self.person_assigned)
         latest_notification = BugNotification.selectFirst(orderBy='-id')
         notifications, omitted, messages = construct_email_notifications(
             [latest_notification])
@@ -117,10 +104,8 @@
         """Assign a team, who is not subscribed to a bug, a bug task and
         ensure that team members do not receive an email about the bug
         task changes."""
-        self.bug_task.transitionToAssignee(self.team_assigned)
-        notify(ObjectModifiedEvent(
-            self.bug_task, self.bug_task_before_modification,
-            ['assignee'], user=self.user))
+        with notify_modified(self.bug_task, ['assignee'], user=self.user):
+            self.bug_task.transitionToAssignee(self.team_assigned)
         latest_notification = BugNotification.selectFirst(orderBy='-id')
         notifications, omitted, messages = construct_email_notifications(
             [latest_notification])

=== modified file 'lib/lp/bugs/mail/tests/test_bug_task_modification.py'
--- lib/lp/bugs/mail/tests/test_bug_task_modification.py	2012-08-08 07:22:51 +0000
+++ lib/lp/bugs/mail/tests/test_bug_task_modification.py	2019-01-10 11:51:47 +0000
@@ -1,19 +1,16 @@
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test for emails sent after bug task modification."""
 
-from lazr.lifecycle.event import ObjectModifiedEvent
-from lazr.lifecycle.snapshot import Snapshot
 import transaction
 from zope.component import getUtility
-from zope.event import notify
-from zope.interface import providedBy
 
 from lp.bugs.interfaces.bugtask import BugTaskStatus
 from lp.bugs.model.bugnotification import BugNotification
 from lp.bugs.scripts.bugnotification import construct_email_notifications
 from lp.services.webapp.interfaces import ILaunchBag
+from lp.services.webapp.snapshot import notify_modified
 from lp.testing import TestCaseWithFactory
 from lp.testing.layers import DatabaseFunctionalLayer
 
@@ -31,15 +28,12 @@
         self.product = self.factory.makeProduct(owner=self.user)
         self.bug = self.factory.makeBug(target=self.product)
         self.bug_task = self.bug.getBugTask(self.product)
-        self.bug_task_before_modification = Snapshot(self.bug_task,
-            providing=providedBy(self.bug_task))
 
     def test_for_bug_modifier_header(self):
         """Test X-Launchpad-Bug-Modifier appears when a bug is modified."""
-        self.bug_task.transitionToStatus(BugTaskStatus.CONFIRMED, self.user)
-        notify(ObjectModifiedEvent(
-            self.bug_task, self.bug_task_before_modification,
-            ['status'], user=self.user))
+        with notify_modified(self.bug_task, ['status'], user=self.user):
+            self.bug_task.transitionToStatus(
+                BugTaskStatus.CONFIRMED, self.user)
         transaction.commit()
         latest_notification = BugNotification.selectFirst(orderBy='-id')
         notifications, omitted, messages = construct_email_notifications(

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2016-07-27 12:54:40 +0000
+++ lib/lp/bugs/model/bug.py	2019-01-10 11:51:47 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Launchpad bug-related database table classes."""
@@ -26,10 +26,7 @@
 import operator
 import re
 
-from lazr.lifecycle.event import (
-    ObjectCreatedEvent,
-    ObjectModifiedEvent,
-    )
+from lazr.lifecycle.event import ObjectCreatedEvent
 from lazr.lifecycle.snapshot import Snapshot
 import pytz
 from sqlobject import (
@@ -70,10 +67,7 @@
 from zope.component import getUtility
 from zope.contenttype import guess_content_type
 from zope.event import notify
-from zope.interface import (
-    implementer,
-    providedBy,
-    )
+from zope.interface import implementer
 from zope.security.interfaces import Unauthorized
 from zope.security.proxy import (
     ProxyFactory,
@@ -234,6 +228,7 @@
 from lp.services.webapp.publisher import (
     get_raw_form_value_from_current_request,
     )
+from lp.services.webapp.snapshot import notify_modified
 from lp.services.xref.interfaces import IXRefSet
 
 
@@ -1501,20 +1496,12 @@
             'A question cannot be created from this bug without a '
             'valid bugtask.')
 
-        bugtask_before_modification = Snapshot(
-            bugtask, providing=providedBy(bugtask))
-        bugtask.transitionToStatus(BugTaskStatus.INVALID, person)
-        edited_fields = ['status']
-        if comment is not None:
-            self.newMessage(
-                owner=person, subject=self.followup_subject(),
-                content=comment)
-        notify(
-            ObjectModifiedEvent(
-                object=bugtask,
-                object_before_modification=bugtask_before_modification,
-                edited_fields=edited_fields,
-                user=person))
+        with notify_modified(bugtask, ['status'], user=person):
+            bugtask.transitionToStatus(BugTaskStatus.INVALID, person)
+            if comment is not None:
+                self.newMessage(
+                    owner=person, subject=self.followup_subject(),
+                    content=comment)
 
         question_target = IQuestionTarget(bugtask.target)
         question = question_target.createQuestionFromBug(self)
@@ -1759,11 +1746,8 @@
         if bugtask.status == status:
             return None
 
-        bugtask_before_modification = Snapshot(
-            bugtask, providing=providedBy(bugtask))
-        bugtask.transitionToStatus(status, user)
-        notify(ObjectModifiedEvent(
-            bugtask, bugtask_before_modification, ['status'], user=user))
+        with notify_modified(bugtask, ['status'], user=user):
+            bugtask.transitionToStatus(status, user)
 
         return bugtask
 

=== modified file 'lib/lp/bugs/model/bugtask.py'
--- lib/lp/bugs/model/bugtask.py	2018-11-12 16:04:10 +0000
+++ lib/lp/bugs/model/bugtask.py	2019-01-10 11:51:47 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Classes that implement IBugTask and its related interfaces."""
@@ -30,11 +30,7 @@
     )
 import re
 
-from lazr.lifecycle.event import (
-    ObjectDeletedEvent,
-    ObjectModifiedEvent,
-    )
-from lazr.lifecycle.snapshot import Snapshot
+from lazr.lifecycle.event import ObjectDeletedEvent
 import pytz
 from sqlobject import (
     ForeignKey,
@@ -143,6 +139,7 @@
 from lp.services.propertycache import get_property_cache
 from lp.services.searchbuilder import any
 from lp.services.webapp.interfaces import ILaunchBag
+from lp.services.webapp.snapshot import notify_modified
 from lp.services.xref.interfaces import IXRefSet
 
 
@@ -839,16 +836,13 @@
             # END TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.
             ):
             janitor = getUtility(ILaunchpadCelebrities).janitor
-            bugtask_before_modification = Snapshot(
-                self, providing=providedBy(self))
-            # Create a bug message explaining why the janitor auto-confirmed
-            # the bugtask.
-            msg = ("Status changed to 'Confirmed' because the bug "
-                   "affects multiple users.")
-            self.bug.newMessage(owner=janitor, content=msg)
-            self.transitionToStatus(BugTaskStatus.CONFIRMED, janitor)
-            notify(ObjectModifiedEvent(
-                self, bugtask_before_modification, ['status'], user=janitor))
+            with notify_modified(self, ['status'], user=janitor):
+                # Create a bug message explaining why the janitor
+                # auto-confirmed the bugtask.
+                msg = ("Status changed to 'Confirmed' because the bug "
+                       "affects multiple users.")
+                self.bug.newMessage(owner=janitor, content=msg)
+                self.transitionToStatus(BugTaskStatus.CONFIRMED, janitor)
 
     def canTransitionToStatus(self, new_status, user):
         """See `IBugTask`."""

=== modified file 'lib/lp/bugs/model/tests/test_bugtask.py'
--- lib/lp/bugs/model/tests/test_bugtask.py	2018-02-14 01:27:28 +0000
+++ lib/lp/bugs/model/tests/test_bugtask.py	2019-01-10 11:51:47 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -9,7 +9,6 @@
 import subprocess
 import unittest
 
-from lazr.lifecycle.event import ObjectModifiedEvent
 from lazr.lifecycle.snapshot import Snapshot
 from lazr.restfulclient.errors import Unauthorized
 from storm.store import Store
@@ -17,7 +16,6 @@
 from testtools.testcase import ExpectedException
 import transaction
 from zope.component import getUtility
-from zope.event import notify
 from zope.interface import providedBy
 from zope.security.interfaces import Unauthorized as ZopeUnAuthorized
 from zope.security.proxy import removeSecurityProxy
@@ -89,6 +87,7 @@
 from lp.services.searchbuilder import any
 from lp.services.webapp.authorization import check_permission
 from lp.services.webapp.interfaces import ILaunchBag
+from lp.services.webapp.snapshot import notify_modified
 from lp.soyuz.interfaces.archive import ArchivePurpose
 from lp.testing import (
     admin_logged_in,
@@ -563,16 +562,11 @@
         ubuntu_team = getUtility(IPersonSet).getByEmail('support@xxxxxxxxxx')
         bug_upstream_firefox_crashes.bug.subscribe(ubuntu_team, ubuntu_team)
 
-        old_state = Snapshot(
-            bug_upstream_firefox_crashes.bug, providing=IBug)
-        self.assertTrue(
-            bug_upstream_firefox_crashes.bug.setPrivate(True, foobar))
-
-        bug_set_private = ObjectModifiedEvent(
-            bug_upstream_firefox_crashes.bug, old_state,
-            ["id", "title", "private"])
-
-        notify(bug_set_private)
+        with notify_modified(
+                bug_upstream_firefox_crashes.bug, ["id", "title", "private"]):
+            self.assertTrue(
+                bug_upstream_firefox_crashes.bug.setPrivate(True, foobar))
+
         flush_database_updates()
 
         # If we now login as someone who was neither implicitly nor explicitly
@@ -2345,11 +2339,10 @@
         task = self.factory.makeBugTask(target=old)
         p = self.factory.makePerson()
         self.assertEqual(old, task.target)
-        old_state = Snapshot(task, providing=providedBy(task))
         with person_logged_in(task.owner):
-            task.bug.subscribe(p, p)
-            task.transitionToTarget(new, p)
-            notify(ObjectModifiedEvent(task, old_state, ["target"]))
+            with notify_modified(task, ["target"]):
+                task.bug.subscribe(p, p)
+                task.transitionToTarget(new, p)
         return task
 
     def assertTransitionWorks(self, a, b):

=== modified file 'lib/lp/bugs/scripts/bugexpire.py'
--- lib/lp/bugs/scripts/bugexpire.py	2012-12-20 14:55:13 +0000
+++ lib/lp/bugs/scripts/bugexpire.py	2019-01-10 11:51:47 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """BugTask expiration rules."""
@@ -10,11 +10,7 @@
 
 from logging import getLogger
 
-from lazr.lifecycle.event import ObjectModifiedEvent
-from lazr.lifecycle.snapshot import Snapshot
 from zope.component import getUtility
-from zope.event import notify
-from zope.interface import providedBy
 
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.bugs.interfaces.bugtask import (
@@ -27,6 +23,7 @@
     setupInteraction,
     )
 from lp.services.webapp.interfaces import IPlacelessAuthUtility
+from lp.services.webapp.snapshot import notify_modified
 
 
 class BugJanitor:
@@ -87,19 +84,16 @@
                 if bugtask.conjoined_master:
                     continue
 
-                bugtask_before_modification = Snapshot(
-                    bugtask, providing=providedBy(bugtask))
-                bugtask.transitionToStatus(
-                    BugTaskStatus.EXPIRED, self.janitor)
-                content = message_template % (
-                    bugtask.bugtargetdisplayname, self.days_before_expiration)
-                bugtask.bug.newMessage(
-                    owner=self.janitor,
-                    subject=bugtask.bug.followup_subject(),
-                    content=content)
-                notify(ObjectModifiedEvent(
-                    bugtask, bugtask_before_modification,
-                    ['status'], user=self.janitor))
+                with notify_modified(bugtask, ['status'], user=self.janitor):
+                    bugtask.transitionToStatus(
+                        BugTaskStatus.EXPIRED, self.janitor)
+                    content = message_template % (
+                        bugtask.bugtargetdisplayname,
+                        self.days_before_expiration)
+                    bugtask.bug.newMessage(
+                        owner=self.janitor,
+                        subject=bugtask.bug.followup_subject(),
+                        content=content)
                 # We commit after each expiration because emails are sent
                 # immediately in zopeless. This minimize the risk of
                 # duplicate expiration emails being sent in case an error

=== modified file 'lib/lp/bugs/tests/test_bugchanges.py'
--- lib/lp/bugs/tests/test_bugchanges.py	2018-01-02 10:54:31 +0000
+++ lib/lp/bugs/tests/test_bugchanges.py	2019-01-10 11:51:47 +0000
@@ -1,20 +1,15 @@
-# Copyright 2009-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for recording changes done to a bug."""
 
-from lazr.lifecycle.event import (
-    ObjectCreatedEvent,
-    ObjectModifiedEvent,
-    )
-from lazr.lifecycle.snapshot import Snapshot
+from lazr.lifecycle.event import ObjectCreatedEvent
 from testtools.matchers import (
     MatchesStructure,
     StartsWith,
     )
 from zope.component import getUtility
 from zope.event import notify
-from zope.interface import providedBy
 
 from lp.app.enums import InformationType
 from lp.bugs.enums import BugNotificationLevel
@@ -27,6 +22,7 @@
 from lp.bugs.scripts.bugnotification import construct_email_notifications
 from lp.services.librarian.browser import ProxiedLibraryFileAlias
 from lp.services.webapp.publisher import canonical_url
+from lp.services.webapp.snapshot import notify_modified
 from lp.testing import (
     api_url,
     launchpadlib_for,
@@ -100,15 +96,13 @@
 
         :return: The value of `attribute` before modification.
         """
-        obj_before_modification = Snapshot(obj, providing=providedBy(obj))
-        if attribute == 'duplicateof':
-            obj.markAsDuplicate(new_value)
-        else:
-            setattr(obj, attribute, new_value)
-        notify(ObjectModifiedEvent(
-            obj, obj_before_modification, [attribute], self.user))
-
-        return getattr(obj_before_modification, attribute)
+        with notify_modified(
+                obj, [attribute], user=self.user) as obj_before_modification:
+            if attribute == 'duplicateof':
+                obj.markAsDuplicate(new_value)
+            else:
+                setattr(obj, attribute, new_value)
+            return getattr(obj_before_modification, attribute)
 
     def getNewNotifications(self, bug=None):
         if bug is None:
@@ -673,12 +667,9 @@
         # log and notifications.
         bug = self.factory.makeBug()
         self.saveOldChanges(bug=bug)
-        bug_before_modification = Snapshot(bug, providing=providedBy(bug))
-        bug.transitionToInformationType(
-            InformationType.PRIVATESECURITY, self.user)
-        notify(ObjectModifiedEvent(
-            bug, bug_before_modification, ['information_type'],
-            user=self.user))
+        with notify_modified(bug, ['information_type'], user=self.user):
+            bug.transitionToInformationType(
+                InformationType.PRIVATESECURITY, self.user)
 
         information_type_change_activity = {
             'person': self.user,
@@ -998,13 +989,9 @@
     def test_change_bugtask_importance(self):
         # When a bugtask's importance is changed, BugActivity and
         # BugNotification get updated.
-        bug_task_before_modification = Snapshot(
-            self.bug_task, providing=providedBy(self.bug_task))
-        self.bug_task.transitionToImportance(
-            BugTaskImportance.HIGH, user=self.user)
-        notify(ObjectModifiedEvent(
-            self.bug_task, bug_task_before_modification,
-            ['importance'], user=self.user))
+        with notify_modified(self.bug_task, ['importance'], user=self.user):
+            self.bug_task.transitionToImportance(
+                BugTaskImportance.HIGH, user=self.user)
 
         # This checks the activity's attribute and target attributes.
         activity = self.bug.activity[-1]
@@ -1033,13 +1020,9 @@
     def test_change_bugtask_status(self):
         # When a bugtask's status is changed, BugActivity and
         # BugNotification get updated.
-        bug_task_before_modification = Snapshot(
-            self.bug_task, providing=providedBy(self.bug_task))
-        self.bug_task.transitionToStatus(
-            BugTaskStatus.FIXCOMMITTED, user=self.user)
-        notify(ObjectModifiedEvent(
-            self.bug_task, bug_task_before_modification, ['status'],
-            user=self.user))
+        with notify_modified(self.bug_task, ['status'], user=self.user):
+            self.bug_task.transitionToStatus(
+                BugTaskStatus.FIXCOMMITTED, user=self.user)
 
         expected_activity = {
             'person': self.user,
@@ -1063,16 +1046,13 @@
     def test_target_bugtask_to_product(self):
         # When a bugtask's target is changed, BugActivity and
         # BugNotification get updated.
-        bug_task_before_modification = Snapshot(
-            self.bug_task, providing=providedBy(self.bug_task))
-
-        new_target = self.factory.makeProduct(owner=self.user)
-        target_lifecycle_subscriber = self.newSubscriber(
-            new_target, "target-lifecycle", BugNotificationLevel.LIFECYCLE)
-        self.bug_task.transitionToTarget(new_target, self.user)
-        notify(ObjectModifiedEvent(
-            self.bug_task, bug_task_before_modification,
-            ['target', 'product'], user=self.user))
+        with notify_modified(
+                self.bug_task, ['target', 'product'],
+                user=self.user) as bug_task_before_modification:
+            new_target = self.factory.makeProduct(owner=self.user)
+            target_lifecycle_subscriber = self.newSubscriber(
+                new_target, "target-lifecycle", BugNotificationLevel.LIFECYCLE)
+            self.bug_task.transitionToTarget(new_target, self.user)
 
         expected_activity = {
             'person': self.user,
@@ -1115,14 +1095,10 @@
             owner=self.user, target=target)
         self.saveOldChanges(source_package_bug)
 
-        bug_task_before_modification = Snapshot(
-            source_package_bug_task,
-            providing=providedBy(source_package_bug_task))
-        source_package_bug_task.transitionToTarget(new_target, self.user)
-
-        notify(ObjectModifiedEvent(
-            source_package_bug_task, bug_task_before_modification,
-            ['target', 'sourcepackagename'], user=self.user))
+        with notify_modified(
+                source_package_bug_task, ['target', 'sourcepackagename'],
+                user=self.user) as bug_task_before_modification:
+            source_package_bug_task.transitionToTarget(new_target, self.user)
 
         expected_activity = {
             'person': self.user,
@@ -1221,13 +1197,8 @@
     def test_assign_bugtask(self):
         # Assigning a bug task to someone adds entries to the bug
         # activity and notifications sets.
-        bug_task_before_modification = Snapshot(
-            self.bug_task, providing=providedBy(self.bug_task))
-
-        self.bug_task.transitionToAssignee(self.user)
-        notify(ObjectModifiedEvent(
-            self.bug_task, bug_task_before_modification,
-            ['assignee'], user=self.user))
+        with notify_modified(self.bug_task, ['assignee'], user=self.user):
+            self.bug_task.transitionToAssignee(self.user)
 
         expected_activity = {
             'person': self.user,
@@ -1257,14 +1228,8 @@
         # bug activity and notifications sets.
 
         old_assignee = bug_task.assignee
-        bug_task_before_modification = Snapshot(
-            bug_task, providing=providedBy(bug_task))
-
-        bug_task.transitionToAssignee(None)
-
-        notify(ObjectModifiedEvent(
-            bug_task, bug_task_before_modification,
-            ['assignee'], user=self.user))
+        with notify_modified(bug_task, ['assignee'], user=self.user):
+            bug_task.transitionToAssignee(None)
 
         expected_activity = {
             'person': self.user,

=== modified file 'lib/lp/bugs/tests/test_bugnotification.py'
--- lib/lp/bugs/tests/test_bugnotification.py	2018-02-02 10:06:24 +0000
+++ lib/lp/bugs/tests/test_bugnotification.py	2019-01-10 11:51:47 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests related to bug notifications."""
@@ -7,20 +7,13 @@
 
 from datetime import datetime
 
-from lazr.lifecycle.event import ObjectModifiedEvent
-from lazr.lifecycle.snapshot import Snapshot
 import pytz
 from storm.store import Store
 import transaction
 from zope.component import getUtility
-from zope.event import notify
-from zope.interface import providedBy
 
 from lp.answers.tests.test_question_notifications import pop_questionemailjobs
-from lp.bugs.interfaces.bugtask import (
-    BugTaskStatus,
-    IBugTask,
-    )
+from lp.bugs.interfaces.bugtask import BugTaskStatus
 from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
 from lp.bugs.model.bugnotification import (
     BugNotification,
@@ -32,6 +25,7 @@
 from lp.services.config import config
 from lp.services.messages.interfaces.message import IMessageSet
 from lp.services.messages.model.message import MessageSet
+from lp.services.webapp.snapshot import notify_modified
 from lp.testing import (
     person_logged_in,
     TestCaseWithFactory,
@@ -67,11 +61,9 @@
         # Ensure that notifications are sent to subscribers of a
         # question linked to the expired bug.
         bugtask = self.bug.default_bugtask
-        bugtask_before_modification = Snapshot(bugtask, providing=IBugTask)
-        bugtask.transitionToStatus(BugTaskStatus.EXPIRED, self.product.owner)
-        bug_modified = ObjectModifiedEvent(
-            bugtask, bugtask_before_modification, ["status"])
-        notify(bug_modified)
+        with notify_modified(bugtask, ["status"]):
+            bugtask.transitionToStatus(
+                BugTaskStatus.EXPIRED, self.product.owner)
         recipients = [
             job.metadata['recipient_set'] for job in pop_questionemailjobs()]
         self.assertContentEqual(
@@ -388,12 +380,9 @@
 
     def test_duplicate_edit_notifications(self):
         # Bug edits for a duplicate are sent to duplicate subscribers only.
-        bug_before_modification = Snapshot(
-            self.dupe_bug, providing=providedBy(self.dupe_bug))
-        self.dupe_bug.description = 'A changed description'
-        notify(ObjectModifiedEvent(
-            self.dupe_bug, bug_before_modification, ['description'],
-            user=self.dupe_bug.owner))
+        with notify_modified(
+                self.dupe_bug, ['description'], user=self.dupe_bug.owner):
+            self.dupe_bug.description = 'A changed description'
         latest_notification = BugNotification.selectFirst(orderBy='-id')
         recipients = set(
             recipient.person


Follow ups