Skip to content
Snippets Groups Projects
Commit a6015d8c authored by Paul McCarthy's avatar Paul McCarthy
Browse files

settings module rewritten so it no longer uses wx.

parent 719595f7
No related branches found
No related tags found
No related merge requests found
......@@ -4,119 +4,324 @@
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides a simple API to :func:`read`, :func:`write`,
:func:`delete`, and :func:`clear` persistent application settings.
.. note:: Currently the configuration management API provided by :mod:`wx`
(http://docs.wxwidgets.org/trunk/overview_config.html) is used for
storing application settings. This means that it is not possible
to persist settings from a non-GUI application.
But that's the whole point of this module, to abstract away the
underlying persistence method. In the future I will replace
``wx.Config`` with something that does not rely upon the presence
of ``wx``.
"""This module provides functions for storing and retrieving persistent
configuration settings and data files.
The :func:`initialise` function must be called to initialise the module. Then,
the following functions can be called at the module-level:
.. autosummary::
:nosignatures:
Settings.read
Settings.write
Settings.delete
Settings.readFile
Settings.writeFile
Settings.deleteFile
Settings.clear
These functions will have no effect before :func:`initialise` is called.
Two types of configuration data are available:
- Key-value pairs - access these via the ``read``, ``write`` and ``delete``
functions. These are stored in a single file, via ``pickle``. Anything
that can be pickled can be stored.
- Separate files, either text or binary. Access these via the ``readFile``,
``writeFile`, and ``deleteFile` functions.
Both of the above data types will be stored in a configuration directory.
The location of this directory differs from platform to platform, but is
likely to be either `~/.fslpy/` or `~/.config/fslpy/`.
"""
import logging
from __future__ import absolute_import
from .platform import platform as fslplatform
import os
import os.path as op
import sys
import atexit
import shutil
import pickle
import logging
import tempfile
import platform
log = logging.getLogger(__name__)
_CONFIG_ID = 'uk.ac.ox.fmrib.fslpy'
"""The configuration identifier passed to ``wx.Config``. This identifier
should be the same as the identifier given to the OSX application bundle
(see https://git.fmrib.ox.ac.uk/paulmc/fslpy_build).
_CONFIG_ID = 'fslpy'
"""The default configuration identifier, used as the directory name for
storing configuration files.
"""
def strToBool(s):
"""Currently the ``settings`` module is not type aware, so boolean
values are saved as strings ``'True'`` or ``'False'``. This makes
conversion back to boolean a bit annoying, as ``'False'`` evaluates
to ``True``.
def initialise(*args, **kwargs):
"""Initialise the ``settings`` module. This function creates a
:class:`Settings` instance, and enables the module-level
functions. All settings are passed through to :meth:`Settings.__init__`.
"""
mod = sys.modules[__name__]
settings = Settings(*args, **kwargs)
mod.settings = settings
mod.read = settings.read
mod.write = settings.write
mod.delete = settings.delete
mod.readFile = settings.readFile
mod.writeFile = settings.writeFile
mod.deleteFile = settings.deleteFile
mod.clear = settings.clear
# These are all overwritten by
# the initialise function.
def read(*args, **kwargs):
pass
def write(*args, **kwargs):
pass
def delete(*args, **kwargs):
pass
def readFile(*args, **kwargs):
pass
def writeFile(*args, **kwargs):
pass
def deleteFile(*args, **kwargs):
pass
def clear(*args, **kwarg):
pass
class Settings(object):
"""The ``Settings`` class contains all of the logic provided by the
``settings`` module. It is not meant to be instantiated directly
(although you may do so if you wish).
.. autosummary::
:nosignatures:
read
write
delete
readFile
writeFile
deleteFile
clear
"""
This function may be used for a more sensible `str` -> `bool`
conversion
.. note:: In the future, the ``settings`` module will hopefully be
type-aware, so use of this function will no longer be necessary.
"""
s = str(s).lower()
if s == 'true': return True
elif s == 'false': return False
else: return bool(s)
def __init__(self, cfgid=_CONFIG_ID, cfgdir=None, writeOnExit=True):
"""Create a ``Settings`` instance.
:arg cfgid: Configuration ID, used as the name of the
configuration directory.
def read(name, default=None):
"""Reads a setting with the given ``name``, return ``default`` if
there is no setting called ``name``.
"""
:arg cfgdir: Store configuration settings in this directory,
instead of the default.
if not fslplatform.haveGui:
return default
:arg writeOnExit: If ``True`` (the default), an ``atexit`` function
is registered, which calls :meth:`writeConfigFile`.
"""
import wx
if cfgdir is None:
cfgdir = self.__getConfigDir(cfgid)
config = wx.Config(_CONFIG_ID)
value = config.Read(name)
self.__configID = cfgid
self.__configDir = cfgdir
self.__config = self.__readConfigFile()
log.debug('Read {}/{}: {}'.format(
_CONFIG_ID,
name,
'(no value)' if value == '' else value))
if writeOnExit:
atexit.register(self.writeConfigFile)
if value == '': return default
else: return value
@property
def configID(self):
"""Returns the configuration identifier. """
return self.__configID
def write(name, value):
"""Writes a setting with the given ``name`` and ``value``."""
if not fslplatform.haveGui:
return
@property
def configDir(self):
"""Returns the location of the configuration directory. """
return self.__configDir
import wx
value = str(value)
config = wx.Config(_CONFIG_ID)
def read(self, name, default=None):
"""Reads a setting with the given ``name``, return ``default`` if
there is no setting called ``name``.
"""
log.debug('Writing {}/{}: {}'.format(_CONFIG_ID, name, value))
log.debug('Reading {}/{}'.format(self.__configID, name))
return self.__config.get(name, default)
config.Write(name, value)
def write(self, name, value):
"""Writes the given ``value`` to the given file ``path``. """
def delete(name):
"""Delete the setting with the given ``name``. """
log.debug('Writing {}/{}: {}'.format(self.__configID, name, value))
self.__config[name] = value
if not fslplatform.haveGui:
return
import wx
def delete(self, name):
"""Delete the setting with the given ``name``. """
config = wx.Config(_CONFIG_ID)
log.debug('Deleting {}/{}'.format(self.__configID, name))
self.__config.pop(name, None)
log.debug('Deleting {}/{}'.format(_CONFIG_ID, name))
config.DeleteEntry(name)
def readFile(self, path, mode='t'):
"""Reads and returns the contents of the given file ``path``.
Returns ``None`` if the path does not exist.
"""
mode = 'r' + mode
path = self.__fixPath(path)
path = op.join(self.__configDir, path)
if op.exists(path):
with open(path, mode) as f:
return f.read()
else:
return None
def writeFile(self, path, value, mode='t'):
"""Writes the given ``value`` to the given file ``path``. """
mode = 'w' + mode
path = self.__fixPath(path)
path = op.join(self.__configDir, path)
pathdir = op.dirname(path)
if not op.exists(pathdir):
os.makedirs(pathdir)
with open(path, mode) as f:
f.write(value)
def deleteFile(self, path):
"""Deletes the given file ``path``. """
path = self.__fixPath(path)
path = op.join(self.__configDir, path)
if op.exists(path):
os.remove(path)
def clear(self):
"""Delete all configuration settings and files. """
log.debug('Clearing all settings in {}'.format(self.__configID))
self.__config = {}
for path in os.listdir(self.__configDir):
path = op.join(self.__configDir, path)
if op.isdir(path):
shutil.rmtree(path)
else:
os.remove(path)
def __fixPath(self, path):
"""Ensures that the given path (passed into :meth:`readFile`,
:meth:`writeFile`, or :meth:`deleteFile`) is cross-platform
compatible.
"""
return op.join(*path.split('/'))
def __getConfigDir(self, cid):
"""Returns a directory in which configuration files can be stored.
.. note:: If, for whatever reason, a configuration directory could not
be located or created, a temporary directory will be used.
This means that all settings read during this session will
be lost on exit.
"""
cfgdir = None
homedir = op.expanduser('~')
# On linux, if $XDG_CONFIG_HOME is set, use $XDG_CONFIG_HOME/fslpy/
# Otherwise, use $HOME/.config/fslpy/
#
# https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
if platform.system().lower().startswith('linux'):
basedir = os.environ.get('XDG_CONFIG_HOME')
if basedir is None:
basedir = op.join(homedir, '.config')
cfgdir = op.join(basedir, cid)
# On all other platforms, use $HOME/.fslpy/
else:
cfgdir = op.join(homedir, '.{}'.format(cid))
# Try and create the config directory
# tree if it does not exist
if not op.exists(cfgdir):
try:
os.makedirs(cfgdir)
except:
log.warning(
'Unable to create {} configuration '
'directory: {}'.format(cid, cfgdir),
exc_info=True)
cfgdir = None
# If dir creation failed, use a temporary
# directory, and delete it on exit
if cfgdir is None:
cfgdir = tempfile.mkdtemp()
atexit.register(shutil.rmtree, cfgdir, ignore_errors=True)
log.debug('{} configuration directory: {}'.format(cid, cfgdir))
return cfgdir
def clear():
"""Delete all settings. """
if not fslplatform.haveGui:
return
import wx
def __readConfigFile(self):
"""Called by :meth:`__init__`. Reads any settings that were stored
in a file, and returns them in a dictionary.
"""
config = wx.Config(_CONFIG_ID)
configFile = op.join(self.__configDir, 'config.pkl')
log.debug('Clearing all settings in {}'.format(_CONFIG_ID))
log.debug('Reading {} configuration from: {}'.format(
self.__configID, configFile))
try:
with open(configFile, 'rb') as f:
return pickle.load(f)
except:
log.warning('Unable to load stored {} configuration file '
'{}'.format(self.__configID, configFile),
exc_info=True)
return {}
config.DeleteAll()
def writeConfigFile(self):
"""Writes all settings to a file."""
config = self.__config
configFile = op.join(self.__configDir, 'config.pkl')
log.debug('Writing {} configuration to: {}'.format(
self.__configID, configFile))
try:
with open(configFile, 'wb') as f:
pickle.dump(config, f)
except:
log.warning('Unable to save {} configuration file '
'{}'.format(self.__configID, configFile),
exc_info=True)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment