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.
This commit is contained in:
Serhiy Storchaka 2019-10-08 14:32:25 +03:00 committed by GitHub
parent d05b000c6b
commit b690a2759e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 87 additions and 67 deletions

View File

@ -36,7 +36,7 @@ class RunTest(unittest.TestCase):
self.assertIn('UnhashableException: ex1', tb[10])
# PseudoFile tests.
# StdioFile tests.
class S(str):
def __str__(self):
@ -68,14 +68,14 @@ class MockShell:
self.lines = list(lines)[::-1]
class PseudeInputFilesTest(unittest.TestCase):
class StdInputFilesTest(unittest.TestCase):
def test_misc(self):
shell = MockShell()
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
f = run.StdInputFile(shell, 'stdin')
self.assertIsInstance(f, io.TextIOBase)
self.assertEqual(f.encoding, 'utf-8')
self.assertIsNone(f.errors)
self.assertEqual(f.errors, 'strict')
self.assertIsNone(f.newlines)
self.assertEqual(f.name, '<stdin>')
self.assertFalse(f.closed)
@ -86,7 +86,7 @@ class PseudeInputFilesTest(unittest.TestCase):
def test_unsupported(self):
shell = MockShell()
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
f = run.StdInputFile(shell, 'stdin')
self.assertRaises(OSError, f.fileno)
self.assertRaises(OSError, f.tell)
self.assertRaises(OSError, f.seek, 0)
@ -95,7 +95,7 @@ class PseudeInputFilesTest(unittest.TestCase):
def test_read(self):
shell = MockShell()
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
f = run.StdInputFile(shell, 'stdin')
shell.push(['one\n', 'two\n', ''])
self.assertEqual(f.read(), 'one\ntwo\n')
shell.push(['one\n', 'two\n', ''])
@ -115,7 +115,7 @@ class PseudeInputFilesTest(unittest.TestCase):
def test_readline(self):
shell = MockShell()
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
f = run.StdInputFile(shell, 'stdin')
shell.push(['one\n', 'two\n', 'three\n', 'four\n'])
self.assertEqual(f.readline(), 'one\n')
self.assertEqual(f.readline(-1), 'two\n')
@ -140,7 +140,7 @@ class PseudeInputFilesTest(unittest.TestCase):
def test_readlines(self):
shell = MockShell()
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
f = run.StdInputFile(shell, 'stdin')
shell.push(['one\n', 'two\n', ''])
self.assertEqual(f.readlines(), ['one\n', 'two\n'])
shell.push(['one\n', 'two\n', ''])
@ -161,7 +161,7 @@ class PseudeInputFilesTest(unittest.TestCase):
def test_close(self):
shell = MockShell()
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
f = run.StdInputFile(shell, 'stdin')
shell.push(['one\n', 'two\n', ''])
self.assertFalse(f.closed)
self.assertEqual(f.readline(), 'one\n')
@ -171,14 +171,14 @@ class PseudeInputFilesTest(unittest.TestCase):
self.assertRaises(TypeError, f.close, 1)
class PseudeOutputFilesTest(unittest.TestCase):
class StdOutputFilesTest(unittest.TestCase):
def test_misc(self):
shell = MockShell()
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
f = run.StdOutputFile(shell, 'stdout')
self.assertIsInstance(f, io.TextIOBase)
self.assertEqual(f.encoding, 'utf-8')
self.assertIsNone(f.errors)
self.assertEqual(f.errors, 'strict')
self.assertIsNone(f.newlines)
self.assertEqual(f.name, '<stdout>')
self.assertFalse(f.closed)
@ -189,7 +189,7 @@ class PseudeOutputFilesTest(unittest.TestCase):
def test_unsupported(self):
shell = MockShell()
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
f = run.StdOutputFile(shell, 'stdout')
self.assertRaises(OSError, f.fileno)
self.assertRaises(OSError, f.tell)
self.assertRaises(OSError, f.seek, 0)
@ -198,16 +198,36 @@ class PseudeOutputFilesTest(unittest.TestCase):
def test_write(self):
shell = MockShell()
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
f = run.StdOutputFile(shell, 'stdout')
f.write('test')
self.assertEqual(shell.written, [('test', 'stdout')])
shell.reset()
f.write('t\xe8st')
self.assertEqual(shell.written, [('t\xe8st', 'stdout')])
f.write('t\xe8\u015b\U0001d599')
self.assertEqual(shell.written, [('t\xe8\u015b\U0001d599', 'stdout')])
shell.reset()
f.write(S('t\xe8st'))
self.assertEqual(shell.written, [('t\xe8st', 'stdout')])
f.write(S('t\xe8\u015b\U0001d599'))
self.assertEqual(shell.written, [('t\xe8\u015b\U0001d599', 'stdout')])
self.assertEqual(type(shell.written[0][0]), str)
shell.reset()
self.assertRaises(TypeError, f.write)
self.assertEqual(shell.written, [])
self.assertRaises(TypeError, f.write, b'test')
self.assertRaises(TypeError, f.write, 123)
self.assertEqual(shell.written, [])
self.assertRaises(TypeError, f.write, 'test', 'spam')
self.assertEqual(shell.written, [])
def test_write_stderr_nonencodable(self):
shell = MockShell()
f = run.StdOutputFile(shell, 'stderr', 'iso-8859-15', 'backslashreplace')
f.write('t\xe8\u015b\U0001d599\xa4')
self.assertEqual(shell.written, [('t\xe8\\u015b\\U0001d599\\xa4', 'stderr')])
shell.reset()
f.write(S('t\xe8\u015b\U0001d599\xa4'))
self.assertEqual(shell.written, [('t\xe8\\u015b\\U0001d599\\xa4', 'stderr')])
self.assertEqual(type(shell.written[0][0]), str)
shell.reset()
@ -221,7 +241,7 @@ class PseudeOutputFilesTest(unittest.TestCase):
def test_writelines(self):
shell = MockShell()
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
f = run.StdOutputFile(shell, 'stdout')
f.writelines([])
self.assertEqual(shell.written, [])
shell.reset()
@ -251,7 +271,7 @@ class PseudeOutputFilesTest(unittest.TestCase):
def test_close(self):
shell = MockShell()
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
f = run.StdOutputFile(shell, 'stdout')
self.assertFalse(f.closed)
f.write('test')
f.close()

View File

@ -15,6 +15,7 @@ from idlelib.config import idleConf
if idlelib.testing: # Set True by test.test_idle to avoid setlocale.
encoding = 'utf-8'
errors = 'surrogateescape'
else:
# Try setting the locale, so that we can find out
# what encoding to use
@ -24,15 +25,9 @@ else:
except (ImportError, locale.Error):
pass
locale_decode = 'ascii'
if sys.platform == 'win32':
# On Windows, we could use "mbcs". However, to give the user
# a portable encoding name, we need to find the code page
try:
locale_encoding = locale.getdefaultlocale()[1]
codecs.lookup(locale_encoding)
except LookupError:
pass
encoding = 'utf-8'
errors = 'surrogateescape'
else:
try:
# Different things can fail here: the locale module may not be
@ -40,30 +35,30 @@ else:
# resulting codeset may be unknown to Python. We ignore all
# these problems, falling back to ASCII
locale_encoding = locale.nl_langinfo(locale.CODESET)
if locale_encoding is None or locale_encoding == '':
# situation occurs on macOS
locale_encoding = 'ascii'
codecs.lookup(locale_encoding)
if locale_encoding:
codecs.lookup(locale_encoding)
except (NameError, AttributeError, LookupError):
# Try getdefaultlocale: it parses environment variables,
# which may give a clue. Unfortunately, getdefaultlocale has
# bugs that can cause ValueError.
try:
locale_encoding = locale.getdefaultlocale()[1]
if locale_encoding is None or locale_encoding == '':
# situation occurs on macOS
locale_encoding = 'ascii'
codecs.lookup(locale_encoding)
if locale_encoding:
codecs.lookup(locale_encoding)
except (ValueError, LookupError):
pass
locale_encoding = locale_encoding.lower()
encoding = locale_encoding
# Encoding is used in multiple files; locale_encoding nowhere.
# The only use of 'encoding' below is in _decode as initial value
# of deprecated block asking user for encoding.
# Perhaps use elsewhere should be reviewed.
if locale_encoding:
encoding = locale_encoding.lower()
errors = 'strict'
else:
# POSIX locale or macOS
encoding = 'ascii'
errors = 'surrogateescape'
# Encoding is used in multiple files; locale_encoding nowhere.
# The only use of 'encoding' below is in _decode as initial value
# of deprecated block asking user for encoding.
# Perhaps use elsewhere should be reviewed.
coding_re = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)', re.ASCII)
blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII)

View File

@ -54,7 +54,7 @@ from idlelib.editor import EditorWindow, fixwordbreaks
from idlelib.filelist import FileList
from idlelib.outwin import OutputWindow
from idlelib import rpc
from idlelib.run import idle_formatwarning, PseudoInputFile, PseudoOutputFile
from idlelib.run import idle_formatwarning, StdInputFile, StdOutputFile
from idlelib.undo import UndoDelegator
HOST = '127.0.0.1' # python execution server on localhost loopback
@ -902,10 +902,14 @@ class PyShell(OutputWindow):
self.save_stderr = sys.stderr
self.save_stdin = sys.stdin
from idlelib import iomenu
self.stdin = PseudoInputFile(self, "stdin", iomenu.encoding)
self.stdout = PseudoOutputFile(self, "stdout", iomenu.encoding)
self.stderr = PseudoOutputFile(self, "stderr", iomenu.encoding)
self.console = PseudoOutputFile(self, "console", iomenu.encoding)
self.stdin = StdInputFile(self, "stdin",
iomenu.encoding, iomenu.errors)
self.stdout = StdOutputFile(self, "stdout",
iomenu.encoding, iomenu.errors)
self.stderr = StdOutputFile(self, "stderr",
iomenu.encoding, "backslashreplace")
self.console = StdOutputFile(self, "console",
iomenu.encoding, iomenu.errors)
if not use_subprocess:
sys.stdout = self.stdout
sys.stderr = self.stderr

View File

@ -401,17 +401,22 @@ class MyRPCServer(rpc.RPCServer):
# Pseudofiles for shell-remote communication (also used in pyshell)
class PseudoFile(io.TextIOBase):
class StdioFile(io.TextIOBase):
def __init__(self, shell, tags, encoding=None):
def __init__(self, shell, tags, encoding='utf-8', errors='strict'):
self.shell = shell
self.tags = tags
self._encoding = encoding
self._errors = errors
@property
def encoding(self):
return self._encoding
@property
def errors(self):
return self._errors
@property
def name(self):
return '<%s>' % self.tags
@ -420,7 +425,7 @@ class PseudoFile(io.TextIOBase):
return True
class PseudoOutputFile(PseudoFile):
class StdOutputFile(StdioFile):
def writable(self):
return True
@ -428,19 +433,12 @@ class PseudoOutputFile(PseudoFile):
def write(self, s):
if self.closed:
raise ValueError("write to closed file")
if type(s) is not str:
if not isinstance(s, str):
raise TypeError('must be str, not ' + type(s).__name__)
# See issue #19481
s = str.__str__(s)
s = str.encode(s, self.encoding, self.errors).decode(self.encoding, self.errors)
return self.shell.write(s, self.tags)
class PseudoInputFile(PseudoFile):
def __init__(self, shell, tags, encoding=None):
PseudoFile.__init__(self, shell, tags, encoding)
self._line_buffer = ''
class StdInputFile(StdioFile):
_line_buffer = ''
def readable(self):
return True
@ -495,12 +493,12 @@ class MyHandler(rpc.RPCHandler):
executive = Executive(self)
self.register("exec", executive)
self.console = self.get_remote_proxy("console")
sys.stdin = PseudoInputFile(self.console, "stdin",
iomenu.encoding)
sys.stdout = PseudoOutputFile(self.console, "stdout",
iomenu.encoding)
sys.stderr = PseudoOutputFile(self.console, "stderr",
iomenu.encoding)
sys.stdin = StdInputFile(self.console, "stdin",
iomenu.encoding, iomenu.errors)
sys.stdout = StdOutputFile(self.console, "stdout",
iomenu.encoding, iomenu.errors)
sys.stderr = StdOutputFile(self.console, "stderr",
iomenu.encoding, "backslashreplace")
sys.displayhook = rpc.displayhook
# page help() text to shell.

View File

@ -0,0 +1,3 @@
IDLE no longer fails when write non-encodable characters to stderr. It now
escapes them with a backslash, as the regular Python interpreter. Added the
``errors`` field to the standard streams.