Skip to content

Commit dfb44cf

Browse files
fix Python 2 running encoded source files
1 parent 27c041f commit dfb44cf

File tree

3 files changed

+43
-14
lines changed

3 files changed

+43
-14
lines changed

bpython/inspection.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ def is_callable(obj):
284284

285285

286286
get_encoding_re = LazyReCompile(r'coding[:=]\s*([-\w.]+)')
287+
get_encoding_line_re = LazyReCompile(r'^.*coding[:=]\s*[-\w.]+.*$')
287288

288289

289290
def get_encoding(obj):
@@ -295,6 +296,15 @@ def get_encoding(obj):
295296
return 'ascii'
296297

297298

299+
def get_encoding_comment(source):
300+
"""Returns encoding line without the newline, or None is not found"""
301+
for line in source.splitlines()[:2]:
302+
m = get_encoding_line_re.search(line)
303+
if m:
304+
return m.group(0)
305+
return None
306+
307+
298308
def get_encoding_file(fname):
299309
"""Try to obtain encoding information from a Python source file."""
300310
with io.open(fname, 'rt', encoding='ascii', errors='ignore') as f:

bpython/repl.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,30 @@ def runsource(self, source, filename=None, symbol='single',
109109
110110
source, filename and symbol are passed on to
111111
code.InteractiveInterpreter.runsource. If encode is True, the source
112-
will be encoded. On Python 3.X, encode will be ignored."""
113-
if not py3 and encode:
114-
source = u'# coding: %s\n\n%s' % (self.encoding, source)
115-
source = source.encode(self.encoding)
112+
will be encoded. On Python 3.X, encode will be ignored.
113+
114+
encode doesn't encode the source, it just adds an encoding comment
115+
that specifies the encoding of the source.
116+
encode should only be used for interactive interpreter input,
117+
files should always have an encoding comment or be ASCII.
118+
119+
In Python 3, source must be a unicode string
120+
In Python 2, source may be latin-1 bytestring or unicode string,
121+
following the interface of code.InteractiveInterpreter"""
122+
if encode and not py3:
123+
if isinstance(source, str):
124+
# encoding only makes sense for bytestrings
125+
assert isinstance(source, str)
126+
source = b'# coding: %s\n\n%s' % (self.encoding, source)
127+
else:
128+
# 2 blank lines still need to be added because this
129+
# interpreter always adds 2 lines to stack trace line
130+
# numbers in Python 2
131+
comment = inspection.get_encoding_comment(source)
132+
if comment:
133+
source = source.replace(comment, u'%s\n\n' % comment, 1)
134+
else:
135+
source = u'\n\n' + source
116136
if filename is None:
117137
filename = filename_for_console_input(source)
118138
with self.timer:
@@ -138,11 +158,11 @@ def showsyntaxerror(self, filename=None):
138158
pass
139159
else:
140160
# Stuff in the right filename and right lineno
141-
if not py3:
142-
lineno -= 2
143161
# strip linecache line number
144162
if re.match(r'<bpython-input-\d+>', filename):
145163
filename = '<input>'
164+
if filename == '<input>' and not py3:
165+
lineno -= 2
146166
value = SyntaxError(msg, (filename, lineno, offset, line))
147167
sys.last_value = value
148168
list = traceback.format_exception_only(type, value)
@@ -166,8 +186,7 @@ def showtraceback(self):
166186
fname = '<input>'
167187
tblist[i] = (fname, lineno, module, something)
168188
# Set the right lineno (encoding header adds an extra line)
169-
if not py3:
170-
if fname == '<input>':
189+
if fname == '<input>' and not py3:
171190
tblist[i] = (fname, lineno - 2, module, something)
172191

173192
l = traceback.format_list(tblist)

bpython/test/test_interpreter.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def append_to_a(message):
5050
i.write = append_to_a
5151

5252
def f():
53-
return 1/0
53+
return 1 / 0
5454

5555
def g():
5656
return f()
@@ -73,7 +73,7 @@ def g():
7373

7474
@unittest.skipIf(py3, "runsource() accepts only unicode in Python 3")
7575
def test_runsource_bytes(self):
76-
i = interpreter.Interp(encoding='latin-1')
76+
i = interpreter.Interp(encoding=b'latin-1')
7777

7878
i.runsource("a = b'\xfe'".encode('latin-1'), encode=False)
7979
self.assertIsInstance(i.locals['a'], str)
@@ -85,23 +85,23 @@ def test_runsource_bytes(self):
8585

8686
@unittest.skipUnless(py3, "Only a syntax error in Python 3")
8787
def test_runsource_bytes_over_128_syntax_error_py3(self):
88-
i = interpreter.Interp(encoding='latin-1')
88+
i = interpreter.Interp(encoding=b'latin-1')
8989
i.showsyntaxerror = mock.Mock(return_value=None)
9090

9191
i.runsource("a = b'\xfe'", encode=True)
9292
i.showsyntaxerror.assert_called_with(mock.ANY)
9393

9494
@unittest.skipIf(py3, "encode is Python 2 only")
9595
def test_runsource_bytes_over_128_syntax_error_py2(self):
96-
i = interpreter.Interp(encoding='latin-1')
96+
i = interpreter.Interp(encoding=b'latin-1')
9797

98-
i.runsource("a = b'\xfe'", encode=True)
98+
i.runsource(b"a = b'\xfe'", encode=True)
9999
self.assertIsInstance(i.locals['a'], type(b''))
100100
self.assertEqual(i.locals['a'], b"\xfe")
101101

102102
@unittest.skipIf(py3, "encode is Python 2 only")
103103
def test_runsource_unicode(self):
104-
i = interpreter.Interp(encoding='latin-1')
104+
i = interpreter.Interp(encoding=b'latin-1')
105105

106106
i.runsource("a = u'\xfe'", encode=True)
107107
self.assertIsInstance(i.locals['a'], type(u''))

0 commit comments

Comments
 (0)