Skip to content

Commit a60f28d

Browse files
Merge branch 'fix-625'
2 parents fa3dbbf + 9a88e73 commit a60f28d

File tree

4 files changed

+254
-39
lines changed

4 files changed

+254
-39
lines changed

bpython/inspection.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,24 +283,33 @@ def is_callable(obj):
283283
return callable(obj)
284284

285285

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

288288

289289
def get_encoding(obj):
290290
"""Try to obtain encoding information of the source of an object."""
291291
for line in inspect.findsource(obj)[0][:2]:
292-
m = get_encoding_re.search(line)
292+
m = get_encoding_line_re.search(line)
293293
if m:
294294
return m.group(1)
295295
return 'ascii'
296296

297297

298+
def get_encoding_comment(source):
299+
"""Returns encoding line without the newline, or None is not found"""
300+
for line in source.splitlines()[:2]:
301+
m = get_encoding_line_re.search(line)
302+
if m:
303+
return m.group(0)
304+
return None
305+
306+
298307
def get_encoding_file(fname):
299308
"""Try to obtain encoding information from a Python source file."""
300309
with io.open(fname, 'rt', encoding='ascii', errors='ignore') as f:
301310
for unused in range(2):
302311
line = f.readline()
303-
match = get_encoding_re.search(line)
312+
match = get_encoding_line_re.search(line)
304313
if match:
305314
return match.group(1)
306315
return 'ascii'

bpython/repl.py

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,13 @@ def __init__(self, locals=None, encoding=None):
8585
necessarily must be with the current factoring) and then an exception
8686
callback can be added to the Interpreter instance afterwards - more
8787
specifically, this is so that autoindentation does not occur after a
88-
traceback."""
88+
traceback.
89+
90+
encoding is only used in Python 2, where it may be necessary to add an
91+
encoding comment to a source bytestring before running it.
92+
encoding must be a bytestring in Python 2 because it will be templated
93+
into a bytestring source as part of an encoding comment.
94+
"""
8995

9096
self.encoding = encoding or sys.getdefaultencoding()
9197
self.syntaxerror_callback = None
@@ -98,15 +104,55 @@ def reset_running_time(self):
98104
self.running_time = 0
99105

100106
def runsource(self, source, filename=None, symbol='single',
101-
encode=True):
107+
encode='auto'):
102108
"""Execute Python code.
103109
104110
source, filename and symbol are passed on to
105-
code.InteractiveInterpreter.runsource. If encode is True, the source
106-
will be encoded. On Python 3.X, encode will be ignored."""
107-
if not py3 and encode:
108-
source = u'# coding: %s\n\n%s' % (self.encoding, source)
109-
source = source.encode(self.encoding)
111+
code.InteractiveInterpreter.runsource. If encode is True,
112+
an encoding comment will be added to the source.
113+
On Python 3.X, encode will be ignored.
114+
115+
encode should only be used for interactive interpreter input,
116+
files should always already have an encoding comment or be ASCII.
117+
By default an encoding line will be added if no filename is given.
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+
123+
Because adding an encoding comment to a unicode string in Python 2
124+
would cause a syntax error to be thrown which would reference code
125+
the user did not write, setting encoding to True when source is a
126+
unicode string in Python 2 will throw a ValueError."""
127+
# str means bytestring in Py2
128+
if encode and not py3 and isinstance(source, unicode):
129+
if encode != 'auto':
130+
raise ValueError("can't add encoding line to unicode input")
131+
encode = False
132+
if encode and filename is not None:
133+
# files have encoding comments or implicit encoding of ASCII
134+
if encode != 'auto':
135+
raise ValueError("shouldn't add encoding line to file contents")
136+
encode = False
137+
138+
if encode and not py3 and isinstance(source, str):
139+
# encoding makes sense for bytestrings, so long as there
140+
# isn't already an encoding comment
141+
comment = inspection.get_encoding_comment(source)
142+
if comment:
143+
# keep the existing encoding comment, but add two lines
144+
# because this interp always adds 2 to stack trace line
145+
# numbers in Python 2
146+
source = source.replace(comment, b'%s\n\n' % comment, 1)
147+
else:
148+
source = b'# coding: %s\n\n%s' % (self.encoding, source)
149+
elif not py3 and filename is None:
150+
# 2 blank lines still need to be added
151+
# because this interpreter always adds 2 to stack trace line
152+
# numbers in Python 2 when the filename is "<input>"
153+
newlines = u'\n\n' if isinstance(source, unicode) else b'\n\n'
154+
source = newlines + source
155+
# we know we're in Python 2 here, so ok to reference unicode
110156
if filename is None:
111157
filename = filename_for_console_input(source)
112158
with self.timer:
@@ -132,11 +178,11 @@ def showsyntaxerror(self, filename=None):
132178
pass
133179
else:
134180
# Stuff in the right filename and right lineno
135-
if not py3:
136-
lineno -= 2
137181
# strip linecache line number
138182
if re.match(r'<bpython-input-\d+>', filename):
139183
filename = '<input>'
184+
if filename == '<input>' and not py3:
185+
lineno -= 2
140186
value = SyntaxError(msg, (filename, lineno, offset, line))
141187
sys.last_value = value
142188
list = traceback.format_exception_only(type, value)
@@ -160,8 +206,7 @@ def showtraceback(self):
160206
fname = '<input>'
161207
tblist[i] = (fname, lineno, module, something)
162208
# Set the right lineno (encoding header adds an extra line)
163-
if not py3:
164-
if fname == '<input>':
209+
if fname == '<input>' and not py3:
165210
tblist[i] = (fname, lineno - 2, module, something)
166211

167212
l = traceback.format_list(tblist)

bpython/test/test_args.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1+
# encoding: utf-8
2+
3+
import re
14
import subprocess
25
import sys
36
import tempfile
47
from textwrap import dedent
58

69
from bpython import args
7-
from bpython.test import FixLanguageTestCase as TestCase
8-
9-
try:
10-
import unittest2 as unittest
11-
except ImportError:
12-
import unittest
10+
from bpython.test import (FixLanguageTestCase as TestCase, unittest)
1311

1412
try:
1513
from nose.plugins.attrib import attr
@@ -39,6 +37,42 @@ def test_exec_dunder_file(self):
3937

4038
self.assertEquals(stderr.strip(), f.name)
4139

40+
def test_exec_nonascii_file(self):
41+
with tempfile.NamedTemporaryFile(mode="w") as f:
42+
f.write(dedent('''\
43+
#!/usr/bin/env python2
44+
# coding: utf-8
45+
"你好 # nonascii"
46+
'''))
47+
f.flush()
48+
try:
49+
subprocess.check_call([
50+
'python', '-m', 'bpython.curtsies',
51+
f.name])
52+
except subprocess.CalledProcessError:
53+
self.fail('Error running module with nonascii characters')
54+
55+
def test_exec_nonascii_file_linenums(self):
56+
with tempfile.NamedTemporaryFile(mode="w") as f:
57+
f.write(dedent("""\
58+
#!/usr/bin/env python2
59+
# coding: utf-8
60+
1/0
61+
"""))
62+
f.flush()
63+
p = subprocess.Popen(
64+
[sys.executable, "-m", "bpython.curtsies",
65+
f.name],
66+
stderr=subprocess.PIPE,
67+
universal_newlines=True)
68+
(_, stderr) = p.communicate()
69+
70+
self.assertIn('line 3', clean_colors(stderr))
71+
72+
73+
def clean_colors(s):
74+
return re.sub(r'\x1b[^m]*m', '', s)
75+
4276

4377
class TestParse(TestCase):
4478

0 commit comments

Comments
 (0)