Fixes how instances methods are not deregistered
It appears that instance methods do not get removed due to not passing the 'is' check, so switch to using a new reflection utility method which can handle this equality check in a way that actually works. Also adds tests to make sure this does not occur again. Closes-Bug: 1257550 Change-Id: Iab47dd62cb61de0d93d0fe8d90e59772beebeaeb
This commit is contained in:
		| @@ -16,8 +16,11 @@ | |||||||
| #    License for the specific language governing permissions and limitations | #    License for the specific language governing permissions and limitations | ||||||
| #    under the License. | #    under the License. | ||||||
|  |  | ||||||
|  | import collections | ||||||
|  | import functools | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
|  | from taskflow import states | ||||||
| from taskflow import test | from taskflow import test | ||||||
| from taskflow.tests import utils as test_utils | from taskflow.tests import utils as test_utils | ||||||
| from taskflow.utils import lock_utils | from taskflow.utils import lock_utils | ||||||
| @@ -61,6 +64,46 @@ class ClassWithInit(object): | |||||||
|         pass |         pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CallbackEqualityTest(test.TestCase): | ||||||
|  |     def test_different_simple_callbacks(self): | ||||||
|  |  | ||||||
|  |         def a(): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         def b(): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         self.assertFalse(reflection.is_same_callback(a, b)) | ||||||
|  |  | ||||||
|  |     def test_static_instance_callbacks(self): | ||||||
|  |  | ||||||
|  |         class A(object): | ||||||
|  |  | ||||||
|  |             @staticmethod | ||||||
|  |             def b(a, b, c): | ||||||
|  |                 pass | ||||||
|  |  | ||||||
|  |         a = A() | ||||||
|  |         b = A() | ||||||
|  |  | ||||||
|  |         self.assertTrue(reflection.is_same_callback(a.b, b.b)) | ||||||
|  |  | ||||||
|  |     def test_different_instance_callbacks(self): | ||||||
|  |  | ||||||
|  |         class A(object): | ||||||
|  |             def b(self): | ||||||
|  |                 pass | ||||||
|  |  | ||||||
|  |             def __eq__(self, other): | ||||||
|  |                 return True | ||||||
|  |  | ||||||
|  |         b = A() | ||||||
|  |         c = A() | ||||||
|  |  | ||||||
|  |         self.assertFalse(reflection.is_same_callback(b.b, c.b)) | ||||||
|  |         self.assertTrue(reflection.is_same_callback(b.b, c.b, strict=False)) | ||||||
|  |  | ||||||
|  |  | ||||||
| class GetCallableNameTest(test.TestCase): | class GetCallableNameTest(test.TestCase): | ||||||
|  |  | ||||||
|     def test_mere_function(self): |     def test_mere_function(self): | ||||||
| @@ -99,6 +142,88 @@ class GetCallableNameTest(test.TestCase): | |||||||
|                                          '__call__'))) |                                          '__call__'))) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class NotifierTest(test.TestCase): | ||||||
|  |  | ||||||
|  |     def test_notify_called(self): | ||||||
|  |         call_collector = [] | ||||||
|  |  | ||||||
|  |         def call_me(state, details): | ||||||
|  |             call_collector.append((state, details)) | ||||||
|  |  | ||||||
|  |         notifier = misc.TransitionNotifier() | ||||||
|  |         notifier.register(misc.TransitionNotifier.ANY, call_me) | ||||||
|  |         notifier.notify(states.SUCCESS, {}) | ||||||
|  |         notifier.notify(states.SUCCESS, {}) | ||||||
|  |  | ||||||
|  |         self.assertEqual(2, len(call_collector)) | ||||||
|  |         self.assertEqual(1, len(notifier)) | ||||||
|  |  | ||||||
|  |     def test_notify_register_deregister(self): | ||||||
|  |  | ||||||
|  |         def call_me(state, details): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         class A(object): | ||||||
|  |             def call_me_too(self, state, details): | ||||||
|  |                 pass | ||||||
|  |  | ||||||
|  |         notifier = misc.TransitionNotifier() | ||||||
|  |         notifier.register(misc.TransitionNotifier.ANY, call_me) | ||||||
|  |         a = A() | ||||||
|  |         notifier.register(misc.TransitionNotifier.ANY, a.call_me_too) | ||||||
|  |  | ||||||
|  |         self.assertEqual(2, len(notifier)) | ||||||
|  |         notifier.deregister(misc.TransitionNotifier.ANY, call_me) | ||||||
|  |         notifier.deregister(misc.TransitionNotifier.ANY, a.call_me_too) | ||||||
|  |         self.assertEqual(0, len(notifier)) | ||||||
|  |  | ||||||
|  |     def test_notify_reset(self): | ||||||
|  |  | ||||||
|  |         def call_me(state, details): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         notifier = misc.TransitionNotifier() | ||||||
|  |         notifier.register(misc.TransitionNotifier.ANY, call_me) | ||||||
|  |         self.assertEqual(1, len(notifier)) | ||||||
|  |  | ||||||
|  |         notifier.reset() | ||||||
|  |         self.assertEqual(0, len(notifier)) | ||||||
|  |  | ||||||
|  |     def test_bad_notify(self): | ||||||
|  |  | ||||||
|  |         def call_me(state, details): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         notifier = misc.TransitionNotifier() | ||||||
|  |         self.assertRaises(KeyError, notifier.register, | ||||||
|  |                           misc.TransitionNotifier.ANY, call_me, | ||||||
|  |                           kwargs={'details': 5}) | ||||||
|  |  | ||||||
|  |     def test_selective_notify(self): | ||||||
|  |         call_counts = collections.defaultdict(list) | ||||||
|  |  | ||||||
|  |         def call_me_on(registered_state, state, details): | ||||||
|  |             call_counts[registered_state].append((state, details)) | ||||||
|  |  | ||||||
|  |         notifier = misc.TransitionNotifier() | ||||||
|  |         notifier.register(states.SUCCESS, | ||||||
|  |                           functools.partial(call_me_on, states.SUCCESS)) | ||||||
|  |         notifier.register(misc.TransitionNotifier.ANY, | ||||||
|  |                           functools.partial(call_me_on, | ||||||
|  |                                             misc.TransitionNotifier.ANY)) | ||||||
|  |  | ||||||
|  |         self.assertEqual(2, len(notifier)) | ||||||
|  |         notifier.notify(states.SUCCESS, {}) | ||||||
|  |  | ||||||
|  |         self.assertEqual(1, len(call_counts[misc.TransitionNotifier.ANY])) | ||||||
|  |         self.assertEqual(1, len(call_counts[states.SUCCESS])) | ||||||
|  |  | ||||||
|  |         notifier.notify(states.FAILURE, {}) | ||||||
|  |         self.assertEqual(2, len(call_counts[misc.TransitionNotifier.ANY])) | ||||||
|  |         self.assertEqual(1, len(call_counts[states.SUCCESS])) | ||||||
|  |         self.assertEqual(2, len(call_counts)) | ||||||
|  |  | ||||||
|  |  | ||||||
| class GetCallableArgsTest(test.TestCase): | class GetCallableArgsTest(test.TestCase): | ||||||
|  |  | ||||||
|     def test_mere_function(self): |     def test_mere_function(self): | ||||||
|   | |||||||
| @@ -214,8 +214,16 @@ class TransitionNotifier(object): | |||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self._listeners = collections.defaultdict(list) |         self._listeners = collections.defaultdict(list) | ||||||
|  |  | ||||||
|  |     def __len__(self): | ||||||
|  |         """Returns how many callbacks are registered""" | ||||||
|  |  | ||||||
|  |         count = 0 | ||||||
|  |         for (_s, callbacks) in six.iteritems(self._listeners): | ||||||
|  |             count += len(callbacks) | ||||||
|  |         return count | ||||||
|  |  | ||||||
|     def reset(self): |     def reset(self): | ||||||
|         self._listeners = collections.defaultdict(list) |         self._listeners.clear() | ||||||
|  |  | ||||||
|     def notify(self, state, details): |     def notify(self, state, details): | ||||||
|         listeners = list(self._listeners.get(self.ANY, [])) |         listeners = list(self._listeners.get(self.ANY, [])) | ||||||
| @@ -255,7 +263,7 @@ class TransitionNotifier(object): | |||||||
|         if state not in self._listeners: |         if state not in self._listeners: | ||||||
|             return |             return | ||||||
|         for i, (cb, args, kwargs) in enumerate(self._listeners[state]): |         for i, (cb, args, kwargs) in enumerate(self._listeners[state]): | ||||||
|             if cb is callback: |             if reflection.is_same_callback(cb, callback): | ||||||
|                 self._listeners[state].pop(i) |                 self._listeners[state].pop(i) | ||||||
|                 break |                 break | ||||||
|  |  | ||||||
|   | |||||||
| @@ -93,6 +93,29 @@ def get_method_self(method): | |||||||
|         return None |         return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def is_same_callback(callback1, callback2, strict=True): | ||||||
|  |     """Returns if the two callbacks are the same.""" | ||||||
|  |     if callback1 is callback2: | ||||||
|  |         # This happens when plain methods are given (or static/non-bound | ||||||
|  |         # methods). | ||||||
|  |         return True | ||||||
|  |     if callback1 == callback2: | ||||||
|  |         if not strict: | ||||||
|  |             return True | ||||||
|  |         # If two bound method are equal if functions themselves are equal | ||||||
|  |         # and objects they are applied to are equal. This means that a bound | ||||||
|  |         # method could be the same bound method on another object if the | ||||||
|  |         # objects have __eq__ methods that return true (when in fact it is a | ||||||
|  |         # different bound method). Python u so crazy! | ||||||
|  |         try: | ||||||
|  |             self1 = six.get_method_self(callback1) | ||||||
|  |             self2 = six.get_method_self(callback2) | ||||||
|  |             return self1 is self2 | ||||||
|  |         except AttributeError: | ||||||
|  |             pass | ||||||
|  |     return False | ||||||
|  |  | ||||||
|  |  | ||||||
| def is_bound_method(method): | def is_bound_method(method): | ||||||
|     """Returns if the method given is a bound to a object or not.""" |     """Returns if the method given is a bound to a object or not.""" | ||||||
|     return bool(get_method_self(method)) |     return bool(get_method_self(method)) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Joshua Harlow
					Joshua Harlow