--- /dev/null
+.. change::
+ :tags: feature, orm
+ :tickets: 8375
+
+ Added new parameter :paramref:`_orm.AttributeEvents.include_key`, which
+ will include the dictionary or list key for operations such as
+ ``__setitem__()`` (e.g. ``obj[key] = value``) and ``__delitem__()`` (e.g.
+ ``del obj[key]``), using a new keyword parameter "key" or "keys", depending
+ on event, e.g. :paramref:`_orm.AttributeEvents.append.key`,
+ :paramref:`_orm.AttributeEvents.bulk_replace.keys`. This allows event
+ handlers to take into account the key that was passed to the operation and
+ is of particular importance for dictionary operations working with
+ :class:`_orm.MappedCollection`.
+
from .interfaces import MANYTOMANY as MANYTOMANY
from .interfaces import MANYTOONE as MANYTOONE
from .interfaces import MapperProperty as MapperProperty
+from .interfaces import NO_KEY as NO_KEY
from .interfaces import ONETOMANY as ONETOMANY
from .interfaces import PropComparator as PropComparator
from .interfaces import UserDefinedOption as UserDefinedOption
dict_: _InstanceDict,
value: _T,
initiator: Optional[AttributeEventToken],
+ key: Optional[Any],
) -> _T:
for fn in self.dispatch.append:
- value = fn(state, value, initiator or self._append_token)
+ value = fn(state, value, initiator or self._append_token, key=key)
state._modified_event(dict_, self, NO_VALUE, True)
dict_: _InstanceDict,
value: _T,
initiator: Optional[AttributeEventToken],
+ key: Optional[Any],
) -> _T:
for fn in self.dispatch.append_wo_mutation:
- value = fn(state, value, initiator or self._append_token)
+ value = fn(state, value, initiator or self._append_token, key=key)
return value
state: InstanceState[Any],
dict_: _InstanceDict,
initiator: Optional[AttributeEventToken],
+ key: Optional[Any],
) -> None:
"""A special event used for pop() operations.
dict_: _InstanceDict,
value: Any,
initiator: Optional[AttributeEventToken],
+ key: Optional[Any],
) -> None:
if self.trackparent and value is not None:
self.sethasparent(instance_state(value), state, False)
for fn in self.dispatch.remove:
- fn(state, value, initiator or self._remove_token)
+ fn(state, value, initiator or self._remove_token, key=key)
state._modified_event(dict_, self, NO_VALUE, True)
state, dict_, user_data=None, passive=passive
)
if collection is PASSIVE_NO_RESULT:
- value = self.fire_append_event(state, dict_, value, initiator)
+ value = self.fire_append_event(
+ state, dict_, value, initiator, key=NO_KEY
+ )
assert (
self.key not in dict_
), "Collection was loaded during event handling."
state, state.dict, user_data=None, passive=passive
)
if collection is PASSIVE_NO_RESULT:
- self.fire_remove_event(state, dict_, value, initiator)
+ self.fire_remove_event(state, dict_, value, initiator, key=NO_KEY)
assert (
self.key not in dict_
), "Collection was loaded during event handling."
_adapt: bool = True,
) -> None:
iterable = orig_iterable = value
+ new_keys = None
# pulling a new collection first so that an adaptation exception does
# not trigger a lazy load of the old collection.
if hasattr(iterable, "_sa_iterator"):
iterable = iterable._sa_iterator()
elif setting_type is dict:
+ new_keys = list(iterable)
iterable = iterable.values()
else:
iterable = iter(iterable)
+ elif util.duck_type_collection(iterable) is dict:
+ new_keys = list(value)
+
new_values = list(iterable)
evt = self._bulk_replace_token
- self.dispatch.bulk_replace(state, new_values, evt)
+ self.dispatch.bulk_replace(state, new_values, evt, keys=new_keys)
old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT)
if old is PASSIVE_NO_RESULT:
)
)
- def emit_backref_from_scalar_set_event(state, child, oldchild, initiator):
+ def emit_backref_from_scalar_set_event(
+ state, child, oldchild, initiator, **kw
+ ):
if oldchild is child:
return child
if (
)
return child
- def emit_backref_from_collection_append_event(state, child, initiator):
+ def emit_backref_from_collection_append_event(
+ state, child, initiator, **kw
+ ):
if child is None:
return
)
return child
- def emit_backref_from_collection_remove_event(state, child, initiator):
+ def emit_backref_from_collection_remove_event(
+ state, child, initiator, **kw
+ ):
if (
child is not None
and child is not PASSIVE_NO_RESULT
emit_backref_from_collection_append_event,
retval=True,
raw=True,
+ include_key=True,
)
else:
event.listen(
emit_backref_from_scalar_set_event,
retval=True,
raw=True,
+ include_key=True,
)
# TODO: need coverage in test/orm/ of remove event
event.listen(
emit_backref_from_collection_remove_event,
retval=True,
raw=True,
+ include_key=True,
)
DEFAULT_MANAGER_ATTR = "_sa_class_manager"
DEFAULT_STATE_ATTR = "_sa_instance_state"
-EXT_CONTINUE = util.symbol("EXT_CONTINUE")
-EXT_STOP = util.symbol("EXT_STOP")
-EXT_SKIP = util.symbol("EXT_SKIP")
+
+class EventConstants(Enum):
+ EXT_CONTINUE = 1
+ EXT_STOP = 2
+ EXT_SKIP = 3
+ NO_KEY = 4
+ """indicates an :class:`.AttributeEvent` event that did not have any
+ key argument.
+
+ .. versionadded:: 2.0
+
+ """
+
+
+EXT_CONTINUE, EXT_STOP, EXT_SKIP, NO_KEY = tuple(EventConstants)
class RelationshipDirection(Enum):
from typing import Union
import weakref
+from .base import NO_KEY
from .. import exc as sa_exc
from .. import util
from ..util.compat import inspect_getfullargspec
def __bool__(self):
return True
- def fire_append_wo_mutation_event(self, item, initiator=None):
+ def fire_append_wo_mutation_event(self, item, initiator=None, key=NO_KEY):
"""Notify that a entity is entering the collection but is already
present.
self._reset_empty()
return self.attr.fire_append_wo_mutation_event(
- self.owner_state, self.owner_state.dict, item, initiator
+ self.owner_state, self.owner_state.dict, item, initiator, key
)
else:
return item
- def fire_append_event(self, item, initiator=None):
+ def fire_append_event(self, item, initiator=None, key=NO_KEY):
"""Notify that a entity has entered the collection.
Initiator is a token owned by the InstrumentedAttribute that
self._reset_empty()
return self.attr.fire_append_event(
- self.owner_state, self.owner_state.dict, item, initiator
+ self.owner_state, self.owner_state.dict, item, initiator, key
)
else:
return item
- def fire_remove_event(self, item, initiator=None):
+ def fire_remove_event(self, item, initiator=None, key=NO_KEY):
"""Notify that a entity has been removed from the collection.
Initiator is the InstrumentedAttribute that initiated the membership
self._reset_empty()
self.attr.fire_remove_event(
- self.owner_state, self.owner_state.dict, item, initiator
+ self.owner_state, self.owner_state.dict, item, initiator, key
)
- def fire_pre_remove_event(self, initiator=None):
+ def fire_pre_remove_event(self, initiator=None, key=NO_KEY):
"""Notify that an entity is about to be removed from the collection.
Only called if the entity cannot be removed after calling
if self.invalidated:
self._warn_invalidated()
self.attr.fire_pre_remove_event(
- self.owner_state, self.owner_state.dict, initiator=initiator
+ self.owner_state,
+ self.owner_state.dict,
+ initiator=initiator,
+ key=key,
)
def __getstate__(self):
if _sa_initiator is not False:
executor = collection._sa_adapter
if executor:
- executor.fire_append_wo_mutation_event(item, _sa_initiator)
+ executor.fire_append_wo_mutation_event(
+ item, _sa_initiator, key=None
+ )
-def __set(collection, item, _sa_initiator=None):
+def __set(collection, item, _sa_initiator, key):
"""Run set events.
This event always occurs before the collection is actually mutated.
if _sa_initiator is not False:
executor = collection._sa_adapter
if executor:
- item = executor.fire_append_event(item, _sa_initiator)
+ item = executor.fire_append_event(item, _sa_initiator, key=key)
return item
-def __del(collection, item, _sa_initiator=None):
+def __del(collection, item, _sa_initiator, key):
"""Run del events.
This event occurs before the collection is actually mutated, *except*
if _sa_initiator is not False:
executor = collection._sa_adapter
if executor:
- executor.fire_remove_event(item, _sa_initiator)
+ executor.fire_remove_event(item, _sa_initiator, key=key)
def __before_pop(collection, _sa_initiator=None):
def append(fn):
def append(self, item, _sa_initiator=None):
- item = __set(self, item, _sa_initiator)
+ item = __set(self, item, _sa_initiator, NO_KEY)
fn(self, item)
_tidy(append)
def remove(fn):
def remove(self, value, _sa_initiator=None):
- __del(self, value, _sa_initiator)
+ __del(self, value, _sa_initiator, NO_KEY)
# testlib.pragma exempt:__eq__
fn(self, value)
def insert(fn):
def insert(self, index, value):
- value = __set(self, value)
+ value = __set(self, value, None, index)
fn(self, index, value)
_tidy(insert)
if not isinstance(index, slice):
existing = self[index]
if existing is not None:
- __del(self, existing)
- value = __set(self, value)
+ __del(self, existing, None, index)
+ value = __set(self, value, None, index)
fn(self, index, value)
else:
# slice assignment requires __delitem__, insert, __len__
def __delitem__(self, index):
if not isinstance(index, slice):
item = self[index]
- __del(self, item)
+ __del(self, item, None, index)
fn(self, index)
else:
# slice deletion requires __getslice__ and a slice-groking
# __getitem__ for stepped deletion
# note: not breaking this into atomic dels
for item in self[index]:
- __del(self, item)
+ __del(self, item, None, index)
fn(self, index)
_tidy(__delitem__)
def pop(self, index=-1):
__before_pop(self)
item = fn(self, index)
- __del(self, item)
+ __del(self, item, None, index)
return item
_tidy(pop)
def clear(fn):
def clear(self, index=-1):
for item in self:
- __del(self, item)
+ __del(self, item, None, index)
fn(self)
_tidy(clear)
def __setitem__(fn):
def __setitem__(self, key, value, _sa_initiator=None):
if key in self:
- __del(self, self[key], _sa_initiator)
- value = __set(self, value, _sa_initiator)
+ __del(self, self[key], _sa_initiator, key)
+ value = __set(self, value, _sa_initiator, key)
fn(self, key, value)
_tidy(__setitem__)
def __delitem__(fn):
def __delitem__(self, key, _sa_initiator=None):
if key in self:
- __del(self, self[key], _sa_initiator)
+ __del(self, self[key], _sa_initiator, key)
fn(self, key)
_tidy(__delitem__)
def clear(fn):
def clear(self):
for key in self:
- __del(self, self[key])
+ __del(self, self[key], None, key)
fn(self)
_tidy(clear)
else:
item = fn(self, key, default)
if _to_del:
- __del(self, item)
+ __del(self, item, None, key)
return item
_tidy(pop)
def popitem(self):
__before_pop(self)
item = fn(self)
- __del(self, item[1])
+ __del(self, item[1], None, 1)
return item
_tidy(popitem)
def add(fn):
def add(self, value, _sa_initiator=None):
if value not in self:
- value = __set(self, value, _sa_initiator)
+ value = __set(self, value, _sa_initiator, NO_KEY)
else:
__set_wo_mutation(self, value, _sa_initiator)
# testlib.pragma exempt:__hash__
def discard(self, value, _sa_initiator=None):
# testlib.pragma exempt:__hash__
if value in self:
- __del(self, value, _sa_initiator)
+ __del(self, value, _sa_initiator, NO_KEY)
# testlib.pragma exempt:__hash__
fn(self, value)
def remove(self, value, _sa_initiator=None):
# testlib.pragma exempt:__hash__
if value in self:
- __del(self, value, _sa_initiator)
+ __del(self, value, _sa_initiator, NO_KEY)
# testlib.pragma exempt:__hash__
fn(self, value)
item = fn(self)
# for set in particular, we have no way to access the item
# that will be popped before pop is called.
- __del(self, item)
+ __del(self, item, None, NO_KEY)
return item
_tidy(pop)
from . import mapperlib
from .attributes import QueryableAttribute
from .base import _mapper_or_none
+from .base import NO_KEY
from .query import Query
from .scoping import scoped_session
from .session import Session
raw=False,
retval=False,
propagate=False,
+ include_key=False,
):
target, fn = event_key.dispatch_target, event_key._listen_fn
if active_history:
target.dispatch._active_history = True
- if not raw or not retval:
+ if not raw or not retval or not include_key:
- def wrap(target, *arg):
+ def wrap(target, *arg, **kw):
if not raw:
target = target.obj()
if not retval:
value = arg[0]
else:
value = None
- fn(target, *arg)
+ if include_key:
+ fn(target, *arg, **kw)
+ else:
+ fn(target, *arg)
return value
else:
- return fn(target, *arg)
+ if include_key:
+ return fn(target, *arg, **kw)
+ else:
+ return fn(target, *arg)
event_key = event_key.with_wrapper(wrap)
if active_history:
mgr[target.key].dispatch._active_history = True
- def append(self, target, value, initiator):
+ def append(self, target, value, initiator, *, key=NO_KEY):
"""Receive a collection append event.
The append event is invoked for each element as it is appended
from its original value by backref handlers in order to control
chained event propagation, as well as be inspected for information
about the source of the event.
+ :param key: When the event is established using the
+ :paramref:`.AttributeEvents.include_key` parameter set to
+ True, this will be the key used in the operation, such as
+ ``collection[some_key_or_index] = value``.
+ The parameter is not passed
+ to the event at all if the the
+ :paramref:`.AttributeEvents.include_key`
+ was not used to set up the event; this is to allow backwards
+ compatibility with existing event handlers that don't include the
+ ``key`` parameter.
+
+ .. versionadded:: 2.0
+
:return: if the event was registered with ``retval=True``,
the given value, or a new effective value, should be returned.
"""
- def append_wo_mutation(self, target, value, initiator):
+ def append_wo_mutation(self, target, value, initiator, *, key=NO_KEY):
"""Receive a collection append event where the collection was not
actually mutated.
from its original value by backref handlers in order to control
chained event propagation, as well as be inspected for information
about the source of the event.
+ :param key: When the event is established using the
+ :paramref:`.AttributeEvents.include_key` parameter set to
+ True, this will be the key used in the operation, such as
+ ``collection[some_key_or_index] = value``.
+ The parameter is not passed
+ to the event at all if the the
+ :paramref:`.AttributeEvents.include_key`
+ was not used to set up the event; this is to allow backwards
+ compatibility with existing event handlers that don't include the
+ ``key`` parameter.
+
+ .. versionadded:: 2.0
:return: No return value is defined for this event.
"""
- def bulk_replace(self, target, values, initiator):
+ def bulk_replace(self, target, values, initiator, *, keys=None):
"""Receive a collection 'bulk replace' event.
This event is invoked for a sequence of values as they are incoming
handler can modify this list in place.
:param initiator: An instance of :class:`.attributes.Event`
representing the initiation of the event.
+ :param keys: When the event is established using the
+ :paramref:`.AttributeEvents.include_key` parameter set to
+ True, this will be the sequence of keys used in the operation,
+ typically only for a dictionary update. The parameter is not passed
+ to the event at all if the the
+ :paramref:`.AttributeEvents.include_key`
+ was not used to set up the event; this is to allow backwards
+ compatibility with existing event handlers that don't include the
+ ``key`` parameter.
+
+ .. versionadded:: 2.0
.. seealso::
"""
- def remove(self, target, value, initiator):
+ def remove(self, target, value, initiator, *, key=NO_KEY):
"""Receive a collection remove event.
:param target: the object instance receiving the event.
passed as a :class:`.attributes.Event` object, and may be
modified by backref handlers within a chain of backref-linked
events.
+ :param key: When the event is established using the
+ :paramref:`.AttributeEvents.include_key` parameter set to
+ True, this will be the key used in the operation, such as
+ ``del collection[some_key_or_index]``. The parameter is not passed
+ to the event at all if the the
+ :paramref:`.AttributeEvents.include_key`
+ was not used to set up the event; this is to allow backwards
+ compatibility with existing event handlers that don't include the
+ ``key`` parameter.
+
+ .. versionadded:: 2.0
:return: No return value is defined for this event.
from .base import InspectionAttrInfo as InspectionAttrInfo
from .base import MANYTOMANY as MANYTOMANY # noqa: F401
from .base import MANYTOONE as MANYTOONE # noqa: F401
+from .base import NO_KEY as NO_KEY # noqa: F401
from .base import NotExtension as NotExtension # noqa: F401
from .base import ONETOMANY as ONETOMANY # noqa: F401
from .base import RelationshipDirection as RelationshipDirection # noqa: F401
"""
key = prop.key
- def append(state, item, initiator):
+ def append(state, item, initiator, **kw):
# process "save_update" cascade rules for when
# an instance is appended to the list of another instance
sess._save_or_update_state(item_state)
return item
- def remove(state, item, initiator):
+ def remove(state, item, initiator, **kw):
if item is None:
return
# item
item_state._orphaned_outside_of_session = True
- def set_(state, newvalue, oldvalue, initiator):
+ def set_(state, newvalue, oldvalue, initiator, **kw):
# process "save_update" cascade rules for when an instance
# is attached to another instance
if oldvalue is newvalue:
sess.expunge(oldvalue)
return newvalue
- event.listen(descriptor, "append_wo_mutation", append, raw=True)
- event.listen(descriptor, "append", append, raw=True, retval=True)
- event.listen(descriptor, "remove", remove, raw=True, retval=True)
- event.listen(descriptor, "set", set_, raw=True, retval=True)
+ event.listen(
+ descriptor, "append_wo_mutation", append, raw=True, include_key=True
+ )
+ event.listen(
+ descriptor, "append", append, raw=True, retval=True, include_key=True
+ )
+ event.listen(
+ descriptor, "remove", remove, raw=True, retval=True, include_key=True
+ )
+ event.listen(
+ descriptor, "set", set_, raw=True, retval=True, include_key=True
+ )
class UOWTransaction:
from sqlalchemy.orm import attributes
from sqlalchemy.orm import exc as orm_exc
from sqlalchemy.orm import instrumentation
+from sqlalchemy.orm import NO_KEY
+from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.orm.collections import collection
from sqlalchemy.orm.state import InstanceState
from sqlalchemy.testing import assert_raises
from sqlalchemy.testing.util import all_partial_orderings
from sqlalchemy.testing.util import gc_collect
-
# global for pickling tests
MyTest = None
MyTest2 = None
class Bar(fixtures.BasicEntity):
pass
- from sqlalchemy.orm.collections import attribute_mapped_collection
-
instrumentation.register_class(Foo)
instrumentation.register_class(Bar)
_register_attribute(
)
+class CollectionKeyTest(fixtures.ORMTest):
+ @testing.fixture
+ def dict_collection(self):
+ class Foo(fixtures.BasicEntity):
+ pass
+
+ class Bar(fixtures.BasicEntity):
+ def __init__(self, name):
+ self.name = name
+
+ instrumentation.register_class(Foo)
+ instrumentation.register_class(Bar)
+ _register_attribute(
+ Foo,
+ "someattr",
+ uselist=True,
+ useobject=True,
+ typecallable=attribute_mapped_collection("name"),
+ )
+ _register_attribute(
+ Bar,
+ "name",
+ uselist=False,
+ useobject=False,
+ )
+
+ return Foo, Bar
+
+ @testing.fixture
+ def list_collection(self):
+ class Foo(fixtures.BasicEntity):
+ pass
+
+ class Bar(fixtures.BasicEntity):
+ pass
+
+ instrumentation.register_class(Foo)
+ instrumentation.register_class(Bar)
+ _register_attribute(
+ Foo,
+ "someattr",
+ uselist=True,
+ useobject=True,
+ )
+
+ return Foo, Bar
+
+ def test_listen_w_list_key(self, list_collection):
+ Foo, Bar = list_collection
+
+ m1 = Mock()
+
+ event.listen(Foo.someattr, "append", m1, include_key=True)
+ event.listen(Foo.someattr, "remove", m1, include_key=True)
+
+ f1 = Foo()
+ b1, b2, b3 = Bar(), Bar(), Bar()
+ f1.someattr.append(b1)
+ f1.someattr.append(b2)
+ f1.someattr[1] = b3
+ del f1.someattr[0]
+ append_token, remove_token = (
+ Foo.someattr.impl._append_token,
+ Foo.someattr.impl._remove_token,
+ )
+
+ eq_(
+ m1.mock_calls,
+ [
+ call(
+ f1,
+ b1,
+ append_token,
+ key=NO_KEY,
+ ),
+ call(
+ f1,
+ b2,
+ append_token,
+ key=NO_KEY,
+ ),
+ call(
+ f1,
+ b2,
+ remove_token,
+ key=1,
+ ),
+ call(
+ f1,
+ b3,
+ append_token,
+ key=1,
+ ),
+ call(
+ f1,
+ b1,
+ remove_token,
+ key=0,
+ ),
+ ],
+ )
+
+ def test_listen_w_dict_key(self, dict_collection):
+ Foo, Bar = dict_collection
+
+ m1 = Mock()
+
+ event.listen(Foo.someattr, "append", m1, include_key=True)
+ event.listen(Foo.someattr, "remove", m1, include_key=True)
+
+ f1 = Foo()
+ b1, b2, b3 = Bar("b1"), Bar("b2"), Bar("b3")
+ f1.someattr["k1"] = b1
+ f1.someattr.update({"k2": b2, "k3": b3})
+
+ del f1.someattr["k2"]
+
+ append_token, remove_token = (
+ Foo.someattr.impl._append_token,
+ Foo.someattr.impl._remove_token,
+ )
+
+ eq_(
+ m1.mock_calls,
+ [
+ call(
+ f1,
+ b1,
+ append_token,
+ key="k1",
+ ),
+ call(
+ f1,
+ b2,
+ append_token,
+ key="k2",
+ ),
+ call(
+ f1,
+ b3,
+ append_token,
+ key="k3",
+ ),
+ call(
+ f1,
+ b2,
+ remove_token,
+ key="k2",
+ ),
+ ],
+ )
+
+ def test_dict_bulk_replace_w_key(self, dict_collection):
+ Foo, Bar = dict_collection
+
+ m1 = Mock()
+
+ event.listen(Foo.someattr, "bulk_replace", m1, include_key=True)
+ event.listen(Foo.someattr, "append", m1, include_key=True)
+ event.listen(Foo.someattr, "remove", m1, include_key=True)
+
+ f1 = Foo()
+ b1, b2, b3, b4 = Bar("b1"), Bar("b2"), Bar("b3"), Bar("b4")
+ f1.someattr = {"b1": b1, "b3": b3}
+ f1.someattr = {"b2": b2, "b3": b3, "b4": b4}
+
+ bulk_replace_token = Foo.someattr.impl._bulk_replace_token
+
+ eq_(
+ m1.mock_calls,
+ [
+ call(f1, [b1, b3], bulk_replace_token, keys=["b1", "b3"]),
+ call(f1, b1, bulk_replace_token, key="b1"),
+ call(f1, b3, bulk_replace_token, key="b3"),
+ call(
+ f1,
+ [b2, b3, b4],
+ bulk_replace_token,
+ keys=["b2", "b3", "b4"],
+ ),
+ call(f1, b2, bulk_replace_token, key="b2"),
+ call(f1, b4, bulk_replace_token, key="b4"),
+ call(f1, b1, bulk_replace_token, key=NO_KEY),
+ ],
+ )
+
+ def test_listen_wo_dict_key(self, dict_collection):
+ Foo, Bar = dict_collection
+
+ m1 = Mock()
+
+ event.listen(Foo.someattr, "append", m1)
+ event.listen(Foo.someattr, "remove", m1)
+
+ f1 = Foo()
+ b1, b2, b3 = Bar("b1"), Bar("b2"), Bar("b3")
+ f1.someattr["k1"] = b1
+ f1.someattr.update({"k2": b2, "k3": b3})
+
+ del f1.someattr["k2"]
+
+ append_token, remove_token = (
+ Foo.someattr.impl._append_token,
+ Foo.someattr.impl._remove_token,
+ )
+
+ eq_(
+ m1.mock_calls,
+ [
+ call(
+ f1,
+ b1,
+ append_token,
+ ),
+ call(
+ f1,
+ b2,
+ append_token,
+ ),
+ call(
+ f1,
+ b3,
+ append_token,
+ ),
+ call(
+ f1,
+ b2,
+ remove_token,
+ ),
+ ],
+ )
+
+
class ListenerTest(fixtures.ORMTest):
def test_receive_changes(self):
"""test that Listeners can mutate the given value."""