From 85bfacfd2213705f64c57a06d36ab9759b710c34 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Tue, 22 Dec 2015 17:30:58 +0000 Subject: [PATCH 01/36] Initial code for new InteractiveShell subclass using prompt_toolkit --- IPython/terminal/ptshell.py | 64 +++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 IPython/terminal/ptshell.py diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py new file mode 100644 index 00000000000..f059f7055ff --- /dev/null +++ b/IPython/terminal/ptshell.py @@ -0,0 +1,64 @@ +from IPython.core.interactiveshell import InteractiveShell + +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.shortcuts import create_prompt_layout +from prompt_toolkit.filters import Condition +from prompt_toolkit.interface import AcceptAction, Application, CommandLineInterface +from prompt_toolkit.layout.lexers import PygmentsLexer + +from pygments.lexers import Python3Lexer +from pygments.token import Token + + +class PTInteractiveShell(InteractiveShell): + def _multiline(self, cli): + doc = cli.current_buffer.document + if not doc.on_last_line: + cli.run_in_terminal(lambda: print('Not on last line')) + return False + status, indent = self.input_splitter.check_complete(doc.text) + return status == 'incomplete' + + def _multiline2(self): + return self._multiline(self.pt_cli) + + pt_cli = None + + def get_prompt_tokens(self, cli): + return [ + (Token.Prompt, 'In ['), + (Token.Prompt, str(self.execution_count)), + (Token.Prompt, ']: '), + ] + + + def init_prompt_toolkit_cli(self): + layout = create_prompt_layout( + get_prompt_tokens=self.get_prompt_tokens, + lexer=PygmentsLexer(Python3Lexer), + multiline=Condition(self._multiline), + ) + buffer = Buffer( + is_multiline=Condition(self._multiline2), + accept_action=AcceptAction.RETURN_DOCUMENT, + ) + app = Application(layout=layout, buffer=buffer) + self.pt_cli = CommandLineInterface(app) + + def __init__(self, *args, **kwargs): + super(PTInteractiveShell, self).__init__(*args, **kwargs) + self.init_prompt_toolkit_cli() + self.keep_running = True + + def ask_exit(self): + self.keep_running = False + + def interact(self): + while self.keep_running: + document = self.pt_cli.run() + if document: + self.run_cell(document.text) + + +if __name__ == '__main__': + PTInteractiveShell().interact() From a9b0c6ca598179c4c07291f3cc4111a021d92078 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 23 Dec 2015 16:44:43 +0000 Subject: [PATCH 02/36] Refine multiline behaviour --- IPython/terminal/ptshell.py | 48 ++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index f059f7055ff..bf1c6ed2730 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -1,9 +1,9 @@ from IPython.core.interactiveshell import InteractiveShell -from prompt_toolkit.buffer import Buffer -from prompt_toolkit.shortcuts import create_prompt_layout -from prompt_toolkit.filters import Condition -from prompt_toolkit.interface import AcceptAction, Application, CommandLineInterface +from prompt_toolkit.shortcuts import create_prompt_application +from prompt_toolkit.interface import CommandLineInterface +from prompt_toolkit.key_binding.manager import KeyBindingManager +from prompt_toolkit.keys import Keys from prompt_toolkit.layout.lexers import PygmentsLexer from pygments.lexers import Python3Lexer @@ -11,17 +11,6 @@ class PTInteractiveShell(InteractiveShell): - def _multiline(self, cli): - doc = cli.current_buffer.document - if not doc.on_last_line: - cli.run_in_terminal(lambda: print('Not on last line')) - return False - status, indent = self.input_splitter.check_complete(doc.text) - return status == 'incomplete' - - def _multiline2(self): - return self._multiline(self.pt_cli) - pt_cli = None def get_prompt_tokens(self, cli): @@ -33,16 +22,27 @@ def get_prompt_tokens(self, cli): def init_prompt_toolkit_cli(self): - layout = create_prompt_layout( - get_prompt_tokens=self.get_prompt_tokens, - lexer=PygmentsLexer(Python3Lexer), - multiline=Condition(self._multiline), - ) - buffer = Buffer( - is_multiline=Condition(self._multiline2), - accept_action=AcceptAction.RETURN_DOCUMENT, + kbmanager = KeyBindingManager.for_prompt() + @kbmanager.registry.add_binding(Keys.ControlJ) # Ctrl+J == Enter, seemingly + def _(event): + b = event.current_buffer + if not b.document.on_last_line: + b.newline() + return + + status, indent = self.input_splitter.check_complete(b.document.text) + + if (status != 'incomplete') and b.accept_action.is_returnable: + b.accept_action.validate_and_handle(event.cli, b) + else: + b.insert_text('\n' + (' ' * indent)) + + app = create_prompt_application(multiline=True, + lexer=PygmentsLexer(Python3Lexer), + get_prompt_tokens=self.get_prompt_tokens, + key_bindings_registry=kbmanager.registry, ) - app = Application(layout=layout, buffer=buffer) + self.pt_cli = CommandLineInterface(app) def __init__(self, *args, **kwargs): From f52460428307785b7c5440cb62100f85fd022269 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 23 Dec 2015 16:46:01 +0000 Subject: [PATCH 03/36] Store history so prompt number increases --- IPython/terminal/ptshell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index bf1c6ed2730..cfbb8c5f273 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -57,7 +57,7 @@ def interact(self): while self.keep_running: document = self.pt_cli.run() if document: - self.run_cell(document.text) + self.run_cell(document.text, store_history=True) if __name__ == '__main__': From 239389ae8394bc72f4ee3131880d8318729b343c Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 8 Jan 2016 13:05:38 +0000 Subject: [PATCH 04/36] Hook up command history and populate it from IPython's history DB --- IPython/terminal/ptshell.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index cfbb8c5f273..d78cea338f1 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -1,5 +1,6 @@ from IPython.core.interactiveshell import InteractiveShell +from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.shortcuts import create_prompt_application from prompt_toolkit.interface import CommandLineInterface from prompt_toolkit.key_binding.manager import KeyBindingManager @@ -35,12 +36,23 @@ def _(event): if (status != 'incomplete') and b.accept_action.is_returnable: b.accept_action.validate_and_handle(event.cli, b) else: - b.insert_text('\n' + (' ' * indent)) + b.insert_text('\n' + (' ' * (indent or 0))) + + # Pre-populate history from IPython's history database + history = InMemoryHistory() + last_cell = u"" + for _, _, cell in self.history_manager.get_tail(self.history_load_length, + include_latest=True): + # Ignore blank lines and consecutive duplicates + cell = cell.rstrip() + if cell and (cell != last_cell): + history.append(cell) app = create_prompt_application(multiline=True, lexer=PygmentsLexer(Python3Lexer), get_prompt_tokens=self.get_prompt_tokens, key_bindings_registry=kbmanager.registry, + history=history, ) self.pt_cli = CommandLineInterface(app) From e870f74a0ccace04051f0687767c3758b3274b39 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 8 Jan 2016 13:18:39 +0000 Subject: [PATCH 05/36] Do sensible things on Ctrl-C and Ctrl-D --- IPython/terminal/ptshell.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index d78cea338f1..49884951f74 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -38,6 +38,10 @@ def _(event): else: b.insert_text('\n' + (' ' * (indent or 0))) + @kbmanager.registry.add_binding(Keys.ControlC) + def _(event): + event.current_buffer.reset() + # Pre-populate history from IPython's history database history = InMemoryHistory() last_cell = u"" @@ -67,9 +71,15 @@ def ask_exit(self): def interact(self): while self.keep_running: - document = self.pt_cli.run() - if document: - self.run_cell(document.text, store_history=True) + try: + document = self.pt_cli.run() + except EOFError: + if self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'): + self.ask_exit() + + else: + if document: + self.run_cell(document.text, store_history=True) if __name__ == '__main__': From db2ff6f587b9d7ee1036832c68205df20c7ff9cc Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 8 Jan 2016 14:08:17 +0000 Subject: [PATCH 06/36] Add completion support --- IPython/terminal/ptshell.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 49884951f74..734b5c9f6d5 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -1,5 +1,6 @@ from IPython.core.interactiveshell import InteractiveShell +from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.shortcuts import create_prompt_application from prompt_toolkit.interface import CommandLineInterface @@ -11,6 +12,21 @@ from pygments.token import Token +class IPythonPTCompleter(Completer): + """Adaptor to provide IPython completions to prompt_toolkit""" + def __init__(self, ipy_completer): + self.ipy_completer = ipy_completer + + def get_completions(self, document, complete_event): + used, matches = self.ipy_completer.complete( + line_buffer=document.current_line, + cursor_pos=document.cursor_position_col + ) + start_pos = -len(used) + for m in matches: + yield Completion(m, start_position=start_pos) + + class PTInteractiveShell(InteractiveShell): pt_cli = None @@ -57,6 +73,7 @@ def _(event): get_prompt_tokens=self.get_prompt_tokens, key_bindings_registry=kbmanager.registry, history=history, + completer=IPythonPTCompleter(self.Completer), ) self.pt_cli = CommandLineInterface(app) @@ -83,4 +100,4 @@ def interact(self): if __name__ == '__main__': - PTInteractiveShell().interact() + PTInteractiveShell.instance().interact() From ba5734e80ca6c629ee5a6b90f275acaf04edd59d Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 8 Jan 2016 14:20:34 +0000 Subject: [PATCH 07/36] Improve order of completions --- IPython/core/completer.py | 56 ++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 4c793b43c17..7953dbb065a 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -170,41 +170,38 @@ def compress_user(path, tilde_expand, tilde_val): -def penalize_magics_key(word): - """key for sorting that penalizes magic commands in the ordering +def completions_sorting_key(word): + """key for sorting completions - Normal words are left alone. - - Magic commands have the initial % moved to the end, e.g. - %matplotlib is transformed as follows: - - %matplotlib -> matplotlib% - - [The choice of the final % is arbitrary.] - - Since "matplotlib" < "matplotlib%" as strings, - "timeit" will appear before the magic "%timeit" in the ordering - - For consistency, move "%%" to the end, so cell magics appear *after* - line magics with the same name. - - A check is performed that there are no other "%" in the string; - if there are, then the string is not a magic command and is left unchanged. + This does several things: + - Lowercase all completions, so they are sorted alphabetically with + upper and lower case words mingled + - Demote any completions starting with underscores to the end + - Insert any %magic and %%cellmagic completions in the alphabetical order + by their name """ + # Case insensitive sort + word = word.lower() - # Move any % signs from start to end of the key - # provided there are no others elsewhere in the string + prio1, prio2 = 0, 0 - if word[:2] == "%%": - if not "%" in word[2:]: - return word[2:] + "%%" + if word.startswith('__'): + prio1 = 2 + elif word.startswith('_'): + prio1 = 1 - if word[:1] == "%": + if word.startswith('%%'): + # If there's another % in there, this is something else, so leave it alone + if not "%" in word[2:]: + word = word[2:] + prio2 = 2 + elif word.startswith('%'): if not "%" in word[1:]: - return word[1:] + "%" - - return word + word = word[1:] + prio2 = 1 + + return prio1, word, prio2 @undoc @@ -1206,8 +1203,7 @@ def complete(self, text=None, line_buffer=None, cursor_pos=None): # simply collapse the dict into a list for readline, but we'd have # richer completion semantics in other evironments. - # use penalize_magics_key to put magics after variables with same name - self.matches = sorted(set(self.matches), key=penalize_magics_key) + self.matches = sorted(set(self.matches), key=completions_sorting_key) #io.rprint('COMP TEXT, MATCHES: %r, %r' % (text, self.matches)) # dbg return text, self.matches From fbb4e634fed001b43a71d76d38012def5ee1a1c7 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 8 Jan 2016 14:54:50 +0000 Subject: [PATCH 08/36] Nicer default colours --- IPython/terminal/ptshell.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 734b5c9f6d5..4dee5d19257 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -7,6 +7,7 @@ from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.keys import Keys from prompt_toolkit.layout.lexers import PygmentsLexer +from prompt_toolkit.styles import PygmentsStyle from pygments.lexers import Python3Lexer from pygments.token import Token @@ -33,7 +34,7 @@ class PTInteractiveShell(InteractiveShell): def get_prompt_tokens(self, cli): return [ (Token.Prompt, 'In ['), - (Token.Prompt, str(self.execution_count)), + (Token.PromptNum, str(self.execution_count)), (Token.Prompt, ']: '), ] @@ -68,12 +69,20 @@ def _(event): if cell and (cell != last_cell): history.append(cell) + style = PygmentsStyle.from_defaults({ + Token.Prompt: '#009900', + Token.PromptNum: '#00ff00 bold', + Token.Number: '#007700', + Token.Operator: '#bbbbbb', + }) + app = create_prompt_application(multiline=True, lexer=PygmentsLexer(Python3Lexer), get_prompt_tokens=self.get_prompt_tokens, key_bindings_registry=kbmanager.registry, history=history, completer=IPythonPTCompleter(self.Completer), + style=style, ) self.pt_cli = CommandLineInterface(app) From 342b17513a11fe906ff0a25b39ce7dee8beb0c64 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 11 Jan 2016 14:08:58 +0000 Subject: [PATCH 09/36] Add blank line before input prompt --- IPython/terminal/ptshell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 4dee5d19257..c846638b701 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -106,6 +106,7 @@ def interact(self): else: if document: self.run_cell(document.text, store_history=True) + print(self.separate_in, end='') if __name__ == '__main__': From e778cbacc703f6204637c45748340a5ce093e7f8 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Tue, 12 Jan 2016 13:59:39 +0000 Subject: [PATCH 10/36] Turn out prompt & traceback colours on when using prompt_toolkit --- IPython/terminal/ptshell.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index c846638b701..881d76c426e 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -29,6 +29,8 @@ def get_completions(self, document, complete_event): class PTInteractiveShell(InteractiveShell): + colors_force = True + pt_cli = None def get_prompt_tokens(self, cli): From 55845333e1d253107321299801fc055b9a46e885 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Tue, 12 Jan 2016 14:06:33 +0000 Subject: [PATCH 11/36] Move printing blank line to before each input prompt --- IPython/terminal/ptshell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 881d76c426e..65d82de0e9b 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -99,6 +99,7 @@ def ask_exit(self): def interact(self): while self.keep_running: + print(self.separate_in, end='') try: document = self.pt_cli.run() except EOFError: @@ -108,7 +109,6 @@ def interact(self): else: if document: self.run_cell(document.text, store_history=True) - print(self.separate_in, end='') if __name__ == '__main__': From 4a9dcfdc275f54b18cb0a361857117f7b8a5e09c Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Tue, 12 Jan 2016 14:26:09 +0000 Subject: [PATCH 12/36] Don't try to complete on an empty line --- IPython/terminal/ptshell.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 65d82de0e9b..364ec7b1ebd 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -19,6 +19,9 @@ def __init__(self, ipy_completer): self.ipy_completer = ipy_completer def get_completions(self, document, complete_event): + if not document.current_line.strip(): + return + used, matches = self.ipy_completer.complete( line_buffer=document.current_line, cursor_pos=document.cursor_position_col From 535e24011597ff5c9c15cc423415bf259d759fbd Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Tue, 12 Jan 2016 15:45:53 +0000 Subject: [PATCH 13/36] Only use our Enter handling when the default buffer is active Fixes Ctrl-R search --- IPython/terminal/ptshell.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 364ec7b1ebd..3b5e436f28e 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -1,6 +1,8 @@ from IPython.core.interactiveshell import InteractiveShell from prompt_toolkit.completion import Completer, Completion +from prompt_toolkit.enums import DEFAULT_BUFFER +from prompt_toolkit.filters import HasFocus, HasSelection from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.shortcuts import create_prompt_application from prompt_toolkit.interface import CommandLineInterface @@ -46,7 +48,9 @@ def get_prompt_tokens(self, cli): def init_prompt_toolkit_cli(self): kbmanager = KeyBindingManager.for_prompt() - @kbmanager.registry.add_binding(Keys.ControlJ) # Ctrl+J == Enter, seemingly + # Ctrl+J == Enter, seemingly + @kbmanager.registry.add_binding(Keys.ControlJ, + filter=HasFocus(DEFAULT_BUFFER) & ~HasSelection()) def _(event): b = event.current_buffer if not b.document.on_last_line: From 904d56ef88ea9d98dff4bc43149527792f5a3b8b Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Tue, 12 Jan 2016 15:52:14 +0000 Subject: [PATCH 14/36] Enable history search with up arrow --- IPython/terminal/ptshell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 3b5e436f28e..b63c0fe448e 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -91,6 +91,7 @@ def _(event): key_bindings_registry=kbmanager.registry, history=history, completer=IPythonPTCompleter(self.Completer), + enable_history_search=True, style=style, ) From 3d5a073dae0e0976c49131d0d51ce28900b1e3d5 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Tue, 12 Jan 2016 16:30:21 +0000 Subject: [PATCH 15/36] Improve default colours for light & dark terminal backgrounds --- IPython/terminal/ptshell.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index b63c0fe448e..9cf1da8c795 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -82,7 +82,8 @@ def _(event): Token.Prompt: '#009900', Token.PromptNum: '#00ff00 bold', Token.Number: '#007700', - Token.Operator: '#bbbbbb', + Token.Operator: 'noinherit', + Token.String: '#BB6622', }) app = create_prompt_application(multiline=True, From e9ee3ec0000a5331a264337a30e5f02cea9c2723 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 14 Jan 2016 17:56:07 +0000 Subject: [PATCH 16/36] Add config option for vi mode --- IPython/terminal/ptshell.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 9cf1da8c795..36f6c3cb7f6 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -1,4 +1,5 @@ from IPython.core.interactiveshell import InteractiveShell +from traitlets import Bool from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.enums import DEFAULT_BUFFER @@ -7,6 +8,8 @@ from prompt_toolkit.shortcuts import create_prompt_application from prompt_toolkit.interface import CommandLineInterface from prompt_toolkit.key_binding.manager import KeyBindingManager +from prompt_toolkit.key_binding.vi_state import InputMode +from prompt_toolkit.key_binding.bindings.vi import ViStateFilter from prompt_toolkit.keys import Keys from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.styles import PygmentsStyle @@ -38,6 +41,10 @@ class PTInteractiveShell(InteractiveShell): pt_cli = None + vi_mode = Bool(False, config=True, + help="Use vi style keybindings at the prompt", + ) + def get_prompt_tokens(self, cli): return [ (Token.Prompt, 'In ['), @@ -47,10 +54,14 @@ def get_prompt_tokens(self, cli): def init_prompt_toolkit_cli(self): - kbmanager = KeyBindingManager.for_prompt() + kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode) + insert_mode = ViStateFilter(kbmanager.get_vi_state, InputMode.INSERT) # Ctrl+J == Enter, seemingly @kbmanager.registry.add_binding(Keys.ControlJ, - filter=HasFocus(DEFAULT_BUFFER) & ~HasSelection()) + filter=(HasFocus(DEFAULT_BUFFER) + & ~HasSelection() + & insert_mode + )) def _(event): b = event.current_buffer if not b.document.on_last_line: From cde57a32a66a50637361122a0e190d7c65544058 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 14 Jan 2016 18:17:43 +0000 Subject: [PATCH 17/36] Config options for syntax higlighting style --- IPython/terminal/ptshell.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 36f6c3cb7f6..64e6659a9ab 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -1,5 +1,5 @@ from IPython.core.interactiveshell import InteractiveShell -from traitlets import Bool +from traitlets import Bool, Unicode, Dict from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.enums import DEFAULT_BUFFER @@ -14,6 +14,7 @@ from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.styles import PygmentsStyle +from pygments.styles import get_style_by_name from pygments.lexers import Python3Lexer from pygments.token import Token @@ -45,6 +46,14 @@ class PTInteractiveShell(InteractiveShell): help="Use vi style keybindings at the prompt", ) + highlighting_style = Unicode('', config=True, + help="The name of a Pygments style to use for syntax highlighting" + ) + + highlighting_style_overrides = Dict(config=True, + help="Override highlighting format for specific tokens" + ) + def get_prompt_tokens(self, cli): return [ (Token.Prompt, 'In ['), @@ -89,13 +98,22 @@ def _(event): if cell and (cell != last_cell): history.append(cell) - style = PygmentsStyle.from_defaults({ + style_overrides = { Token.Prompt: '#009900', Token.PromptNum: '#00ff00 bold', - Token.Number: '#007700', - Token.Operator: 'noinherit', - Token.String: '#BB6622', - }) + } + if self.highlighting_style: + style_cls = get_style_by_name(self.highlighting_style) + else: + style_cls = get_style_by_name('default') + style_overrides.update({ + Token.Number: '#007700', + Token.Operator: 'noinherit', + Token.String: '#BB6622', + }) + style_overrides.update(self.highlighting_style_overrides) + style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls, + style_dict=style_overrides) app = create_prompt_application(multiline=True, lexer=PygmentsLexer(Python3Lexer), From 0cb9e90494c27bb2aee1ff046348a0f2eeeb44c3 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 14 Jan 2016 18:26:19 +0000 Subject: [PATCH 18/36] Python 2.7 support & highlighting in prompt_toolkit interface --- IPython/terminal/ptshell.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 64e6659a9ab..94a073f67f6 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -1,4 +1,8 @@ +"""IPython terminal interface using prompt_toolkit in place of readline""" +from __future__ import print_function + from IPython.core.interactiveshell import InteractiveShell +from IPython.utils.py3compat import PY3 from traitlets import Bool, Unicode, Dict from prompt_toolkit.completion import Completer, Completion @@ -15,7 +19,7 @@ from prompt_toolkit.styles import PygmentsStyle from pygments.styles import get_style_by_name -from pygments.lexers import Python3Lexer +from pygments.lexers import Python3Lexer, PythonLexer from pygments.token import Token @@ -116,7 +120,7 @@ def _(event): style_dict=style_overrides) app = create_prompt_application(multiline=True, - lexer=PygmentsLexer(Python3Lexer), + lexer=PygmentsLexer(Python3Lexer if PY3 else PythonLexer), get_prompt_tokens=self.get_prompt_tokens, key_bindings_registry=kbmanager.registry, history=history, From 995b7940fcf75078804b44fdd9feef5c44abac6a Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Sat, 16 Jan 2016 10:55:39 +0000 Subject: [PATCH 19/36] Add continuation prompts --- IPython/terminal/ptshell.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 94a073f67f6..5742ddbe552 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -65,6 +65,11 @@ def get_prompt_tokens(self, cli): (Token.Prompt, ']: '), ] + def get_continuation_tokens(self, cli, width): + return [ + (Token.Prompt, (' ' * (width - 2)) + ': '), + ] + def init_prompt_toolkit_cli(self): kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode) @@ -122,6 +127,7 @@ def _(event): app = create_prompt_application(multiline=True, lexer=PygmentsLexer(Python3Lexer if PY3 else PythonLexer), get_prompt_tokens=self.get_prompt_tokens, + get_continuation_tokens=self.get_continuation_tokens, key_bindings_registry=kbmanager.registry, history=history, completer=IPythonPTCompleter(self.Completer), From c0fffd17e4392b1d2434182406d5245a7dc65c99 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Sat, 16 Jan 2016 17:28:14 +0000 Subject: [PATCH 20/36] Write & borrow some inputhooks for prompt_toolkit --- IPython/terminal/pt_inputhooks/__init__.py | 16 ++++ IPython/terminal/pt_inputhooks/gtk.py | 55 +++++++++++++ IPython/terminal/pt_inputhooks/gtk3.py | 12 +++ IPython/terminal/pt_inputhooks/qt.py | 11 +++ IPython/terminal/pt_inputhooks/tk.py | 93 ++++++++++++++++++++++ IPython/terminal/ptshell.py | 19 ++++- 6 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 IPython/terminal/pt_inputhooks/__init__.py create mode 100644 IPython/terminal/pt_inputhooks/gtk.py create mode 100644 IPython/terminal/pt_inputhooks/gtk3.py create mode 100644 IPython/terminal/pt_inputhooks/qt.py create mode 100644 IPython/terminal/pt_inputhooks/tk.py diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py new file mode 100644 index 00000000000..6ad691905cd --- /dev/null +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -0,0 +1,16 @@ +import importlib +import os + +aliases = { + 'qt4': 'qt' +} + +def get_inputhook_func(gui): + if gui in aliases: + return get_inputhook_func(aliases[gui]) + + if gui == 'qt5': + os.environ['QT_API'] = 'pyqt5' + + mod = importlib.import_module('IPython.terminal.pt_inputhooks.'+gui) + return mod.inputhook diff --git a/IPython/terminal/pt_inputhooks/gtk.py b/IPython/terminal/pt_inputhooks/gtk.py new file mode 100644 index 00000000000..49bfeb3ce2b --- /dev/null +++ b/IPython/terminal/pt_inputhooks/gtk.py @@ -0,0 +1,55 @@ +# Code borrowed from python-prompt-toolkit examples +# https://github.com/jonathanslenders/python-prompt-toolkit/blob/77cdcfbc7f4b4c34a9d2f9a34d422d7152f16209/examples/inputhook.py + +# Copyright (c) 2014, Jonathan Slenders +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# * Neither the name of the {organization} nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +PyGTK input hook for prompt_toolkit. + +Listens on the pipe prompt_toolkit sets up for a notification that it should +return control to the terminal event loop. +""" + +import gtk, gobject + +def inputhook(context): + """ + When the eventloop of prompt-toolkit is idle, call this inputhook. + + This will run the GTK main loop until the file descriptor + `context.fileno()` becomes ready. + + :param context: An `InputHookContext` instance. + """ + def _main_quit(*a, **kw): + gtk.main_quit() + return False + + gobject.io_add_watch(context.fileno(), gobject.IO_IN, _main_quit) + gtk.main() diff --git a/IPython/terminal/pt_inputhooks/gtk3.py b/IPython/terminal/pt_inputhooks/gtk3.py new file mode 100644 index 00000000000..5c6c545457b --- /dev/null +++ b/IPython/terminal/pt_inputhooks/gtk3.py @@ -0,0 +1,12 @@ +"""prompt_toolkit input hook for GTK 3 +""" + +from gi.repository import Gtk, GLib + +def _main_quit(*args, **kwargs): + Gtk.main_quit() + return False + +def inputhook(context): + GLib.io_add_watch(context.fileno(), GLib.IO_IN, _main_quit) + Gtk.main() diff --git a/IPython/terminal/pt_inputhooks/qt.py b/IPython/terminal/pt_inputhooks/qt.py new file mode 100644 index 00000000000..1fd4e9290f3 --- /dev/null +++ b/IPython/terminal/pt_inputhooks/qt.py @@ -0,0 +1,11 @@ +from IPython.external.qt_for_kernel import QtCore, QtGui + +def inputhook(context): + app = QtCore.QCoreApplication.instance() + if not app: + return + event_loop = QtCore.QEventLoop(app) + notifier = QtCore.QSocketNotifier(context.fileno(), QtCore.QSocketNotifier.Read) + notifier.setEnabled(True) + notifier.activated.connect(event_loop.exit) + event_loop.exec_() diff --git a/IPython/terminal/pt_inputhooks/tk.py b/IPython/terminal/pt_inputhooks/tk.py new file mode 100644 index 00000000000..24313a8396a --- /dev/null +++ b/IPython/terminal/pt_inputhooks/tk.py @@ -0,0 +1,93 @@ +# Code borrowed from ptpython +# https://github.com/jonathanslenders/ptpython/blob/86b71a89626114b18898a0af463978bdb32eeb70/ptpython/eventloop.py + +# Copyright (c) 2015, Jonathan Slenders +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# * Neither the name of the {organization} nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +Wrapper around the eventloop that gives some time to the Tkinter GUI to process +events when it's loaded and while we are waiting for input at the REPL. This +way we don't block the UI of for instance ``turtle`` and other Tk libraries. + +(Normally Tkinter registeres it's callbacks in ``PyOS_InputHook`` to integrate +in readline. ``prompt-toolkit`` doesn't understand that input hook, but this +will fix it for Tk.) +""" +import time + +import _tkinter +try: + import tkinter +except ImportError: + import Tkinter as tkinter # Python 2 + +def inputhook(inputhook_context): + """ + Inputhook for Tk. + Run the Tk eventloop until prompt-toolkit needs to process the next input. + """ + # Get the current TK application. + root = tkinter._default_root + + def wait_using_filehandler(): + """ + Run the TK eventloop until the file handler that we got from the + inputhook becomes readable. + """ + # Add a handler that sets the stop flag when `prompt-toolkit` has input + # to process. + stop = [False] + def done(*a): + stop[0] = True + + root.createfilehandler(inputhook_context.fileno(), _tkinter.READABLE, done) + + # Run the TK event loop as long as we don't receive input. + while root.dooneevent(_tkinter.ALL_EVENTS): + if stop[0]: + break + + root.deletefilehandler(inputhook_context.fileno()) + + def wait_using_polling(): + """ + Windows TK doesn't support 'createfilehandler'. + So, run the TK eventloop and poll until input is ready. + """ + while not inputhook_context.input_is_ready(): + while root.dooneevent(_tkinter.ALL_EVENTS | _tkinter.DONT_WAIT): + pass + # Sleep to make the CPU idle, but not too long, so that the UI + # stays responsive. + time.sleep(.01) + + if root is not None: + if hasattr(root, 'createfilehandler'): + wait_using_filehandler() + else: + wait_using_polling() diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 5742ddbe552..babaa0756dc 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -9,7 +9,7 @@ from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import HasFocus, HasSelection from prompt_toolkit.history import InMemoryHistory -from prompt_toolkit.shortcuts import create_prompt_application +from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop from prompt_toolkit.interface import CommandLineInterface from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.key_binding.vi_state import InputMode @@ -22,6 +22,8 @@ from pygments.lexers import Python3Lexer, PythonLexer from pygments.token import Token +from .pt_inputhooks import get_inputhook_func + class IPythonPTCompleter(Completer): """Adaptor to provide IPython completions to prompt_toolkit""" @@ -40,7 +42,6 @@ def get_completions(self, document, complete_event): for m in matches: yield Completion(m, start_position=start_pos) - class PTInteractiveShell(InteractiveShell): colors_force = True @@ -70,7 +71,6 @@ def get_continuation_tokens(self, cli, width): (Token.Prompt, (' ' * (width - 2)) + ': '), ] - def init_prompt_toolkit_cli(self): kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode) insert_mode = ViStateFilter(kbmanager.get_vi_state, InputMode.INSERT) @@ -135,7 +135,8 @@ def _(event): style=style, ) - self.pt_cli = CommandLineInterface(app) + self.pt_cli = CommandLineInterface(app, + eventloop=create_eventloop(self.inputhook)) def __init__(self, *args, **kwargs): super(PTInteractiveShell, self).__init__(*args, **kwargs) @@ -158,6 +159,16 @@ def interact(self): if document: self.run_cell(document.text, store_history=True) + _inputhook = None + def inputhook(self, context): + if self._inputhook is not None: + self._inputhook(context) + + def enable_gui(self, gui=None): + if gui: + self._inputhook = get_inputhook_func(gui) + else: + self._inputhook = None if __name__ == '__main__': PTInteractiveShell.instance().interact() From a9ca5b1b43881bbdc1d4cb4686f66617667cee9e Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 18 Jan 2016 11:06:57 +0000 Subject: [PATCH 21/36] More tweaks to highlighting colours --- IPython/terminal/ptshell.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index babaa0756dc..4e04bcdf755 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -115,10 +115,16 @@ def _(event): style_cls = get_style_by_name(self.highlighting_style) else: style_cls = get_style_by_name('default') + # The default theme needs to be visible on both a dark background + # and a light background, because we can't tell what the terminal + # looks like. These tweaks to the default theme help with that. style_overrides.update({ Token.Number: '#007700', Token.Operator: 'noinherit', Token.String: '#BB6622', + Token.Name.Function: '#2080D0', + Token.Name.Class: 'bold #2080D0', + Token.Name.Namespace: 'bold #2080D0', }) style_overrides.update(self.highlighting_style_overrides) style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls, From 48013ca5e8b0f7cf853e2b76b74b448169834f97 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 18 Jan 2016 14:24:28 +0000 Subject: [PATCH 22/36] Add prompt_toolkit input hooks for wx --- IPython/terminal/pt_inputhooks/__init__.py | 2 +- IPython/terminal/pt_inputhooks/gtk.py | 1 + IPython/terminal/pt_inputhooks/wx.py | 148 +++++++++++++++++++++ 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 IPython/terminal/pt_inputhooks/wx.py diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py index 6ad691905cd..94709ec1986 100644 --- a/IPython/terminal/pt_inputhooks/__init__.py +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -2,7 +2,7 @@ import os aliases = { - 'qt4': 'qt' + 'qt4': 'qt', } def get_inputhook_func(gui): diff --git a/IPython/terminal/pt_inputhooks/gtk.py b/IPython/terminal/pt_inputhooks/gtk.py index 49bfeb3ce2b..33600488bc1 100644 --- a/IPython/terminal/pt_inputhooks/gtk.py +++ b/IPython/terminal/pt_inputhooks/gtk.py @@ -35,6 +35,7 @@ Listens on the pipe prompt_toolkit sets up for a notification that it should return control to the terminal event loop. """ +from __future__ import absolute_import import gtk, gobject diff --git a/IPython/terminal/pt_inputhooks/wx.py b/IPython/terminal/pt_inputhooks/wx.py new file mode 100644 index 00000000000..4371b21cb47 --- /dev/null +++ b/IPython/terminal/pt_inputhooks/wx.py @@ -0,0 +1,148 @@ +"""Enable wxPython to be used interacively in prompt_toolkit +""" +from __future__ import absolute_import + +import sys +import signal +import time +from timeit import default_timer as clock +import wx + + +def inputhook_wx1(context): + """Run the wx event loop by processing pending events only. + + This approach seems to work, but its performance is not great as it + relies on having PyOS_InputHook called regularly. + """ + try: + app = wx.GetApp() + if app is not None: + assert wx.Thread_IsMain() + + # Make a temporary event loop and process system events until + # there are no more waiting, then allow idle events (which + # will also deal with pending or posted wx events.) + evtloop = wx.EventLoop() + ea = wx.EventLoopActivator(evtloop) + while evtloop.Pending(): + evtloop.Dispatch() + app.ProcessIdle() + del ea + except KeyboardInterrupt: + pass + return 0 + +class EventLoopTimer(wx.Timer): + + def __init__(self, func): + self.func = func + wx.Timer.__init__(self) + + def Notify(self): + self.func() + +class EventLoopRunner(object): + + def Run(self, time, input_is_ready): + self.input_is_ready = input_is_ready + self.evtloop = wx.EventLoop() + self.timer = EventLoopTimer(self.check_stdin) + self.timer.Start(time) + self.evtloop.Run() + + def check_stdin(self): + if self.input_is_ready(): + self.timer.Stop() + self.evtloop.Exit() + +def inputhook_wx2(context): + """Run the wx event loop, polling for stdin. + + This version runs the wx eventloop for an undetermined amount of time, + during which it periodically checks to see if anything is ready on + stdin. If anything is ready on stdin, the event loop exits. + + The argument to elr.Run controls how often the event loop looks at stdin. + This determines the responsiveness at the keyboard. A setting of 1000 + enables a user to type at most 1 char per second. I have found that a + setting of 10 gives good keyboard response. We can shorten it further, + but eventually performance would suffer from calling select/kbhit too + often. + """ + try: + app = wx.GetApp() + if app is not None: + assert wx.Thread_IsMain() + elr = EventLoopRunner() + # As this time is made shorter, keyboard response improves, but idle + # CPU load goes up. 10 ms seems like a good compromise. + elr.Run(time=10, # CHANGE time here to control polling interval + input_is_ready=context.input_is_ready) + except KeyboardInterrupt: + pass + return 0 + +def inputhook_wx3(context): + """Run the wx event loop by processing pending events only. + + This is like inputhook_wx1, but it keeps processing pending events + until stdin is ready. After processing all pending events, a call to + time.sleep is inserted. This is needed, otherwise, CPU usage is at 100%. + This sleep time should be tuned though for best performance. + """ + # We need to protect against a user pressing Control-C when IPython is + # idle and this is running. We trap KeyboardInterrupt and pass. + try: + app = wx.GetApp() + if app is not None: + assert wx.Thread_IsMain() + + # The import of wx on Linux sets the handler for signal.SIGINT + # to 0. This is a bug in wx or gtk. We fix by just setting it + # back to the Python default. + if not callable(signal.getsignal(signal.SIGINT)): + signal.signal(signal.SIGINT, signal.default_int_handler) + + evtloop = wx.EventLoop() + ea = wx.EventLoopActivator(evtloop) + t = clock() + while not context.input_is_ready(): + while evtloop.Pending(): + t = clock() + evtloop.Dispatch() + app.ProcessIdle() + # We need to sleep at this point to keep the idle CPU load + # low. However, if sleep to long, GUI response is poor. As + # a compromise, we watch how often GUI events are being processed + # and switch between a short and long sleep time. Here are some + # stats useful in helping to tune this. + # time CPU load + # 0.001 13% + # 0.005 3% + # 0.01 1.5% + # 0.05 0.5% + used_time = clock() - t + if used_time > 10.0: + # print 'Sleep for 1 s' # dbg + time.sleep(1.0) + elif used_time > 0.1: + # Few GUI events coming in, so we can sleep longer + # print 'Sleep for 0.05 s' # dbg + time.sleep(0.05) + else: + # Many GUI events coming in, so sleep only very little + time.sleep(0.001) + del ea + except KeyboardInterrupt: + pass + return 0 + +if sys.platform == 'darwin': + # On OSX, evtloop.Pending() always returns True, regardless of there being + # any events pending. As such we can't use implementations 1 or 3 of the + # inputhook as those depend on a pending/dispatch loop. + inputhook = inputhook_wx2 +else: + # This is our default implementation + inputhook = inputhook_wx3 From 1f47f59a7d4518e750009aedce596a150ad0a865 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 18 Jan 2016 14:32:38 +0000 Subject: [PATCH 23/36] Add inputhook for pyglet --- IPython/terminal/pt_inputhooks/pyglet.py | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 IPython/terminal/pt_inputhooks/pyglet.py diff --git a/IPython/terminal/pt_inputhooks/pyglet.py b/IPython/terminal/pt_inputhooks/pyglet.py new file mode 100644 index 00000000000..1c5ec442504 --- /dev/null +++ b/IPython/terminal/pt_inputhooks/pyglet.py @@ -0,0 +1,68 @@ +"""Enable pyglet to be used interacively with prompt_toolkit +""" +from __future__ import absolute_import + +import os +import sys +import time +from timeit import default_timer as clock +import pyglet + +# On linux only, window.flip() has a bug that causes an AttributeError on +# window close. For details, see: +# http://groups.google.com/group/pyglet-users/browse_thread/thread/47c1aab9aa4a3d23/c22f9e819826799e?#c22f9e819826799e + +if sys.platform.startswith('linux'): + def flip(window): + try: + window.flip() + except AttributeError: + pass +else: + def flip(window): + window.flip() + + +def inputhook(context): + """Run the pyglet event loop by processing pending events only. + + This keeps processing pending events until stdin is ready. After + processing all pending events, a call to time.sleep is inserted. This is + needed, otherwise, CPU usage is at 100%. This sleep time should be tuned + though for best performance. + """ + # We need to protect against a user pressing Control-C when IPython is + # idle and this is running. We trap KeyboardInterrupt and pass. + try: + t = clock() + while not context.input_is_ready(): + pyglet.clock.tick() + for window in pyglet.app.windows: + window.switch_to() + window.dispatch_events() + window.dispatch_event('on_draw') + flip(window) + + # We need to sleep at this point to keep the idle CPU load + # low. However, if sleep to long, GUI response is poor. As + # a compromise, we watch how often GUI events are being processed + # and switch between a short and long sleep time. Here are some + # stats useful in helping to tune this. + # time CPU load + # 0.001 13% + # 0.005 3% + # 0.01 1.5% + # 0.05 0.5% + used_time = clock() - t + if used_time > 10.0: + # print 'Sleep for 1 s' # dbg + time.sleep(1.0) + elif used_time > 0.1: + # Few GUI events coming in, so we can sleep longer + # print 'Sleep for 0.05 s' # dbg + time.sleep(0.05) + else: + # Many GUI events coming in, so sleep only very little + time.sleep(0.001) + except KeyboardInterrupt: + pass From 24fa5b78c0f9e963a899c2e0c94f8f61be6428f8 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 18 Jan 2016 14:47:22 +0000 Subject: [PATCH 24/36] Add GLUT input hook --- IPython/terminal/pt_inputhooks/glut.py | 141 ++++++++++++++++++++++++ examples/IPython Kernel/gui/gui-glut.py | 2 +- 2 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 IPython/terminal/pt_inputhooks/glut.py diff --git a/IPython/terminal/pt_inputhooks/glut.py b/IPython/terminal/pt_inputhooks/glut.py new file mode 100644 index 00000000000..f336e6830f5 --- /dev/null +++ b/IPython/terminal/pt_inputhooks/glut.py @@ -0,0 +1,141 @@ +"""GLUT Input hook for interactive use with prompt_toolkit +""" +from __future__ import print_function + + +# GLUT is quite an old library and it is difficult to ensure proper +# integration within IPython since original GLUT does not allow to handle +# events one by one. Instead, it requires for the mainloop to be entered +# and never returned (there is not even a function to exit he +# mainloop). Fortunately, there are alternatives such as freeglut +# (available for linux and windows) and the OSX implementation gives +# access to a glutCheckLoop() function that blocks itself until a new +# event is received. This means we have to setup the idle callback to +# ensure we got at least one event that will unblock the function. +# +# Furthermore, it is not possible to install these handlers without a window +# being first created. We choose to make this window invisible. This means that +# display mode options are set at this level and user won't be able to change +# them later without modifying the code. This should probably be made available +# via IPython options system. + +import sys +import time +import signal +import OpenGL.GLUT as glut +import OpenGL.platform as platform +from timeit import default_timer as clock + +# Frame per second : 60 +# Should probably be an IPython option +glut_fps = 60 + +# Display mode : double buffeed + rgba + depth +# Should probably be an IPython option +glut_display_mode = (glut.GLUT_DOUBLE | + glut.GLUT_RGBA | + glut.GLUT_DEPTH) + +glutMainLoopEvent = None +if sys.platform == 'darwin': + try: + glutCheckLoop = platform.createBaseFunction( + 'glutCheckLoop', dll=platform.GLUT, resultType=None, + argTypes=[], + doc='glutCheckLoop( ) -> None', + argNames=(), + ) + except AttributeError: + raise RuntimeError( + '''Your glut implementation does not allow interactive sessions''' + '''Consider installing freeglut.''') + glutMainLoopEvent = glutCheckLoop +elif glut.HAVE_FREEGLUT: + glutMainLoopEvent = glut.glutMainLoopEvent +else: + raise RuntimeError( + '''Your glut implementation does not allow interactive sessions. ''' + '''Consider installing freeglut.''') + + +def glut_display(): + # Dummy display function + pass + +def glut_idle(): + # Dummy idle function + pass + +def glut_close(): + # Close function only hides the current window + glut.glutHideWindow() + glutMainLoopEvent() + +def glut_int_handler(signum, frame): + # Catch sigint and print the defaultipyt message + signal.signal(signal.SIGINT, signal.default_int_handler) + print('\nKeyboardInterrupt') + # Need to reprint the prompt at this stage + +# Initialisation code +glut.glutInit( sys.argv ) +glut.glutInitDisplayMode( glut_display_mode ) +# This is specific to freeglut +if bool(glut.glutSetOption): + glut.glutSetOption( glut.GLUT_ACTION_ON_WINDOW_CLOSE, + glut.GLUT_ACTION_GLUTMAINLOOP_RETURNS ) +glut.glutCreateWindow( b'ipython' ) +glut.glutReshapeWindow( 1, 1 ) +glut.glutHideWindow( ) +glut.glutWMCloseFunc( glut_close ) +glut.glutDisplayFunc( glut_display ) +glut.glutIdleFunc( glut_idle ) + + +def inputhook(context): + """Run the pyglet event loop by processing pending events only. + + This keeps processing pending events until stdin is ready. After + processing all pending events, a call to time.sleep is inserted. This is + needed, otherwise, CPU usage is at 100%. This sleep time should be tuned + though for best performance. + """ + # We need to protect against a user pressing Control-C when IPython is + # idle and this is running. We trap KeyboardInterrupt and pass. + + signal.signal(signal.SIGINT, glut_int_handler) + + try: + t = clock() + + # Make sure the default window is set after a window has been closed + if glut.glutGetWindow() == 0: + glut.glutSetWindow( 1 ) + glutMainLoopEvent() + return 0 + + while not context.input_is_ready(): + glutMainLoopEvent() + # We need to sleep at this point to keep the idle CPU load + # low. However, if sleep to long, GUI response is poor. As + # a compromise, we watch how often GUI events are being processed + # and switch between a short and long sleep time. Here are some + # stats useful in helping to tune this. + # time CPU load + # 0.001 13% + # 0.005 3% + # 0.01 1.5% + # 0.05 0.5% + used_time = clock() - t + if used_time > 10.0: + # print 'Sleep for 1 s' # dbg + time.sleep(1.0) + elif used_time > 0.1: + # Few GUI events coming in, so we can sleep longer + # print 'Sleep for 0.05 s' # dbg + time.sleep(0.05) + else: + # Many GUI events coming in, so sleep only very little + time.sleep(0.001) + except KeyboardInterrupt: + pass diff --git a/examples/IPython Kernel/gui/gui-glut.py b/examples/IPython Kernel/gui/gui-glut.py index 2643b3e6d85..573690b48d1 100755 --- a/examples/IPython Kernel/gui/gui-glut.py +++ b/examples/IPython Kernel/gui/gui-glut.py @@ -38,7 +38,7 @@ def resize(width,height): else: interactive = False -glut.glutCreateWindow('gui-glut') +glut.glutCreateWindow(b'gui-glut') glut.glutDisplayFunc(display) glut.glutReshapeFunc(resize) # This is necessary on osx to be able to close the window From f34eb22e490f0876aeb269ca823b81e7cfee85ad Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 18 Jan 2016 14:54:03 +0000 Subject: [PATCH 25/36] Fix some deprecation warnings in GTK3 example --- examples/IPython Kernel/gui/gui-gtk3.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/IPython Kernel/gui/gui-gtk3.py b/examples/IPython Kernel/gui/gui-gtk3.py index 1ee7c98d642..935c026d5e3 100644 --- a/examples/IPython Kernel/gui/gui-gtk3.py +++ b/examples/IPython Kernel/gui/gui-gtk3.py @@ -20,10 +20,10 @@ def delete_event(widget, event, data=None): def destroy(widget, data=None): Gtk.main_quit() -window = Gtk.Window(Gtk.WindowType.TOPLEVEL) +window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL) window.connect("delete_event", delete_event) window.connect("destroy", destroy) -button = Gtk.Button("Hello World") +button = Gtk.Button(label="Hello World") button.connect("clicked", hello_world, None) window.add(button) From ae59e6e44bfde48d33dd37d46f093a93788a903e Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 18 Jan 2016 15:24:53 +0000 Subject: [PATCH 26/36] Initialise threads for pygtk input hook --- IPython/terminal/pt_inputhooks/gtk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/IPython/terminal/pt_inputhooks/gtk.py b/IPython/terminal/pt_inputhooks/gtk.py index 33600488bc1..8f27e12c46d 100644 --- a/IPython/terminal/pt_inputhooks/gtk.py +++ b/IPython/terminal/pt_inputhooks/gtk.py @@ -39,6 +39,9 @@ import gtk, gobject +# Enable threading in GTK. (Otherwise, GTK will keep the GIL.) +gtk.gdk.threads_init() + def inputhook(context): """ When the eventloop of prompt-toolkit is idle, call this inputhook. From 7da2783b15a56a229ebe76d0edf874ca5c95494f Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 18 Jan 2016 15:37:51 +0000 Subject: [PATCH 27/36] Implement pre-filling prompt from set_next_input() --- IPython/terminal/ptshell.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 4e04bcdf755..40df8bd3165 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -152,11 +152,19 @@ def __init__(self, *args, **kwargs): def ask_exit(self): self.keep_running = False + rl_next_input = None + + def pre_prompt(self): + if self.rl_next_input: + self.pt_cli.application.buffer.text = self.rl_next_input + self.rl_next_input = None + def interact(self): while self.keep_running: print(self.separate_in, end='') + try: - document = self.pt_cli.run() + document = self.pt_cli.run(pre_run=self.pre_prompt) except EOFError: if self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'): self.ask_exit() From 0a16e9d0e624a99578587eaa288ab1b6a3836a98 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Tue, 19 Jan 2016 14:00:47 +0000 Subject: [PATCH 28/36] Integrate colorama for coloured output on Windows --- IPython/terminal/ptshell.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 40df8bd3165..4da49b4845e 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -1,6 +1,8 @@ """IPython terminal interface using prompt_toolkit in place of readline""" from __future__ import print_function +import sys + from IPython.core.interactiveshell import InteractiveShell from IPython.utils.py3compat import PY3 from traitlets import Bool, Unicode, Dict @@ -144,6 +146,20 @@ def _(event): self.pt_cli = CommandLineInterface(app, eventloop=create_eventloop(self.inputhook)) + def init_io(self): + if sys.platform not in {'win32', 'cli'}: + return + + import colorama + colorama.init() + + # For some reason we make these wrappers around stdout/stderr. + # For now, we need to reset them so all output gets coloured. + # https://github.com/ipython/ipython/issues/8669 + from IPython.utils import io + io.stdout = io.IOStream(sys.stdout) + io.stderr = io.IOStream(sys.stderr) + def __init__(self, *args, **kwargs): super(PTInteractiveShell, self).__init__(*args, **kwargs) self.init_prompt_toolkit_cli() From 730093369725dbc19112e01c909bde40620aa999 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 21 Jan 2016 12:52:27 +0000 Subject: [PATCH 29/36] Fix %edit - editor attribute was missing --- IPython/terminal/ptshell.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 4da49b4845e..895c9ac27b4 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -25,6 +25,7 @@ from pygments.token import Token from .pt_inputhooks import get_inputhook_func +from .interactiveshell import get_default_editor class IPythonPTCompleter(Completer): @@ -61,6 +62,10 @@ class PTInteractiveShell(InteractiveShell): help="Override highlighting format for specific tokens" ) + editor = Unicode(get_default_editor(), config=True, + help="Set the editor used by IPython (default to $EDITOR/vi/notepad)." + ) + def get_prompt_tokens(self, cli): return [ (Token.Prompt, 'In ['), From 0fe95653b2e0091f97364b62d4fb83d58dd3a3be Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 27 Jan 2016 16:19:25 +0000 Subject: [PATCH 30/36] Fix set_next_input on Python 2 --- IPython/terminal/ptshell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 895c9ac27b4..9f81418ef9e 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -4,7 +4,7 @@ import sys from IPython.core.interactiveshell import InteractiveShell -from IPython.utils.py3compat import PY3 +from IPython.utils.py3compat import PY3, cast_unicode_py2 from traitlets import Bool, Unicode, Dict from prompt_toolkit.completion import Completer, Completion @@ -177,7 +177,7 @@ def ask_exit(self): def pre_prompt(self): if self.rl_next_input: - self.pt_cli.application.buffer.text = self.rl_next_input + self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input) self.rl_next_input = None def interact(self): From b40a2dd0e08895c827eb936da6c8e8f5997cd51d Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 27 Jan 2016 16:38:31 +0000 Subject: [PATCH 31/36] Make behaviour more natural with blank lines at the end of input --- IPython/terminal/ptshell.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 9f81418ef9e..58b2ad6f486 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -89,11 +89,13 @@ def init_prompt_toolkit_cli(self): )) def _(event): b = event.current_buffer - if not b.document.on_last_line: + d = b.document + if not (d.on_last_line or d.cursor_position_row >= d.line_count + - d.empty_line_count_at_the_end()): b.newline() return - status, indent = self.input_splitter.check_complete(b.document.text) + status, indent = self.input_splitter.check_complete(d.text) if (status != 'incomplete') and b.accept_action.is_returnable: b.accept_action.validate_and_handle(event.cli, b) From 90e30a8f28cc55e870c9a40a5e64a5238b143474 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 3 Feb 2016 12:31:17 +0000 Subject: [PATCH 32/36] Add a mainloop() method to mimic existing shell API --- IPython/terminal/ptshell.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 58b2ad6f486..5c13e87f75b 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -196,6 +196,16 @@ def interact(self): if document: self.run_cell(document.text, store_history=True) + def mainloop(self): + # An extra layer of protection in case someone mashing Ctrl-C breaks + # out of our internal code. + while True: + try: + self.interact() + break + except KeyboardInterrupt: + print("\nKeyboardInterrupt escaped interact()\n") + _inputhook = None def inputhook(self, context): if self._inputhook is not None: From e9384160dd1e213ed690cecffa11224dfcbe0037 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 3 Feb 2016 12:34:07 +0000 Subject: [PATCH 33/36] Config option to enable mouse support --- IPython/terminal/ptshell.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 5c13e87f75b..e751b85bd9e 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -54,6 +54,10 @@ class PTInteractiveShell(InteractiveShell): help="Use vi style keybindings at the prompt", ) + mouse_support = Bool(False, config=True, + help="Enable mouse support in the prompt" + ) + highlighting_style = Unicode('', config=True, help="The name of a Pygments style to use for syntax highlighting" ) @@ -148,6 +152,7 @@ def _(event): completer=IPythonPTCompleter(self.Completer), enable_history_search=True, style=style, + mouse_support=self.mouse_support, ) self.pt_cli = CommandLineInterface(app, From ac0799c5812a9daaed1438311a1d75c7723399bf Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 22 Feb 2016 16:59:32 +0000 Subject: [PATCH 34/36] Disable continuation prompts to work with stable release of prompt_toolkit --- IPython/terminal/ptshell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index e751b85bd9e..4c7418f8fef 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -146,7 +146,9 @@ def _(event): app = create_prompt_application(multiline=True, lexer=PygmentsLexer(Python3Lexer if PY3 else PythonLexer), get_prompt_tokens=self.get_prompt_tokens, - get_continuation_tokens=self.get_continuation_tokens, + # The line below is waiting for a new release of + # prompt_toolkit (> 0.57) + #get_continuation_tokens=self.get_continuation_tokens, key_bindings_registry=kbmanager.registry, history=history, completer=IPythonPTCompleter(self.Completer), From 67d7c405d3ee45570b44d4c37a5c23b5f21068d8 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 22 Feb 2016 17:02:41 +0000 Subject: [PATCH 35/36] Switch over to use prompt_toolkit in IPython --- IPython/terminal/ipapp.py | 2 +- setup.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/IPython/terminal/ipapp.py b/IPython/terminal/ipapp.py index fe3d0083269..f2c61a92541 100755 --- a/IPython/terminal/ipapp.py +++ b/IPython/terminal/ipapp.py @@ -32,7 +32,7 @@ InteractiveShellApp, shell_flags, shell_aliases ) from IPython.extensions.storemagic import StoreMagics -from IPython.terminal.interactiveshell import TerminalInteractiveShell +from .ptshell import PTInteractiveShell as TerminalInteractiveShell from IPython.utils import warn from IPython.paths import get_ipython_dir from traitlets import ( diff --git a/setup.py b/setup.py index c16bd1d1ed4..fe58b5fd272 100755 --- a/setup.py +++ b/setup.py @@ -195,6 +195,7 @@ def run(self): 'pickleshare', 'simplegeneric>0.8', 'traitlets', + 'prompt_toolkit', # We will require > 0.57 once a new release is made ] # Platform-specific dependencies: @@ -204,8 +205,6 @@ def run(self): extras_require.update({ ':sys_platform != "win32"': ['pexpect'], ':sys_platform == "darwin"': ['appnope'], - ':sys_platform == "darwin" and platform_python_implementation == "CPython"': ['gnureadline'], - 'terminal:sys_platform == "win32"': ['pyreadline>=2'], 'test:python_version == "2.7"': ['mock'], }) # FIXME: re-specify above platform dependencies for pip < 6 From 7505eae52c463ab83b0b9ce6744af1fb837e3c05 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 22 Feb 2016 17:27:19 +0000 Subject: [PATCH 36/36] Fix 'interactive' tests using pipes to a subprocess --- IPython/core/tests/test_shellapp.py | 6 ++++-- IPython/terminal/ptshell.py | 20 ++++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/IPython/core/tests/test_shellapp.py b/IPython/core/tests/test_shellapp.py index 197e8286776..6e2e31b0fa6 100644 --- a/IPython/core/tests/test_shellapp.py +++ b/IPython/core/tests/test_shellapp.py @@ -52,8 +52,9 @@ def test_py_script_file_attribute_interactively(self): src = "True\n" self.mktmp(src) + out = 'In [1]: False\n\nIn [2]:' err = SQLITE_NOT_AVAILABLE_ERROR if sqlite_err_maybe else None - tt.ipexec_validate(self.fname, 'False', err, options=['-i'], + tt.ipexec_validate(self.fname, out, err, options=['-i'], commands=['"__file__" in globals()', 'exit()']) @dec.skip_win32 @@ -63,6 +64,7 @@ def test_py_script_file_compiler_directive(self): src = "from __future__ import division\n" self.mktmp(src) + out = 'In [1]: float\n\nIn [2]:' err = SQLITE_NOT_AVAILABLE_ERROR if sqlite_err_maybe else None - tt.ipexec_validate(self.fname, 'float', err, options=['-i'], + tt.ipexec_validate(self.fname, out, err, options=['-i'], commands=['type(1/2)', 'exit()']) diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index 4c7418f8fef..97f8bb5e9bf 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -4,7 +4,7 @@ import sys from IPython.core.interactiveshell import InteractiveShell -from IPython.utils.py3compat import PY3, cast_unicode_py2 +from IPython.utils.py3compat import PY3, cast_unicode_py2, input from traitlets import Bool, Unicode, Dict from prompt_toolkit.completion import Completer, Completion @@ -83,6 +83,14 @@ def get_continuation_tokens(self, cli, width): ] def init_prompt_toolkit_cli(self): + if not sys.stdin.isatty(): + # Piped input - e.g. for tests. Fall back to plain non-interactive + # output. This is very limited, and only accepts a single line. + def prompt(): + return cast_unicode_py2(input('In [%d]: ' % self.execution_count)) + self.prompt_for_code = prompt + return + kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode) insert_mode = ViStateFilter(kbmanager.get_vi_state, InputMode.INSERT) # Ctrl+J == Enter, seemingly @@ -160,6 +168,10 @@ def _(event): self.pt_cli = CommandLineInterface(app, eventloop=create_eventloop(self.inputhook)) + def prompt_for_code(self): + document = self.pt_cli.run(pre_run=self.pre_prompt) + return document.text + def init_io(self): if sys.platform not in {'win32', 'cli'}: return @@ -194,14 +206,14 @@ def interact(self): print(self.separate_in, end='') try: - document = self.pt_cli.run(pre_run=self.pre_prompt) + code = self.prompt_for_code() except EOFError: if self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'): self.ask_exit() else: - if document: - self.run_cell(document.text, store_history=True) + if code: + self.run_cell(code, store_history=True) def mainloop(self): # An extra layer of protection in case someone mashing Ctrl-C breaks