22
33import time
44import threading
5+
56from bd2k .util .threading import BoundedEmptySemaphore
67
78
@@ -16,23 +17,23 @@ class GlobalThrottle:
1617 prevent the resource from becoming swamped after longer pauses.
1718 """
1819
19- def __init__ (self , min_interval , max_unused ):
20+ def __init__ ( self , min_interval , max_unused ):
2021 self .min_interval = min_interval
2122 self .semaphore = BoundedEmptySemaphore ( max_unused )
2223 self .thread_start_lock = threading .Lock ( )
2324 self .thread_started = False
2425 self .thread = threading .Thread ( target = self .generator )
2526 self .thread .daemon = True
2627
27- def generator (self ):
28+ def generator ( self ):
2829 while True :
2930 try :
3031 self .semaphore .release ( )
3132 except ValueError :
3233 pass
3334 time .sleep ( self .min_interval )
3435
35- def throttle (self , wait = True ):
36+ def throttle ( self , wait = True ):
3637 """
3738 If the wait parameter is True, this method returns True after suspending the current
3839 thread as necessary to ensure that no less than the configured minimum interval passed
@@ -49,8 +50,8 @@ def throttle(self, wait=True):
4950 self .thread_started = True
5051 return self .semaphore .acquire ( blocking = wait )
5152
52- def __call__ (self , function ):
53- def wrapper (* args , ** kwargs ):
53+ def __call__ ( self , function ):
54+ def wrapper ( * args , ** kwargs ):
5455 self .throttle ( )
5556 return function ( * args , ** kwargs )
5657
@@ -60,10 +61,12 @@ def wrapper(*args, **kwargs):
6061class LocalThrottle :
6162 """
6263 A thread-safe rate limiter that throttles each thread independently. Can be used as a
63- function or method decorator or as a simple object, using the throttle().
64+ function or method decorator or as a simple object, via its .throttle() method.
65+
66+ The use as a decorator is deprecated in favor of throttle().
6467 """
6568
66- def __init__ (self , min_interval ):
69+ def __init__ ( self , min_interval ):
6770 """
6871 Initialize this local throttle.
6972
@@ -74,7 +77,7 @@ def __init__(self, min_interval):
7477 self .per_thread = threading .local ( )
7578 self .per_thread .last_invocation = None
7679
77- def throttle (self , wait = True ):
80+ def throttle ( self , wait = True ):
7881 """
7982 If the wait parameter is True, this method returns True after suspending the current
8083 thread as necessary to ensure that no less than the configured minimum interval has
@@ -97,10 +100,104 @@ def throttle(self, wait=True):
97100 self .per_thread .last_invocation = now
98101 return True
99102
100-
101- def __call__ (self , function ):
102- def wrapper (* args , ** kwargs ):
103+ def __call__ ( self , function ):
104+ def wrapper ( * args , ** kwargs ):
103105 self .throttle ( )
104106 return function ( * args , ** kwargs )
105107
106- return wrapper
108+ return wrapper
109+
110+
111+ class throttle ( object ):
112+ """
113+ A context manager for ensuring that the execution of its body takes at least a given amount
114+ of time, sleeping if necessary. It is a simpler version of LocalThrottle if used as a
115+ decorator.
116+
117+ Ensures that body takes at least the given amount of time.
118+
119+ >>> start = time.time()
120+ >>> with throttle(1):
121+ ... pass
122+ >>> 1 <= time.time() - start <= 1.1
123+ True
124+
125+ Ditto when used as a decorator.
126+
127+ >>> @throttle(1)
128+ ... def f():
129+ ... pass
130+ >>> start = time.time()
131+ >>> f()
132+ >>> 1 <= time.time() - start <= 1.1
133+ True
134+
135+ If the body takes longer by itself, don't throttle.
136+
137+ >>> start = time.time()
138+ >>> with throttle(1):
139+ ... time.sleep(2)
140+ >>> 2 <= time.time() - start <= 2.1
141+ True
142+
143+ Ditto when used as a decorator.
144+
145+ >>> @throttle(1)
146+ ... def f():
147+ ... time.sleep(2)
148+ >>> start = time.time()
149+ >>> f()
150+ >>> 2 <= time.time() - start <= 2.1
151+ True
152+
153+ If an exception occurs, don't throttle.
154+
155+ >>> start = time.time()
156+ >>> try:
157+ ... with throttle(1):
158+ ... raise ValueError('foo')
159+ ... except ValueError:
160+ ... end = time.time()
161+ ... raise
162+ Traceback (most recent call last):
163+ ...
164+ ValueError: foo
165+ >>> 0 <= end - start <= 0.1
166+ True
167+
168+ Ditto when used as a decorator.
169+
170+ >>> @throttle(1)
171+ ... def f():
172+ ... raise ValueError('foo')
173+ >>> start = time.time()
174+ >>> try:
175+ ... f()
176+ ... except ValueError:
177+ ... end = time.time()
178+ ... raise
179+ Traceback (most recent call last):
180+ ...
181+ ValueError: foo
182+ >>> 0 <= end - start <= 0.1
183+ True
184+ """
185+
186+ def __init__ ( self , min_interval ):
187+ self .min_interval = min_interval
188+
189+ def __enter__ ( self ):
190+ self .start = time .time ( )
191+
192+ def __exit__ ( self , exc_type , exc_val , exc_tb ):
193+ if exc_type is None :
194+ duration = time .time ( ) - self .start
195+ remainder = self .min_interval - duration
196+ if remainder > 0 :
197+ time .sleep ( remainder )
198+
199+ def __call__ ( self , function ):
200+ def wrapper ( * args , ** kwargs ):
201+ with self :
202+ return function ( * args , ** kwargs )
203+ return wrapper
0 commit comments