Skip to content

Commit 0513f1a

Browse files
Merge pull request bpython#368 from mlauter/master
Fix bpython#194
2 parents 80f59e5 + 55ce0e7 commit 0513f1a

File tree

3 files changed

+202
-4
lines changed

3 files changed

+202
-4
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import code
2+
import traceback
3+
import sys
4+
from pygments.style import Style
5+
from pygments.token import *
6+
from pygments.formatter import Formatter
7+
from curtsies.bpythonparse import parse
8+
from codeop import CommandCompiler, compile_command
9+
from pygments.lexers import get_lexer_by_name
10+
from pygments.styles import get_style_by_name
11+
12+
default_colors = {
13+
Generic.Error:'R',
14+
Keyword:'d',
15+
Name:'c',
16+
Name.Builtin:'g',
17+
Comment:'b',
18+
String:'m',
19+
Error:'r',
20+
Literal:'d',
21+
Number:'M',
22+
Number.Integer:'d',
23+
Operator:'d',
24+
Punctuation:'d',
25+
Token:'d',
26+
Whitespace:'d',
27+
Token.Punctuation.Parenthesis:'R',
28+
Name.Function:'d',
29+
Name.Class:'d',
30+
}
31+
32+
class BPythonFormatter(Formatter):
33+
"""This is subclassed from the custom formatter for bpython.
34+
Its format() method receives the tokensource
35+
and outfile params passed to it from the
36+
Pygments highlight() method and slops
37+
them into the appropriate format string
38+
as defined above, then writes to the outfile
39+
object the final formatted string. This does not write real strings. It writes format string (FmtStr) objects.
40+
41+
See the Pygments source for more info; it's pretty
42+
straightforward."""
43+
44+
def __init__(self, color_scheme, **options):
45+
self.f_strings = {}
46+
for k, v in color_scheme.iteritems():
47+
self.f_strings[k] = '\x01%s' % (v,)
48+
Formatter.__init__(self, **options)
49+
50+
def format(self, tokensource, outfile):
51+
o = ''
52+
53+
for token, text in tokensource:
54+
while token not in self.f_strings:
55+
token = token.parent
56+
o += "%s\x03%s\x04" % (self.f_strings[token], text)
57+
outfile.write(parse(o.rstrip()))
58+
59+
class Interp(code.InteractiveInterpreter):
60+
def __init__(self, locals=None):
61+
"""Constructor.
62+
63+
The optional 'locals' argument specifies the dictionary in
64+
which code will be executed; it defaults to a newly created
65+
dictionary with key "__name__" set to "__console__" and key
66+
"__doc__" set to None.
67+
68+
We include an argument for the outfile to pass to the formatter for it to write to.
69+
70+
"""
71+
if locals is None:
72+
locals = {"__name__": "__console__", "__doc__": None}
73+
self.locals = locals
74+
self.compile = CommandCompiler()
75+
76+
# typically changed after being instantiated
77+
self.write = lambda stuff: sys.stderr.write(stuff)
78+
self.outfile = self
79+
80+
def showsyntaxerror(self, filename=None):
81+
"""Display the syntax error that just occurred.
82+
83+
This doesn't display a stack trace because there isn't one.
84+
85+
If a filename is given, it is stuffed in the exception instead
86+
of what was there before (because Python's parser always uses
87+
"<string>" when reading from a string).
88+
89+
The output is written by self.write(), below.
90+
91+
"""
92+
type, value, sys.last_traceback = sys.exc_info()
93+
sys.last_type = type
94+
sys.last_value = value
95+
if filename and type is SyntaxError:
96+
# Work hard to stuff the correct filename in the exception
97+
try:
98+
msg, (dummy_filename, lineno, offset, line) = value
99+
except:
100+
# Not the format we expect; leave it alone
101+
pass
102+
else:
103+
# Stuff in the right filename
104+
value = SyntaxError(msg, (filename, lineno, offset, line))
105+
sys.last_value = value
106+
l = traceback.format_exception_only(type, value)
107+
tbtext = ''.join(l)
108+
lexer = get_lexer_by_name("pytb")
109+
self.format(tbtext,lexer)
110+
111+
def showtraceback(self):
112+
"""Display the exception that just occurred.
113+
114+
We remove the first stack item because it is our own code.
115+
116+
117+
"""
118+
type, value, tb = sys.exc_info()
119+
sys.last_type = type
120+
sys.last_value = value
121+
sys.last_traceback = tb
122+
tblist = traceback.extract_tb(tb)
123+
del tblist[:1]
124+
l = traceback.format_list(tblist)
125+
if l:
126+
l.insert(0, "Traceback (most recent call last):\n")
127+
l[len(l):] = traceback.format_exception_only(type, value)
128+
tbtext = ''.join(l)
129+
lexer = get_lexer_by_name("pytb", stripall=True)
130+
131+
self.format(tbtext,lexer)
132+
133+
134+
def format(self, tbtext, lexer):
135+
traceback_informative_formatter = BPythonFormatter(default_colors)
136+
traceback_code_formatter = BPythonFormatter({Token: ('d')})
137+
tokens= list(lexer.get_tokens(tbtext))
138+
139+
no_format_mode = False
140+
cur_line = []
141+
for token, text in tokens:
142+
if text.endswith('\n'):
143+
cur_line.append((token,text))
144+
if no_format_mode:
145+
traceback_code_formatter.format(cur_line, self.outfile)
146+
no_format_mode = False
147+
else:
148+
traceback_informative_formatter.format(cur_line, self.outfile)
149+
cur_line = []
150+
elif text == ' ' and cur_line == []:
151+
no_format_mode = True
152+
cur_line.append((token,text))
153+
else:
154+
cur_line.append((token,text))
155+
assert cur_line == [], cur_line

bpython/curtsiesfrontend/repl.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from pygments import format
1818
from pygments.lexers import PythonLexer
1919
from pygments.formatters import TerminalFormatter
20+
from interpreter import Interp
2021

2122
import blessings
2223

@@ -236,7 +237,8 @@ def __init__(self,
236237
# would be unsafe because initial
237238
# state was passed in
238239
if interp is None:
239-
interp = code.InteractiveInterpreter(locals=locals_)
240+
interp = Interp(locals=locals_)
241+
interp.writetb = self.send_to_stderr
240242
if banner is None:
241243
if config.help_key:
242244
banner = _('Welcome to bpython!') + ' ' + (_('Press <%s> for help.') % config.help_key)
@@ -867,9 +869,7 @@ def send_to_stderr(self, error):
867869
lines = error.split('\n')
868870
if lines[-1]:
869871
self.current_stdouterr_line += lines[-1]
870-
self.display_lines.extend([func_for_letter(self.config.color_scheme['error'])(line)
871-
for line in sum([paint.display_linize(line, self.width, blank_line=True)
872-
for line in lines[:-1]], [])])
872+
self.display_lines.extend(sum([paint.display_linize(line, self.width, blank_line=True) for line in lines[:-1]], []))
873873

874874
def send_to_stdin(self, line):
875875
if line.endswith('\n'):
@@ -1202,6 +1202,7 @@ def reevaluate(self, insert_into_history=False):
12021202

12031203
if not self.weak_rewind:
12041204
self.interp = self.interp.__class__()
1205+
self.interp.writetb = self.send_to_stderr
12051206
self.coderunner.interp = self.interp
12061207

12071208
self.buffer = []

bpython/test/test_interpreter.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import unittest
2+
3+
from bpython.curtsiesfrontend import interpreter
4+
from curtsies.fmtfuncs import *
5+
6+
class TestInterpreter(unittest.TestCase):
7+
def test_syntaxerror(self):
8+
i = interpreter.Interp()
9+
a = []
10+
11+
def append_to_a(message):
12+
a.append(message)
13+
14+
i.write = append_to_a
15+
i.runsource('1.1.1.1')
16+
17+
expected = ''+u''+u' File '+green(u'"<input>"')+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'
18+
19+
self.assertEquals(str(plain('').join(a)), str(expected))
20+
self.assertEquals(plain('').join(a), expected)
21+
22+
def test_traceback(self):
23+
i = interpreter.Interp()
24+
a = []
25+
26+
def append_to_a(message):
27+
a.append(message)
28+
29+
i.write = append_to_a
30+
31+
def f():
32+
return 1/0
33+
34+
def g():
35+
return f()
36+
37+
i.runsource('g()')
38+
39+
expected = u'Traceback (most recent call last):\n'+''+u' File '+green(u'"<input>"')+u', line '+bold (magenta(u'1'))+u', in '+cyan(u'<module>')+u'\n'+''+bold(red(u'NameError'))+u': '+cyan(u"name 'g' is not defined")+u'\n'
40+
41+
self.assertEquals(str(plain('').join(a)), str(expected))
42+
self.assertEquals(plain('').join(a), expected)

0 commit comments

Comments
 (0)