diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2013-07-26 14:21:58 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2013-07-26 14:21:58 -0400 |
| commit | 4505425a38b079a8e2a59fdbe31bc033de25e871 (patch) | |
| tree | d0dd695935aee0e26f8cd9ca36bc39f6df7e66ef /test/base/test_events.py | |
| parent | 550141b14c8e165218cd32c27d91541eeee86d2a (diff) | |
| download | sqlalchemy-4505425a38b079a8e2a59fdbe31bc033de25e871.tar.gz | |
- Removal of event listeners is now implemented. The feature is
provided via the :func:`.event.remove` function.
[ticket:2268]
- reorganization of event.py module into a package; with the addition of the
docstring work as well as the new registry for removal, there's a lot more code now.
the package separates concerns and provides a top-level doc for each subsection
of functionality
- the remove feature works by providing the EventKey object which associates
the user-provided arguments to listen() with a global, weak-referencing registry.
This registry stores a collection of _ListenerCollection and _DispatchDescriptor
objects associated with each set of arguments, as well as the wrapped function
which was applied to that collection. The EventKey can then be recreated for
a removal, all the _ListenerCollection and _DispatchDescriptor objects are located,
and the correct wrapped function is removed from each one.
Diffstat (limited to 'test/base/test_events.py')
| -rw-r--r-- | test/base/test_events.py | 202 |
1 files changed, 178 insertions, 24 deletions
diff --git a/test/base/test_events.py b/test/base/test_events.py index 1e0568f27..d2bfb0932 100644 --- a/test/base/test_events.py +++ b/test/base/test_events.py @@ -6,15 +6,12 @@ from sqlalchemy import event, exc from sqlalchemy.testing import fixtures from sqlalchemy.testing.util import gc_collect from sqlalchemy.testing.mock import Mock, call - +from sqlalchemy import testing class EventsTest(fixtures.TestBase): """Test class- and instance-level event registration.""" def setUp(self): - assert 'event_one' not in event._registrars - assert 'event_two' not in event._registrars - class TargetEvents(event.Events): def event_one(self, x, y): pass @@ -30,7 +27,7 @@ class EventsTest(fixtures.TestBase): self.Target = Target def tearDown(self): - event._remove_dispatcher(self.Target.__dict__['dispatch'].events) + event.base._remove_dispatcher(self.Target.__dict__['dispatch'].events) def test_register_class(self): def listen(x, y): @@ -84,7 +81,7 @@ class EventsTest(fixtures.TestBase): eq_(len(self.Target().dispatch.event_one), 2) eq_(len(t1.dispatch.event_one), 3) - def test_append_vs_insert(self): + def test_append_vs_insert_cls(self): def listen_one(x, y): pass @@ -103,6 +100,26 @@ class EventsTest(fixtures.TestBase): [listen_three, listen_one, listen_two] ) + def test_append_vs_insert_instance(self): + def listen_one(x, y): + pass + + def listen_two(x, y): + pass + + def listen_three(x, y): + pass + + target = self.Target() + event.listen(target, "event_one", listen_one) + event.listen(target, "event_one", listen_two) + event.listen(target, "event_one", listen_three, insert=True) + + eq_( + list(target.dispatch.event_one), + [listen_three, listen_one, listen_two] + ) + def test_decorator(self): @event.listens_for(self.Target, "event_one") def listen_one(x, y): @@ -189,7 +206,7 @@ class NamedCallTest(fixtures.TestBase): self.TargetOne = TargetOne def tearDown(self): - event._remove_dispatcher(self.TargetOne.__dict__['dispatch'].events) + event.base._remove_dispatcher(self.TargetOne.__dict__['dispatch'].events) def test_kw_accept(self): @@ -261,7 +278,7 @@ class LegacySignatureTest(fixtures.TestBase): self.TargetOne = TargetOne def tearDown(self): - event._remove_dispatcher(self.TargetOne.__dict__['dispatch'].events) + event.base._remove_dispatcher(self.TargetOne.__dict__['dispatch'].events) def test_legacy_accept(self): canary = Mock() @@ -375,7 +392,7 @@ class ClsLevelListenTest(fixtures.TestBase): def tearDown(self): - event._remove_dispatcher(self.TargetOne.__dict__['dispatch'].events) + event.base._remove_dispatcher(self.TargetOne.__dict__['dispatch'].events) def setUp(self): class TargetEventsOne(event.Events): @@ -386,7 +403,7 @@ class ClsLevelListenTest(fixtures.TestBase): self.TargetOne = TargetOne def tearDown(self): - event._remove_dispatcher( + event.base._remove_dispatcher( self.TargetOne.__dict__['dispatch'].events) def test_lis_subcalss_lis(self): @@ -473,8 +490,8 @@ class AcceptTargetsTest(fixtures.TestBase): self.TargetTwo = TargetTwo def tearDown(self): - event._remove_dispatcher(self.TargetOne.__dict__['dispatch'].events) - event._remove_dispatcher(self.TargetTwo.__dict__['dispatch'].events) + event.base._remove_dispatcher(self.TargetOne.__dict__['dispatch'].events) + event.base._remove_dispatcher(self.TargetTwo.__dict__['dispatch'].events) def test_target_accept(self): """Test that events of the same name are routed to the correct @@ -543,7 +560,7 @@ class CustomTargetsTest(fixtures.TestBase): self.Target = Target def tearDown(self): - event._remove_dispatcher(self.Target.__dict__['dispatch'].events) + event.base._remove_dispatcher(self.Target.__dict__['dispatch'].events) def test_indirect(self): def listen(x, y): @@ -593,14 +610,14 @@ class ListenOverrideTest(fixtures.TestBase): def setUp(self): class TargetEvents(event.Events): @classmethod - def _listen(cls, target, identifier, fn, add=False): + def _listen(cls, event_key, add=False): + fn = event_key.fn if add: def adapt(x, y): fn(x + y) - else: - adapt = fn + event_key = event_key.with_wrapper(adapt) - event.Events._listen(target, identifier, adapt) + event_key.base_listen() def event_one(self, x, y): pass @@ -610,7 +627,7 @@ class ListenOverrideTest(fixtures.TestBase): self.Target = Target def tearDown(self): - event._remove_dispatcher(self.Target.__dict__['dispatch'].events) + event.base._remove_dispatcher(self.Target.__dict__['dispatch'].events) def test_listen_override(self): listen_one = Mock() @@ -700,7 +717,7 @@ class JoinTest(fixtures.TestBase): for cls in (self.TargetElement, self.TargetFactory, self.BaseTarget): if 'dispatch' in cls.__dict__: - event._remove_dispatcher(cls.__dict__['dispatch'].events) + event.base._remove_dispatcher(cls.__dict__['dispatch'].events) def test_neither(self): element = self.TargetFactory().create() @@ -842,13 +859,19 @@ class JoinTest(fixtures.TestBase): element.run_event(2) element.run_event(3) - # c1 gets no events due to _JoinedListener - # fixing the "parent" at construction time. - # this can be changed to be "live" at the cost - # of performance. + # if _JoinedListener fixes .listeners + # at construction time, then we don't get + # the new listeners. + #eq_(l1.mock_calls, []) + + # alternatively, if _JoinedListener shares the list + # using a @property, then we get them, at the arguable + # expense of the extra method call to access the .listeners + # collection eq_( - l1.mock_calls, [] + l1.mock_calls, [call(element, 2), call(element, 3)] ) + eq_( l2.mock_calls, [call(element, 1), call(element, 2), call(element, 3)] @@ -892,3 +915,134 @@ class JoinTest(fixtures.TestBase): l1.mock_calls, [call(element, 1), call(element, 2), call(element, 3)] ) + +class RemovalTest(fixtures.TestBase): + def _fixture(self): + class TargetEvents(event.Events): + def event_one(self, x, y): + pass + + def event_two(self, x): + pass + + def event_three(self, x): + pass + + class Target(object): + dispatch = event.dispatcher(TargetEvents) + return Target + + def test_clslevel(self): + Target = self._fixture() + + m1 = Mock() + + event.listen(Target, "event_two", m1) + + t1 = Target() + t1.dispatch.event_two("x") + + event.remove(Target, "event_two", m1) + + t1.dispatch.event_two("y") + + eq_(m1.mock_calls, [call("x")]) + + def test_clslevel_subclass(self): + Target = self._fixture() + class SubTarget(Target): + pass + + m1 = Mock() + + event.listen(Target, "event_two", m1) + + t1 = SubTarget() + t1.dispatch.event_two("x") + + event.remove(Target, "event_two", m1) + + t1.dispatch.event_two("y") + + eq_(m1.mock_calls, [call("x")]) + + def test_propagate(self): + Target = self._fixture() + + m1 = Mock() + + t1 = Target() + t2 = Target() + + event.listen(t1, "event_one", m1, propagate=True) + event.listen(t1, "event_two", m1, propagate=False) + + t2.dispatch._update(t1.dispatch) + + t1.dispatch.event_one("t1e1x") + t1.dispatch.event_two("t1e2x") + t2.dispatch.event_one("t2e1x") + t2.dispatch.event_two("t2e2x") + + event.remove(t1, "event_one", m1) + event.remove(t1, "event_two", m1) + + t1.dispatch.event_one("t1e1y") + t1.dispatch.event_two("t1e2y") + t2.dispatch.event_one("t2e1y") + t2.dispatch.event_two("t2e2y") + + eq_(m1.mock_calls, + [call('t1e1x'), call('t1e2x'), + call('t2e1x')]) + + @testing.requires.predictable_gc + def test_listener_collection_removed_cleanup(self): + from sqlalchemy.event import registry + + Target = self._fixture() + + m1 = Mock() + + t1 = Target() + + event.listen(t1, "event_one", m1) + + key = (id(t1), "event_one", id(m1)) + + assert key in registry._key_to_collection + collection_ref = list(registry._key_to_collection[key])[0] + assert collection_ref in registry._collection_to_key + + t1.dispatch.event_one("t1") + + del t1 + + gc_collect() + + assert key not in registry._key_to_collection + assert collection_ref not in registry._collection_to_key + + def test_remove_not_listened(self): + Target = self._fixture() + + m1 = Mock() + + t1 = Target() + + event.listen(t1, "event_one", m1, propagate=True) + event.listen(t1, "event_three", m1) + + event.remove(t1, "event_one", m1) + assert_raises_message( + exc.InvalidRequestError, + r"No listeners found for event <.*Target.*> / 'event_two' / <Mock.*> ", + event.remove, t1, "event_two", m1 + ) + + event.remove(t1, "event_three", m1) + + + + + |
