Skip to content

Commit 23f16c7

Browse files
gh-66172: Don't let a corrupt config file prevent IDLE from starting
If a user configuration file cannot be parsed, rename it with a ".bad" suffix, use default settings, and warn the user with a message box.
1 parent 2303eea commit 23f16c7

4 files changed

Lines changed: 58 additions & 2 deletions

File tree

Lib/idlelib/config.py

Lines changed: 24 additions & 2 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

@@ -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 as err:
802+
# Move an unparsable 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 unparsable 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 parsed. '
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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,31 @@ 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(self):
316+
# gh-66172: an unparsable user file is renamed, not fatal, not lost.
317+
conf = self.new_config(_utest=True)
318+
tmpdir = tempfile.TemporaryDirectory()
319+
self.addCleanup(tmpdir.cleanup)
320+
badpath = os.path.join(tmpdir.name, 'config-extensions.cfg')
321+
with open(badpath, '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(badpath)
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, badpath)
332+
# The bad file is moved aside, not left to be overwritten or deleted.
333+
self.assertFalse(os.path.exists(badpath))
334+
with open(badpath + '.bad') as f:
335+
self.assertEqual(f.read(), 'enable=1\n')
336+
message = conf.file_load_error_message()
337+
self.assertIn(badpath, message)
338+
self.assertIn('MissingSectionHeaderError', message)
339+
315340
def test_save_user_cfg_files(self):
316341
conf = self.mock_config()
317342

Lib/idlelib/pyshell.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,6 +1610,12 @@ def main():
16101610
from idlelib.run import fix_scaling
16111611
fix_scaling(root)
16121612

1613+
# Warn about configuration files that could not be parsed (gh-66172).
1614+
config_error = idleConf.file_load_error_message()
1615+
if config_error:
1616+
messagebox.showwarning('IDLE Configuration Warning', config_error,
1617+
parent=root)
1618+
16131619
# set application icon
16141620
icondir = os.path.join(os.path.dirname(__file__), 'Icons')
16151621
if system() == 'Windows':
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)