Refactor everything.

(Maybe I should have broken this up better?)
This commit is contained in:
Jamie Bliss 2016-08-23 23:44:05 -04:00
parent 2ffafe2ea6
commit 1c1122b742
3 changed files with 126 additions and 103 deletions

View file

@ -1,71 +1,47 @@
"""Event tests"""
from xonsh.events import Events
import pytest
from xonsh.events import EventManager
def test_calling():
e = Events()
e.on_test.doc("Test event")
@pytest.fixture
def events():
return EventManager()
def test_event_calling(events):
called = False
@e.on_test
@events.on_test
def _(spam):
nonlocal called
called = spam
e.on_test.fire("eggs")
events.on_test.fire("eggs")
assert called == "eggs"
def test_until_true():
e = Events()
e.on_test.doc("Test event")
def test_event_returns(events):
called = 0
@e.test
@events.on_test
def on_test():
nonlocal called
called += 1
return True
return 1
@e.on_test
@events.on_test
def second():
nonlocal called
called += 1
return True
return 2
e.on_test.until_true()
vals = events.on_test.fire()
assert called == 1
assert called == 2
assert set(vals) == {1, 2}
def test_until_false():
e = Events()
e.on_test.doc("Test event")
def test_validator(events):
called = None
called = 0
@e.on_test
def first():
nonlocal called
called += 1
return False
@e.on_test
def second():
nonlocal called
called += 1
return False
e.on_test.until_false()
assert called == 1
def test_validator():
e = Events()
e.on_test.doc("Test event")
called = 0
@e.on_test
@events.on_test
def first(n):
nonlocal called
called += 1
@ -75,23 +51,24 @@ def test_validator():
def v(n):
return n == 'spam'
@e.on_test
@events.on_test
def second(n):
nonlocal called
called += 1
return False
e.on_test.fire('egg')
called = 0
events.on_test.fire('egg')
assert called == 1
called = 0
e.on_test.fire('spam')
events.on_test.fire('spam')
assert called == 2
def test_eventdoc():
def test_eventdoc(events):
docstring = "Test event"
e = Events()
e.on_test.doc(docstring)
events.doc('on_test', docstring)
import inspect
assert inspect.getdoc(e.on_test) == docstring
assert inspect.getdoc(events.on_test) == docstring

View file

@ -35,7 +35,7 @@ from xonsh.tools import (
XonshCalledProcessError, XonshBlockError
)
from xonsh.commands_cache import CommandsCache
from xonsh.events import Events
from xonsh.events import events
import xonsh.completers.init
@ -715,7 +715,7 @@ def load_builtins(execer=None, config=None, login=False, ctx=None):
builtins.__xonsh_ensure_list_of_strs__ = ensure_list_of_strs
builtins.__xonsh_list_of_strs_or_callables__ = list_of_strs_or_callables
builtins.__xonsh_completers__ = xonsh.completers.init.default_completers()
builtins.__xonsh_events__ = Events()
builtins.__xonsh_events__ = events
# public built-ins
builtins.XonshError = XonshError
builtins.XonshBlockError = XonshBlockError

View file

@ -7,22 +7,14 @@ The best way to "declare" an event is something like::
__xonsh_events__.on_spam.doc("Comes with eggs")
"""
import abc
import collections.abc
import traceback
import sys
class Event(set):
"""
A given event that handlers can register against.
Acts as a ``set`` for registered handlers.
Note that ordering is never guaranteed.
"""
@classmethod
def doc(cls, text):
cls.__doc__ = text
def __call__(self, func):
class AbstractEvent(collections.abc.MutableSet, abc.ABC):
def __call__(self, handler):
"""
Registers a handler. It's suggested to use this as a decorator.
@ -33,60 +25,86 @@ class Event(set):
at all.
"""
# Using Pythons "private" munging to minimize hypothetical collisions
func.__validator = None
self.add(func)
handler.__validator = None
self.add(handler)
def validator(vfunc):
"""
Adds a validator function to a handler to limit when it is considered.
"""
func.__validator = vfunc
func.validator = validator
handler.__validator = vfunc
handler.validator = validator
return func
return handler
def calleach(self, *pargs, **kwargs):
def _filterhandlers(self, *pargs, **kwargs):
"""
The core handler caller that all others build on.
This works as a generator. Each handler is called in turn and its
results are yielded.
If the generator is interupted, no further handlers are called.
Helper method for implementing classes. Generates the handlers that pass validation.
"""
for handler in self:
if handler.__validator is not None and not handler.__validator(*pargs, **kwargs):
continue
yield handler(*pargs, **kwargs)
yield handler
@abc.abstractmethod
def fire(self, *pargs, **kwargs):
"""
Fires an event, calling registered handlers with the given arguments.
"""
class Event(AbstractEvent):
"""
A given event that handlers can register against.
Acts as a ``set`` for registered handlers.
Note that ordering is never guaranteed.
"""
# Wish I could just pull from set...
def __init__(self):
self._handlers = set()
def __len__(self):
return len(self._handlers)
def __contains__(self, item):
return item in self._handlers
def __iter__(self):
yield from self._handlers
def add(self, item):
return self._handlers.add(item)
def discard(self, item):
return self._handlers.discard(item)
def fire(self, *pargs, **kwargs):
"""
The simplest use case: Calls each handler in turn with the provided
arguments and ignore the return values.
Fires each event, returning a non-unique iterable of the results.
"""
for _ in self.calleach(*pargs, **kwargs):
pass
def until_true(self, *pargs, **kwargs):
"""
Calls each handler until one returns something truthy.
Returns that truthy value.
"""
for rv in self.calleach(*pargs, **kwargs):
if rv:
return rv
def until_false(self, *pargs, **kwargs):
"""
Calls each handler until one returns something falsey.
"""
for rv in self.calleach(*pargs, **kwargs):
if not rv:
return rv
vals = []
for handler in self._filterhandlers(*pargs, **kwargs):
try:
rv = handler(*pargs, **kwargs)
except Exception:
print("Exception raised in event handler; ignored.", file=sys.stderr)
traceback.print_exc()
else:
vals.append(rv)
return vals
class Events:
class LoadEvent(Event):
"""
A kind of event in which each handler is called exactly once.
"""
def __call__(self, *pargs, **kwargs):
raise NotImplementedError("See #1550")
class EventManager:
"""
Container for all events in a system.
@ -95,7 +113,35 @@ class Events:
Each event is just an attribute. They're created dynamically on first use.
"""
def doc(self, name, docstring):
"""
Applies a docstring to an event.
"""
type(getattr(self, name)).__doc__ = docstring
def transmogrify(self, name, klass):
"""
Converts an event from one species to another.
Please note: Some species may do special things with handlers. This is lost.
"""
if isinstance(klass, str):
klass = globals()[klass]
if not issubclass(klass, AbstractEvent):
raise ValueError("Invalid event class; must be a subclass of AbstractEvent")
oldevent = getattr(self, name)
newevent = type(name, (klass,), {'__doc__': type(oldevent).__doc__})()
setattr(self, name, newevent)
for handler in oldevent:
newevent.add(handler)
def __getattr__(self, name):
e = type(name, (Event,), {'__doc__': None})()
setattr(self, name, e)
return e
events = EventManager()