From 0533c80c9c80e03503d7b78bac15e08f1de57f53 Mon Sep 17 00:00:00 2001 From: mlauter Date: Mon, 18 Aug 2014 16:33:30 -0400 Subject: [PATCH 1/6] stop painting standard error red --- bpython/curtsiesfrontend/repl.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 0bac4f2e3..311c74195 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -787,9 +787,7 @@ def send_to_stderr(self, error): lines = error.split('\n') if lines[-1]: self.current_stdouterr_line += lines[-1] - self.display_lines.extend([func_for_letter(self.config.color_scheme['error'])(line) - for line in sum([paint.display_linize(line, self.width, blank_line=True) - for line in lines[:-1]], [])]) + self.display_lines.extend(sum([paint.display_linize(line, self.width, blank_line=True) for line in lines[:-1]], [])) def send_to_stdin(self, line): if line.endswith('\n'): From 8edeadc7e2dd1de98c7a6e1639800dc0e48061f4 Mon Sep 17 00:00:00 2001 From: mlauter Date: Mon, 18 Aug 2014 18:27:18 -0400 Subject: [PATCH 2/6] add new interactive interpreter subclass with pretty pygments traceback coloring --- bpython/curtsiesfrontend/interpreter.py | 168 ++++++++++++++++++++++++ bpython/curtsiesfrontend/repl.py | 3 +- 2 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 bpython/curtsiesfrontend/interpreter.py diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py new file mode 100644 index 000000000..83586c89a --- /dev/null +++ b/bpython/curtsiesfrontend/interpreter.py @@ -0,0 +1,168 @@ +import code +import traceback +import sys +from pygments.style import Style +from pygments.token import * +from pygments.formatter import Formatter +from curtsies.bpythonparse import parse +from codeop import CommandCompiler, compile_command +from pygments.lexers import get_lexer_by_name +from pygments.styles import get_style_by_name + +default_colors = { + Generic.Error:'R', + Keyword:'d', + Name:'c', + Name.Builtin:'g', + Comment:'b', + String:'m', + Error:'r', + Literal:'d', + Number:'M', + Number.Integer:'d', + Operator:'d', + Punctuation:'d', + Token:'d', + Whitespace:'d', + Token.Punctuation.Parenthesis:'R', + Name.Function:'d', + Name.Class:'d', + } + + +class BPythonFormatter(Formatter): + """This is subclassed from the custom formatter for bpython. + Its format() method receives the tokensource + and outfile params passed to it from the + Pygments highlight() method and slops + them into the appropriate format string + as defined above, then writes to the outfile + object the final formatted string. + + See the Pygments source for more info; it's pretty + straightforward.""" + + def __init__(self, color_scheme, **options): + self.f_strings = {} + for k, v in color_scheme.iteritems(): + self.f_strings[k] = '\x01%s' % (v,) + Formatter.__init__(self, **options) + + def format(self, tokensource, outfile): + o = '' + + for token, text in tokensource: + while token not in self.f_strings: + token = token.parent + o += "%s\x03%s\x04" % (self.f_strings[token], text) + outfile.write(str(parse(o.rstrip()))) + +class Interp(code.InteractiveInterpreter): + def __init__(self, locals=None, outfile=sys.__stderr__): + """Constructor. + + The optional 'locals' argument specifies the dictionary in + which code will be executed; it defaults to a newly created + dictionary with key "__name__" set to "__console__" and key + "__doc__" set to None. + + We include an argument for the outfile to pass to the formatter for it to write to. + + """ + if locals is None: + locals = {"__name__": "__console__", "__doc__": None} + self.locals = locals + self.compile = CommandCompiler() + self.outfile = outfile + + def showsyntaxerror(self, filename=None): + """Display the syntax error that just occurred. + + This doesn't display a stack trace because there isn't one. + + If a filename is given, it is stuffed in the exception instead + of what was there before (because Python's parser always uses + "" when reading from a string). + + The output is written by self.write(), below. + + """ + type, value, sys.last_traceback = sys.exc_info() + sys.last_type = type + sys.last_value = value + if filename and type is SyntaxError: + # Work hard to stuff the correct filename in the exception + try: + msg, (dummy_filename, lineno, offset, line) = value + except: + # Not the format we expect; leave it alone + pass + else: + # Stuff in the right filename + value = SyntaxError(msg, (filename, lineno, offset, line)) + sys.last_value = value + l = traceback.format_exception_only(type, value) + tbtext = ''.join(l) + lexer = get_lexer_by_name("pytb") + traceback_informative_formatter = BPythonFormatter(default_colors) + traceback_code_formatter = BPythonFormatter({Token: ('d')}) + tokens= list(lexer.get_tokens(tbtext)) + no_format_mode = False + cur_line = [] + for token, text in tokens: + if text.endswith('\n'): + cur_line.append((token,text)) + if no_format_mode: + traceback_code_formatter.format(cur_line,self.outfile) + no_format_mode = False + else: + traceback_informative_formatter.format(cur_line,self.outfile) + cur_line = [] + elif text == ' ' and cur_line == []: + no_format_mode = True + cur_line.append((token,text)) + else: + cur_line.append((token,text)) + assert cur_line == [], cur_line + + def showtraceback(self): + """Display the exception that just occurred. + + We remove the first stack item because it is our own code. + + + """ + type, value, tb = sys.exc_info() + sys.last_type = type + sys.last_value = value + sys.last_traceback = tb + tblist = traceback.extract_tb(tb) + del tblist[:1] + l = traceback.format_list(tblist) + if l: + l.insert(0, "Traceback (most recent call last):\n") + l[len(l):] = traceback.format_exception_only(type, value) + tbtext = ''.join(l) + lexer = get_lexer_by_name("pytb", stripall=True) + traceback_informative_formatter = BPythonFormatter(default_colors) + traceback_code_formatter = BPythonFormatter({Token: ('d')}) + tokens= list(lexer.get_tokens(tbtext)) + + no_format_mode = False + cur_line = [] + for token, text in tokens: + if text.endswith('\n'): + cur_line.append((token,text)) + if no_format_mode: + traceback_code_formatter.format(cur_line,self.outfile) + no_format_mode = False + else: + traceback_informative_formatter.format(cur_line,self.outfile) + cur_line = [] + elif text == ' ' and cur_line == []: + no_format_mode = True + cur_line.append((token,text)) + else: + cur_line.append((token,text)) + assert cur_line == [] + diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 311c74195..3626d05e7 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -17,6 +17,7 @@ from pygments import format from pygments.lexers import PythonLexer from pygments.formatters import TerminalFormatter +from interpreter import Interp import blessings @@ -237,7 +238,7 @@ def __init__(self, # would be unsafe because initial # state was passed in if interp is None: - interp = code.InteractiveInterpreter(locals=locals_) + interp = Interp(locals=locals_) if banner is None: banner = _('Welcome to bpython! Press <%s> for help.') % config.help_key config.autocomplete_mode = autocomplete.SIMPLE # only one implemented currently From b5dac4d9888f358c121aad6960e30b55265783a4 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Mon, 25 Aug 2014 11:59:25 -0400 Subject: [PATCH 3/6] Incorporate new interpreter class into the main repl and use our write traceback method to send to standard error. --- bpython/curtsiesfrontend/interpreter.py | 16 +++++++++------- bpython/curtsiesfrontend/repl.py | 2 ++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 83586c89a..11cd6aa08 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -29,7 +29,6 @@ Name.Class:'d', } - class BPythonFormatter(Formatter): """This is subclassed from the custom formatter for bpython. Its format() method receives the tokensource @@ -58,7 +57,7 @@ def format(self, tokensource, outfile): outfile.write(str(parse(o.rstrip()))) class Interp(code.InteractiveInterpreter): - def __init__(self, locals=None, outfile=sys.__stderr__): + def __init__(self, locals=None): """Constructor. The optional 'locals' argument specifies the dictionary in @@ -73,7 +72,10 @@ def __init__(self, locals=None, outfile=sys.__stderr__): locals = {"__name__": "__console__", "__doc__": None} self.locals = locals self.compile = CommandCompiler() - self.outfile = outfile + + # typically changed after being instantiated + self.write = lambda stuff: sys.stderr.write(stuff) + self.outfile = self def showsyntaxerror(self, filename=None): """Display the syntax error that just occurred. @@ -113,10 +115,10 @@ def showsyntaxerror(self, filename=None): if text.endswith('\n'): cur_line.append((token,text)) if no_format_mode: - traceback_code_formatter.format(cur_line,self.outfile) + traceback_code_formatter.format(cur_line, self.outfile) no_format_mode = False else: - traceback_informative_formatter.format(cur_line,self.outfile) + traceback_informative_formatter.format(cur_line, self.outfile) cur_line = [] elif text == ' ' and cur_line == []: no_format_mode = True @@ -154,10 +156,10 @@ def showtraceback(self): if text.endswith('\n'): cur_line.append((token,text)) if no_format_mode: - traceback_code_formatter.format(cur_line,self.outfile) + traceback_code_formatter.format(cur_line, self.outfile) no_format_mode = False else: - traceback_informative_formatter.format(cur_line,self.outfile) + traceback_informative_formatter.format(cur_line, self.outfile) cur_line = [] elif text == ' ' and cur_line == []: no_format_mode = True diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 3626d05e7..f8d9e4b2d 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -239,6 +239,7 @@ def __init__(self, # state was passed in if interp is None: interp = Interp(locals=locals_) + interp.writetb = self.send_to_stderr if banner is None: banner = _('Welcome to bpython! Press <%s> for help.') % config.help_key config.autocomplete_mode = autocomplete.SIMPLE # only one implemented currently @@ -1107,6 +1108,7 @@ def reevaluate(self, insert_into_history=False): if not self.weak_rewind: self.interp = self.interp.__class__() + self.interp.writetb = self.send_to_stderr self.coderunner.interp = self.interp self.buffer = [] From b40ce7d5563ca867e7d13cf358c0b2cc3f516259 Mon Sep 17 00:00:00 2001 From: mlauter Date: Thu, 4 Sep 2014 11:39:20 -0400 Subject: [PATCH 4/6] stop str-ing in Interp.write, send FmtStr to stderr instead --- bpython/curtsiesfrontend/interpreter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 11cd6aa08..5459692b2 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -36,7 +36,7 @@ class BPythonFormatter(Formatter): Pygments highlight() method and slops them into the appropriate format string as defined above, then writes to the outfile - object the final formatted string. + object the final formatted string. This does not write real strings. It writes format string (FmtStr) objects. See the Pygments source for more info; it's pretty straightforward.""" @@ -54,7 +54,7 @@ def format(self, tokensource, outfile): while token not in self.f_strings: token = token.parent o += "%s\x03%s\x04" % (self.f_strings[token], text) - outfile.write(str(parse(o.rstrip()))) + outfile.write(parse(o.rstrip())) class Interp(code.InteractiveInterpreter): def __init__(self, locals=None): @@ -146,6 +146,8 @@ def showtraceback(self): l[len(l):] = traceback.format_exception_only(type, value) tbtext = ''.join(l) lexer = get_lexer_by_name("pytb", stripall=True) + + traceback_informative_formatter = BPythonFormatter(default_colors) traceback_code_formatter = BPythonFormatter({Token: ('d')}) tokens= list(lexer.get_tokens(tbtext)) From 8aca9a5de7170602ae922c7bc78c49ab09464345 Mon Sep 17 00:00:00 2001 From: mlauter Date: Thu, 4 Sep 2014 12:45:52 -0400 Subject: [PATCH 5/6] factor out redundant code from showtraceback and showsyntaxerror --- bpython/curtsiesfrontend/interpreter.py | 27 +++++-------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 5459692b2..7013b436b 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -106,26 +106,7 @@ def showsyntaxerror(self, filename=None): l = traceback.format_exception_only(type, value) tbtext = ''.join(l) lexer = get_lexer_by_name("pytb") - traceback_informative_formatter = BPythonFormatter(default_colors) - traceback_code_formatter = BPythonFormatter({Token: ('d')}) - tokens= list(lexer.get_tokens(tbtext)) - no_format_mode = False - cur_line = [] - for token, text in tokens: - if text.endswith('\n'): - cur_line.append((token,text)) - if no_format_mode: - traceback_code_formatter.format(cur_line, self.outfile) - no_format_mode = False - else: - traceback_informative_formatter.format(cur_line, self.outfile) - cur_line = [] - elif text == ' ' and cur_line == []: - no_format_mode = True - cur_line.append((token,text)) - else: - cur_line.append((token,text)) - assert cur_line == [], cur_line + self.format(tbtext,lexer) def showtraceback(self): """Display the exception that just occurred. @@ -147,7 +128,10 @@ def showtraceback(self): tbtext = ''.join(l) lexer = get_lexer_by_name("pytb", stripall=True) + self.format(tbtext,lexer) + + def format(self, tbtext, lexer): traceback_informative_formatter = BPythonFormatter(default_colors) traceback_code_formatter = BPythonFormatter({Token: ('d')}) tokens= list(lexer.get_tokens(tbtext)) @@ -168,5 +152,4 @@ def showtraceback(self): cur_line.append((token,text)) else: cur_line.append((token,text)) - assert cur_line == [] - + assert cur_line == [], cur_line From 55ce0e753ae5d5ad73f7319e7fd7cb933e1649c4 Mon Sep 17 00:00:00 2001 From: mlauter Date: Thu, 4 Sep 2014 12:46:16 -0400 Subject: [PATCH 6/6] add test for interpreter --- bpython/test/test_interpreter.py | 42 ++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 bpython/test/test_interpreter.py diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py new file mode 100644 index 000000000..bb2cc29cb --- /dev/null +++ b/bpython/test/test_interpreter.py @@ -0,0 +1,42 @@ +import unittest + +from bpython.curtsiesfrontend import interpreter +from curtsies.fmtfuncs import * + +class TestInterpreter(unittest.TestCase): + def test_syntaxerror(self): + i = interpreter.Interp() + a = [] + + def append_to_a(message): + a.append(message) + + i.write = append_to_a + i.runsource('1.1.1.1') + + expected = ''+u''+u' File '+green(u'""')+u', line '+bold(magenta(u'1'))+u'\n'+u' '+u'1.1'+u'.'+u'1.1'+u'\n'+u' '+u' '+u'^'+u'\n'+bold(red(u'SyntaxError'))+u': '+cyan(u'invalid syntax')+u'\n' + + self.assertEquals(str(plain('').join(a)), str(expected)) + self.assertEquals(plain('').join(a), expected) + + def test_traceback(self): + i = interpreter.Interp() + a = [] + + def append_to_a(message): + a.append(message) + + i.write = append_to_a + + def f(): + return 1/0 + + def g(): + return f() + + i.runsource('g()') + + expected = u'Traceback (most recent call last):\n'+''+u' File '+green(u'""')+u', line '+bold (magenta(u'1'))+u', in '+cyan(u'')+u'\n'+''+bold(red(u'NameError'))+u': '+cyan(u"name 'g' is not defined")+u'\n' + + self.assertEquals(str(plain('').join(a)), str(expected)) + self.assertEquals(plain('').join(a), expected)