summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Bruhin <me@the-compiler.org>2019-09-13 16:15:52 +0200
committerFlorian Bruhin <me@the-compiler.org>2019-09-13 17:02:22 +0200
commit6ec8a65dcc36bc0504922d378895ffb2dd8ddb53 (patch)
tree4ea786caac726bae96988d46399a5f83d970a3dd
parent1c2ad6023ca9d1d38be1b26ab5adcbbf767e4a1f (diff)
downloadqutebrowser-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.py4
-rw-r--r--qutebrowser/misc/throttle.py90
-rw-r--r--tests/unit/mainwindow/statusbar/test_percentage.py2
-rw-r--r--tests/unit/misc/test_throttle.py43
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)