diff options
| author | Raphael Collet <rco@odoo.com> | 2020-08-28 14:11:21 +0000 |
|---|---|---|
| committer | Raphael Collet <rco@odoo.com> | 2020-09-03 09:05:21 +0000 |
| commit | d4b2e9224839aed8fc160ebe5a89e0f7d4c6a5bb (patch) | |
| tree | 1d69605221a479b1cf9406b69a8acf4ac94964c6 /odoo/tools/misc.py | |
| parent | 800c55db56689baa9b67b7243e494246a98b3484 (diff) | |
[FIX] core: cursor hooks API and implementation
Python 3.8 changed the equality rules for bound methods to be based on
the *identity* of the receiver (`__self__`) rather than its *equality*.
This means that in 3.7, methods from different instances will compare
(and hash) equal, thereby landing in the same map "slot", but that isn't
the case in 3.8.
While it's usually not relevant, it's an issue for `GroupCalls` which is
indexed by a function: in 3.7, that being a method from recordsets
comparing equal will deduplicate them, but not anymore in 3.8, leading
to duplicated callbacks (exactly the thing GroupCalls aims to avoid).
Also, the API of `GroupCalls` turned out to be unusual and weird. The
bug above is fixed by using a plain list for callbacks, thereby avoiding
comparisons between registered functions. The API is now:
callbacks.add(func) # add func to callbacks
callbacks.run() # run all callbacks in addition order
callbacks.clear() # remove all callbacks
In order to handle aggregated data, the `callbacks` object provides a
dictionary `callbacks.data` that any callback function can freely use.
For the sake of consistency, the `callbacks.data` dict is automatically
cleared upon execution of callbacks.
Discovered by @william-andre
Related to odoo#56583
References:
* https://bugs.python.org/issue1617161
* python/cpython#7848
* https://docs.python.org/3/whatsnew/changelog.html#python-3-8-0-alpha-1
(no direct link because individual entries are not linkable, look for
bpo-1617161)
X-original-commit: a986f4929bab95759086cc77ca5e60795f82e91a
Diffstat (limited to 'odoo/tools/misc.py')
| -rw-r--r-- | odoo/tools/misc.py | 96 |
1 files changed, 57 insertions, 39 deletions
diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index 31e391fe30e..5173e046467 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -1084,54 +1084,72 @@ class LastOrderedSet(OrderedSet): OrderedSet.add(self, elem) -class GroupCalls: - """ A collection of callbacks with support for aggregated arguments. Upon - call, every registered function is called once with positional arguments. - When registering a function, a tuple of positional arguments is returned, so - that the caller can modify the arguments in place. This allows to - accumulate some data to process once:: +class Callbacks: + """ A simple queue of callback functions. Upon run, every function is + called (in addition order), and the queue is emptied. - callbacks = GroupCalls() + callbacks = Callbacks() - # register print (by default with a list) - [args] = callbacks.register(print, list) - args.append(42) + # add foo + def foo(): + print("foo") - # add an element to the list to print - [args] = callbacks.register(print, list) - args.append(43) + callbacks.add(foo) - # print "[42, 43]" - callbacks() + # add bar + callbacks.add + def bar(): + print("bar") + + # add foo again + callbacks.add(foo) + + # call foo(), bar(), foo(), then clear the callback queue + callbacks.run() + + The queue also provides a ``data`` dictionary, that may be freely used to + store anything, but is mostly aimed at aggregating data for callbacks. The + dictionary is automatically cleared by ``run()`` once all callback functions + have been called. + + # register foo to process aggregated data + @callbacks.add + def foo(): + print(sum(callbacks.data['foo'])) + + callbacks.data.setdefault('foo', []).append(1) + ... + callbacks.data.setdefault('foo', []).append(2) + ... + callbacks.data.setdefault('foo', []).append(3) + + # call foo(), which prints 6 + callbacks.run() + + Given the global nature of ``data``, the keys should identify in a unique + way the data being stored. It is recommended to use strings with a + structure like ``"{module}.{feature}"``. """ + __slots__ = ['_funcs', 'data'] + def __init__(self): - self._func_args = {} # {func: args} + self._funcs = [] + self.data = {} - def __call__(self): - """ Call all the registered functions (in first addition order) with - their respective arguments. Only recurrent functions remain registered - after the call. - """ - func_args = self._func_args - while func_args: - func = next(iter(func_args)) - args = func_args.pop(func) - func(*args) - - def add(self, func, *types): - """ Register the given function, and return the tuple of positional - arguments to call the function with. If the function is not registered - yet, the list of arguments is made up by invoking the given types. - """ - try: - return self._func_args[func] - except KeyError: - args = self._func_args[func] = [type_() for type_ in types] - return args + def add(self, func): + """ Add the given function. """ + self._funcs.append(func) + + def run(self): + """ Call all the functions (in addition order), then clear. """ + for func in self._funcs: + func() + self.clear() def clear(self): - """ Remove all callbacks from self. """ - self._func_args.clear() + """ Remove all callbacks and data from self. """ + self._funcs.clear() + self.data.clear() class IterableGenerator: |
