Coverage for utils/misc.py : 48%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2from __future__ import annotations
4# -- stdlib --
5from collections import deque
6from contextlib import contextmanager
7from functools import wraps
8from time import time
9from typing import Any, Callable, Dict, Iterable, List, Set, Tuple, Type, TypeVar
10from weakref import WeakSet
11import functools
12import logging
13import re
15# -- third party --
16from gevent.lock import Semaphore
17from gevent.queue import Queue
18import gevent
20# -- own --
22# -- code --
23log = logging.getLogger('util.misc')
24dbgvals: Dict[str, object] = {}
27class ObjectDict(dict):
28 __slots__ = ()
30 def __getattr__(self, k):
31 try:
32 return self[k]
33 except KeyError:
34 raise AttributeError
36 def __setattr__(self, k, v):
37 self[k] = v
39 @classmethod
40 def parse(cls, data):
41 if isinstance(data, dict):
42 return cls({k: cls.parse(v) for k, v in data.items()})
43 elif isinstance(data, (list, tuple, set, frozenset)):
44 return type(data)([cls.parse(v) for v in data])
46 return data
49T = TypeVar('T')
52class BatchList(List[T]):
53 __slots__ = ()
55 def __getattribute__(self, name):
56 try:
57 list_attr = list.__getattribute__(self, name)
58 return list_attr
59 except AttributeError:
60 pass
62 return BatchList(
63 getattr(i, name) for i in self
64 )
66 def __call__(self, *a, **k):
67 return BatchList(
68 f(*a, **k) for f in self
69 )
71 '''
72 @typing.overload
73 def __getitem__(self, i: int) -> T: ...
75 @typing.overload
76 def __getitem__(self, s: slice) -> 'BatchList[T]': ...
78 def __getitem__(self, a):
79 if isinstance(a, slice):
80 return BatchList(list.__getitem__(self, a))
81 else:
82 return list.__getitem__(self, a)
83 '''
85 def exclude(self, elem: T) -> 'BatchList[T]':
86 nl = BatchList(self)
87 try:
88 nl.remove(elem)
89 except ValueError:
90 pass
92 return nl
94 def rotate_to(self, elem: T) -> 'BatchList[T]':
95 i = self.index(elem)
96 n = len(self)
97 return self.__class__((self*2)[i:i+n])
99 def replace(self, old: T, new: T) -> bool:
100 try:
101 self[self.index(old)] = new
102 return True
103 except ValueError:
104 return False
106 def find_replace(self, pred: Callable[[T], bool], new: T) -> bool:
107 for i, v in enumerate(self):
108 if pred(v):
109 self[i] = new
110 return True
111 else:
112 return False
114 def sibling(self, me: T, offset=1) -> T:
115 i = self.index(me)
116 n = len(self)
117 return self[(i + offset) % n]
120def remove_dups(s):
121 seen: Set[Any] = set()
122 for i in s:
123 if i not in seen: 123 ↛ 122line 123 didn't jump to line 122, because the condition on line 123 was never false
124 yield i
125 seen.add(i)
128def classmix(*_classes) -> type:
129 classes: Any = []
130 for c in _classes:
131 if hasattr(c, '_is_mixedclass'):
132 classes.extend(c.__bases__)
133 else:
134 classes.append(c)
136 classes = tuple(remove_dups(classes))
137 cached = cls_cache.get(classes, None)
138 if cached: return cached
140 clsname = ', '.join(cls.__name__ for cls in classes)
141 new_cls = type('Mixed(%s)' % clsname, classes, {'_is_mixedclass': True})
142 cls_cache[classes] = new_cls
143 return new_cls
146cls_cache: Dict[tuple, type] = {}
149def hook(module):
150 def inner(hooker):
151 funcname = hooker.__name__
152 hookee = getattr(module, funcname)
154 @wraps(hookee)
155 def real_hooker(*args, **kwargs):
156 return hooker(hookee, *args, **kwargs)
157 setattr(module, funcname, real_hooker)
158 return real_hooker
159 return inner
162def extendclass(clsname, bases, _dict):
163 for cls in bases:
164 for key, value in _dict.items():
165 if key == '__module__':
166 continue
167 setattr(cls, key, value)
170def partition(pred: Callable[[Any], bool], lst: Iterable[Any]) -> Tuple[List[Any], List[Any]]:
171 f: List[Any]
172 t: List[Any]
173 f, t = [], []
174 for i in lst:
175 (f, t)[pred(i)].append(i)
177 return t, f
180def track(f):
181 @functools.wraps(f)
182 def _wrapper(*a, **k):
183 print('%s: %s %s' % (f.__name__, a, k))
184 return f(*a, **k)
185 return _wrapper
188def flatten(l):
189 rst = []
191 def _flatten(sl):
192 for i in sl:
193 if isinstance(i, (list, tuple, deque)):
194 _flatten(i)
195 else:
196 rst.append(i)
198 _flatten(l)
199 return rst
202def group_by(l, keyfunc):
203 if not l: return []
205 grouped = []
206 group = []
208 lastkey = keyfunc(l[0])
209 for i in l:
210 k = keyfunc(i)
211 if k == lastkey:
212 group.append(i)
213 else:
214 grouped.append(group)
215 group = [i]
216 lastkey = k
218 if group:
219 grouped.append(group)
221 return grouped
224def instantiate(cls):
225 return cls()
228def surpress_and_restart(f):
229 def wrapper(*a, **k):
230 while True:
231 try:
232 return f(*a, **k)
233 except Exception as e:
234 import logging
235 log = logging.getLogger('misc')
236 log.exception(e)
238 return wrapper
241def swallow(f):
242 def wrapper(*a, **k):
243 try:
244 return f(*a, **k)
245 except Exception:
246 pass
248 return wrapper
251def log_failure(logger):
252 def decorate(f):
253 def wrapper(*a, **k):
254 try:
255 return f(*a, **k)
256 except Exception as e:
257 logger.exception(e)
258 raise
260 return wrapper
262 return decorate
265class ObservableEvent(object):
266 listeners: Set[Callable]
268 def __init__(self, weakref=False):
269 self.listeners = WeakSet() if weakref else set()
271 def __iadd__(self, ob):
272 self.listeners.add(ob)
273 return self
275 def __isub__(self, ob):
276 self.listeners.discard(ob)
277 return self
279 def notify(self, *a, **k):
280 for ob in list(self.listeners):
281 ob(*a, **k)
284class GenericPool(object):
285 def __init__(self, factory, size, container_class=Queue):
286 self.factory = factory
287 self.size = size
288 self.container = container_class(size)
289 self.inited = False
291 def __call__(self):
292 @contextmanager
293 def manager():
294 container = self.container
296 if not self.inited:
297 for i in range(self.size):
298 container.put(self.factory())
300 self.inited = True
302 try:
303 item = container.get()
304 yield item
305 except Exception:
306 item = self.factory()
307 raise
308 finally:
309 try:
310 container.put_nowait(item)
311 except Exception:
312 pass
314 return manager()
317def debounce(seconds):
318 def decorate(f):
319 lock = Semaphore(1)
320 last = None
322 def bouncer(fire, *a, **k):
323 nonlocal last
324 gevent.sleep(seconds)
325 last = None
326 fire and f(*a, **k)
328 @wraps(f)
329 def wrapper(*a, **k):
330 nonlocal last
331 rst = lock.acquire(blocking=False)
332 if not rst:
333 return
335 try:
336 run = False
337 if last is None:
338 last = gevent.spawn(bouncer, False)
339 run = True
340 else:
341 last.kill()
342 last = gevent.spawn(bouncer, True, *a, **k)
343 finally:
344 lock.release()
346 run and f(*a, **k)
348 wrapper.__name__ == f.__name__
349 return wrapper
351 return decorate
354class ThrottleState(object):
355 __slots__ = ('running', 'pending', 'args')
357 running: bool
358 pending: bool
359 args: Tuple[tuple, dict]
361 def __init__(self):
362 self.running = self.pending = False
365def throttle(seconds: float) -> Callable[[T], T]:
366 def decorate(f):
367 deadline = -1.0
368 gr = None
370 def bouncer(*a, **k):
371 nonlocal deadline, gr
372 t = deadline - time()
373 while t > 0:
374 gevent.sleep(t)
375 t = deadline - time()
376 gr = None
377 deadline = time() + seconds
378 f(*a, **k)
380 @wraps(f)
381 def wrapper(*a, **k):
382 nonlocal deadline, gr
383 t = deadline - time()
384 if t < 0:
385 f(*a, **k)
386 deadline = time() + seconds
387 else:
388 gr = gr or gevent.spawn(bouncer, *a, **k)
390 wrapper.__name__ == f.__name__
391 return wrapper
393 return decorate
396class InstanceHookMeta(type):
397 # ABCMeta would use __subclasshook__ for instance check. Loses information.
399 def __instancecheck__(cls, inst):
400 return cls.instancecheck(inst)
402 def __subclasscheck__(cls, C):
403 return cls.subclasscheck(C)
405 def instancecheck(cls, inst):
406 return cls.subclasscheck(type(inst))
409class ArgValidationError(Exception):
410 pass
413class ArgTypeError(ArgValidationError):
414 __slots__ = ('position', 'expected', 'actual')
416 def __init__(self, position, expected, actual):
417 self.position = position
418 self.expected = expected
419 self.actual = actual
421 def __unicode__(self):
422 return 'Arg %s should be "%s" type, "%s" found' % (
423 self.position,
424 self.expected.__name__,
425 self.actual.__name__,
426 )
428 def __str__(self):
429 return self.__unicode__().encode('utf-8')
432class ArgCountError(ArgValidationError):
433 __slots__ = ('expected', 'actual')
435 def __init__(self, expected, actual):
436 self.expected = expected
437 self.actual = actual
439 def __unicode__(self):
440 return 'Expecting %s args, %s found' % (
441 self.expected,
442 self.actual,
443 )
445 def __str__(self):
446 return self.__unicode__().encode('utf-8')
449def validate_args(*typelist):
450 def decorate(f):
451 @wraps(f)
452 def wrapper(*args):
453 e, a = len(typelist), len(args)
454 if e != a:
455 raise ArgCountError(e, a)
457 for i, e, v in zip(range(1000), typelist, args):
458 if not isinstance(v, e):
459 raise ArgValidationError(i, e, v.__class__)
461 return f(*args)
463 wrapper.__name__ = f.__name__
464 return wrapper
466 return decorate
469class BusinessException(Exception):
470 snake_case: str
473class BusinessExceptionGenerator(object):
474 def __getattr__(self, k: str) -> Type[BusinessException]:
475 snake_case = '_'.join([
476 i.lower() for i in re.findall(r'[A-Z][a-z]+|[A-Z]+(?![a-z])', k)
477 ])
479 cls = type(k, (BusinessException,), {'name': k, 'snake_case': snake_case})
480 setattr(self, k, cls)
481 return cls
484exceptions = BusinessExceptionGenerator()
487# Helper to make mypy happy
488class MockMeta(type):
489 def mro(cls):
490 bases = cls.__bases__
491 assert len(bases) == 1
492 return [cls, object]