From 18833db0dde6c5c7087227cd531e155f12b0d7a5 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 28 Nov 2016 18:49:17 -0500 Subject: [PATCH 01/10] Add makefile to aggregate commands --- Makefile | 16 ++++++++++++++++ update_ply.xsh | 4 ---- 2 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 Makefile delete mode 100755 update_ply.xsh diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..03b5c309b --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +# Make GNU Make xonshy +SHELL=xonsh +.SHELLFLAGS=-c +.ONESHELL: + +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: + python3 amalgamate.py xonsh \ No newline at end of file diff --git a/update_ply.xsh b/update_ply.xsh deleted file mode 100755 index b011675b3..000000000 --- a/update_ply.xsh +++ /dev/null @@ -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 From dbe14a2673f3b40cbce09d0a69bef21fe8cb58ab Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 28 Nov 2016 18:57:41 -0500 Subject: [PATCH 02/10] Actually implement LoadEvent --- tests/test_events.py | 45 ++++++++++++++++++++++++++++++++++++++++++++ xonsh/events.py | 27 ++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/tests/test_events.py b/tests/test_events.py index d425c4aa3..96644bc65 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -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: diff --git a/xonsh/events.py b/xonsh/events.py index 6601ee4c7..66913c4a5 100644 --- a/xonsh/events.py +++ b/xonsh/events.py @@ -145,7 +145,12 @@ 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). + + 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() self._unfired = set() @@ -167,7 +172,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 +187,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: From d60aca2c5c90e84fc12ed3ef4e83e12f4b0adbe4 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 28 Nov 2016 19:05:43 -0500 Subject: [PATCH 03/10] Add on_ptk_create event --- xonsh/ptk/shell.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/xonsh/ptk/shell.py b/xonsh/ptk/shell.py index c00e7770a..de986ccab 100644 --- a/xonsh/ptk/shell.py +++ b/xonsh/ptk/shell.py @@ -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): From f5959927a350e26d139683e1800959b98b36a0dc Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 28 Nov 2016 19:20:28 -0500 Subject: [PATCH 04/10] Add a collection of init-related events --- xonsh/main.py | 34 ++++++++++++++++++++++++++++++++++ xonsh/shell.py | 2 ++ 2 files changed, 36 insertions(+) diff --git a/xonsh/main.py b/xonsh/main.py index 544411c83..c2a5f391c 100644 --- a/xonsh/main.py +++ b/xonsh/main.py @@ -19,6 +19,39 @@ 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_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_pre_rc', 'LoadEvent') +events.doc('on_pre_rc', """ +on_pre_rc() -> None + +Fired just before xonshrc is loaded, if it is. +""") + +events.transmogrify('on_post_rc', 'LoadEvent') +events.doc('on_post_rc', """ +on_post_rc() -> None + +Fired just before xonshrc is loaded, if it is. +""") def get_setproctitle(): @@ -206,6 +239,7 @@ 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: diff --git a/xonsh/shell.py b/xonsh/shell.py index 1727c1ca4..bbe1d9a86 100644 --- a/xonsh/shell.py +++ b/xonsh/shell.py @@ -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__' From 04521fe244501a575348920ba7fddf8b8a574bec Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 28 Nov 2016 19:25:24 -0500 Subject: [PATCH 05/10] Actually fire on_pre_cmdloop >.> --- xonsh/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xonsh/main.py b/xonsh/main.py index c2a5f391c..2f4acfcbb 100644 --- a/xonsh/main.py +++ b/xonsh/main.py @@ -252,6 +252,7 @@ def main(argv=None): print('Could not find xonsh configuration or run control files.', file=sys.stderr) xonfig_main(['wizard', '--confirm']) + events.on_pre_cmdloop.fire() shell.shell.cmdloop() elif args.mode == XonshMode.single_command: # run a single command and exit From af2bf1851cedb0b4eec20cd8bf60d5445b008a6a Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 28 Nov 2016 19:28:39 -0500 Subject: [PATCH 06/10] Add news item --- news/load-events.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 news/load-events.rst diff --git a/news/load-events.rst b/news/load-events.rst new file mode 100644 index 000000000..ccd59b870 --- /dev/null +++ b/news/load-events.rst @@ -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 From b558b3c4d20fabf36b9af9c4474e28c473418a73 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 28 Nov 2016 19:50:56 -0500 Subject: [PATCH 07/10] oops, rst --- news/load-events.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/load-events.rst b/news/load-events.rst index ccd59b870..74165663f 100644 --- a/news/load-events.rst +++ b/news/load-events.rst @@ -1,7 +1,7 @@ **Added:** * Load events are now available -* New events added: `on_post_init`, `on_pre_cmdloop`, `on_pre_rc`, `on_post_rc`, `on_ptk_create` +* New events added: ``on_post_init``, ``on_pre_cmdloop``, ``on_pre_rc``, ``on_post_rc``, ``on_ptk_create`` **Changed:** None From 717874baa1a1617693bea5ec947803ba0abc1f1f Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 28 Nov 2016 21:08:32 -0500 Subject: [PATCH 08/10] Fix amalgamation command to be more complete. Also add help and be less verbose. --- Makefile | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 03b5c309b..fcc0c6181 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,18 @@ 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 @@ -13,4 +25,7 @@ clean: .PHONY: amalgamate amalgamate: - python3 amalgamate.py xonsh \ No newline at end of file + sys.path.insert(0, '.') + import setup + setup.amalgamate_source() + _ = sys.path.pop(0) From dfdc971c12bd902c6bf284c221029af4b609291f Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 28 Nov 2016 21:39:36 -0500 Subject: [PATCH 09/10] Expand and correct documentation. --- xonsh/events.py | 5 +++-- xonsh/main.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/xonsh/events.py b/xonsh/events.py index 66913c4a5..b31478ff7 100644 --- a/xonsh/events.py +++ b/xonsh/events.py @@ -144,13 +144,14 @@ 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. """ - # NOTE: This is currently NOT THREAD SAFE. def __init__(self): self._fired = set() self._unfired = set() diff --git a/xonsh/main.py b/xonsh/main.py index 2f4acfcbb..32900a05a 100644 --- a/xonsh/main.py +++ b/xonsh/main.py @@ -43,14 +43,14 @@ events.transmogrify('on_pre_rc', 'LoadEvent') events.doc('on_pre_rc', """ on_pre_rc() -> None -Fired just before xonshrc is loaded, if it is. +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 xonshrc is loaded, if it is. +Fired just before rc files are loaded, if they are. """) From 19b5c5fcd5be9de8dcbd8701c60a80fe82e955e1 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Mon, 28 Nov 2016 22:01:01 -0500 Subject: [PATCH 10/10] Add events for cleanup. --- xonsh/main.py | 88 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/xonsh/main.py b/xonsh/main.py index 32900a05a..945194c19 100644 --- a/xonsh/main.py +++ b/xonsh/main.py @@ -31,13 +31,31 @@ 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. +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', """ @@ -242,37 +260,43 @@ def main(argv=None): 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']) - events.on_pre_cmdloop.fire() - 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)