Merge pull request #1989 from xonsh/load-events

Load events
This commit is contained in:
Anthony Scopatz 2016-11-29 13:00:28 -05:00 committed by GitHub
commit 4afd9da1f0
8 changed files with 219 additions and 37 deletions

31
Makefile Normal file
View file

@ -0,0 +1,31 @@
# Make GNU Make xonshy
SHELL=xonsh
.SHELLFLAGS=-c
.ONESHELL:
.SILENT:
# Unlike normal makefiles: executes the entire body in one go under xonsh, and doesn't echo
.PHONY: help
help:
print("""
Utility file for xonsh project. Try these targets:
* amalgamate: Generate __amalgam__.py files
* clean: Remove generated files (namely, the amalgamations)
* xonsh/ply: Pull down most recent ply
""")
xonsh/ply:
git subtree pull --prefix xonsh/ply https://github.com/dabeaz/ply.git master --squash
.PHONY: clean
clean:
find xonsh -name __amalgam__.py -delete -print
.PHONY: amalgamate
amalgamate:
sys.path.insert(0, '.')
import setup
setup.amalgamate_source()
_ = sys.path.pop(0)

14
news/load-events.rst Normal file
View file

@ -0,0 +1,14 @@
**Added:**
* Load events are now available
* New events added: ``on_post_init``, ``on_pre_cmdloop``, ``on_pre_rc``, ``on_post_rc``, ``on_ptk_create``
**Changed:** None
**Deprecated:** None
**Removed:** None
**Fixed:** None
**Security:** None

View file

@ -3,10 +3,12 @@ import inspect
import pytest
from xonsh.events import EventManager, Event, LoadEvent
@pytest.fixture
def events():
return EventManager()
def test_event_calling(events):
called = False
@ -19,6 +21,7 @@ def test_event_calling(events):
assert called == "eggs"
def test_event_returns(events):
called = 0
@ -39,6 +42,7 @@ def test_event_returns(events):
assert called == 2
assert set(vals) == {1, 2}
def test_validator(events):
called = None
@ -92,6 +96,7 @@ def test_transmogrify(events):
assert len(events.on_test) == 1
assert inspect.getdoc(events.on_test) == docstring
def test_transmogrify_by_string(events):
docstring = "Test event"
events.doc('on_test', docstring)
@ -110,6 +115,46 @@ def test_transmogrify_by_string(events):
assert len(events.on_test) == 1
assert inspect.getdoc(events.on_test) == docstring
def test_load(events):
events.transmogrify('on_test', 'LoadEvent')
called = 0
@events.on_test
def on_test():
nonlocal called
called += 1
assert called == 0
events.on_test.fire()
assert called == 1
@events.on_test
def second():
nonlocal called
called += 1
assert called == 2
def test_load_2nd_call(events):
events.transmogrify('on_test', 'LoadEvent')
called = 0
@events.on_test
def on_test():
nonlocal called
called += 1
assert called == 0
events.on_test.fire()
assert called == 1
events.on_test.fire()
assert called == 1
def test_typos(xonsh_builtins):
for name, ev in vars(xonsh_builtins.events).items():
if 'pytest' in name:

View file

@ -1,4 +0,0 @@
#!/usr/bin/env xonsh
# update the ply repo bundled with xonsh
git subtree pull --prefix xonsh/ply https://github.com/dabeaz/ply.git master --squash

View file

@ -144,7 +144,13 @@ class Event(AbstractEvent):
class LoadEvent(AbstractEvent):
"""
An event species where each handler is called exactly once, shortly after either the event is
fired or the handler is registered (whichever is later).
fired or the handler is registered (whichever is later). Additional firings are ignored.
Note: Does not support scatter/gather, due to never knowing when we have all the handlers.
Note: Maintains a strong reference to pargs/kwargs in case of the addition of future handlers.
Note: This is currently NOT thread safe.
"""
def __init__(self):
self._fired = set()
@ -167,7 +173,11 @@ class LoadEvent(AbstractEvent):
This has no effect if the element is already present.
"""
self._fired.add(item)
if self._hasfired:
self._call(item)
self._fired.add(item)
else:
self._unfired.add(item)
def discard(self, item):
"""
@ -178,8 +188,22 @@ class LoadEvent(AbstractEvent):
self._fired.discard(item)
self._unfired.discard(item)
def _call(self, handler):
try:
handler(*self._pargs, **self._kwargs)
except Exception:
print_exception("Exception raised in event handler; ignored.")
def fire(self, *pargs, **kwargs):
raise NotImplementedError("See #1550")
if self._hasfired:
return
self._pargs = pargs
self._kwargs = kwargs
while self._unfired:
handler = self._unfired.pop()
self._call(handler)
self._hasfired = True
return () # Entirely for API compatibility
class EventManager:

View file

@ -19,6 +19,57 @@ from xonsh.codecache import run_script_with_cache, run_code_with_cache
from xonsh.xonfig import xonfig_main
from xonsh.lazyimps import pygments, pyghooks
from xonsh.imphooks import install_hook
from xonsh.events import events
events.transmogrify('on_post_init', 'LoadEvent')
events.doc('on_post_init', """
on_post_init() -> None
Fired after all initialization is finished and we're ready to do work.
NOTE: This is fired before the wizard is automatically started.
""")
events.transmogrify('on_exit', 'LoadEvent')
events.doc('on_exit', """
on_exit() -> None
Fired after all commands have been executed, before tear-down occurs.
NOTE: All the caveats of the atexit module also apply to this event.
""")
events.transmogrify('on_pre_cmdloop', 'LoadEvent')
events.doc('on_pre_cmdloop', """
on_pre_cmdloop() -> None
Fired just before the command loop is started, if it is.
""")
events.transmogrify('on_post_cmdloop', 'LoadEvent')
events.doc('on_post_cmdloop', """
on_post_cmdloop() -> None
Fired just after the command loop finishes, if it is.
NOTE: All the caveats of the atexit module also apply to this event.
""")
events.transmogrify('on_pre_rc', 'LoadEvent')
events.doc('on_pre_rc', """
on_pre_rc() -> None
Fired just before rc files are loaded, if they are.
""")
events.transmogrify('on_post_rc', 'LoadEvent')
events.doc('on_post_rc', """
on_post_rc() -> None
Fired just before rc files are loaded, if they are.
""")
def get_setproctitle():
@ -206,38 +257,46 @@ def main(argv=None):
if argv is None:
argv = sys.argv[1:]
args = premain(argv)
events.on_post_init.fire()
env = builtins.__xonsh_env__
shell = builtins.__xonsh_shell__
if args.mode == XonshMode.interactive:
# enter the shell
env['XONSH_INTERACTIVE'] = True
ignore_sigtstp()
if (env['XONSH_INTERACTIVE'] and
not env['LOADED_CONFIG'] and
not any(os.path.isfile(i) for i in env['XONSHRC'])):
print('Could not find xonsh configuration or run control files.',
file=sys.stderr)
xonfig_main(['wizard', '--confirm'])
shell.shell.cmdloop()
elif args.mode == XonshMode.single_command:
# run a single command and exit
run_code_with_cache(args.command.lstrip(), shell.execer, mode='single')
elif args.mode == XonshMode.script_from_file:
# run a script contained in a file
path = os.path.abspath(os.path.expanduser(args.file))
if os.path.isfile(path):
sys.argv = [args.file] + args.args
env['ARGS'] = sys.argv[:] # $ARGS is not sys.argv
env['XONSH_SOURCE'] = path
run_script_with_cache(args.file, shell.execer, glb=shell.ctx,
loc=None, mode='exec')
else:
print('xonsh: {0}: No such file or directory.'.format(args.file))
elif args.mode == XonshMode.script_from_stdin:
# run a script given on stdin
code = sys.stdin.read()
run_code_with_cache(code, shell.execer, glb=shell.ctx, loc=None,
mode='exec')
try:
if args.mode == XonshMode.interactive:
# enter the shell
env['XONSH_INTERACTIVE'] = True
ignore_sigtstp()
if (env['XONSH_INTERACTIVE'] and
not env['LOADED_CONFIG'] and
not any(os.path.isfile(i) for i in env['XONSHRC'])):
print('Could not find xonsh configuration or run control files.',
file=sys.stderr)
xonfig_main(['wizard', '--confirm'])
events.on_pre_cmdloop.fire()
try:
shell.shell.cmdloop()
finally:
events.on_post_cmdloop.fire()
elif args.mode == XonshMode.single_command:
# run a single command and exit
run_code_with_cache(args.command.lstrip(), shell.execer, mode='single')
elif args.mode == XonshMode.script_from_file:
# run a script contained in a file
path = os.path.abspath(os.path.expanduser(args.file))
if os.path.isfile(path):
sys.argv = [args.file] + args.args
env['ARGS'] = sys.argv[:] # $ARGS is not sys.argv
env['XONSH_SOURCE'] = path
run_script_with_cache(args.file, shell.execer, glb=shell.ctx,
loc=None, mode='exec')
else:
print('xonsh: {0}: No such file or directory.'.format(args.file))
elif args.mode == XonshMode.script_from_stdin:
# run a script given on stdin
code = sys.stdin.read()
run_code_with_cache(code, shell.execer, glb=shell.ctx, loc=None,
mode='exec')
finally:
events.on_exit.fire()
postmain(args)

View file

@ -20,6 +20,15 @@ from xonsh.ptk.completer import PromptToolkitCompleter
from xonsh.ptk.history import PromptToolkitHistory
from xonsh.ptk.key_bindings import load_xonsh_bindings
from xonsh.ptk.shortcuts import Prompter
from xonsh.events import events
events.transmogrify('on_ptk_create', 'LoadEvent')
events.doc('on_ptk_create', """
on_ptk_create(prompter: Prompter, history: PromptToolkitHistory, completer: PromptToolkitCompleter, bindings: KeyBindingManager) ->
Fired after prompt toolkit has been initialized
""")
class PromptToolkitShell(BaseShell):
@ -37,6 +46,8 @@ class PromptToolkitShell(BaseShell):
}
self.key_bindings_manager = KeyBindingManager(**key_bindings_manager_args)
load_xonsh_bindings(self.key_bindings_manager)
# This assumes that PromptToolkitShell is a singleton
events.on_ptk_create.fire(self.prompter, self.history, self.pt_completer, self.key_bindings_manager)
def singleline(self, store_in_history=True, auto_suggest=None,
enable_history_search=True, multiline=True, **kwargs):

View file

@ -148,5 +148,7 @@ class Shell(object):
# load run control files
env = builtins.__xonsh_env__
rc = env.get('XONSHRC') if rc is None else rc
events.on_pre_rc.fire()
self.ctx.update(xonshrc_context(rcfiles=rc, execer=self.execer, initial=self.ctx))
events.on_post_rc.fire()
self.ctx['__name__'] = '__main__'