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