1# -*- coding: utf-8 -*-
2
3from collections import defaultdict, OrderedDict
4from threading import Lock
5
6__all__ = ["EventEmitter", "PyeeException"]
7
8
9class PyeeException(Exception):
10    """An exception internal to pyee."""
11
12    pass
13
14
15class EventEmitter:
16    """The base event emitter class. All other event emitters inherit from
17    this class.
18
19    Most events are registered with an emitter via the ``on`` and ``once``
20    methods, and fired with the ``emit`` method. However, pyee event emitters
21    have two *special* events:
22
23    - ``new_listener``: Fires whenever a new listener is created. Listeners for
24      this event do not fire upon their own creation.
25
26    - ``error``: When emitted raises an Exception by default, behavior can be
27      overriden by attaching callback to the event.
28
29      For example::
30
31          @ee.on('error')
32          def on_error(message):
33              logging.err(message)
34
35          ee.emit('error', Exception('something blew up'))
36
37    All callbacks are handled in a synchronous, blocking manner. As in node.js,
38    raised exceptions are not automatically handled for you---you must catch
39    your own exceptions, and treat them accordingly.
40    """
41
42    def __init__(self):
43        self._events = defaultdict(OrderedDict)
44        self._lock = Lock()
45
46    def on(self, event, f=None):
47        """Registers the function ``f`` to the event name ``event``.
48
49        If ``f`` isn't provided, this method returns a function that
50        takes ``f`` as a callback; in other words, you can use this method
51        as a decorator, like so::
52
53            @ee.on('data')
54            def data_handler(data):
55                print(data)
56
57        In both the decorated and undecorated forms, the event handler is
58        returned. The upshot of this is that you can call decorated handlers
59        directly, as well as use them in remove_listener calls.
60        """
61
62        def _on(f):
63            self._add_event_handler(event, f, f)
64            return f
65
66        if f is None:
67            return _on
68        else:
69            return _on(f)
70
71    def _add_event_handler(self, event, k, v):
72        # Fire 'new_listener' *before* adding the new listener!
73        self.emit("new_listener", event, k)
74
75        # Add the necessary function
76        # Note that k and v are the same for `on` handlers, but
77        # different for `once` handlers, where v is a wrapped version
78        # of k which removes itself before calling k
79        with self._lock:
80            self._events[event][k] = v
81
82    def _emit_run(self, f, args, kwargs):
83        f(*args, **kwargs)
84
85    def _emit_handle_potential_error(self, event, error):
86        if event == "error":
87            if error:
88                raise error
89            else:
90                raise PyeeException("Uncaught, unspecified 'error' event.")
91
92    def _call_handlers(self, event, args, kwargs):
93        handled = False
94
95        with self._lock:
96            funcs = list(self._events[event].values())
97        for f in funcs:
98            self._emit_run(f, args, kwargs)
99            handled = True
100
101        return handled
102
103    def emit(self, event, *args, **kwargs):
104        """Emit ``event``, passing ``*args`` and ``**kwargs`` to each attached
105        function. Returns ``True`` if any functions are attached to ``event``;
106        otherwise returns ``False``.
107
108        Example::
109
110            ee.emit('data', '00101001')
111
112        Assuming ``data`` is an attached function, this will call
113        ``data('00101001')'``.
114        """
115        handled = self._call_handlers(event, args, kwargs)
116
117        if not handled:
118            self._emit_handle_potential_error(event, args[0] if args else None)
119
120        return handled
121
122    def once(self, event, f=None):
123        """The same as ``ee.on``, except that the listener is automatically
124        removed after being called.
125        """
126
127        def _wrapper(f):
128            def g(*args, **kwargs):
129                with self._lock:
130                    # Check that the event wasn't removed already right
131                    # before the lock
132                    if event in self._events and f in self._events[event]:
133                        self._remove_listener(event, f)
134                    else:
135                        return None
136                # f may return a coroutine, so we need to return that
137                # result here so that emit can schedule it
138                return f(*args, **kwargs)
139
140            self._add_event_handler(event, f, g)
141            return f
142
143        if f is None:
144            return _wrapper
145        else:
146            return _wrapper(f)
147
148    def _remove_listener(self, event, f):
149        """Naked unprotected removal."""
150        self._events[event].pop(f)
151
152    def remove_listener(self, event, f):
153        """Removes the function ``f`` from ``event``."""
154        with self._lock:
155            self._remove_listener(event, f)
156
157    def remove_all_listeners(self, event=None):
158        """Remove all listeners attached to ``event``.
159        If ``event`` is ``None``, remove all listeners on all events.
160        """
161        with self._lock:
162            if event is not None:
163                self._events[event] = OrderedDict()
164            else:
165                self._events = defaultdict(OrderedDict)
166
167    def listeners(self, event):
168        """Returns a list of all listeners registered to the ``event``."""
169        return list(self._events[event].keys())
170