1# Copyright (c) 2017 Ultimaker B.V. 2# Copyright (c) Thiago Marcos P. Santos 3# Copyright (c) Christopher S. Case 4# Copyright (c) David H. Bronke 5# Uranium is released under the terms of the LGPLv3 or higher. 6 7import enum #For the compress parameter of postponeSignals. 8import inspect 9import threading 10import os 11import weakref 12from weakref import ReferenceType 13from typing import Any, Union, Callable, TypeVar, Generic, List, Tuple, Iterable, cast, Optional 14import contextlib 15import traceback 16 17import functools 18 19from UM.Event import CallFunctionEvent 20from UM.Decorators import call_if_enabled 21from UM.Logger import Logger 22from UM.Platform import Platform 23from UM import FlameProfiler 24 25MYPY = False 26if MYPY: 27 from UM.Application import Application 28 29 30# Helper functions for tracing signal emission. 31def _traceEmit(signal: Any, *args: Any, **kwargs: Any) -> None: 32 Logger.log("d", "Emitting %s with arguments %s", str(signal.getName()), str(args) + str(kwargs)) 33 34 if signal._Signal__type == Signal.Queued: 35 Logger.log("d", "> Queued signal, postponing emit until next event loop run") 36 37 if signal._Signal__type == Signal.Auto: 38 if Signal._signalQueue is not None and threading.current_thread() is not Signal._signalQueue.getMainThread(): 39 Logger.log("d", "> Auto signal and not on main thread, postponing emit until next event loop run") 40 41 for func in signal._Signal__functions: 42 Logger.log("d", "> Calling %s", str(func)) 43 44 for dest, func in signal._Signal__methods: 45 Logger.log("d", "> Calling %s on %s", str(func), str(dest)) 46 47 for signal in signal._Signal__signals: 48 Logger.log("d", "> Emitting %s", str(signal._Signal__name)) 49 50 51def _traceConnect(signal: Any, *args: Any, **kwargs: Any) -> None: 52 Logger.log("d", "Connecting signal %s to %s", str(signal._Signal__name), str(args[0])) 53 54 55def _traceDisconnect(signal: Any, *args: Any, **kwargs: Any) -> None: 56 Logger.log("d", "Connecting signal %s from %s", str(signal._Signal__name), str(args[0])) 57 58 59def _isTraceEnabled() -> bool: 60 return "URANIUM_TRACE_SIGNALS" in os.environ 61 62 63class SignalQueue: 64 def functionEvent(self, event): 65 pass 66 67 def getMainThread(self): 68 pass 69 70# Integration with the Flame Profiler. 71 72 73def _recordSignalNames() -> bool: 74 return FlameProfiler.enabled() 75 76 77def profileEmit(func): 78 if FlameProfiler.enabled(): 79 @functools.wraps(func) 80 def wrapped(self, *args, **kwargs): 81 FlameProfiler.updateProfileConfig() 82 if FlameProfiler.isRecordingProfile(): 83 with FlameProfiler.profileCall("[SIG] " + self.getName()): 84 func(self, *args, **kwargs) 85 else: 86 func(self, *args, **kwargs) 87 return wrapped 88 89 else: 90 return func 91 92 93class Signal: 94 """Simple implementation of signals and slots. 95 96 Signals and slots can be used as a light weight event system. A class can 97 define signals that other classes can connect functions or methods to, called slots. 98 Whenever the signal is called, it will proceed to call the connected slots. 99 100 To create a signal, create an instance variable of type Signal. Other objects can then 101 use that variable's `connect()` method to connect methods, callable objects or signals 102 to the signal. To emit the signal, call `emit()` on the signal. Arguments can be passed 103 along to the signal, but slots will be required to handle them. When connecting signals 104 to other signals, the connected signal will be emitted whenever the signal is emitted. 105 106 Signal-slot connections are weak references and as such will not prevent objects 107 from being destroyed. In addition, all slots will be implicitly disconnected when 108 the signal is destroyed. 109 110 **WARNING** It is imperative that the signals are created as instance variables, otherwise 111 emitting signals will get confused. To help with this, see the SignalEmitter class. 112 113 Loosely based on http://code.activestate.com/recipes/577980-improved-signalsslots-implementation-in-python/ pylint: disable=wrong-spelling-in-comment 114 :sa SignalEmitter 115 """ 116 117 Direct = 1 118 """Signal types. 119 These indicate the type of a signal, that is, how the signal handles calling the connected 120 slots. 121 122 - Direct connections immediately call the connected slots from the thread that called emit(). 123 - Auto connections will push the call onto the event loop if the current thread is 124 not the main thread, but make a direct call if it is. 125 - Queued connections will always push 126 the call on to the event loop. 127 """ 128 Auto = 2 129 Queued = 3 130 131 def __init__(self, type: int = Auto) -> None: 132 """Initialize the instance. 133 134 :param type: The signal type. Defaults to Auto. 135 """ 136 137 # These collections must be treated as immutable otherwise we lose thread safety. 138 self.__functions = WeakImmutableList() # type: WeakImmutableList[Callable[[], None]] 139 self.__methods = WeakImmutablePairList() # type: WeakImmutablePairList[Any, Callable[[], None]] 140 self.__signals = WeakImmutableList() # type: WeakImmutableList[Signal] 141 142 self.__lock = threading.Lock() # Guards access to the fields above. 143 self.__type = type 144 145 self._postpone_emit = False 146 self._postpone_thread = None # type: Optional[threading.Thread] 147 self._compress_postpone = False # type: bool 148 self._postponed_emits = None # type: Any 149 150 if _recordSignalNames(): 151 try: 152 if Platform.isWindows(): 153 self.__name = inspect.stack()[1][0].f_locals["key"] 154 else: 155 self.__name = inspect.stack()[1].frame.f_locals["key"] 156 except KeyError: 157 self.__name = "Signal" 158 else: 159 self.__name = "Anon" 160 161 def getName(self): 162 return self.__name 163 164 def __call__(self) -> None: 165 """:exception NotImplementedError:""" 166 167 raise NotImplementedError("Call emit() to emit a signal") 168 169 def getType(self) -> int: 170 """Get type of the signal 171 172 :return: Direct(1), Auto(2) or Queued(3) 173 """ 174 175 return self.__type 176 177 @call_if_enabled(_traceEmit, _isTraceEnabled()) 178 @profileEmit 179 def emit(self, *args: Any, **kwargs: Any) -> None: 180 """Emit the signal which indirectly calls all of the connected slots. 181 182 :param args: The positional arguments to pass along. 183 :param kwargs: The keyword arguments to pass along. 184 185 :note If the Signal type is Queued and this is not called from the application thread 186 the call will be posted as an event to the application main thread, which means the 187 function will be called on the next application event loop tick. 188 """ 189 190 # Check to see if we need to postpone emits 191 if self._postpone_emit: 192 if threading.current_thread() != self._postpone_thread: 193 Logger.log("w", "Tried to emit signal from thread %s while emits are being postponed by %s. Traceback:", threading.current_thread(), self._postpone_thread) 194 tb = traceback.format_stack() 195 for line in tb: 196 Logger.log("w", line) 197 198 if self._compress_postpone == CompressTechnique.CompressSingle: 199 # If emits should be compressed, we only emit the last emit that was called 200 self._postponed_emits = (args, kwargs) 201 else: 202 # If emits should not be compressed or compressed per parameter value, we catch all calls to emit and put them in a list to be called later. 203 if not self._postponed_emits: 204 self._postponed_emits = [] 205 self._postponed_emits.append((args, kwargs)) 206 return 207 208 try: 209 if self.__type == Signal.Queued: 210 Signal._app.functionEvent(CallFunctionEvent(self.__performEmit, args, kwargs)) 211 return 212 if self.__type == Signal.Auto: 213 if threading.current_thread() is not Signal._app.getMainThread(): 214 Signal._app.functionEvent(CallFunctionEvent(self.__performEmit, args, kwargs)) 215 return 216 except AttributeError: # If Signal._app is not set 217 return 218 219 self.__performEmit(*args, **kwargs) 220 221 @call_if_enabled(_traceConnect, _isTraceEnabled()) 222 def connect(self, connector: Union["Signal", Callable[[], None]]) -> None: 223 """Connect to this signal. 224 225 :param connector: The signal or slot (function) to connect. 226 """ 227 228 if self._postpone_emit: 229 Logger.log("w", "Tried to connect to signal %s that is currently being postponed, this is not possible", self.__name) 230 return 231 232 with self.__lock: 233 if isinstance(connector, Signal): 234 if connector == self: 235 return 236 self.__signals = self.__signals.append(connector) 237 elif inspect.ismethod(connector): 238 # if SIGNAL_PROFILE: 239 # Logger.log('d', "Connector method qual name: " + connector.__func__.__qualname__) 240 self.__methods = self.__methods.append(cast(Any, connector).__self__, cast(Any, connector).__func__) 241 else: 242 # Once again, update the list of functions using a whole new list. 243 # if SIGNAL_PROFILE: 244 # Logger.log('d', "Connector function qual name: " + connector.__qualname__) 245 246 self.__functions = self.__functions.append(connector) 247 248 @call_if_enabled(_traceDisconnect, _isTraceEnabled()) 249 def disconnect(self, connector): 250 """Disconnect from this signal. 251 252 :param connector: The signal or slot (function) to disconnect. 253 """ 254 255 if self._postpone_emit: 256 Logger.log("w", "Tried to disconnect from signal %s that is currently being postponed, this is not possible", self.__name) 257 return 258 259 with self.__lock: 260 if isinstance(connector, Signal): 261 self.__signals = self.__signals.remove(connector) 262 elif inspect.ismethod(connector): 263 self.__methods = self.__methods.remove(connector.__self__, connector.__func__) 264 else: 265 self.__functions = self.__functions.remove(connector) 266 267 def disconnectAll(self): 268 """Disconnect all connected slots.""" 269 270 if self._postpone_emit: 271 Logger.log("w", "Tried to disconnect from signal %s that is currently being postponed, this is not possible", self.__name) 272 return 273 274 with self.__lock: 275 self.__functions = WeakImmutableList() # type: "WeakImmutableList" 276 self.__methods = WeakImmutablePairList() # type: "WeakImmutablePairList" 277 self.__signals = WeakImmutableList() # type: "WeakImmutableList" 278 279 def __getstate__(self): 280 """To support Pickle 281 282 Since Weak containers cannot be serialized by Pickle we just return an empty dict as state. 283 """ 284 285 return {} 286 287 def __deepcopy__(self, memo): 288 """To properly handle deepcopy in combination with __getstate__ 289 290 Apparently deepcopy uses __getstate__ internally, which is not documented. The reimplementation 291 of __getstate__ then breaks deepcopy. On the other hand, if we do not reimplement it like that, 292 we break pickle. So instead make sure to also reimplement __deepcopy__. 293 """ 294 295 # Snapshot these fields 296 with self.__lock: 297 functions = self.__functions 298 methods = self.__methods 299 signals = self.__signals 300 301 signal = Signal(type = self.__type) 302 signal.__functions = functions 303 signal.__methods = methods 304 signal.__signals = signals 305 return signal 306 307 _app = None # type: Application 308 """To avoid circular references when importing Application, this should be 309 set by the Application instance. 310 """ 311 312 _signalQueue = None # type: Application 313 314 # Private implementation of the actual emit. 315 # This is done to make it possible to freely push function events without needing to maintain state. 316 def __performEmit(self, *args, **kwargs) -> None: 317 # Quickly make some private references to the collections we need to process. 318 # Although the these fields are always safe to use read and use with regards to threading, 319 # we want to operate on a consistent snapshot of the whole set of fields. 320 with self.__lock: 321 functions = self.__functions 322 methods = self.__methods 323 signals = self.__signals 324 325 if not FlameProfiler.isRecordingProfile(): 326 # Call handler functions 327 for func in functions: 328 func(*args, **kwargs) 329 330 # Call handler methods 331 for dest, func in methods: 332 func(dest, *args, **kwargs) 333 334 # Emit connected signals 335 for signal in signals: 336 signal.emit(*args, **kwargs) 337 else: 338 # Call handler functions 339 for func in functions: 340 with FlameProfiler.profileCall(func.__qualname__): 341 func(*args, **kwargs) 342 343 # Call handler methods 344 for dest, func in methods: 345 with FlameProfiler.profileCall(func.__qualname__): 346 func(dest, *args, **kwargs) 347 348 # Emit connected signals 349 for signal in signals: 350 with FlameProfiler.profileCall("[SIG]" + signal.getName()): 351 signal.emit(*args, **kwargs) 352 353 # This __str__() is useful for debugging. 354 # def __str__(self): 355 # function_str = ", ".join([repr(f) for f in self.__functions]) 356 # method_str = ", ".join([ "{dest: " + str(dest) + ", funcs: " + strMethodSet(funcs) + "}" for dest, funcs in self.__methods]) 357 # signal_str = ", ".join([str(signal) for signal in self.__signals]) 358 # return "Signal<{}> {{ __functions={{ {} }}, __methods={{ {} }}, __signals={{ {} }} }}".format(id(self), function_str, method_str, signal_str) 359 360 361#def strMethodSet(method_set): 362# return "{" + ", ".join([str(m) for m in method_set]) + "}" 363 364 365class CompressTechnique(enum.Enum): 366 NoCompression = 0 367 CompressSingle = 1 368 CompressPerParameterValue = 2 369 370@contextlib.contextmanager 371def postponeSignals(*signals, compress: CompressTechnique = CompressTechnique.NoCompression): 372 """A context manager that allows postponing of signal emissions 373 374 This context manager will collect any calls to emit() made for the provided signals 375 and only emit them after exiting. This ensures more batched processing of signals. 376 377 The optional "compress" argument will limit the emit calls to 1. This means that 378 when a bunch of calls are made to the signal's emit() method, only the last call 379 will be emitted on exit. 380 381 **WARNING** When compress is True, only the **last** call will be emitted. This means 382 that any other calls will be ignored, _including their arguments_. 383 384 :param signals: The signals to postpone emits for. 385 :param compress: Whether to enable compression of emits or not. 386 """ 387 388 # To allow for nested postpones on the same signals, we should check if signals are not already 389 # postponed and only change those that are not yet postponed. 390 restore_emit = [] 391 for signal in signals: 392 if not signal._postpone_emit: # Do nothing if the signal has already been changed 393 signal._postpone_emit = True 394 signal._postpone_thread = threading.current_thread() 395 signal._compress_postpone = compress 396 # Since we made changes, make sure to restore the signal after exiting the context manager 397 restore_emit.append(signal) 398 399 # Execute the code block in the "with" statement 400 yield 401 402 for signal in restore_emit: 403 # We are done with the code, restore all changed signals to their "normal" state 404 signal._postpone_emit = False 405 406 if signal._postponed_emits: 407 # Send any signal emits that were collected while emits were being postponed 408 if signal._compress_postpone == CompressTechnique.CompressSingle: 409 signal.emit(*signal._postponed_emits[0], **signal._postponed_emits[1]) 410 elif signal._compress_postpone == CompressTechnique.CompressPerParameterValue: 411 uniques = {(tuple(args), tuple(kwargs.items())) for args, kwargs in signal._postponed_emits} #Have to make them tuples in order to make them hashable. 412 for args, kwargs in uniques: 413 signal.emit(*args, **dict(kwargs)) 414 else: 415 for args, kwargs in signal._postponed_emits: 416 signal.emit(*args, **kwargs) 417 signal._postponed_emits = None 418 419 signal._postpone_thread = None 420 signal._compress_postpone = False 421 422 423def signalemitter(cls): 424 """Class decorator that ensures a class has unique instances of signals. 425 426 Since signals need to be instance variables, normally you would need to create all 427 signals in the class" `__init__` method. However, this makes them rather awkward to 428 document. This decorator instead makes it possible to declare them as class variables, 429 which makes documenting them near the function they are used possible. This decorator 430 adjusts the class' __new__ method to create new signal instances for all class signals. 431 """ 432 433 # First, check if the base class has any signals defined 434 signals = inspect.getmembers(cls, lambda i: isinstance(i, Signal)) 435 if not signals: 436 raise TypeError("Class {0} is marked as signal emitter but no signal were found".format(cls)) 437 438 # Then, replace the class' new method with one that modifies the created instance to have 439 # unique signals. 440 old_new = cls.__new__ 441 def new_new(subclass, *args, **kwargs): 442 if old_new == object.__new__: 443 sub = object.__new__(subclass) 444 else: 445 sub = old_new(subclass, *args, **kwargs) 446 447 for key, value in inspect.getmembers(cls, lambda i: isinstance(i, Signal)): 448 setattr(sub, key, Signal(type = value.getType())) 449 450 return sub 451 452 cls.__new__ = new_new 453 return cls 454 455 456T = TypeVar('T') 457 458 459class WeakImmutableList(Generic[T], Iterable): 460 """Minimal implementation of a weak reference list with immutable tendencies. 461 462 Strictly speaking this isn't immutable because the garbage collector can modify 463 it, but no application code can. Also, this class doesn't implement the Python 464 list API, only the handful of methods we actually need in the code above. 465 """ 466 467 def __init__(self) -> None: 468 self.__list = [] # type: List[ReferenceType[Optional[T]]] 469 470 def append(self, item: T) -> "WeakImmutableList[T]": 471 """Append an item and return a new list 472 473 :param item: the item to append 474 :return: a new list 475 """ 476 477 new_instance = WeakImmutableList() # type: WeakImmutableList[T] 478 new_instance.__list = self.__cleanList() 479 new_instance.__list.append(ReferenceType(item)) 480 return new_instance 481 482 def remove(self, item: T) -> "WeakImmutableList[T]": 483 """Remove an item and return a list 484 485 Note that unlike the normal Python list.remove() method, this ones 486 doesn't throw a ValueError if the item isn't in the list. 487 :param item: item to remove 488 :return: a list which does not have the item. 489 """ 490 491 for item_ref in self.__list: 492 if item_ref() is item: 493 new_instance = WeakImmutableList() # type: WeakImmutableList[T] 494 new_instance.__list = self.__cleanList() 495 new_instance.__list.remove(item_ref) 496 return new_instance 497 else: 498 return self # No changes needed 499 500 # Create a new list with the missing values removed. 501 def __cleanList(self) -> "List[ReferenceType[Optional[T]]]": 502 return [item_ref for item_ref in self.__list if item_ref() is not None] 503 504 def __iter__(self): 505 return WeakImmutableListIterator(self.__list) 506 507 508class WeakImmutableListIterator(Generic[T], Iterable): 509 """Iterator wrapper which filters out missing values. 510 511 It dereferences each weak reference object and filters out the objects 512 which have already disappeared via GC. 513 """ 514 515 def __init__(self, list_): 516 self.__it = list_.__iter__() 517 518 def __iter__(self): 519 return self 520 521 def __next__(self): 522 next_item = self.__it.__next__()() 523 while next_item is None: # Skip missing values 524 next_item = self.__it.__next__()() 525 return next_item 526 527 528U = TypeVar('U') 529 530 531class WeakImmutablePairList(Generic[T, U], Iterable): 532 """A variation of WeakImmutableList which holds a pair of values using weak refernces.""" 533 534 def __init__(self) -> None: 535 self.__list = [] # type: List[Tuple[ReferenceType[T],ReferenceType[U]]] 536 537 def append(self, left_item: T, right_item: U) -> "WeakImmutablePairList[T,U]": 538 """Append an item and return a new list 539 540 :param item: the item to append 541 :return: a new list 542 """ 543 544 new_instance = WeakImmutablePairList() # type: WeakImmutablePairList[T,U] 545 new_instance.__list = self.__cleanList() 546 new_instance.__list.append( (weakref.ref(left_item), weakref.ref(right_item)) ) 547 return new_instance 548 549 def remove(self, left_item: T, right_item: U) -> "WeakImmutablePairList[T,U]": 550 """Remove an item and return a list 551 552 Note that unlike the normal Python list.remove() method, this ones 553 doesn't throw a ValueError if the item isn't in the list. 554 :param item: item to remove 555 :return: a list which does not have the item. 556 """ 557 558 for pair in self.__list: 559 left = pair[0]() 560 right = pair[1]() 561 562 if left is left_item and right is right_item: 563 new_instance = WeakImmutablePairList() # type: WeakImmutablePairList[T,U] 564 new_instance.__list = self.__cleanList() 565 new_instance.__list.remove(pair) 566 return new_instance 567 else: 568 return self # No changes needed 569 570 # Create a new list with the missing values removed. 571 def __cleanList(self) -> List[Tuple[ReferenceType,ReferenceType]]: 572 return [pair for pair in self.__list if pair[0]() is not None and pair[1]() is not None] 573 574 def __iter__(self): 575 return WeakImmutablePairListIterator(self.__list) 576 577 578# A small iterator wrapper which dereferences the weak ref objects and filters 579# out the objects which have already disappeared via GC. 580class WeakImmutablePairListIterator: 581 def __init__(self, list_) -> None: 582 self.__it = list_.__iter__() 583 584 def __iter__(self): 585 return self 586 587 def __next__(self): 588 pair = self.__it.__next__() 589 left = pair[0]() 590 right = pair[1]() 591 while left is None or right is None: # Skip missing values 592 pair = self.__it.__next__() 593 left = pair[0]() 594 right = pair[1]() 595 596 return left, right 597