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.
(cherry picked from commit b690a2759e
)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
parent
aa9d5b8ec3
commit
a1f45008f1
|
@ -36,7 +36,7 @@ class RunTest(unittest.TestCase):
|
||||||
self.assertIn('UnhashableException: ex1', tb[10])
|
self.assertIn('UnhashableException: ex1', tb[10])
|
||||||
|
|
||||||
|
|
||||||
# PseudoFile tests.
|
# StdioFile tests.
|
||||||
|
|
||||||
class S(str):
|
class S(str):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -68,14 +68,14 @@ class MockShell:
|
||||||
self.lines = list(lines)[::-1]
|
self.lines = list(lines)[::-1]
|
||||||
|
|
||||||
|
|
||||||
class PseudeInputFilesTest(unittest.TestCase):
|
class StdInputFilesTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_misc(self):
|
def test_misc(self):
|
||||||
shell = MockShell()
|
shell = MockShell()
|
||||||
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
|
f = run.StdInputFile(shell, 'stdin')
|
||||||
self.assertIsInstance(f, io.TextIOBase)
|
self.assertIsInstance(f, io.TextIOBase)
|
||||||
self.assertEqual(f.encoding, 'utf-8')
|
self.assertEqual(f.encoding, 'utf-8')
|
||||||
self.assertIsNone(f.errors)
|
self.assertEqual(f.errors, 'strict')
|
||||||
self.assertIsNone(f.newlines)
|
self.assertIsNone(f.newlines)
|
||||||
self.assertEqual(f.name, '<stdin>')
|
self.assertEqual(f.name, '<stdin>')
|
||||||
self.assertFalse(f.closed)
|
self.assertFalse(f.closed)
|
||||||
|
@ -86,7 +86,7 @@ class PseudeInputFilesTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_unsupported(self):
|
def test_unsupported(self):
|
||||||
shell = MockShell()
|
shell = MockShell()
|
||||||
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
|
f = run.StdInputFile(shell, 'stdin')
|
||||||
self.assertRaises(OSError, f.fileno)
|
self.assertRaises(OSError, f.fileno)
|
||||||
self.assertRaises(OSError, f.tell)
|
self.assertRaises(OSError, f.tell)
|
||||||
self.assertRaises(OSError, f.seek, 0)
|
self.assertRaises(OSError, f.seek, 0)
|
||||||
|
@ -95,7 +95,7 @@ class PseudeInputFilesTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_read(self):
|
def test_read(self):
|
||||||
shell = MockShell()
|
shell = MockShell()
|
||||||
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
|
f = run.StdInputFile(shell, 'stdin')
|
||||||
shell.push(['one\n', 'two\n', ''])
|
shell.push(['one\n', 'two\n', ''])
|
||||||
self.assertEqual(f.read(), 'one\ntwo\n')
|
self.assertEqual(f.read(), 'one\ntwo\n')
|
||||||
shell.push(['one\n', 'two\n', ''])
|
shell.push(['one\n', 'two\n', ''])
|
||||||
|
@ -115,7 +115,7 @@ class PseudeInputFilesTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_readline(self):
|
def test_readline(self):
|
||||||
shell = MockShell()
|
shell = MockShell()
|
||||||
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
|
f = run.StdInputFile(shell, 'stdin')
|
||||||
shell.push(['one\n', 'two\n', 'three\n', 'four\n'])
|
shell.push(['one\n', 'two\n', 'three\n', 'four\n'])
|
||||||
self.assertEqual(f.readline(), 'one\n')
|
self.assertEqual(f.readline(), 'one\n')
|
||||||
self.assertEqual(f.readline(-1), 'two\n')
|
self.assertEqual(f.readline(-1), 'two\n')
|
||||||
|
@ -140,7 +140,7 @@ class PseudeInputFilesTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_readlines(self):
|
def test_readlines(self):
|
||||||
shell = MockShell()
|
shell = MockShell()
|
||||||
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
|
f = run.StdInputFile(shell, 'stdin')
|
||||||
shell.push(['one\n', 'two\n', ''])
|
shell.push(['one\n', 'two\n', ''])
|
||||||
self.assertEqual(f.readlines(), ['one\n', 'two\n'])
|
self.assertEqual(f.readlines(), ['one\n', 'two\n'])
|
||||||
shell.push(['one\n', 'two\n', ''])
|
shell.push(['one\n', 'two\n', ''])
|
||||||
|
@ -161,7 +161,7 @@ class PseudeInputFilesTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_close(self):
|
def test_close(self):
|
||||||
shell = MockShell()
|
shell = MockShell()
|
||||||
f = run.PseudoInputFile(shell, 'stdin', 'utf-8')
|
f = run.StdInputFile(shell, 'stdin')
|
||||||
shell.push(['one\n', 'two\n', ''])
|
shell.push(['one\n', 'two\n', ''])
|
||||||
self.assertFalse(f.closed)
|
self.assertFalse(f.closed)
|
||||||
self.assertEqual(f.readline(), 'one\n')
|
self.assertEqual(f.readline(), 'one\n')
|
||||||
|
@ -171,14 +171,14 @@ class PseudeInputFilesTest(unittest.TestCase):
|
||||||
self.assertRaises(TypeError, f.close, 1)
|
self.assertRaises(TypeError, f.close, 1)
|
||||||
|
|
||||||
|
|
||||||
class PseudeOutputFilesTest(unittest.TestCase):
|
class StdOutputFilesTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_misc(self):
|
def test_misc(self):
|
||||||
shell = MockShell()
|
shell = MockShell()
|
||||||
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
|
f = run.StdOutputFile(shell, 'stdout')
|
||||||
self.assertIsInstance(f, io.TextIOBase)
|
self.assertIsInstance(f, io.TextIOBase)
|
||||||
self.assertEqual(f.encoding, 'utf-8')
|
self.assertEqual(f.encoding, 'utf-8')
|
||||||
self.assertIsNone(f.errors)
|
self.assertEqual(f.errors, 'strict')
|
||||||
self.assertIsNone(f.newlines)
|
self.assertIsNone(f.newlines)
|
||||||
self.assertEqual(f.name, '<stdout>')
|
self.assertEqual(f.name, '<stdout>')
|
||||||
self.assertFalse(f.closed)
|
self.assertFalse(f.closed)
|
||||||
|
@ -189,7 +189,7 @@ class PseudeOutputFilesTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_unsupported(self):
|
def test_unsupported(self):
|
||||||
shell = MockShell()
|
shell = MockShell()
|
||||||
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
|
f = run.StdOutputFile(shell, 'stdout')
|
||||||
self.assertRaises(OSError, f.fileno)
|
self.assertRaises(OSError, f.fileno)
|
||||||
self.assertRaises(OSError, f.tell)
|
self.assertRaises(OSError, f.tell)
|
||||||
self.assertRaises(OSError, f.seek, 0)
|
self.assertRaises(OSError, f.seek, 0)
|
||||||
|
@ -198,16 +198,36 @@ class PseudeOutputFilesTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_write(self):
|
def test_write(self):
|
||||||
shell = MockShell()
|
shell = MockShell()
|
||||||
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
|
f = run.StdOutputFile(shell, 'stdout')
|
||||||
f.write('test')
|
f.write('test')
|
||||||
self.assertEqual(shell.written, [('test', 'stdout')])
|
self.assertEqual(shell.written, [('test', 'stdout')])
|
||||||
shell.reset()
|
shell.reset()
|
||||||
f.write('t\xe8st')
|
f.write('t\xe8\u015b\U0001d599')
|
||||||
self.assertEqual(shell.written, [('t\xe8st', 'stdout')])
|
self.assertEqual(shell.written, [('t\xe8\u015b\U0001d599', 'stdout')])
|
||||||
shell.reset()
|
shell.reset()
|
||||||
|
|
||||||
f.write(S('t\xe8st'))
|
f.write(S('t\xe8\u015b\U0001d599'))
|
||||||
self.assertEqual(shell.written, [('t\xe8st', 'stdout')])
|
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)
|
self.assertEqual(type(shell.written[0][0]), str)
|
||||||
shell.reset()
|
shell.reset()
|
||||||
|
|
||||||
|
@ -221,7 +241,7 @@ class PseudeOutputFilesTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_writelines(self):
|
def test_writelines(self):
|
||||||
shell = MockShell()
|
shell = MockShell()
|
||||||
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
|
f = run.StdOutputFile(shell, 'stdout')
|
||||||
f.writelines([])
|
f.writelines([])
|
||||||
self.assertEqual(shell.written, [])
|
self.assertEqual(shell.written, [])
|
||||||
shell.reset()
|
shell.reset()
|
||||||
|
@ -251,7 +271,7 @@ class PseudeOutputFilesTest(unittest.TestCase):
|
||||||
|
|
||||||
def test_close(self):
|
def test_close(self):
|
||||||
shell = MockShell()
|
shell = MockShell()
|
||||||
f = run.PseudoOutputFile(shell, 'stdout', 'utf-8')
|
f = run.StdOutputFile(shell, 'stdout')
|
||||||
self.assertFalse(f.closed)
|
self.assertFalse(f.closed)
|
||||||
f.write('test')
|
f.write('test')
|
||||||
f.close()
|
f.close()
|
||||||
|
|
|
@ -15,6 +15,7 @@ from idlelib.config import idleConf
|
||||||
|
|
||||||
if idlelib.testing: # Set True by test.test_idle to avoid setlocale.
|
if idlelib.testing: # Set True by test.test_idle to avoid setlocale.
|
||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
|
errors = 'surrogateescape'
|
||||||
else:
|
else:
|
||||||
# Try setting the locale, so that we can find out
|
# Try setting the locale, so that we can find out
|
||||||
# what encoding to use
|
# what encoding to use
|
||||||
|
@ -24,15 +25,9 @@ else:
|
||||||
except (ImportError, locale.Error):
|
except (ImportError, locale.Error):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
locale_decode = 'ascii'
|
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
# On Windows, we could use "mbcs". However, to give the user
|
encoding = 'utf-8'
|
||||||
# a portable encoding name, we need to find the code page
|
errors = 'surrogateescape'
|
||||||
try:
|
|
||||||
locale_encoding = locale.getdefaultlocale()[1]
|
|
||||||
codecs.lookup(locale_encoding)
|
|
||||||
except LookupError:
|
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
# Different things can fail here: the locale module may not be
|
# 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
|
# resulting codeset may be unknown to Python. We ignore all
|
||||||
# these problems, falling back to ASCII
|
# these problems, falling back to ASCII
|
||||||
locale_encoding = locale.nl_langinfo(locale.CODESET)
|
locale_encoding = locale.nl_langinfo(locale.CODESET)
|
||||||
if locale_encoding is None or locale_encoding == '':
|
if locale_encoding:
|
||||||
# situation occurs on macOS
|
codecs.lookup(locale_encoding)
|
||||||
locale_encoding = 'ascii'
|
|
||||||
codecs.lookup(locale_encoding)
|
|
||||||
except (NameError, AttributeError, LookupError):
|
except (NameError, AttributeError, LookupError):
|
||||||
# Try getdefaultlocale: it parses environment variables,
|
# Try getdefaultlocale: it parses environment variables,
|
||||||
# which may give a clue. Unfortunately, getdefaultlocale has
|
# which may give a clue. Unfortunately, getdefaultlocale has
|
||||||
# bugs that can cause ValueError.
|
# bugs that can cause ValueError.
|
||||||
try:
|
try:
|
||||||
locale_encoding = locale.getdefaultlocale()[1]
|
locale_encoding = locale.getdefaultlocale()[1]
|
||||||
if locale_encoding is None or locale_encoding == '':
|
if locale_encoding:
|
||||||
# situation occurs on macOS
|
codecs.lookup(locale_encoding)
|
||||||
locale_encoding = 'ascii'
|
|
||||||
codecs.lookup(locale_encoding)
|
|
||||||
except (ValueError, LookupError):
|
except (ValueError, LookupError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
locale_encoding = locale_encoding.lower()
|
if locale_encoding:
|
||||||
|
encoding = locale_encoding.lower()
|
||||||
encoding = locale_encoding
|
errors = 'strict'
|
||||||
# Encoding is used in multiple files; locale_encoding nowhere.
|
else:
|
||||||
# The only use of 'encoding' below is in _decode as initial value
|
# POSIX locale or macOS
|
||||||
# of deprecated block asking user for encoding.
|
encoding = 'ascii'
|
||||||
# Perhaps use elsewhere should be reviewed.
|
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)
|
coding_re = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)', re.ASCII)
|
||||||
blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII)
|
blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)', re.ASCII)
|
||||||
|
|
|
@ -54,7 +54,7 @@ from idlelib.editor import EditorWindow, fixwordbreaks
|
||||||
from idlelib.filelist import FileList
|
from idlelib.filelist import FileList
|
||||||
from idlelib.outwin import OutputWindow
|
from idlelib.outwin import OutputWindow
|
||||||
from idlelib import rpc
|
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
|
from idlelib.undo import UndoDelegator
|
||||||
|
|
||||||
HOST = '127.0.0.1' # python execution server on localhost loopback
|
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_stderr = sys.stderr
|
||||||
self.save_stdin = sys.stdin
|
self.save_stdin = sys.stdin
|
||||||
from idlelib import iomenu
|
from idlelib import iomenu
|
||||||
self.stdin = PseudoInputFile(self, "stdin", iomenu.encoding)
|
self.stdin = StdInputFile(self, "stdin",
|
||||||
self.stdout = PseudoOutputFile(self, "stdout", iomenu.encoding)
|
iomenu.encoding, iomenu.errors)
|
||||||
self.stderr = PseudoOutputFile(self, "stderr", iomenu.encoding)
|
self.stdout = StdOutputFile(self, "stdout",
|
||||||
self.console = PseudoOutputFile(self, "console", iomenu.encoding)
|
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:
|
if not use_subprocess:
|
||||||
sys.stdout = self.stdout
|
sys.stdout = self.stdout
|
||||||
sys.stderr = self.stderr
|
sys.stderr = self.stderr
|
||||||
|
|
|
@ -401,17 +401,22 @@ class MyRPCServer(rpc.RPCServer):
|
||||||
|
|
||||||
# Pseudofiles for shell-remote communication (also used in pyshell)
|
# 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.shell = shell
|
||||||
self.tags = tags
|
self.tags = tags
|
||||||
self._encoding = encoding
|
self._encoding = encoding
|
||||||
|
self._errors = errors
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def encoding(self):
|
def encoding(self):
|
||||||
return self._encoding
|
return self._encoding
|
||||||
|
|
||||||
|
@property
|
||||||
|
def errors(self):
|
||||||
|
return self._errors
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return '<%s>' % self.tags
|
return '<%s>' % self.tags
|
||||||
|
@ -420,7 +425,7 @@ class PseudoFile(io.TextIOBase):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class PseudoOutputFile(PseudoFile):
|
class StdOutputFile(StdioFile):
|
||||||
|
|
||||||
def writable(self):
|
def writable(self):
|
||||||
return True
|
return True
|
||||||
|
@ -428,19 +433,12 @@ class PseudoOutputFile(PseudoFile):
|
||||||
def write(self, s):
|
def write(self, s):
|
||||||
if self.closed:
|
if self.closed:
|
||||||
raise ValueError("write to closed file")
|
raise ValueError("write to closed file")
|
||||||
if type(s) is not str:
|
s = str.encode(s, self.encoding, self.errors).decode(self.encoding, self.errors)
|
||||||
if not isinstance(s, str):
|
|
||||||
raise TypeError('must be str, not ' + type(s).__name__)
|
|
||||||
# See issue #19481
|
|
||||||
s = str.__str__(s)
|
|
||||||
return self.shell.write(s, self.tags)
|
return self.shell.write(s, self.tags)
|
||||||
|
|
||||||
|
|
||||||
class PseudoInputFile(PseudoFile):
|
class StdInputFile(StdioFile):
|
||||||
|
_line_buffer = ''
|
||||||
def __init__(self, shell, tags, encoding=None):
|
|
||||||
PseudoFile.__init__(self, shell, tags, encoding)
|
|
||||||
self._line_buffer = ''
|
|
||||||
|
|
||||||
def readable(self):
|
def readable(self):
|
||||||
return True
|
return True
|
||||||
|
@ -495,12 +493,12 @@ class MyHandler(rpc.RPCHandler):
|
||||||
executive = Executive(self)
|
executive = Executive(self)
|
||||||
self.register("exec", executive)
|
self.register("exec", executive)
|
||||||
self.console = self.get_remote_proxy("console")
|
self.console = self.get_remote_proxy("console")
|
||||||
sys.stdin = PseudoInputFile(self.console, "stdin",
|
sys.stdin = StdInputFile(self.console, "stdin",
|
||||||
iomenu.encoding)
|
iomenu.encoding, iomenu.errors)
|
||||||
sys.stdout = PseudoOutputFile(self.console, "stdout",
|
sys.stdout = StdOutputFile(self.console, "stdout",
|
||||||
iomenu.encoding)
|
iomenu.encoding, iomenu.errors)
|
||||||
sys.stderr = PseudoOutputFile(self.console, "stderr",
|
sys.stderr = StdOutputFile(self.console, "stderr",
|
||||||
iomenu.encoding)
|
iomenu.encoding, "backslashreplace")
|
||||||
|
|
||||||
sys.displayhook = rpc.displayhook
|
sys.displayhook = rpc.displayhook
|
||||||
# page help() text to shell.
|
# page help() text to shell.
|
||||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue