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