Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions Lib/idlelib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"""
# TODOs added Oct 2014, tjr

from configparser import ConfigParser
from configparser import ConfigParser, Error as ConfigParserError
import os
import sys

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

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

# See https://bugs.python.org/issue4630#msg356516 for following.
# self.blink_off_time = <first editor text>['insertofftime']
Expand Down Expand Up @@ -795,7 +796,28 @@ def LoadCfgFiles(self):
"Load all configuration files."
for key in self.defaultCfg:
self.defaultCfg[key].Load()
self.userCfg[key].Load() #same keys
try:
self.userCfg[key].Load() # same keys
except (ConfigParserError, UnicodeDecodeError) as err:
# Move an invalid user file aside instead of losing it
# or failing to start (gh-66172).
file = self.userCfg[key].file
self.file_load_errors.append((file, err))
try:
os.replace(file, file + '.bad')
except OSError:
pass

def file_load_error_message(self):
"Return a warning about invalid config files, or None."
if not self.file_load_errors:
return None
files = '\n'.join(
f' {file}:\n {type(err).__name__}: {str(err).splitlines()[0]}'
for file, err in self.file_load_errors)
return ('The following IDLE configuration files could not be read. '
'They were renamed by appending ".bad", and default settings '
'are used instead:\n\n' + files)

def SaveUserCfgFiles(self):
"Write all loaded user configuration files to disk."
Expand Down
43 changes: 43 additions & 0 deletions Lib/idlelib/idle_test/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,49 @@ def test_load_cfg_files(self):
eq(conf.userCfg['foo'].Get('Foo Bar', 'foo'), 'newbar')
eq(conf.userCfg['foo'].GetOptionList('Foo Bar'), ['foo'])

def test_load_cfg_files_bad(self):
# gh-66172: an unparsable user file is renamed, not fatal, not lost.
conf = self.new_config(_utest=True)
tmpdir = tempfile.TemporaryDirectory()
self.addCleanup(tmpdir.cleanup)
badpath = os.path.join(tmpdir.name, 'config-extensions.cfg')
with open(badpath, 'w') as f:
f.write('enable=1\n') # No section header.
conf.defaultCfg['foo'] = config.IdleConfParser('') # Empty, valid.
conf.userCfg['foo'] = config.IdleUserConfParser(badpath)

self.assertIsNone(conf.file_load_error_message())
conf.LoadCfgFiles() # Must not raise.

self.assertEqual(len(conf.file_load_errors), 1)
file, err = conf.file_load_errors[0]
self.assertEqual(file, badpath)
# The bad file is moved aside, not left to be overwritten or deleted.
self.assertFalse(os.path.exists(badpath))
with open(badpath + '.bad') as f:
self.assertEqual(f.read(), 'enable=1\n')
message = conf.file_load_error_message()
self.assertIn(badpath, message)
self.assertIn('MissingSectionHeaderError', message)

def test_load_cfg_files_bad_encoding(self):
# gh-66172: a file that is not valid UTF-8 is handled like a bad parse.
conf = self.new_config(_utest=True)
tmpdir = tempfile.TemporaryDirectory()
self.addCleanup(tmpdir.cleanup)
badpath = os.path.join(tmpdir.name, 'config-main.cfg')
with open(badpath, 'wb') as f:
f.write(b'[Section]\nkey = \xff\n') # Invalid UTF-8.
conf.defaultCfg['foo'] = config.IdleConfParser('') # Empty, valid.
conf.userCfg['foo'] = config.IdleUserConfParser(badpath)

conf.LoadCfgFiles() # Must not raise.

self.assertEqual(len(conf.file_load_errors), 1)
self.assertIsInstance(conf.file_load_errors[0][1], UnicodeDecodeError)
self.assertFalse(os.path.exists(badpath))
self.assertTrue(os.path.exists(badpath + '.bad'))

def test_save_user_cfg_files(self):
conf = self.mock_config()

Expand Down
6 changes: 6 additions & 0 deletions Lib/idlelib/pyshell.py
Original file line number Diff line number Diff line change
Expand Up @@ -1610,6 +1610,12 @@ def main():
from idlelib.run import fix_scaling
fix_scaling(root)

# Warn about configuration files that could not be parsed (gh-66172).
config_error = idleConf.file_load_error_message()
if config_error:
messagebox.showwarning('IDLE Configuration Warning', config_error,
parent=root)

# set application icon
icondir = os.path.join(os.path.dirname(__file__), 'Icons')
if system() == 'Windows':
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
IDLE no longer fails to start when a user configuration file is corrupt.
The unparsable file is renamed with a ".bad" suffix, default settings are
used instead, and a warning lists the affected files.
Loading