1"""
2Events for xonsh.
3
4In all likelihood, you want builtins.events
5
6The best way to "declare" an event is something like::
7
8    events.doc('on_spam', "Comes with eggs")
9"""
10import abc
11import builtins
12import collections.abc
13import inspect
14
15from xonsh.tools import print_exception
16
17
18def has_kwargs(func):
19    return any(
20        p.kind == p.VAR_KEYWORD for p in inspect.signature(func).parameters.values()
21    )
22
23
24def debug_level():
25    if hasattr(builtins, "__xonsh_env__"):
26        return builtins.__xonsh_env__.get("XONSH_DEBUG")
27    # FIXME: Under py.test, return 1(?)
28    else:
29        return 0  # Optimize for speed, not guaranteed correctness
30
31
32class AbstractEvent(collections.abc.MutableSet, abc.ABC):
33    """
34    A given event that handlers can register against.
35
36    Acts as a ``MutableSet`` for registered handlers.
37
38    Note that ordering is never guaranteed.
39    """
40
41    @property
42    def species(self):
43        """
44        The species (basically, class) of the event
45        """
46        return type(self).__bases__[
47            0
48        ]  # events.on_chdir -> <class on_chdir> -> <class Event>
49
50    def __call__(self, handler):
51        """
52        Registers a handler. It's suggested to use this as a decorator.
53
54        A decorator method is added to the handler, validator(). If a validator
55        function is added, it can filter if the handler will be considered. The
56        validator takes the same arguments as the handler. If it returns False,
57        the handler will not called or considered, as if it was not registered
58        at all.
59
60        Parameters
61        ----------
62        handler : callable
63            The handler to register
64
65        Returns
66        -------
67        rtn : callable
68            The handler
69        """
70        #  Using Python's "private" munging to minimize hypothetical collisions
71        handler.__validator = None
72        if debug_level():
73            if not has_kwargs(handler):
74                raise ValueError("Event handlers need a **kwargs for future proofing")
75        self.add(handler)
76
77        def validator(vfunc):
78            """
79            Adds a validator function to a handler to limit when it is considered.
80            """
81            if debug_level():
82                if not has_kwargs(handler):
83                    raise ValueError(
84                        "Event validators need a **kwargs for future proofing"
85                    )
86            handler.__validator = vfunc
87
88        handler.validator = validator
89
90        return handler
91
92    def _filterhandlers(self, handlers, **kwargs):
93        """
94        Helper method for implementing classes. Generates the handlers that pass validation.
95        """
96        for handler in handlers:
97            if handler.__validator is not None and not handler.__validator(**kwargs):
98                continue
99            yield handler
100
101    @abc.abstractmethod
102    def fire(self, **kwargs):
103        """
104        Fires an event, calling registered handlers with the given arguments.
105
106        Parameters
107        ----------
108        **kwargs :
109            Keyword arguments to pass to each handler
110        """
111
112
113class Event(AbstractEvent):
114    """
115    An event species for notify and scatter-gather events.
116    """
117
118    # Wish I could just pull from set...
119    def __init__(self):
120        self._handlers = set()
121
122    def __len__(self):
123        return len(self._handlers)
124
125    def __contains__(self, item):
126        return item in self._handlers
127
128    def __iter__(self):
129        yield from self._handlers
130
131    def add(self, item):
132        """
133        Add an element to a set.
134
135        This has no effect if the element is already present.
136        """
137        self._handlers.add(item)
138
139    def discard(self, item):
140        """
141        Remove an element from a set if it is a member.
142
143        If the element is not a member, do nothing.
144        """
145        self._handlers.discard(item)
146
147    def fire(self, **kwargs):
148        """
149        Fires an event, calling registered handlers with the given arguments. A non-unique iterable
150        of the results is returned.
151
152        Each handler is called immediately. Exceptions are turned in to warnings.
153
154        Parameters
155        ----------
156        **kwargs :
157            Keyword arguments to pass to each handler
158
159        Returns
160        -------
161        vals : iterable
162            Return values of each handler. If multiple handlers return the same value, it will
163            appear multiple times.
164        """
165        vals = []
166        for handler in self._filterhandlers(self._handlers, **kwargs):
167            try:
168                rv = handler(**kwargs)
169            except Exception:
170                print_exception("Exception raised in event handler; ignored.")
171            else:
172                vals.append(rv)
173        return vals
174
175
176class LoadEvent(AbstractEvent):
177    """
178    An event species where each handler is called exactly once, shortly after either the event is
179    fired or the handler is registered (whichever is later). Additional firings are ignored.
180
181    Note: Does not support scatter/gather, due to never knowing when we have all the handlers.
182
183    Note: Maintains a strong reference to pargs/kwargs in case of the addition of future handlers.
184
185    Note: This is currently NOT thread safe.
186    """
187
188    def __init__(self):
189        self._fired = set()
190        self._unfired = set()
191        self._hasfired = False
192
193    def __len__(self):
194        return len(self._fired) + len(self._unfired)
195
196    def __contains__(self, item):
197        return item in self._fired or item in self._unfired
198
199    def __iter__(self):
200        yield from self._fired
201        yield from self._unfired
202
203    def add(self, item):
204        """
205        Add an element to a set.
206
207        This has no effect if the element is already present.
208        """
209        if self._hasfired:
210            self._call(item)
211            self._fired.add(item)
212        else:
213            self._unfired.add(item)
214
215    def discard(self, item):
216        """
217        Remove an element from a set if it is a member.
218
219        If the element is not a member, do nothing.
220        """
221        self._fired.discard(item)
222        self._unfired.discard(item)
223
224    def _call(self, handler):
225        try:
226            handler(**self._kwargs)
227        except Exception:
228            print_exception("Exception raised in event handler; ignored.")
229
230    def fire(self, **kwargs):
231        if self._hasfired:
232            return
233        self._kwargs = kwargs
234        while self._unfired:
235            handler = self._unfired.pop()
236            self._call(handler)
237        self._hasfired = True
238        return ()  # Entirely for API compatibility
239
240
241class EventManager:
242    """
243    Container for all events in a system.
244
245    Meant to be a singleton, but doesn't enforce that itself.
246
247    Each event is just an attribute. They're created dynamically on first use.
248    """
249
250    def doc(self, name, docstring):
251        """
252        Applies a docstring to an event.
253
254        Parameters
255        ----------
256        name : str
257            The name of the event, eg "on_precommand"
258        docstring : str
259            The docstring to apply to the event
260        """
261        type(getattr(self, name)).__doc__ = docstring
262
263    @staticmethod
264    def _mkevent(name, species=Event, doc=None):
265        # NOTE: Also used in `xonsh_events` test fixture
266        # (A little bit of magic to enable docstrings to work right)
267        return type(
268            name,
269            (species,),
270            {
271                "__doc__": doc,
272                "__module__": "xonsh.events",
273                "__qualname__": "events." + name,
274            },
275        )()
276
277    def transmogrify(self, name, species):
278        """
279        Converts an event from one species to another, preserving handlers and docstring.
280
281        Please note: Some species maintain specialized state. This is lost on transmogrification.
282
283        Parameters
284        ----------
285        name : str
286            The name of the event, eg "on_precommand"
287        species : subclass of AbstractEvent
288            The type to turn the event in to.
289        """
290        if isinstance(species, str):
291            species = globals()[species]
292
293        if not issubclass(species, AbstractEvent):
294            raise ValueError("Invalid event class; must be a subclass of AbstractEvent")
295
296        oldevent = getattr(self, name)
297        newevent = self._mkevent(name, species, type(oldevent).__doc__)
298        setattr(self, name, newevent)
299
300        for handler in oldevent:
301            newevent.add(handler)
302
303    def __getattr__(self, name):
304        """Get an event, if it doesn't already exist."""
305        if name.startswith("_"):
306            raise AttributeError
307        # This is only called if the attribute doesn't exist, so create the Event...
308        e = self._mkevent(name)
309        # ... and save it.
310        setattr(self, name, e)
311        # Now it exists, and we won't be called again.
312        return e
313
314
315# Not lazy because:
316# 1. Initialization of EventManager can't be much cheaper
317# 2. It's expected to be used at load time, negating any benefits of using lazy object
318events = EventManager()
319