launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #22993
[Merge] lp:~cjwatson/launchpad/snapshot-modifying-helper into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snapshot-modifying-helper into lp:launchpad.
Commit message:
Add a notify_modified context manager to make modified notifications less tedious, and refactor Git-related code to use it.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snapshot-modifying-helper/+merge/357598
I was getting very fed up of typing this out by hand while doing UI work for Git per-branch permissions, and it didn't feel right, so here's a helper.
There were a few similar but domain-specific helpers in the codebase. One collided with the name I (independently) wanted to use, but there was an alternative already in place, so I migrated everything over to it.
I have a much more complete branch locally that migrates nearly all the by-hand equivalents of this to notify_modified, mainly to prove to myself that it could be done. That's a few thousand more lines of patch and isn't urgent, so I'm not including it here. From that, the remaining cases that notify_modified doesn't handle well are:
* cases where the user argument would need to depend on the operation in the contained block;
* cases where the operation is unconditional but notification is conditional (maybe contextlib.ExitStack or similar could help?);
* cases where we need to return the resulting event;
* cases where there's a customised delta object.
Even with that caveat, I think it's still a significant readability improvement.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snapshot-modifying-helper into lp:launchpad.
=== modified file 'lib/lp/code/interfaces/branchmergeproposal.py'
--- lib/lp/code/interfaces/branchmergeproposal.py 2018-04-25 12:01:33 +0000
+++ lib/lp/code/interfaces/branchmergeproposal.py 2018-10-21 21:17:37 +0000
@@ -25,11 +25,9 @@
'IReviewRequestedEmailJobSource',
'IUpdatePreviewDiffJob',
'IUpdatePreviewDiffJobSource',
- 'notify_modified',
]
-from lazr.lifecycle.event import ObjectModifiedEvent
from lazr.restful.declarations import (
call_with,
export_as_webservice_entry,
@@ -49,7 +47,6 @@
Reference,
ReferenceChoice,
)
-from zope.event import notify
from zope.interface import (
Attribute,
Interface,
@@ -954,22 +951,3 @@
:param delta_text: The text representation of the changed fields.
:param editor: The person who did the editing.
"""
-
-
-# XXX: JonathanLange 2010-01-06: This is only used in the scanner, perhaps it
-# should be moved there.
-
-def notify_modified(proposal, func, *args, **kwargs):
- """Call func, then notify about the changes it made.
-
- :param proposal: the merge proposal to notify about.
- :param func: The callable that will modify the merge proposal.
- :param args: Additional arguments for the method.
- :param kwargs: Keyword arguments for the method.
- :return: The return value of the method.
- """
- from lp.code.adapters.branch import BranchMergeProposalNoPreviewDiffDelta
- snapshot = BranchMergeProposalNoPreviewDiffDelta.snapshot(proposal)
- result = func(*args, **kwargs)
- notify(ObjectModifiedEvent(proposal, snapshot, []))
- return result
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2018-10-18 16:06:38 +0000
+++ lib/lp/code/model/gitrepository.py 2018-10-21 21:17:37 +0000
@@ -70,6 +70,7 @@
IPrivacy,
)
from lp.app.interfaces.services import IService
+from lp.code.adapters.branch import BranchMergeProposalNoPreviewDiffDelta
from lp.code.enums import (
BranchMergeProposalStatus,
GitGranteeType,
@@ -86,7 +87,6 @@
from lp.code.event.git import GitRefsUpdatedEvent
from lp.code.interfaces.branchmergeproposal import (
BRANCH_MERGE_PROPOSAL_FINAL_STATES,
- notify_modified,
)
from lp.code.interfaces.codeimport import ICodeImportSet
from lp.code.interfaces.gitactivity import IGitActivitySet
@@ -1091,9 +1091,8 @@
"Merge detected: %s => %s",
proposal.source_git_ref.identity,
proposal.target_git_ref.identity)
- notify_modified(
- proposal, proposal.markAsMerged,
- merged_revision_id=merged_revision_id)
+ with BranchMergeProposalNoPreviewDiffDelta.monitor(proposal):
+ proposal.markAsMerged(merged_revision_id=merged_revision_id)
def detectMerges(self, paths, logger=None):
"""See `IGitRepository`."""
=== modified file 'lib/lp/code/model/gitrule.py'
--- lib/lp/code/model/gitrule.py 2018-10-19 23:10:41 +0000
+++ lib/lp/code/model/gitrule.py 2018-10-21 21:17:37 +0000
@@ -14,8 +14,6 @@
from collections import OrderedDict
from lazr.enum import DBItem
-from lazr.lifecycle.event import ObjectModifiedEvent
-from lazr.lifecycle.snapshot import Snapshot
from lazr.restful.interfaces import (
IFieldMarshaller,
IJSONPublishable,
@@ -35,11 +33,7 @@
getMultiAdapter,
getUtility,
)
-from zope.event import notify
-from zope.interface import (
- implementer,
- providedBy,
- )
+from zope.interface import implementer
from zope.security.proxy import removeSecurityProxy
from lp.code.enums import (
@@ -64,6 +58,7 @@
from lp.services.database.enumcol import DBEnum
from lp.services.database.stormbase import StormBase
from lp.services.fields import InlineObject
+from lp.services.webapp.snapshot import notify_modified
def git_rule_modified(rule, event):
@@ -191,15 +186,12 @@
can_push=new_grant.can_push,
can_force_push=new_grant.can_force_push)
else:
- grant_before_modification = Snapshot(
- grant, providing=providedBy(grant))
edited_fields = []
- for field in ("can_create", "can_push", "can_force_push"):
- if getattr(grant, field) != getattr(new_grant, field):
- setattr(grant, field, getattr(new_grant, field))
- edited_fields.append(field)
- notify(ObjectModifiedEvent(
- grant, grant_before_modification, edited_fields))
+ with notify_modified(grant, edited_fields):
+ for field in ("can_create", "can_push", "can_force_push"):
+ if getattr(grant, field) != getattr(new_grant, field):
+ setattr(grant, field, getattr(new_grant, field))
+ edited_fields.append(field)
def destroySelf(self, user):
"""See `IGitRule`."""
=== modified file 'lib/lp/code/model/tests/test_branchmergeproposal.py'
--- lib/lp/code/model/tests/test_branchmergeproposal.py 2018-09-19 08:46:20 +0000
+++ lib/lp/code/model/tests/test_branchmergeproposal.py 2018-10-21 21:17:37 +0000
@@ -15,10 +15,7 @@
import hashlib
from unittest import TestCase
-from lazr.lifecycle.event import (
- ObjectCreatedEvent,
- ObjectModifiedEvent,
- )
+from lazr.lifecycle.event import ObjectCreatedEvent
from lazr.restfulclient.errors import BadRequest
from pytz import UTC
from sqlobject import SQLObjectNotFound
@@ -62,7 +59,6 @@
IBranchMergeProposal,
IBranchMergeProposalGetter,
IBranchMergeProposalJobSource,
- notify_modified,
)
from lp.code.model.branchmergeproposal import (
BranchMergeProposal,
@@ -1681,25 +1677,6 @@
self.assertEqual("TooManyRelatedBugs", self.oopses[0]["type"])
-class TestNotifyModified(TestCaseWithFactory):
-
- layer = DatabaseFunctionalLayer
-
- def test_notify_modified_generates_notification(self):
- """notify_modified generates an event.
-
- notify_modified runs the callable with the specified args and kwargs,
- and generates a ObjectModifiedEvent.
- """
- bmp = self.factory.makeBranchMergeProposal()
- login_person(bmp.target_branch.owner)
- self.assertNotifies(
- ObjectModifiedEvent, False, notify_modified, bmp, bmp.markAsMerged,
- merge_reporter=bmp.target_branch.owner)
- self.assertEqual(BranchMergeProposalStatus.MERGED, bmp.queue_status)
- self.assertEqual(bmp.target_branch.owner, bmp.merge_reporter)
-
-
class TestBranchMergeProposalNominateReviewer(TestCaseWithFactory):
"""Test that the appropriate vote references get created."""
=== modified file 'lib/lp/code/model/tests/test_codereviewkarma.py'
--- lib/lp/code/model/tests/test_codereviewkarma.py 2017-10-04 01:53:48 +0000
+++ lib/lp/code/model/tests/test_codereviewkarma.py 2018-10-21 21:17:37 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for karma allocated for code reviews."""
@@ -7,7 +7,7 @@
__metaclass__ = type
-from lp.code.interfaces.branchmergeproposal import notify_modified
+from lp.code.adapters.branch import BranchMergeProposalNoPreviewDiffDelta
from lp.registry.interfaces.karma import IKarmaAssignedEvent
from lp.registry.interfaces.person import IPerson
from lp.testing import (
@@ -117,8 +117,8 @@
reviewer = proposal.target_branch.owner
self.karma_events = []
login_person(reviewer)
- notify_modified(
- proposal, proposal.approveBranch, reviewer, "A rev id.")
+ with BranchMergeProposalNoPreviewDiffDelta.monitor(proposal):
+ proposal.approveBranch(reviewer, "A rev id.")
self.assertOneKarmaEvent(reviewer, 'branchmergeapproved')
def test_approvingOwnCodeReview(self):
@@ -129,8 +129,8 @@
target_branch=target_branch, registrant=reviewer)
self.karma_events = []
login_person(reviewer)
- notify_modified(
- proposal, proposal.approveBranch, reviewer, "A rev id.")
+ with BranchMergeProposalNoPreviewDiffDelta.monitor(proposal):
+ proposal.approveBranch(reviewer, "A rev id.")
self.assertOneKarmaEvent(reviewer, 'branchmergeapprovedown')
def test_rejectedCodeReview(self):
@@ -140,7 +140,8 @@
reviewer = proposal.target_branch.owner
self.karma_events = []
login_person(reviewer)
- notify_modified(proposal, proposal.rejectBranch, reviewer, "A rev id.")
+ with BranchMergeProposalNoPreviewDiffDelta.monitor(proposal):
+ proposal.rejectBranch(reviewer, "A rev id.")
self.assertOneKarmaEvent(reviewer, 'branchmergerejected')
def test_rejectedOwnCodeReview(self):
@@ -153,5 +154,6 @@
target_branch=target_branch, registrant=reviewer)
self.karma_events = []
login_person(reviewer)
- notify_modified(proposal, proposal.rejectBranch, reviewer, "A rev id.")
+ with BranchMergeProposalNoPreviewDiffDelta.monitor(proposal):
+ proposal.rejectBranch(reviewer, "A rev id.")
self.assertOneKarmaEvent(reviewer, 'branchmergerejectedown')
=== modified file 'lib/lp/code/model/tests/test_gitjob.py'
--- lib/lp/code/model/tests/test_gitjob.py 2018-10-16 10:10:10 +0000
+++ lib/lp/code/model/tests/test_gitjob.py 2018-10-21 21:17:37 +0000
@@ -13,7 +13,6 @@
)
import hashlib
-from lazr.lifecycle.event import ObjectModifiedEvent
from lazr.lifecycle.snapshot import Snapshot
import pytz
from testtools.matchers import (
@@ -24,7 +23,6 @@
MatchesStructure,
)
import transaction
-from zope.event import notify
from zope.interface import providedBy
from zope.security.proxy import removeSecurityProxy
@@ -56,6 +54,7 @@
from lp.services.job.runner import JobRunner
from lp.services.utils import seconds_since_epoch
from lp.services.webapp import canonical_url
+from lp.services.webapp.snapshot import notify_modified
from lp.testing import (
person_logged_in,
TestCaseWithFactory,
@@ -395,10 +394,9 @@
repository=repository, ref_pattern="refs/heads/foo")
transaction.commit()
snapshot = Snapshot(repository, providing=providedBy(repository))
- rule_snapshot = Snapshot(rule, providing=providedBy(rule))
with person_logged_in(repository.owner):
- rule.ref_pattern = "refs/heads/bar"
- notify(ObjectModifiedEvent(rule, rule_snapshot, ["ref_pattern"]))
+ with notify_modified(rule, ["ref_pattern"]):
+ rule.ref_pattern = "refs/heads/bar"
self.assertDeltaDescriptionEqual(
[], ["Changed protected ref: refs/heads/foo => refs/heads/bar"],
snapshot, repository)
@@ -451,10 +449,9 @@
can_create=True)
transaction.commit()
snapshot = Snapshot(repository, providing=providedBy(repository))
- grant_snapshot = Snapshot(grant, providing=providedBy(grant))
with person_logged_in(repository.owner):
- grant.can_push = True
- notify(ObjectModifiedEvent(grant, grant_snapshot, ["can_push"]))
+ with notify_modified(grant, ["can_push"]):
+ grant.can_push = True
self.assertDeltaDescriptionEqual(
[],
["Changed access for ~{grantee} to refs/heads/*: "
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2018-10-16 10:10:10 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2018-10-21 21:17:37 +0000
@@ -15,7 +15,6 @@
from bzrlib import urlutils
from lazr.lifecycle.event import ObjectModifiedEvent
-from lazr.lifecycle.snapshot import Snapshot
import pytz
from sqlobject import SQLObjectNotFound
from storm.exceptions import LostObjectError
@@ -29,8 +28,6 @@
)
import transaction
from zope.component import getUtility
-from zope.event import notify
-from zope.interface import providedBy
from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
@@ -130,6 +127,7 @@
from lp.services.utils import seconds_since_epoch
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.interfaces import OAuthPermission
+from lp.services.webapp.snapshot import notify_modified
from lp.testing import (
admin_logged_in,
ANONYMOUS,
@@ -929,9 +927,10 @@
# modified date is set to UTC_NOW.
repository = self.factory.makeGitRepository(
date_created=datetime(2015, 2, 4, 17, 42, 0, tzinfo=pytz.UTC))
- notify(ObjectModifiedEvent(
- removeSecurityProxy(repository), repository,
- [IGitRepository["name"]], user=repository.owner))
+ with notify_modified(
+ removeSecurityProxy(repository), ["name"],
+ user=repository.owner):
+ pass
self.assertSqlAttributeEqualsDate(
repository, "date_last_modified", UTC_NOW)
@@ -987,26 +986,22 @@
subscriber_address = (
removeSecurityProxy(subscriber.preferredemail).email)
transaction.commit()
- repository_before_modification = Snapshot(
- repository, providing=providedBy(repository))
with person_logged_in(repository.owner):
- repository.subscribe(
- repository.owner,
- BranchSubscriptionNotificationLevel.ATTRIBUTEONLY,
- BranchSubscriptionDiffSize.NODIFF,
- CodeReviewNotificationLevel.NOEMAIL,
- repository.owner)
- repository.subscribe(
- subscriber,
- BranchSubscriptionNotificationLevel.ATTRIBUTEONLY,
- BranchSubscriptionDiffSize.NODIFF,
- CodeReviewNotificationLevel.NOEMAIL,
- repository.owner)
- repository.setName("bar", repository.owner)
- repository.addRule("refs/heads/stable/*", repository.owner)
- notify(ObjectModifiedEvent(
- repository, repository_before_modification, ["name"],
- user=repository.owner))
+ with notify_modified(repository, ["name"], user=repository.owner):
+ repository.subscribe(
+ repository.owner,
+ BranchSubscriptionNotificationLevel.ATTRIBUTEONLY,
+ BranchSubscriptionDiffSize.NODIFF,
+ CodeReviewNotificationLevel.NOEMAIL,
+ repository.owner)
+ repository.subscribe(
+ subscriber,
+ BranchSubscriptionNotificationLevel.ATTRIBUTEONLY,
+ BranchSubscriptionDiffSize.NODIFF,
+ CodeReviewNotificationLevel.NOEMAIL,
+ repository.owner)
+ repository.setName("bar", repository.owner)
+ repository.addRule("refs/heads/stable/*", repository.owner)
with dbuser(config.IGitRepositoryModifiedMailJobSource.dbuser):
JobRunner.fromReady(
getUtility(IGitRepositoryModifiedMailJobSource)).runAll()
=== modified file 'lib/lp/code/model/tests/test_gitrule.py'
--- lib/lp/code/model/tests/test_gitrule.py 2018-10-19 23:10:41 +0000
+++ lib/lp/code/model/tests/test_gitrule.py 2018-10-21 21:17:37 +0000
@@ -7,8 +7,6 @@
__metaclass__ = type
-from lazr.lifecycle.event import ObjectModifiedEvent
-from lazr.lifecycle.snapshot import Snapshot
from storm.store import Store
from testtools.matchers import (
Equals,
@@ -19,8 +17,6 @@
MatchesStructure,
)
import transaction
-from zope.event import notify
-from zope.interface import providedBy
from zope.security.proxy import removeSecurityProxy
from lp.code.enums import (
@@ -34,6 +30,7 @@
IGitRuleGrant,
)
from lp.services.database.sqlbase import get_transaction_timestamp
+from lp.services.webapp.snapshot import notify_modified
from lp.testing import (
person_logged_in,
TestCaseWithFactory,
@@ -490,11 +487,9 @@
repository = self.factory.makeGitRepository(owner=owner)
rule = self.factory.makeGitRule(
repository=repository, ref_pattern="refs/heads/*")
- rule_before_modification = Snapshot(rule, providing=providedBy(rule))
with person_logged_in(member):
- rule.ref_pattern = "refs/heads/other/*"
- notify(ObjectModifiedEvent(
- rule, rule_before_modification, ["ref_pattern"]))
+ with notify_modified(rule, ["ref_pattern"]):
+ rule.ref_pattern = "refs/heads/other/*"
self.assertThat(repository.getActivity().first(), MatchesStructure(
repository=Equals(repository),
changer=Equals(member),
@@ -686,14 +681,10 @@
grant = self.factory.makeGitRuleGrant(
repository=repository, grantee=GitGranteeType.REPOSITORY_OWNER,
can_create=True)
- grant_before_modification = Snapshot(
- grant, providing=providedBy(grant))
with person_logged_in(member):
- grant.can_create = False
- grant.can_force_push = True
- notify(ObjectModifiedEvent(
- grant, grant_before_modification,
- ["can_create", "can_force_push"]))
+ with notify_modified(grant, ["can_create", "can_force_push"]):
+ grant.can_create = False
+ grant.can_force_push = True
self.assertThat(repository.getActivity().first(), MatchesStructure(
repository=Equals(repository),
changer=Equals(member),
=== modified file 'lib/lp/codehosting/scanner/mergedetection.py'
--- lib/lp/codehosting/scanner/mergedetection.py 2018-04-10 21:47:06 +0000
+++ lib/lp/codehosting/scanner/mergedetection.py 2018-10-21 21:17:37 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""The way the branch scanner handles merges."""
@@ -12,11 +12,11 @@
from bzrlib.revision import NULL_REVISION
from zope.component import getUtility
+from lp.code.adapters.branch import BranchMergeProposalNoPreviewDiffDelta
from lp.code.enums import BranchLifecycleStatus
from lp.code.interfaces.branchcollection import IAllBranches
from lp.code.interfaces.branchmergeproposal import (
BRANCH_MERGE_PROPOSAL_FINAL_STATES,
- notify_modified,
)
from lp.services.utils import CachingIterator
@@ -62,7 +62,8 @@
if is_development_focus(target):
mark_branch_merged(logger, source)
else:
- notify_modified(proposal, proposal.markAsMerged, merge_revno)
+ with BranchMergeProposalNoPreviewDiffDelta.monitor(proposal):
+ proposal.markAsMerged(merge_revno)
# If there is an explicit merge proposal, change the branch's
# status when it's been merged into a development focus or any
# other series branch.
=== modified file 'lib/lp/services/webapp/snapshot.py'
--- lib/lp/services/webapp/snapshot.py 2015-10-14 15:22:01 +0000
+++ lib/lp/services/webapp/snapshot.py 2018-10-21 21:17:37 +0000
@@ -1,12 +1,20 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Snapshot adapter for the Storm result set."""
-
+"""Snapshot helpers."""
+
+from contextlib import contextmanager
+
+from lazr.lifecycle.event import ObjectModifiedEvent
from lazr.lifecycle.interfaces import ISnapshotValueFactory
+from lazr.lifecycle.snapshot import Snapshot
from storm.zope.interfaces import IResultSet
from zope.component import adapter
-from zope.interface import implementer
+from zope.event import notify
+from zope.interface import (
+ implementer,
+ providedBy,
+ )
from lp.services.helpers import shortlist
@@ -17,9 +25,58 @@
@implementer(ISnapshotValueFactory)
@adapter(IResultSet) # And ISQLObjectResultSet.
def snapshot_sql_result(value):
+ """Snapshot adapter for the Storm result set."""
# SQLMultipleJoin and SQLRelatedJoin return
# SelectResults, which doesn't really help the Snapshot
# object. We therefore list()ify the values; this isn't
# perfect but allows deltas to be generated reliably.
return shortlist(
value, longest_expected=100, hardlimit=HARD_LIMIT_FOR_SNAPSHOT)
+
+
+@contextmanager
+def notify_modified(obj, edited_fields, snapshot_names=None, user=None):
+ """A context manager that notifies about modifications to an object.
+
+ Use this as follows::
+
+ with notify_modified(obj, ["attr"]):
+ obj.attr = value
+
+ Or::
+
+ edited_fields = set()
+ with notify_modified(obj, edited_fields):
+ if obj.attr != new_attr:
+ obj.attr = new_attr
+ edited_fields.add("attr")
+
+ Or even::
+
+ edited_fields = set()
+ with notify_modified(obj, edited_fields) as previous_obj:
+ do_something()
+ if obj.attr != previous_obj.attr:
+ edited_fields.add("attr")
+
+ :param obj: The object being modified.
+ :param edited_fields: An iterable of fields being modified. This is not
+ used until after the block wrapped by the context manager has
+ finished executing, so you can safely pass a mutable object and add
+ items to it from inside the block as you determine which fields are
+ being modified. A notification will only be sent if `edited_fields`
+ is non-empty.
+ :param snapshot_names: If not None, only snapshot these names. This may
+ be used if snapshotting some of the object's attributes is expensive
+ in some contexts (and they can't be wrapped by `doNotSnapshot` for
+ some reason).
+ :param user: If not None, the user making these changes. If None,
+ defaults to the principal registered in the current interaction.
+ """
+ obj_before_modification = Snapshot(
+ obj, names=snapshot_names, providing=providedBy(obj))
+ yield obj_before_modification
+ edited_fields = list(edited_fields)
+ if edited_fields:
+ notify(ObjectModifiedEvent(
+ obj, obj_before_modification, edited_fields, user=user))
=== added file 'lib/lp/services/webapp/tests/test_snapshot.py'
--- lib/lp/services/webapp/tests/test_snapshot.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/webapp/tests/test_snapshot.py 2018-10-21 21:17:37 +0000
@@ -0,0 +1,107 @@
+# Copyright 2018 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+
+from lazr.lifecycle.interfaces import IObjectModifiedEvent
+from lazr.lifecycle.snapshot import Snapshot
+from testtools.matchers import (
+ Equals,
+ MatchesAll,
+ MatchesListwise,
+ MatchesStructure,
+ )
+from zope.authentication.interfaces import IUnauthenticatedPrincipal
+from zope.interface import (
+ implementer,
+ Interface,
+ )
+from zope.schema import Int
+
+from lp.services.webapp.snapshot import notify_modified
+from lp.testing import (
+ EventRecorder,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import ZopelessDatabaseLayer
+from lp.testing.matchers import Provides
+
+
+class IThing(Interface):
+
+ attr = Int()
+
+
+@implementer(IThing)
+class Thing:
+
+ def __init__(self, attr):
+ self.attr = attr
+
+
+class TestNotifyModified(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def test_generates_notification(self):
+ obj = Thing(0)
+ with EventRecorder() as recorder:
+ with notify_modified(obj, ["attr"]):
+ obj.attr = 1
+ self.assertThat(recorder.events, MatchesListwise([
+ MatchesAll(
+ Provides(IObjectModifiedEvent),
+ MatchesStructure(
+ object=MatchesStructure(attr=Equals(1)),
+ object_before_modification=MatchesAll(
+ Provides(IThing),
+ MatchesStructure(attr=Equals(0))),
+ edited_fields=Equals(["attr"]),
+ user=Provides(IUnauthenticatedPrincipal))),
+ ]))
+
+ def test_mutate_edited_fields_within_block(self):
+ obj = Thing(0)
+ with EventRecorder() as recorder:
+ edited_fields = set()
+ with notify_modified(obj, edited_fields):
+ obj.attr = 1
+ edited_fields.add("attr")
+ self.assertThat(recorder.events, MatchesListwise([
+ MatchesAll(
+ Provides(IObjectModifiedEvent),
+ MatchesStructure(
+ object=MatchesStructure(attr=Equals(1)),
+ object_before_modification=MatchesAll(
+ Provides(IThing),
+ MatchesStructure(attr=Equals(0))),
+ edited_fields=Equals(["attr"]),
+ user=Provides(IUnauthenticatedPrincipal))),
+ ]))
+
+ def test_yields_previous_object(self):
+ obj = Thing(0)
+ with notify_modified(obj, []) as previous_obj:
+ obj.attr = 1
+ self.assertIsInstance(previous_obj, Snapshot)
+ self.assertEqual(0, previous_obj.attr)
+
+ def test_different_user(self):
+ obj = Thing(0)
+ user = self.factory.makePerson()
+ with EventRecorder() as recorder:
+ with notify_modified(obj, ["attr"], user=user):
+ obj.attr = 1
+ self.assertThat(recorder.events, MatchesListwise([
+ MatchesAll(
+ Provides(IObjectModifiedEvent),
+ MatchesStructure(
+ object=MatchesStructure(attr=Equals(1)),
+ object_before_modification=MatchesAll(
+ Provides(IThing),
+ MatchesStructure(attr=Equals(0))),
+ edited_fields=Equals(["attr"]),
+ user=Equals(user))),
+ ]))
Follow ups