Skip to content

Commit b690a27

Browse files
bpo-36698: IDLE no longer fails when write non-encodable characters to stderr. (GH-16583)
It now escapes them with a backslash, as the regular Python interpreter. Added the "errors" field to the standard streams.
1 parent d05b000 commit b690a27

File tree

5 files changed

+87
-67
lines changed

5 files changed

+87
-67
lines changed

Lib/idlelib/idle_test/test_run.py

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def __eq__(self, other):
3636
self.assertIn('UnhashableException: ex1', tb[10])
3737

3838

39-
# PseudoFile tests.
39+
# StdioFile tests.
4040

4141
class S(str):
4242
def __str__(self):
@@ -68,14 +68,14 @@ def push(self, lines):
6868
self.lines = list(lines)[::-1]
6969

7070

71-
class PseudeInputFilesTest(unittest.TestCase):
71+
class StdInputFilesTest(unittest.TestCase):
7272

7373
def test_misc(self):
7474
shell = MockShell()
75-
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
75+
f = run.StdInputFile(shell, 'stdin')
7676
self.assertIsInstance(f, io.TextIOBase)
7777
self.assertEqual(f.encoding, 'utf-8')
78-
self.assertIsNone(f.errors)
78+
self.assertEqual(f.errors, 'strict')
7979
self.assertIsNone(f.newlines)
8080
self.assertEqual(f.name, '<stdin>')
8181
self.assertFalse(f.closed)
@@ -86,7 +86,7 @@ def test_misc(self):
8686

8787
def test_unsupported(self):
8888
shell = MockShell()
89-
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
89+
f = run.StdInputFile(shell, 'stdin')
9090
self.assertRaises(OSError, f.fileno)
9191
self.assertRaises(OSError, f.tell)
9292
self.assertRaises(OSError, f.seek, 0)
@@ -95,7 +95,7 @@ def test_unsupported(self):
9595

9696
def test_read(self):
9797
shell = MockShell()
98-
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
98+
f = run.StdInputFile(shell, 'stdin')
9999
shell.push(['one\n', 'two\n', ''])
100100
self.assertEqual(f.read(), 'one\ntwo\n')
101101
shell.push(['one\n', 'two\n', ''])
@@ -115,7 +115,7 @@ def test_read(self):
115115

116116
def test_readline(self):
117117
shell = MockShell()
118-
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
118+
f = run.StdInputFile(shell, 'stdin')
119119
shell.push(['one\n', 'two\n', 'three\n', 'four\n'])
120120
self.assertEqual(f.readline(), 'one\n')
121121
self.assertEqual(f.readline(-1), 'two\n')
@@ -140,7 +140,7 @@ def test_readline(self):
140140

141141
def test_readlines(self):
142142
shell = MockShell()
143-
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
143+
f = run.StdInputFile(shell, 'stdin')
144144
shell.push(['one\n', 'two\n', ''])
145145
self.assertEqual(f.readlines(), ['one\n', 'two\n'])
146146
shell.push(['one\n', 'two\n', ''])
@@ -161,7 +161,7 @@ def test_readlines(self):
161161

162162
def test_close(self):
163163
shell = MockShell()
164-
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
164+
f = run.StdInputFile(shell, 'stdin')
165165
shell.push(['one\n', 'two\n', ''])
166166
self.assertFalse(f.closed)
167167
self.assertEqual(f.readline(), 'one\n')
@@ -171,14 +171,14 @@ def test_close(self):
171171
self.assertRaises(TypeError, f.close, 1)
172172

173173

174-
class PseudeOutputFilesTest(unittest.TestCase):
174+
class StdOutputFilesTest(unittest.TestCase):
175175

176176
def test_misc(self):
177177
shell = MockShell()
178-
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
178+
f = run.StdOutputFile(shell, 'stdout')
179179
self.assertIsInstance(f, io.TextIOBase)
180180
self.assertEqual(f.encoding, 'utf-8')
181-
self.assertIsNone(f.errors)
181+
self.assertEqual(f.errors, 'strict')
182182
self.assertIsNone(f.newlines)
183183
self.assertEqual(f.name, '<stdout>')
184184
self.assertFalse(f.closed)
@@ -189,7 +189,7 @@ def test_misc(self):
189189

190190
def test_unsupported(self):
191191
shell = MockShell()
192-
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
192+
f = run.StdOutputFile(shell, 'stdout')
193193
self.assertRaises(OSError, f.fileno)
194194
self.assertRaises(OSError, f.tell)
195195
self.assertRaises(OSError, f.seek, 0)
@@ -198,16 +198,36 @@ def test_unsupported(self):
198198

199199
def test_write(self):
200200
shell = MockShell()
201-
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
201+
f = run.StdOutputFile(shell, 'stdout')
202202
f.write('test')
203203
self.assertEqual(shell.written, [('test', 'stdout')])
204204
shell.reset()
205-
f.write('t\xe8st')
206-
self.assertEqual(shell.written, [('t\xe8st', 'stdout')])
205+
f.write('t\xe8\u015b\U0001d599')
206+
self.assertEqual(shell.written, [('t\xe8\u015b\U0001d599', 'stdout')])
207207
shell.reset()
208208

209-
f.write(S('t\xe8st'))
210-
self.assertEqual(shell.written, [('t\xe8st', 'stdout')])
209+
f.write(S('t\xe8\u015b\U0001d599'))
210+
self.assertEqual(shell.written, [('t\xe8\u015b\U0001d599', 'stdout')])
211+
self.assertEqual(type(shell.written[0][0]), str)
212+
shell.reset()
213+
214+
self.assertRaises(TypeError, f.write)
215+
self.assertEqual(shell.written, [])
216+
self.assertRaises(TypeError, f.write, b'test')
217+
self.assertRaises(TypeError, f.write, 123)
218+
self.assertEqual(shell.written, [])
219+
self.assertRaises(TypeError, f.write, 'test', 'spam')
220+
self.assertEqual(shell.written, [])
221+
222+
def test_write_stderr_nonencodable(self):
223+
shell = MockShell()
224+
f = run.StdOutputFile(shell, 'stderr', 'iso-8859-15', 'backslashreplace')
225+
f.write('t\xe8\u015b\U0001d599\xa4')
226+
self.assertEqual(shell.written, [('t\xe8\\u015b\\U0001d599\\xa4', 'stderr')])
227+
shell.reset()
228+
229+
f.write(S('t\xe8\u015b\U0001d599\xa4'))
230+
self.assertEqual(shell.written, [('t\xe8\\u015b\\U0001d599\\xa4', 'stderr')])
211231
self.assertEqual(type(shell.written[0][0]), str)
212232
shell.reset()
213233

@@ -221,7 +241,7 @@ def test_write(self):
221241

222242
def test_writelines(self):
223243
shell = MockShell()
224-
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
244+
f = run.StdOutputFile(shell, 'stdout')
225245
f.writelines([])
226246
self.assertEqual(shell.written, [])
227247
shell.reset()
@@ -251,7 +271,7 @@ def test_writelines(self):
251271

252272
def test_close(self):
253273
shell = MockShell()
254-
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
274+
f = run.StdOutputFile(shell, 'stdout')
255275
self.assertFalse(f.closed)
256276
f.write('test')
257277
f.close()

Lib/idlelib/iomenu.py

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
if idlelib.testing: # Set True by test.test_idle to avoid setlocale.
1717
encoding = 'utf-8'
18+
errors = 'surrogateescape'
1819
else:
1920
# Try setting the locale, so that we can find out
2021
# what encoding to use
@@ -24,46 +25,40 @@
2425
except (ImportError, locale.Error):
2526
pass
2627

27-
locale_decode = 'ascii'
2828
if sys.platform == 'win32':
29-
# On Windows, we could use "mbcs". However, to give the user
30-
# a portable encoding name, we need to find the code page
31-
try:
32-
locale_encoding = locale.getdefaultlocale()[1]
33-
codecs.lookup(locale_encoding)
34-
except LookupError:
35-
pass
29+
encoding = 'utf-8'
30+
errors = 'surrogateescape'
3631
else:
3732
try:
3833
# Different things can fail here: the locale module may not be
3934
# loaded, it may not offer nl_langinfo, or CODESET, or the
4035
# resulting codeset may be unknown to Python. We ignore all
4136
# these problems, falling back to ASCII
4237
locale_encoding = locale.nl_langinfo(locale.CODESET)
43-
if locale_encoding is None or locale_encoding == '':
44-
# situation occurs on macOS
45-
locale_encoding = 'ascii'
46-
codecs.lookup(locale_encoding)
38+
if locale_encoding:
39+
codecs.lookup(locale_encoding)
4740
except (NameError, AttributeError, LookupError):
4841
# Try getdefaultlocale: it parses environment variables,
4942
# which may give a clue. Unfortunately, getdefaultlocale has
5043
# bugs that can cause ValueError.
5144
try:
5245
locale_encoding = locale.getdefaultlocale()[1]
53-
if locale_encoding is None or locale_encoding == '':
54-
# situation occurs on macOS
55-
locale_encoding = 'ascii'
56-
codecs.lookup(locale_encoding)
46+
if locale_encoding:
47+
codecs.lookup(locale_encoding)
5748
except (ValueError, LookupError):
5849
pass
5950

60-
locale_encoding = locale_encoding.lower()
61-
62-
encoding = locale_encoding
63-
# Encoding is used in multiple files; locale_encoding nowhere.
64-
# The only use of 'encoding' below is in _decode as initial value
65-
# of deprecated block asking user for encoding.
66-
# Perhaps use elsewhere should be reviewed.
51+
if locale_encoding:
52+
encoding = locale_encoding.lower()
53+
errors = 'strict'
54+
else:
55+
# POSIX locale or macOS
56+
encoding = 'ascii'
57+
errors = 'surrogateescape'
58+
# Encoding is used in multiple files; locale_encoding nowhere.
59+
# The only use of 'encoding' below is in _decode as initial value
60+
# of deprecated block asking user for encoding.
61+
# Perhaps use elsewhere should be reviewed.
6762

6863
coding_re = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)', re.ASCII)
6964
blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII)

Lib/idlelib/pyshell.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
from idlelib.filelist import FileList
5555
from idlelib.outwin import OutputWindow
5656
from idlelib import rpc
57-
from idlelib.run import idle_formatwarning, PseudoInputFile, PseudoOutputFile
57+
from idlelib.run import idle_formatwarning, StdInputFile, StdOutputFile
5858
from idlelib.undo import UndoDelegator
5959

6060
HOST = '127.0.0.1' # python execution server on localhost loopback
@@ -902,10 +902,14 @@ def __init__(self, flist=None):
902902
self.save_stderr = sys.stderr
903903
self.save_stdin = sys.stdin
904904
from idlelib import iomenu
905-
self.stdin = PseudoInputFile(self, "stdin", iomenu.encoding)
906-
self.stdout = PseudoOutputFile(self, "stdout", iomenu.encoding)
907-
self.stderr = PseudoOutputFile(self, "stderr", iomenu.encoding)
908-
self.console = PseudoOutputFile(self, "console", iomenu.encoding)
905+
self.stdin = StdInputFile(self, "stdin",
906+
iomenu.encoding, iomenu.errors)
907+
self.stdout = StdOutputFile(self, "stdout",
908+
iomenu.encoding, iomenu.errors)
909+
self.stderr = StdOutputFile(self, "stderr",
910+
iomenu.encoding, "backslashreplace")
911+
self.console = StdOutputFile(self, "console",
912+
iomenu.encoding, iomenu.errors)
909913
if not use_subprocess:
910914
sys.stdout = self.stdout
911915
sys.stderr = self.stderr

Lib/idlelib/run.py

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -401,17 +401,22 @@ def handle_error(self, request, client_address):
401401

402402
# Pseudofiles for shell-remote communication (also used in pyshell)
403403

404-
class PseudoFile(io.TextIOBase):
404+
class StdioFile(io.TextIOBase):
405405

406-
def __init__(self, shell, tags, encoding=None):
406+
def __init__(self, shell, tags, encoding='utf-8', errors='strict'):
407407
self.shell = shell
408408
self.tags = tags
409409
self._encoding = encoding
410+
self._errors = errors
410411

411412
@property
412413
def encoding(self):
413414
return self._encoding
414415

416+
@property
417+
def errors(self):
418+
return self._errors
419+
415420
@property
416421
def name(self):
417422
return '<%s>' % self.tags
@@ -420,27 +425,20 @@ def isatty(self):
420425
return True
421426

422427

423-
class PseudoOutputFile(PseudoFile):
428+
class StdOutputFile(StdioFile):
424429

425430
def writable(self):
426431
return True
427432

428433
def write(self, s):
429434
if self.closed:
430435
raise ValueError("write to closed file")
431-
if type(s) is not str:
432-
if not isinstance(s, str):
433-
raise TypeError('must be str, not ' + type(s).__name__)
434-
# See issue #19481
435-
s = str.__str__(s)
436+
s = str.encode(s, self.encoding, self.errors).decode(self.encoding, self.errors)
436437
return self.shell.write(s, self.tags)
437438

438439

439-
class PseudoInputFile(PseudoFile):
440-
441-
def __init__(self, shell, tags, encoding=None):
442-
PseudoFile.__init__(self, shell, tags, encoding)
443-
self._line_buffer = ''
440+
class StdInputFile(StdioFile):
441+
_line_buffer = ''
444442

445443
def readable(self):
446444
return True
@@ -495,12 +493,12 @@ def handle(self):
495493
executive = Executive(self)
496494
self.register("exec", executive)
497495
self.console = self.get_remote_proxy("console")
498-
sys.stdin = PseudoInputFile(self.console, "stdin",
499-
iomenu.encoding)
500-
sys.stdout = PseudoOutputFile(self.console, "stdout",
501-
iomenu.encoding)
502-
sys.stderr = PseudoOutputFile(self.console, "stderr",
503-
iomenu.encoding)
496+
sys.stdin = StdInputFile(self.console, "stdin",
497+
iomenu.encoding, iomenu.errors)
498+
sys.stdout = StdOutputFile(self.console, "stdout",
499+
iomenu.encoding, iomenu.errors)
500+
sys.stderr = StdOutputFile(self.console, "stderr",
501+
iomenu.encoding, "backslashreplace")
504502

505503
sys.displayhook = rpc.displayhook
506504
# page help() text to shell.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
IDLE no longer fails when write non-encodable characters to stderr. It now
2+
escapes them with a backslash, as the regular Python interpreter. Added the
3+
``errors`` field to the standard streams.

0 commit comments

Comments
 (0)