diff options
author | Florian Bruhin <me@the-compiler.org> | 2019-09-13 16:15:52 +0200 |
---|---|---|
committer | Florian Bruhin <me@the-compiler.org> | 2019-09-13 17:02:22 +0200 |
commit | 6ec8a65dcc36bc0504922d378895ffb2dd8ddb53 (patch) | |
tree | 4ea786caac726bae96988d46399a5f83d970a3dd | |
parent | 1c2ad6023ca9d1d38be1b26ab5adcbbf767e4a1f (diff) | |
download | qutebrowser-6ec8a65dcc36bc0504922d378895ffb2dd8ddb53.tar.gz qutebrowser-6ec8a65dcc36bc0504922d378895ffb2dd8ddb53.zip |
Don't use a decorator for throttle.
If this is implemented as a decorator, there's only one global throttle which
is shared between windows.
-rw-r--r-- | qutebrowser/mainwindow/statusbar/percentage.py | 4 | ||||
-rw-r--r-- | qutebrowser/misc/throttle.py | 90 | ||||
-rw-r--r-- | tests/unit/mainwindow/statusbar/test_percentage.py | 2 | ||||
-rw-r--r-- | tests/unit/misc/test_throttle.py | 43 |
4 files changed, 64 insertions, 75 deletions
diff --git a/qutebrowser/mainwindow/statusbar/percentage.py b/qutebrowser/mainwindow/statusbar/percentage.py index ca28e5523..e39b672f2 100644 --- a/qutebrowser/mainwindow/statusbar/percentage.py +++ b/qutebrowser/mainwindow/statusbar/percentage.py @@ -33,6 +33,7 @@ class Percentage(textbase.TextBase): """Constructor. Set percentage to 0%.""" super().__init__(parent, elidemode=Qt.ElideNone) self._strings = self._calc_strings() + self._set_text = throttle.Throttle(self.setText, 100, parent=self) self.set_perc(0, 0) def set_raw(self): @@ -51,7 +52,6 @@ class Percentage(textbase.TextBase): return strings @pyqtSlot(int, int) - @throttle.throttle(100) def set_perc(self, x, y): # pylint: disable=unused-argument """Setter to be used as a Qt slot. @@ -59,7 +59,7 @@ class Percentage(textbase.TextBase): x: The x percentage (int), currently ignored. y: The y percentage (int) """ - self.setText(self._strings[y]) + self._set_text(self._strings[y]) def on_tab_changed(self, tab): """Update scroll position when tab changed.""" diff --git a/qutebrowser/misc/throttle.py b/qutebrowser/misc/throttle.py index c4e00f8a3..9bc8c620c 100644 --- a/qutebrowser/misc/throttle.py +++ b/qutebrowser/misc/throttle.py @@ -17,11 +17,10 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. -"""A simple throttling decorator.""" +"""A throttle for throttling function calls.""" import typing import time -import functools import attr from PyQt5.QtCore import QObject @@ -36,61 +35,63 @@ class _CallArgs: kwargs = attr.ib() # type: typing.Mapping[str, typing.Any] -class throttle: # noqa: N801,N806 pylint: disable=invalid-name +class Throttle(QObject): - """A simple function decorator to throttle calls. + """A throttle to throttle calls. If a request comes in, it will be processed immediately. If another request comes in too soon, it is ignored, but will be processed when a timeout ends. If another request comes in, it will update the pending request. """ - def __init__(self, delay_ms: int) -> None: - """Save arguments for throttle decorator. + def __init__(self, + func: typing.Callable, + delay_ms: int, + parent: QObject = None) -> None: + """Constructor. Args: delay_ms: The time to wait before allowing another call of the function. -1 disables the wrapper. + func: The function/method to call on __call__. + parent: The parent object. """ + super().__init__(parent) self._delay_ms = delay_ms + self._func = func self._pending_call = None # type: typing.Optional[_CallArgs] self._last_call_ms = None # type: typing.Optional[int] - self._timer = usertypes.Timer(None, 'throttle-timer') + self._timer = usertypes.Timer(self, 'throttle-timer') self._timer.setSingleShot(True) - def __call__(self, func: typing.Callable) -> typing.Callable: - @functools.wraps(func) - def wrapped_fn(*args, **kwargs): - cur_time_ms = int(time.monotonic() * 1000) - if self._pending_call is None: - if (self._last_call_ms is None or - cur_time_ms - self._last_call_ms > self._delay_ms): - # Call right now - self._last_call_ms = cur_time_ms - func(*args, **kwargs) - return - - # Start a pending call - def call_pending(): - func(*self._pending_call.args, **self._pending_call.kwargs) - self._pending_call = None - self._last_call_ms = int(time.monotonic() * 1000) - - self._timer.setInterval(self._delay_ms - - (cur_time_ms - self._last_call_ms)) - # Disconnect any existing calls, continue if no connections. - try: - self._timer.timeout.disconnect() - except TypeError: - pass - self._timer.timeout.connect(call_pending) - self._timer.start() - - # Update arguments for an existing pending call - self._pending_call = _CallArgs(args=args, kwargs=kwargs) - - wrapped_fn.throttle = self # type: ignore - return wrapped_fn + def _call_pending(self): + """Start a pending call.""" + self._func(*self._pending_call.args, **self._pending_call.kwargs) + self._pending_call = None + self._last_call_ms = int(time.monotonic() * 1000) + + def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any: + cur_time_ms = int(time.monotonic() * 1000) + if self._pending_call is None: + if (self._last_call_ms is None or + cur_time_ms - self._last_call_ms > self._delay_ms): + # Call right now + self._last_call_ms = cur_time_ms + self._func(*args, **kwargs) + return + + self._timer.setInterval(self._delay_ms - + (cur_time_ms - self._last_call_ms)) + # Disconnect any existing calls, continue if no connections. + try: + self._timer.timeout.disconnect() + except TypeError: + pass + self._timer.timeout.connect(self._call_pending) + self._timer.start() + + # Update arguments for an existing pending call + self._pending_call = _CallArgs(args=args, kwargs=kwargs) def set_delay(self, delay_ms: int) -> None: """Set the delay to wait between invocation of this function.""" @@ -99,12 +100,3 @@ class throttle: # noqa: N801,N806 pylint: disable=invalid-name def cancel(self) -> None: """Cancel any pending instance of this timer.""" self._timer.stop() - - def set_parent(self, parent: QObject) -> None: - """Set the parent for the throttle's QTimer. - - Calling this is strongly recommended if throttle is used inside a - QObject. This ensures that the underlying method doesn't get called - after the C++ object was deleted. - """ - self._timer.setParent(parent) diff --git a/tests/unit/mainwindow/statusbar/test_percentage.py b/tests/unit/mainwindow/statusbar/test_percentage.py index 515a7c873..45846bd16 100644 --- a/tests/unit/mainwindow/statusbar/test_percentage.py +++ b/tests/unit/mainwindow/statusbar/test_percentage.py @@ -30,7 +30,7 @@ def percentage(qtbot): """Fixture providing a Percentage widget.""" widget = Percentage() # Force immediate update of percentage widget - widget.set_perc.throttle.set_delay(-1) # pylint: disable=no-member + widget._set_text.set_delay(-1) qtbot.add_widget(widget) return widget diff --git a/tests/unit/misc/test_throttle.py b/tests/unit/misc/test_throttle.py index a62938802..f58de2d1e 100644 --- a/tests/unit/misc/test_throttle.py +++ b/tests/unit/misc/test_throttle.py @@ -25,7 +25,7 @@ import sip import pytest from PyQt5.QtCore import QObject -from qutebrowser.misc.throttle import throttle +from qutebrowser.misc import throttle @pytest.fixture @@ -33,22 +33,24 @@ def func(): return mock.Mock(spec=[]) -def test_immediate(func, qapp): - throttled_func = throttle(100)(func) +@pytest.fixture +def throttled_func(func): + return throttle.Throttle(func, 100) + + +def test_immediate(throttled_func, func, qapp): throttled_func("foo") throttled_func("foo") func.assert_called_once_with("foo") -def test_immediate_kwargs(func, qapp): - throttled_func = throttle(100)(func) +def test_immediate_kwargs(throttled_func, func, qapp): throttled_func(foo="bar") throttled_func(foo="bar") func.assert_called_once_with(foo="bar") -def test_delayed(func, qtbot): - throttled_func = throttle(100)(func) +def test_delayed(throttled_func, func, qtbot): throttled_func("foo") throttled_func("foo") throttled_func("foo") @@ -61,8 +63,7 @@ def test_delayed(func, qtbot): func.assert_called_once_with("bar") -def test_delayed_immediate_delayed(func, qtbot): - throttled_func = throttle(100)(func) +def test_delayed_immediate_delayed(throttled_func, func, qtbot): throttled_func("foo") throttled_func("foo") throttled_func("foo") @@ -86,8 +87,7 @@ def test_delayed_immediate_delayed(func, qtbot): func.assert_called_once_with("bop") -def test_delayed_delayed(func, qtbot): - throttled_func = throttle(100)(func) +def test_delayed_delayed(throttled_func, func, qtbot): throttled_func("foo") throttled_func("foo") throttled_func("foo") @@ -109,15 +109,14 @@ def test_delayed_delayed(func, qtbot): func.reset_mock() -def test_cancel(func, qtbot): - throttled_func = throttle(100)(func) +def test_cancel(throttled_func, func, qtbot): throttled_func("foo") throttled_func("foo") throttled_func("foo") throttled_func("bar") func.assert_called_once_with("foo") func.reset_mock() - throttled_func.throttle.cancel() + throttled_func.cancel() qtbot.wait(150) @@ -126,8 +125,8 @@ def test_cancel(func, qtbot): def test_set(func, qtbot): - throttled_func = throttle(1000)(func) - throttled_func.throttle.set_delay(100) + throttled_func = throttle.Throttle(func, 100) + throttled_func.set_delay(100) throttled_func("foo") throttled_func("foo") throttled_func("foo") @@ -144,17 +143,15 @@ def test_set(func, qtbot): def test_deleted_object(qtbot): class Obj(QObject): - def __init__(self, parent=None): - super().__init__(parent) - self.func.throttle.set_parent(self) # pylint: disable=no-member - - @throttle(100) def func(self): self.setObjectName("test") obj = Obj() - obj.func() - obj.func() + + throttled_func = throttle.Throttle(obj.func, 100, parent=obj) + throttled_func() + throttled_func() + sip.delete(obj) qtbot.wait(150) |