Skip to content

Commit cf001f2

Browse files
authored
Merge branch 'main' into inspect-signature-fast-params
2 parents dc3d4ff + fd0970c commit cf001f2

5 files changed

Lines changed: 102 additions & 10 deletions

File tree

Lib/idlelib/config.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"""
2626
# TODOs added Oct 2014, tjr
2727

28-
from configparser import ConfigParser
28+
from configparser import ConfigParser, Error as ConfigParserError
2929
import os
3030
import sys
3131

@@ -74,7 +74,7 @@ def GetOptionList(self, section):
7474
def Load(self):
7575
"Load the configuration file from disk."
7676
if self.file and os.path.exists(self.file):
77-
with open(self.file, encoding='utf-8', errors='replace') as f:
77+
with open(self.file, encoding='utf-8') as f:
7878
self.read_file(f)
7979

8080
class IdleUserConfParser(IdleConfParser):
@@ -159,6 +159,7 @@ def __init__(self, _utest=False):
159159
self.defaultCfg = {}
160160
self.userCfg = {}
161161
self.cfg = {} # TODO use to select userCfg vs defaultCfg
162+
self.file_load_errors = [] # (file, error) for unparsable cfg files.
162163

163164
# See https://bugs.python.org/issue4630#msg356516 for following.
164165
# self.blink_off_time = <first editor text>['insertofftime']
@@ -795,7 +796,28 @@ def LoadCfgFiles(self):
795796
"Load all configuration files."
796797
for key in self.defaultCfg:
797798
self.defaultCfg[key].Load()
798-
self.userCfg[key].Load() #same keys
799+
try:
800+
self.userCfg[key].Load() # same keys
801+
except (ConfigParserError, UnicodeDecodeError) as err:
802+
# Move an invalid user file aside instead of losing it
803+
# or failing to start (gh-66172).
804+
file = self.userCfg[key].file
805+
self.file_load_errors.append((file, err))
806+
try:
807+
os.replace(file, file + '.bad')
808+
except OSError:
809+
pass
810+
811+
def file_load_error_message(self):
812+
"Return a warning about invalid config files, or None."
813+
if not self.file_load_errors:
814+
return None
815+
files = '\n'.join(
816+
f' {file}:\n {type(err).__name__}: {str(err).splitlines()[0]}'
817+
for file, err in self.file_load_errors)
818+
return ('The following IDLE configuration files could not be read. '
819+
'They were renamed by appending ".bad", and default settings '
820+
'are used instead:\n\n' + files)
799821

800822
def SaveUserCfgFiles(self):
801823
"Write all loaded user configuration files to disk."

Lib/idlelib/idle_test/test_config.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,49 @@ def test_load_cfg_files(self):
312312
eq(conf.userCfg['foo'].Get('Foo Bar', 'foo'), 'newbar')
313313
eq(conf.userCfg['foo'].GetOptionList('Foo Bar'), ['foo'])
314314

315+
def test_load_cfg_files_bad_format(self):
316+
# gh-66172: rename an unparsable user file and save the exception.
317+
conf = self.new_config(_utest=True)
318+
tmpdir = tempfile.TemporaryDirectory()
319+
self.addCleanup(tmpdir.cleanup)
320+
confpath = os.path.join(tmpdir.name, 'config-extensions.cfg')
321+
with open(confpath, 'w') as f:
322+
f.write('enable=1\n') # No section header.
323+
conf.defaultCfg['foo'] = config.IdleConfParser('') # Empty, valid.
324+
conf.userCfg['foo'] = config.IdleUserConfParser(confpath)
325+
326+
self.assertIsNone(conf.file_load_error_message())
327+
conf.LoadCfgFiles() # Must not raise.
328+
329+
self.assertEqual(len(conf.file_load_errors), 1)
330+
file, err = conf.file_load_errors[0]
331+
self.assertEqual(file, confpath)
332+
# The bad file is moved aside, not left to be overwritten or deleted.
333+
self.assertFalse(os.path.exists(confpath))
334+
with open(confpath + '.bad') as f:
335+
self.assertEqual(f.read(), 'enable=1\n')
336+
message = conf.file_load_error_message()
337+
self.assertIn(confpath, message)
338+
self.assertIn('MissingSectionHeaderError', message)
339+
340+
def test_load_cfg_files_bad_encoding(self):
341+
# gh-66172: a file that is not valid UTF-8 is handled like a bad parse.
342+
conf = self.new_config(_utest=True)
343+
tmpdir = tempfile.TemporaryDirectory()
344+
self.addCleanup(tmpdir.cleanup)
345+
confpath = os.path.join(tmpdir.name, 'config-main.cfg')
346+
with open(confpath, 'wb') as f:
347+
f.write(b'[Section]\nkey = \xff\n') # Invalid UTF-8.
348+
conf.defaultCfg['foo'] = config.IdleConfParser('') # Empty, valid.
349+
conf.userCfg['foo'] = config.IdleUserConfParser(confpath)
350+
351+
conf.LoadCfgFiles() # Must not raise.
352+
353+
self.assertEqual(len(conf.file_load_errors), 1)
354+
self.assertIsInstance(conf.file_load_errors[0][1], UnicodeDecodeError)
355+
self.assertFalse(os.path.exists(confpath))
356+
self.assertTrue(os.path.exists(confpath + '.bad'))
357+
315358
def test_save_user_cfg_files(self):
316359
conf = self.mock_config()
317360

Lib/idlelib/pyshell.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1612,6 +1612,12 @@ def main():
16121612
root.withdraw()
16131613
fix_scaling(root)
16141614

1615+
# Warn about configuration files that could not be parsed (gh-66172).
1616+
config_error = idleConf.file_load_error_message()
1617+
if config_error:
1618+
messagebox.showwarning('IDLE Configuration Warning', config_error,
1619+
parent=root)
1620+
16151621
# set application icon
16161622
icondir = os.path.join(os.path.dirname(__file__), 'Icons')
16171623
if system() == 'Windows':

Lib/test/test_nturl2path.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import os
12
import sys
23
import unittest
34
import urllib.parse
45

6+
from test.support import os_helper
57
from test.support import warnings_helper
68

79

@@ -36,7 +38,6 @@ def test_pathname2url(self):
3638
self.assertEqual(fn('C:\\a\\b.c\\'), '///C:/a/b.c/')
3739
self.assertEqual(fn('C:\\a\\\\b.c'), '///C:/a//b.c')
3840
self.assertEqual(fn('C:\\a\\b%#c'), '///C:/a/b%25%23c')
39-
self.assertEqual(fn('C:\\a\\b\xe9'), '///C:/a/b%C3%A9')
4041
self.assertEqual(fn('C:\\foo\\bar\\spam.foo'), "///C:/foo/bar/spam.foo")
4142
# NTFS alternate data streams
4243
self.assertEqual(fn('C:\\foo:bar'), '///C:/foo%3Abar')
@@ -47,7 +48,7 @@ def test_pathname2url(self):
4748
self.assertEqual(fn("\\\\\\folder\\test\\"), '///folder/test/')
4849
self.assertEqual(fn('\\\\some\\share\\'), '//some/share/')
4950
self.assertEqual(fn('\\\\some\\share\\a\\b.c'), '//some/share/a/b.c')
50-
self.assertEqual(fn('\\\\some\\share\\a\\b%#c\xe9'), '//some/share/a/b%25%23c%C3%A9')
51+
self.assertEqual(fn('\\\\some\\share\\a\\b%#c'), '//some/share/a/b%25%23c')
5152
# Alternate path separator
5253
self.assertEqual(fn('C:/a/b.c'), '///C:/a/b.c')
5354
self.assertEqual(fn('//some/share/a/b.c'), '//some/share/a/b.c')
@@ -60,14 +61,28 @@ def test_pathname2url(self):
6061
for url in urls:
6162
self.assertEqual(fn(nturl2path.url2pathname(url)), url)
6263

64+
@unittest.skipUnless(os_helper.FS_NONASCII, 'need os_helper.FS_NONASCII')
65+
def test_pathname2url_nonascii(self):
66+
encoding = sys.getfilesystemencoding()
67+
errors = sys.getfilesystemencodeerrors()
68+
char = os_helper.FS_NONASCII
69+
quoted = urllib.parse.quote(char, encoding=encoding, errors=errors)
70+
self.assertEqual(nturl2path.pathname2url(f'C:\\a\\b{char}'),
71+
'///C:/a/b' + quoted)
72+
self.assertEqual(nturl2path.pathname2url(f'\\\\some\\share\\a\\b{char}'),
73+
'//some/share/a/b' + quoted)
74+
75+
@unittest.skipUnless(os_helper.TESTFN_UNDECODABLE,
76+
'need os_helper.TESTFN_UNDECODABLE')
6377
def test_pathname2url_surrogates(self):
6478
# gh-156713: the filesystem encoding and error handler are used,
6579
# so that paths containing surrogate characters can be converted.
6680
encoding = sys.getfilesystemencoding()
6781
errors = sys.getfilesystemencodeerrors()
68-
tail = urllib.parse.quote('a\udcff', encoding=encoding, errors=errors)
69-
self.assertEqual(nturl2path.pathname2url('C:\\a\udcff'),
70-
'///C:/' + tail)
82+
path = os.fsdecode(os_helper.TESTFN_UNDECODABLE)
83+
url = urllib.parse.quote(path, encoding=encoding, errors=errors)
84+
self.assertEqual(nturl2path.pathname2url('C:\\' + path),
85+
'///C:/' + url)
7186

7287
def test_url2pathname(self):
7388
fn = nturl2path.url2pathname
@@ -114,14 +129,17 @@ def test_url2pathname(self):
114129
self.assertEqual(fn(nturl2path.pathname2url(path)), path)
115130

116131

132+
@unittest.skipUnless(os_helper.TESTFN_UNDECODABLE,
133+
'need os_helper.TESTFN_UNDECODABLE')
117134
def test_url2pathname_surrogates(self):
118135
# gh-156713: the filesystem encoding and error handler are used, so
119136
# that URLs containing percent-encoded surrogates can be converted.
120137
encoding = sys.getfilesystemencoding()
121138
errors = sys.getfilesystemencodeerrors()
122-
url = urllib.parse.quote('a\udcff', encoding=encoding, errors=errors)
139+
path = os.fsdecode(os_helper.TESTFN_UNDECODABLE)
140+
url = urllib.parse.quote(path, encoding=encoding, errors=errors)
123141
self.assertEqual(nturl2path.url2pathname('///C:/' + url),
124-
'C:\\a\udcff')
142+
'C:\\' + path)
125143

126144

127145
if __name__ == '__main__':
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
IDLE no longer fails to start when a user configuration file is corrupt.
2+
The unparsable file is renamed with a ".bad" suffix, default settings are
3+
used instead, and a warning lists the affected files.

0 commit comments

Comments
 (0)