1# Copyright (C) 2008-2010 Adam Olsen 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2, or (at your option) 6# any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software 15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16# 17# 18# The developers of the Exaile media player hereby grant permission 19# for non-GPL compatible GStreamer and Exaile plugins to be used and 20# distributed together with GStreamer and Exaile. This permission is 21# above and beyond the permissions granted by the GPL license by which 22# Exaile is covered. If you modify this code, you may extend this 23# exception to your version of the code, but you are not obligated to 24# do so. If you do not wish to do so, delete this exception statement 25# from your version. 26 27""" 28 Central storage of application and user settings 29""" 30 31import ast 32from configparser import RawConfigParser, NoSectionError, NoOptionError 33import logging 34import os 35import sys 36from typing import Any, ClassVar 37 38logger = logging.getLogger(__name__) 39 40from xl import event, xdg 41from xl.common import VersionError, glib_wait, glib_wait_seconds 42from xl.nls import gettext as _ 43 44MANAGER = None 45 46 47class SettingsManager(RawConfigParser): 48 """ 49 Manages Exaile's settings 50 """ 51 52 VERSION: ClassVar[int] = 2 53 54 _last_serial: ClassVar[int] = 0 55 56 # xl.common.glib_wait needs instances of this class to be hashable to use 57 # as key in its WeakKeyDictionary. We simply use an increasing serial 58 # number as this hash. 59 _serial: int 60 61 def __init__(self, location=None, default_location=None): 62 """ 63 Sets up the settings manager. Expects a location 64 to a file where settings will be stored. Also sets up 65 periodic saves to disk. 66 67 :param location: the location to save the settings to, 68 settings will never be stored if this is None 69 :type location: str or None 70 :param default_location: the default location to 71 initialize settings from 72 """ 73 RawConfigParser.__init__(self) 74 75 self.location = location 76 self._saving = False 77 self._dirty = False 78 79 self._serial = self.__class__._last_serial = self.__class__._last_serial + 1 80 81 if default_location is not None: 82 try: 83 self.read(default_location) 84 except Exception: 85 pass 86 87 if location is not None: 88 try: 89 self.read(self.location) or self.read( 90 self.location + ".new" 91 ) or self.read(self.location + ".old") 92 except Exception: 93 pass 94 95 version = self.get_option('settings/version') 96 if version and version > self.VERSION: 97 raise VersionError(_('Settings version is newer than current.')) 98 if version != self.VERSION: 99 self.set_option('settings/version', self.VERSION) 100 101 # save settings every 30 seconds 102 if location is not None: 103 self._timeout_save() 104 105 def __hash__(self): 106 return self._serial 107 108 @glib_wait_seconds(30) 109 def _timeout_save(self): 110 """Save every 30 seconds""" 111 self.save() 112 return True 113 114 def copy_settings(self, settings): 115 """ 116 Copies one all of the settings contained 117 in this instance to another 118 119 :param settings: the settings object to copy to 120 :type settings: :class:`xl.settings.SettingsManager` 121 """ 122 for section in self.sections(): 123 for (key, value) in self.items(section): 124 settings._set_direct('%s/%s' % (section, key), value) 125 126 def clone(self): 127 """ 128 Creates a copy of this settings object 129 """ 130 settings = SettingsManager(None) 131 self.copy_settings(settings) 132 return settings 133 134 def set_option(self, option, value, save=True): 135 """ 136 Set an option (in ``section/key`` syntax) to the specified value 137 138 :param option: the full path to an option 139 :type option: string 140 :param value: the value the option should be assigned 141 :type value: any 142 :param save: If True, cause the settings to be written to file 143 """ 144 value = self._val_to_str(value) 145 splitvals = option.split('/') 146 section, key = "/".join(splitvals[:-1]), splitvals[-1] 147 148 try: 149 self.set(section, key, value) 150 except NoSectionError: 151 self.add_section(section) 152 self.set(section, key, value) 153 154 self._dirty = True 155 156 if save: 157 self.delayed_save() 158 159 section = section.replace('/', '_') 160 161 event.log_event('option_set', self, option) 162 event.log_event('%s_option_set' % section, self, option) 163 164 def get_option(self, option: str, default: Any = None) -> Any: 165 """ 166 Get the value of an option (in ``section/key`` syntax), 167 returning *default* if the key does not exist yet 168 169 :param option: the full path to an option 170 :param default: a default value to use as fallback 171 :returns: the option value or *default* 172 """ 173 splitvals = option.split('/') 174 section, key = "/".join(splitvals[:-1]), splitvals[-1] 175 176 try: 177 value = self.get(section, key) 178 value = self._str_to_val(value) 179 except NoSectionError: 180 value = default 181 except NoOptionError: 182 value = default 183 184 return value 185 186 def has_option(self, option): 187 """ 188 Returns information about the existence 189 of a particular option 190 191 :param option: the option path 192 :type option: string 193 :returns: whether the option exists or not 194 :rtype: bool 195 """ 196 splitvals = option.split('/') 197 section, key = "/".join(splitvals[:-1]), splitvals[-1] 198 199 return RawConfigParser.has_option(self, section, key) 200 201 def remove_option(self, option): 202 """ 203 Removes an option (in ``section/key`` syntax), 204 thus will not be saved anymore 205 206 :param option: the option path 207 :type option: string 208 """ 209 splitvals = option.split('/') 210 section, key = "/".join(splitvals[:-1]), splitvals[-1] 211 212 RawConfigParser.remove_option(self, section, key) 213 214 def _set_direct(self, option, value): 215 """ 216 Sets the option directly to the value, 217 only for use in copying settings. 218 219 :param option: the option path 220 :type option: string 221 :param value: the value to set 222 :type value: any 223 """ 224 splitvals = option.split('/') 225 section, key = "/".join(splitvals[:-1]), splitvals[-1] 226 227 try: 228 self.set(section, key, value) 229 except NoSectionError: 230 self.add_section(section) 231 self.set(section, key, value) 232 233 event.log_event('option_set', self, option) 234 235 def _val_to_str(self, value): 236 """ 237 Turns a value of some type into a string so it 238 can be a configuration value. 239 """ 240 for kind, type_ in ( 241 # bool is subclass of int so it must appear earlier 242 ('B', bool), 243 ('I', int), 244 ('F', float), 245 ('L', list), 246 ('D', dict), 247 ): 248 if isinstance(value, type_): 249 return '%s: %r' % (kind, value) 250 if isinstance(value, str): 251 return 'S: %s' % value # Not quoted, hence %s 252 253 raise ValueError( 254 "Don't know how to store setting %r of type %s" % (value, type(value)) 255 ) 256 257 def _str_to_val(self, configstr): 258 """ 259 Convert setting strings back to normal values. 260 """ 261 try: 262 kind, value = configstr.split(': ', 1) 263 except ValueError: 264 return '' 265 266 if kind == 'B': # Must appear before I 267 return value == 'True' 268 if kind in 'SU': # U is for backwards compatibility (Python 2 unicode) 269 return value 270 try: 271 if kind == 'I': 272 return int(value) 273 if kind == 'F': 274 return float(value) 275 for kind in 'LD': 276 return ast.literal_eval(value) 277 except Exception: 278 logger.exception("Failed decoding value %r of kind %s", value, kind) 279 return value 280 281 raise ValueError("Unknown type of setting on: %s", configstr) 282 283 @glib_wait(500) 284 def delayed_save(self): 285 '''Save options after a delay, waiting for multiple saves to accumulate''' 286 self.save() 287 288 def save(self): 289 """ 290 Save the settings to disk 291 """ 292 if self.location is None: 293 logger.debug("Save requested but not saving settings, " "location is None") 294 return 295 296 if self._saving or not self._dirty: 297 return 298 299 self._saving = True 300 301 logger.debug("Saving settings...") 302 303 with open(self.location + ".new", 'w') as f: 304 self.write(f) 305 306 try: 307 # make it readable by current user only, to protect private data 308 os.fchmod(f.fileno(), 384) 309 except Exception: 310 pass # fail gracefully, eg if on windows 311 312 f.flush() 313 314 try: 315 os.rename(self.location, self.location + ".old") 316 except Exception: 317 pass # if it doesn'texist we don't care 318 319 os.rename(self.location + ".new", self.location) 320 321 try: 322 os.remove(self.location + ".old") 323 except Exception: 324 pass 325 326 self._saving = False 327 self._dirty = False 328 329 330location = xdg.get_config_dir() 331 332 333# Provide a mechanism for setting up default settings for different platforms 334if sys.platform == 'win32': 335 __settings_file = 'settings-win32.ini' 336elif sys.platform == 'darwin': 337 __settings_file = 'settings-osx.ini' 338else: 339 __settings_file = 'settings.ini' 340 341 342MANAGER = SettingsManager( 343 os.path.join(location, "settings.ini"), xdg.get_config_path("settings.ini") 344) 345 346get_option = MANAGER.get_option 347set_option = MANAGER.set_option 348 349# vim: et sts=4 sw=4 350