From 97429bdee9adadbfbb8ebb2ccb2c98d03c36e6bd 2016-05-27 20:14:31 From: Matthias Bussonnier Date: 2016-05-27 20:14:31 Subject: [PATCH] Merge pull request #9487 from takluyver/ptk-ipdb Ipdb with prompt_toolkit --- diff --git a/IPython/core/completer.py b/IPython/core/completer.py index d023e82..f11d60f 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -652,6 +652,9 @@ class IPCompleter(Completer): self.dict_key_matches, ] + # This is set externally by InteractiveShell + self.custom_completers = None + def all_completions(self, text): """ Wrapper around the complete method for the benefit of emacs. @@ -1072,6 +1075,9 @@ class IPCompleter(Completer): return u'', [] def dispatch_custom_completer(self, text): + if not self.custom_completers: + return + line = self.line_buffer if not line.strip(): return None diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py index dcbd65d..115e0aa 100644 --- a/IPython/core/debugger.py +++ b/IPython/core/debugger.py @@ -37,6 +37,7 @@ from IPython.utils import coloransi, py3compat from IPython.core.excolors import exception_colors from IPython.testing.skipdoctest import skip_doctest + prompt = 'ipdb> ' #We have to check this directly from sys.argv, config struct not yet available @@ -120,12 +121,6 @@ class Tracer(object): sys.excepthook = functools.partial(BdbQuit_excepthook, excepthook=sys.excepthook) def_colors = 'NoColor' - try: - # Limited tab completion support - import readline - readline.parse_and_bind('tab: complete') - except ImportError: - pass else: # In ipython, we use its custom exception handler mechanism def_colors = ip.colors @@ -205,7 +200,7 @@ class Pdb(OldPdb, object): except (TypeError, ValueError): raise ValueError("Context must be a positive integer") - OldPdb.__init__(self,completekey,stdin,stdout) + OldPdb.__init__(self, completekey, stdin, stdout) # IPython changes... self.shell = get_ipython() @@ -245,31 +240,17 @@ class Pdb(OldPdb, object): self.parser = PyColorize.Parser() # Set the prompt - the default prompt is '(Pdb)' - Colors = cst.active_colors - if color_scheme == 'NoColor': - self.prompt = prompt - else: - # The colour markers are wrapped by bytes 01 and 02 so that readline - # can calculate the width. - self.prompt = u'\x01%s\x02%s\x01%s\x02' % (Colors.prompt, prompt, Colors.Normal) + self.prompt = prompt def set_colors(self, scheme): """Shorthand access to the color table scheme selector method.""" self.color_scheme_table.set_active_scheme(scheme) def interaction(self, frame, traceback): - self.shell.set_completer_frame(frame) - while True: - try: - OldPdb.interaction(self, frame, traceback) - break - except KeyboardInterrupt: - self.shell.write('\n' + self.shell.get_exception_only()) - break - finally: - # Pdb sets readline delimiters, so set them back to our own - if self.shell.readline is not None: - self.shell.readline.set_completer_delims(self.shell.readline_delims) + try: + OldPdb.interaction(self, frame, traceback) + except KeyboardInterrupt: + sys.stdout.write('\n' + self.shell.get_exception_only()) def parseline(self, line): if line.startswith("!!"): @@ -284,18 +265,15 @@ class Pdb(OldPdb, object): def new_do_up(self, arg): OldPdb.do_up(self, arg) - self.shell.set_completer_frame(self.curframe) do_u = do_up = decorate_fn_with_doc(new_do_up, OldPdb.do_up) def new_do_down(self, arg): OldPdb.do_down(self, arg) - self.shell.set_completer_frame(self.curframe) do_d = do_down = decorate_fn_with_doc(new_do_down, OldPdb.do_down) def new_do_frame(self, arg): OldPdb.do_frame(self, arg) - self.shell.set_completer_frame(self.curframe) def new_do_quit(self, arg): @@ -312,9 +290,6 @@ class Pdb(OldPdb, object): self.msg("Restart doesn't make sense here. Using 'quit' instead.") return self.do_quit(arg) - def postloop(self): - self.shell.set_completer_frame(None) - def print_stack_trace(self, context=None): if context is None: context = self.context diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index c6e913a..3bcc3e0 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -42,6 +42,7 @@ from IPython.core.autocall import ExitAutocall from IPython.core.builtin_trap import BuiltinTrap from IPython.core.events import EventManager, available_events from IPython.core.compilerop import CachingCompiler, check_linecache_ipython +from IPython.core.debugger import Pdb from IPython.core.display_trap import DisplayTrap from IPython.core.displayhook import DisplayHook from IPython.core.displaypub import DisplayPublisher @@ -1584,6 +1585,8 @@ class InteractiveShell(SingletonConfigurable): # Things related to exception handling and tracebacks (not debugging) #------------------------------------------------------------------------- + debugger_cls = Pdb + def init_traceback_handlers(self, custom_exceptions): # Syntax error handler. self.SyntaxTB = ultratb.SyntaxTB(color_scheme='NoColor') @@ -1594,7 +1597,8 @@ class InteractiveShell(SingletonConfigurable): self.InteractiveTB = ultratb.AutoFormattedTB(mode = 'Plain', color_scheme='NoColor', tb_offset = 1, - check_cache=check_linecache_ipython) + check_cache=check_linecache_ipython, + debugger_cls=self.debugger_cls) # The instance will store a pointer to the system-wide exception hook, # so that runtime code (such as magics) can access it. This is because diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index 0d8bb98..38ba74e 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -809,7 +809,7 @@ class VerboseTB(TBTools): def __init__(self, color_scheme='Linux', call_pdb=False, ostream=None, tb_offset=0, long_header=False, include_vars=True, - check_cache=None): + check_cache=None, debugger_cls = None): """Specify traceback offset, headers and color scheme. Define how many frames to drop from the tracebacks. Calling it with @@ -830,6 +830,8 @@ class VerboseTB(TBTools): check_cache = linecache.checkcache self.check_cache = check_cache + self.debugger_cls = debugger_cls or debugger.Pdb + def format_records(self, records, last_unique, recursion_repeat): """Format the stack frames of the traceback""" frames = [] @@ -1217,7 +1219,7 @@ class VerboseTB(TBTools): if force or self.call_pdb: if self.pdb is None: - self.pdb = debugger.Pdb( + self.pdb = self.debugger_cls( self.color_scheme_table.active_scheme_name) # the system displayhook may have changed, restore the original # for pdb @@ -1278,7 +1280,7 @@ class FormattedTB(VerboseTB, ListTB): def __init__(self, mode='Plain', color_scheme='Linux', call_pdb=False, ostream=None, tb_offset=0, long_header=False, include_vars=False, - check_cache=None): + check_cache=None, debugger_cls=None): # NEVER change the order of this list. Put new modes at the end: self.valid_modes = ['Plain', 'Context', 'Verbose'] @@ -1287,7 +1289,7 @@ class FormattedTB(VerboseTB, ListTB): VerboseTB.__init__(self, color_scheme=color_scheme, call_pdb=call_pdb, ostream=ostream, tb_offset=tb_offset, long_header=long_header, include_vars=include_vars, - check_cache=check_cache) + check_cache=check_cache, debugger_cls=debugger_cls) # Different types of tracebacks are joined with different separators to # form a single string. They are taken from this dict diff --git a/IPython/terminal/debugger.py b/IPython/terminal/debugger.py new file mode 100644 index 0000000..315c956 --- /dev/null +++ b/IPython/terminal/debugger.py @@ -0,0 +1,73 @@ +from IPython.core.debugger import Pdb + +from IPython.core.completer import IPCompleter +from .ptutils import IPythonPTCompleter + +from prompt_toolkit.token import Token +from prompt_toolkit.shortcuts import create_prompt_application +from prompt_toolkit.interface import CommandLineInterface +from prompt_toolkit.enums import EditingMode + +class TerminalPdb(Pdb): + def __init__(self, *args, **kwargs): + Pdb.__init__(self, *args, **kwargs) + self._ptcomp = None + self.pt_init() + + def pt_init(self): + def get_prompt_tokens(cli): + return [(Token.Prompt, self.prompt)] + + if self._ptcomp is None: + compl = IPCompleter(shell=self.shell, + namespace={}, + global_namespace={}, + use_readline=False, + parent=self.shell, + ) + self._ptcomp = IPythonPTCompleter(compl) + + self._pt_app = create_prompt_application( + editing_mode=getattr(EditingMode, self.shell.editing_mode.upper()), + history=self.shell.debugger_history, + completer= self._ptcomp, + enable_history_search=True, + mouse_support=self.shell.mouse_support, + get_prompt_tokens=get_prompt_tokens + ) + self.pt_cli = CommandLineInterface(self._pt_app, eventloop=self.shell._eventloop) + + def cmdloop(self, intro=None): + """Repeatedly issue a prompt, accept input, parse an initial prefix + off the received input, and dispatch to action methods, passing them + the remainder of the line as argument. + + override the same methods from cmd.Cmd to provide prompt toolkit replacement. + """ + if not self.use_rawinput: + raise ValueError('Sorry ipdb does not support use_rawinput=False') + + self.preloop() + + try: + if intro is not None: + self.intro = intro + if self.intro: + self.stdout.write(str(self.intro)+"\n") + stop = None + while not stop: + if self.cmdqueue: + line = self.cmdqueue.pop(0) + else: + self._ptcomp.ipy_completer.namespace = self.curframe_locals + self._ptcomp.ipy_completer.global_namespace = self.curframe.f_globals + try: + line = self.pt_cli.run(reset_current_buffer=True).text + except EOFError: + line = 'EOF' + line = self.precmd(line) + stop = self.onecmd(line) + stop = self.postcmd(stop, line) + self.postloop() + except Exception: + raise diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index b4f89d5..b229284 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -4,18 +4,15 @@ from __future__ import print_function import os import sys import signal -import unicodedata from warnings import warn -from wcwidth import wcwidth from IPython.core.error import TryNext from IPython.core.interactiveshell import InteractiveShell -from IPython.utils.py3compat import PY3, cast_unicode_py2, input +from IPython.utils.py3compat import cast_unicode_py2, input from IPython.utils.terminal import toggle_set_term_title, set_term_title from IPython.utils.process import abbrev_cwd from traitlets import Bool, Unicode, Dict, Integer, observe -from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER, EditingMode from prompt_toolkit.filters import HasFocus, HasSelection, Condition, ViInsertMode, EmacsInsertMode, IsDone from prompt_toolkit.history import InMemoryHistory @@ -23,70 +20,16 @@ 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.keys import Keys -from prompt_toolkit.layout.lexers import Lexer -from prompt_toolkit.layout.lexers import PygmentsLexer from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor from prompt_toolkit.styles import PygmentsStyle, DynamicStyle from pygments.styles import get_style_by_name, get_all_styles -from pygments.lexers import Python3Lexer, BashLexer, PythonLexer from pygments.token import Token +from .debugger import TerminalPdb from .pt_inputhooks import get_inputhook_func from .interactiveshell import get_default_editor, TerminalMagics - - -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): - if not document.current_line.strip(): - return - - 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: - m = unicodedata.normalize('NFC', m) - - # When the first character of the completion has a zero length, - # then it's probably a decomposed unicode character. E.g. caused by - # the "\dot" completion. Try to compose again with the previous - # character. - if wcwidth(m[0]) == 0: - if document.cursor_position + start_pos > 0: - char_before = document.text[document.cursor_position + start_pos - 1] - m = unicodedata.normalize('NFC', char_before + m) - - # Yield the modified completion instead, if this worked. - if wcwidth(m[0:1]) == 1: - yield Completion(m, start_position=start_pos - 1) - continue - - # TODO: Use Jedi to determine meta_text - # (Jedi currently has a bug that results in incorrect information.) - # meta_text = '' - # yield Completion(m, start_position=start_pos, - # display_meta=meta_text) - yield Completion(m, start_position=start_pos) - -class IPythonPTLexer(Lexer): - """ - Wrapper around PythonLexer and BashLexer. - """ - def __init__(self): - self.python_lexer = PygmentsLexer(Python3Lexer if PY3 else PythonLexer) - self.shell_lexer = PygmentsLexer(BashLexer) - - def lex_document(self, cli, document): - if document.text.startswith('!'): - return self.shell_lexer.lex_document(cli, document) - else: - return self.python_lexer.lex_document(cli, document) +from .ptutils import IPythonPTCompleter, IPythonPTLexer class TerminalInteractiveShell(InteractiveShell): @@ -100,6 +43,8 @@ class TerminalInteractiveShell(InteractiveShell): self._update_layout() pt_cli = None + debugger_history = None + debugger_cls = TerminalPdb autoedit_syntax = Bool(False, help="auto editing of files with syntax errors.", @@ -362,6 +307,8 @@ class TerminalInteractiveShell(InteractiveShell): self.init_term_title() self.keep_running = True + self.debugger_history = InMemoryHistory() + def ask_exit(self): self.keep_running = False diff --git a/IPython/terminal/ptutils.py b/IPython/terminal/ptutils.py new file mode 100644 index 0000000..4dafab1 --- /dev/null +++ b/IPython/terminal/ptutils.py @@ -0,0 +1,62 @@ +import unicodedata +from wcwidth import wcwidth + +from IPython.utils.py3compat import PY3 + +from prompt_toolkit.completion import Completer, Completion +from prompt_toolkit.layout.lexers import Lexer +from prompt_toolkit.layout.lexers import PygmentsLexer + +from pygments.lexers import Python3Lexer, BashLexer, PythonLexer + +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): + if not document.current_line.strip(): + return + + 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: + m = unicodedata.normalize('NFC', m) + + # When the first character of the completion has a zero length, + # then it's probably a decomposed unicode character. E.g. caused by + # the "\dot" completion. Try to compose again with the previous + # character. + if wcwidth(m[0]) == 0: + if document.cursor_position + start_pos > 0: + char_before = document.text[document.cursor_position + start_pos - 1] + m = unicodedata.normalize('NFC', char_before + m) + + # Yield the modified completion instead, if this worked. + if wcwidth(m[0:1]) == 1: + yield Completion(m, start_position=start_pos - 1) + continue + + # TODO: Use Jedi to determine meta_text + # (Jedi currently has a bug that results in incorrect information.) + # meta_text = '' + # yield Completion(m, start_position=start_pos, + # display_meta=meta_text) + yield Completion(m, start_position=start_pos) + +class IPythonPTLexer(Lexer): + """ + Wrapper around PythonLexer and BashLexer. + """ + def __init__(self): + self.python_lexer = PygmentsLexer(Python3Lexer if PY3 else PythonLexer) + self.shell_lexer = PygmentsLexer(BashLexer) + + def lex_document(self, cli, document): + if document.text.startswith('!'): + return self.shell_lexer.lex_document(cli, document) + else: + return self.python_lexer.lex_document(cli, document)