UserSettings get support for .ini (config files) in addition to JSON files

This commit is contained in:
PySimpleGUI 2021-10-11 08:39:59 -04:00
parent 3f4f38a849
commit d14f110a80
1 changed files with 293 additions and 48 deletions

View File

@ -1,6 +1,6 @@
#!/usr/bin/python3 #!/usr/bin/python3
version = __version__ = "4.49.0.9 Unreleased" version = __version__ = "4.49.0.10 Unreleased"
_change_log = """ _change_log = """
@ -27,6 +27,20 @@ _change_log = """
Removed print when patch of 8.6.9 ttk treeview code is patched Removed print when patch of 8.6.9 ttk treeview code is patched
4.49.0.9 4.49.0.9
Removed print when bind callback happens. Was there for debugging and forgot to remove. Removed print when bind callback happens. Was there for debugging and forgot to remove.
4.49.0.10
Phase 1 Support of config files for UserSettings APIs - load, read/modify, save are done. The basic version is done.
This first phase supports them only via the object interface
Read access: settings[section][key]
Modify existing section and key: settings[section][key] = new_value
Create a new key in an existing section: settings[section][new_key] = new_value
Create a new section and key: settings[new_section][new_key] = new_value
Get a section as a dictionary-like object: settings[section]
This object can be modified and it will modify the settings file as a result.
Has a get and set method just like a normal setting.
Delete a section: settings.delete_section(section)
Save the INI file: settings.save()
Option to convert bools and None. Normally stored as strings. Will convert them to Python True, False, None automatically (on by default)
""" """
__version__ = version.split()[0] # For PEP 396 and PEP 345 __version__ = version.split()[0] # For PEP 396 and PEP 345
@ -178,6 +192,7 @@ import threading
import itertools import itertools
import os import os
import json import json
import configparser
import queue import queue
try: try:
@ -19189,18 +19204,22 @@ class UserSettings:
# to access the user settings without diarectly using the UserSettings class # to access the user settings without diarectly using the UserSettings class
_default_for_function_interface = None # type: UserSettings _default_for_function_interface = None # type: UserSettings
def __init__(self, filename=None, path=None, silent_on_error=False, autosave=True): def __init__(self, filename=None, path=None, silent_on_error=False, autosave=True, use_config_file=False, retain_config_comments=True, convert_bools_and_none=True):
""" """
User Settings User Settings
:param filename: The name of the file to use. Can be a full path and filename or just filename :param filename: The name of the file to use. Can be a full path and filename or just filename
:type filename: (str or None) :type filename: (str or None)
:param path: The folder that the settings file will be stored in. Do not include the filename. :param path: The folder that the settings file will be stored in. Do not include the filename.
:type path: (str or None) :type path: (str or None)
:param silent_on_error: If True errors will not be reported :param silent_on_error: If True errors will not be reported
:type silent_on_error: (bool) :type silent_on_error: (bool)
:param autosave: If True the settings file is saved after every update :param autosave: If True the settings file is saved after every update
:type autosave: (bool) :type autosave: (bool)
:param use_config_file: If True then the file format will be a config.ini rather than json
:type use_config_file: (bool)
:param retain_config_comments: Not yet implemented
:type retain_config_comments: (bool)
""" """
self.path = path self.path = path
@ -19210,9 +19229,131 @@ class UserSettings:
self.default_value = None self.default_value = None
self.silent_on_error = silent_on_error self.silent_on_error = silent_on_error
self.autosave = autosave self.autosave = autosave
self.use_config_file = use_config_file
self.retain_config_comments = retain_config_comments
self.convert_bools = convert_bools_and_none
if use_config_file:
self.config = configparser.ConfigParser()
self.config.optionxform = str
# self.config_dict = {}
self.section_class_dict = {} # type: dict[self._SectionDict]
if filename is not None or path is not None: if filename is not None or path is not None:
self.load(filename=filename, path=path) self.load(filename=filename, path=path)
########################################################################################################
## FIRST is the SectionDict helper class
########################################################################################################
class _SectionDict:
item_count = 0
def __init__(self, section_name, section_dict, config, user_settings_parent): # (str, Dict, configparser.ConfigParser)
self.section_name = section_name
self.section_dict = section_dict # type: Dict
self.new_section = False
self.config = config # type: configparser.ConfigParser
self.user_settings_parent = user_settings_parent # type: UserSettings
UserSettings._SectionDict.item_count += 1
if self.user_settings_parent.convert_bools:
for key, value in self.section_dict.items():
if value == 'True':
value = True
self.section_dict[key] = value
elif value == 'False':
value = False
self.section_dict[key] = value
elif value == 'None':
value = None
self.section_dict[key] = value
# print(f'++++++ making a new SectionDict with name = {section_name}')
def __repr__(self):
"""
Converts the settings dictionary into a string for easy display
:return: the dictionary as a string
:rtype: (str)
"""
# return '{} :\n {}'.format(self.section_name, pprint.pformat(self.section_dict))
return '{} :\n {}\n'.format(self.section_name, self.section_dict)
def get(self, key, default=None):
"""
Returns the value of a specified setting. If the setting is not found in the settings dictionary, then
the user specified default value will be returned. It no default is specified and nothing is found, then
the "default value" is returned. This default can be specified in this call, or previously defined
by calling set_default. If nothing specified now or previously, then None is returned as default.
:param key: Key used to lookup the setting in the settings dictionary
:type key: (Any)
:param default: Value to use should the key not be found in the dictionary
:type default: (Any)
:return: Value of specified settings
:rtype: (Any)
"""
value = self.section_dict.get(key, default)
if self.user_settings_parent.convert_bools:
if value == 'True':
value = True
elif value == 'False':
value = False
return value
def set(self, key, value):
value = str(value) # all values must be strings
if self.new_section:
self.config.add_section(self.section_name)
self.new_section = False
self.config.set(section=self.section_name, option=key, value=value)
self.section_dict[key] = value
if self.user_settings_parent.autosave:
self.user_settings_parent.save()
def delete_section(self):
# print(f'** Section Dict deleting section = {self.section_name}')
self.config.remove_section(section=self.section_name)
def __getitem__(self, item):
# print('*** In SectionDict Get ***')
return self.get(item)
def __setitem__(self, item, value):
"""
Enables setting a setting by using [ ] notation like a dictionary.
Your code will have this kind of design pattern:
settings = sg.UserSettings()
settings[item] = value
:param item: The key for the setting to change. Needs to be a hashable type. Basically anything but a list
:type item: Any
:param value: The value to set the setting to
:type value: Any
"""
# print(f'*** In SectionDict SET *** item = {item} value = {value}')
self.set(item, value)
self.section_dict[item] = value
def __delitem__(self, item):
"""
Delete an individual user setting. This is the same as calling delete_entry. The syntax
for deleting the item using this manner is:
del settings['entry']
:param item: The key for the setting to delete
:type item: Any
"""
# print(f'** In SectionDict delete! section name = {self.section_name} item = {item} ')
self.config.remove_option(section=self.section_name, option=item)
del self.section_dict[item]
if self.user_settings_parent.autosave:
self.user_settings_parent.save()
# self.section_dict.pop(item)
# print(f'id of section_dict = {id(self.section_dict)} id of self = {id(self)} item count = {self.item_count}')
# print(self.section_dict)
########################################################################################################
def __repr__(self): def __repr__(self):
""" """
Converts the settings dictionary into a string for easy display Converts the settings dictionary into a string for easy display
@ -19220,8 +19361,18 @@ class UserSettings:
:return: the dictionary as a string :return: the dictionary as a string
:rtype: (str) :rtype: (str)
""" """
return pprint.pformat(self.dict) if not self.use_config_file:
return str(self.dict) # previouisly returned just a string version of the dictionary return pprint.pformat(self.dict)
else:
rvalue = '-------------------- Settings ----------------------\n'
for section in self.section_class_dict.keys():
rvalue += str(self.section_class_dict[section])
rvalue += '\n-------------------- Settings End----------------------\n'
return rvalue
# return str(self.dict) # previouisly returned just a string version of the dictionary
def set_default_value(self, default): def set_default_value(self, default):
""" """
@ -19251,7 +19402,10 @@ class UserSettings:
elif self.filename is not None: elif self.filename is not None:
filename = self.filename filename = self.filename
else: else:
filename = os.path.splitext(os.path.basename(sys.modules["__main__"].__file__))[0] + '.json' if not self.use_config_file:
filename = os.path.splitext(os.path.basename(sys.modules["__main__"].__file__))[0] + '.json'
else:
filename = os.path.splitext(os.path.basename(sys.modules["__main__"].__file__))[0] + '.ini'
if path is None: if path is None:
if self.path is not None: if self.path is not None:
@ -19309,11 +19463,40 @@ class UserSettings:
:return: The full pathname of the settings file that has both the path and filename combined. :return: The full pathname of the settings file that has both the path and filename combined.
:rtype: (str) :rtype: (str)
""" """
if filename is not None or path is not None or (filename is None and path is None): if filename is not None or path is not None or (filename is None and path is None and self.full_filename is None):
self.set_location(filename=filename, path=path) self.set_location(filename=filename, path=path)
self.read() self.read()
return self.full_filename return self.full_filename
def merge_comments_from_file(self, full_filename):
print('--- merging comments -----')
merged_lines = []
with open(full_filename, 'r') as f:
new_file_contents = f.readlines()
current_section = ''
for line in new_file_contents:
if len(line) == 0: # skip blank lines
merged_lines.append(line)
continue
if line[0] == '[': # if a new section
current_section = line[:line.index(']')]
merged_lines.append(line)
continue
if len(line.lstrip()):
if line.lstrip()[0] == '#': # if a comment line, save it
merged_lines.append(line)
# Process a line with an = in it
try:
key = line[:line.index('=')]
merged_lines.append(line)
except:
merged_lines.append(line)
print('--- merging complete ----')
print(*merged_lines)
def save(self, filename=None, path=None): def save(self, filename=None, path=None):
""" """
Saves the current settings dictionary. If a filename or path is specified in the call, then it will override any Saves the current settings dictionary. If a filename or path is specified in the call, then it will override any
@ -19332,13 +19515,21 @@ class UserSettings:
if not os.path.exists(self.path): if not os.path.exists(self.path):
os.makedirs(self.path) os.makedirs(self.path)
with open(self.full_filename, 'w') as f: with open(self.full_filename, 'w') as f:
json.dump(self.dict, f) if not self.use_config_file:
json.dump(self.dict, f)
else:
self.config.write(f)
except Exception as e: except Exception as e:
if not self.silent_on_error: if not self.silent_on_error:
print('*** Error saving settings to file:***\n', self.full_filename, e) _error_popup_with_traceback('UserSettings.save error', '*** UserSettings.save() Error saving settings to file:***\n', self.full_filename, e)
print(_create_error_message())
# if self.use_config_file and self.retain_config_comments:
# self.merge_comments_from_file(self.full_filename)
return self.full_filename return self.full_filename
def load(self, filename=None, path=None): def load(self, filename=None, path=None):
""" """
Specifies the path and filename to use for the settings and reads the contents of the file. Specifies the path and filename to use for the settings and reads the contents of the file.
@ -19376,8 +19567,8 @@ class UserSettings:
os.remove(self.full_filename) os.remove(self.full_filename)
except Exception as e: except Exception as e:
if not self.silent_on_error: if not self.silent_on_error:
print('*** User settings delete filename warning ***\n', e) _error_popup_with_traceback('UserSettings delete_file warning ***', 'Exception trying to perform os.remove', e)
print(_create_error_message()) # print(_create_error_message())
self.dict = {} self.dict = {}
def write_new_dictionary(self, settings_dict): def write_new_dictionary(self, settings_dict):
@ -19392,10 +19583,24 @@ class UserSettings:
self.dict = settings_dict self.dict = settings_dict
self.save() self.save()
# def as_dict(config):
# """
# Converts a ConfigParser object into a dictionary.
#
# The resulting dictionary has sections as keys which point to a dict of the
# sections options as key => value pairs.
# """
# the_dict = {}
# for section in config.sections():
# the_dict[section] = {}
# for key, val in config.items(section):
# the_dict[section][key] = val
# return the_dict
def read(self): def read(self):
""" """
Reads settings file and returns the dictionary. Reads settings file and returns the dictionary.
If you have anything changed in an existing settings dictionary, you will lose your changes.
:return: settings dictionary :return: settings dictionary
:rtype: (dict) :rtype: (dict)
""" """
@ -19404,11 +19609,26 @@ class UserSettings:
try: try:
if os.path.exists(self.full_filename): if os.path.exists(self.full_filename):
with open(self.full_filename, 'r') as f: with open(self.full_filename, 'r') as f:
self.dict = json.load(f) if not self.use_config_file: # if using json
self.dict = json.load(f)
else: # if using a config file
self.config.read_file(f)
# Make a dictionary of SectionDict classses. Keys are the config.sections().
self.section_class_dict = {}
for section in self.config.sections():
section_dict = dict(self.config[section])
self.section_class_dict[section] = self._SectionDict(section, section_dict, self.config, self)
self.dict = self.section_class_dict
self.config_sections = self.config.sections()
# self.config_dict = {section_name : dict(self.config[section_name]) for section_name in self.config.sections()}
if self.retain_config_comments:
self.config_file_contents = f.readlines()
except Exception as e: except Exception as e:
if not self.silent_on_error: if not self.silent_on_error:
print('*** Error reading settings from file: ***\n', self.full_filename, e) _error_popup_with_traceback('User settings read warning', 'Error reading settings from file', self.full_filename, e)
print(_create_error_message()) # print('*** UserSettings.read - Error reading settings from file: ***\n', self.full_filename, e)
# print(_create_error_message())
return self.dict return self.dict
@ -19426,7 +19646,7 @@ class UserSettings:
return True return True
return False return False
def delete_entry(self, key): def delete_entry(self, key, section=None):
""" """
Deletes an individual entry. If no filename has been specified up to this point, Deletes an individual entry. If no filename has been specified up to this point,
then a default filename will be used. then a default filename will be used.
@ -19437,28 +19657,46 @@ class UserSettings:
""" """
if self.full_filename is None: if self.full_filename is None:
self.set_location() self.set_location()
self.read() self.read()
if key in self.dict: if not self.use_config_file: # Is using JSON file
del self.dict[key] if key in self.dict:
if self.autosave: del self.dict[key]
self.save() if self.autosave:
self.save()
else:
if not self.silent_on_error:
print('*** Warning - key ', key, ' not found in settings ***\n')
print(_create_error_message())
else: else:
if not self.silent_on_error: if section is not None:
print('*** Warning - key ', key, ' not found in settings ***\n') section_dict = self.get(section)
print(_create_error_message()) # print(f'** Trying to delete an entry with a config file in use ** id of section_dict = {id(section_dict)}')
# section_dict = self.section_class_dict[section]
del self.get(section)[key]
# del section_dict[key]
# del section_dict[key]
def delete_section(self, section):
if not self.use_config_file:
return
section_dict = self.section_class_dict.get(section, None)
section_dict.delete_section()
del self.section_class_dict[section]
if self.autosave:
self.save()
def set(self, key, value): def set(self, key, value):
""" """
Sets an individual setting to the specified value. If no filename has been specified up to this point, Sets an individual setting to the specified value. If no filename has been specified up to this point,
then a default filename will be used. then a default filename will be used.
After value has been modified, the settings file is written to disk. After value has been modified, the settings file is written to disk.
Note that this call is not value for a config file normally. If it is, then the key is assumed to be the
Section key and the value written will be the default value.
:param key: Setting to be saved. Can be any valid dictionary key type :param key: Setting to be saved. Can be any valid dictionary key type
:type key: (Any) :type key: (Any)
:param value: Value to save as the setting's value. Can be anything :param value: Value to save as the setting's value. Can be anything
:type value: (Any) :type value: (Any)
:param autosave: If True then the value will be saved to the file
:type autosave: (bool)
:return: value that key was set to :return: value that key was set to
:rtype: (Any) :rtype: (Any)
""" """
@ -19466,9 +19704,13 @@ class UserSettings:
if self.full_filename is None: if self.full_filename is None:
self.set_location() self.set_location()
# if not autosaving, then don't read the file or else will lose changes # if not autosaving, then don't read the file or else will lose changes
if self.autosave or self.dict == {}: if not self.use_config_file:
self.read() if self.autosave or self.dict == {}:
self.dict[key] = value self.read()
self.dict[key] = value
else:
self.section_class_dict[key].set(value, self.default_value)
if self.autosave: if self.autosave:
self.save() self.save()
return value return value
@ -19494,13 +19736,14 @@ class UserSettings:
self.set_location() self.set_location()
if self.autosave or self.dict == {}: if self.autosave or self.dict == {}:
self.read() self.read()
value = self.dict.get(key, default) if not self.use_config_file:
# Previously was saving creating an entry and saving the dictionary if the value = self.dict.get(key, default)
# key was not found. I don't understand why it was originally coded this way. else:
# Hopefully nothing is going to break removing this code. value = self.section_class_dict.get(key, None)
# if key not in self.dict: if key not in list(self.section_class_dict.keys()):
# self.set(key, value) self.section_class_dict[key] = self._SectionDict(key, {}, self.config, self)
# self.save() value = self.section_class_dict[key]
value.new_section = True
return value return value
def get_dict(self): def get_dict(self):
@ -19532,8 +19775,7 @@ class UserSettings:
:param value: The value to set the setting to :param value: The value to set the setting to
:type value: Any :type value: Any
""" """
return self.set(item, value)
self.set(item, value)
def __getitem__(self, item): def __getitem__(self, item):
""" """
@ -19556,7 +19798,10 @@ class UserSettings:
:param item: The key for the setting to delete :param item: The key for the setting to delete
:type item: Any :type item: Any
""" """
self.delete_entry(key=item) if self.use_config_file:
return self.get(item)
else:
self.delete_entry(key=item)
# Create a singleton for the settings information so that the settings functions can be used # Create a singleton for the settings information so that the settings functions can be used