Something went wrong on our end
Forked from
FSL / fslpy
1166 commits behind the upstream repository.
-
Paul McCarthy authoredPaul McCarthy authored
settings.py 11.97 KiB
#!/usr/bin/env python
#
# settings.py - Persistent application settings.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""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.filePath
Settings.readAll
Settings.listFiles
Settings.clear
Some functions are also available to replace the module-level :class:`Settings`
instance:
.. autosummary::
:nosignatures:
set
use
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/`.
"""
from __future__ import absolute_import
import os
import os.path as op
import sys
import copy
import atexit
import shutil
import pickle
import logging
import fnmatch
import tempfile
import platform
import contextlib
log = logging.getLogger(__name__)
_CONFIG_ID = 'fslpy'
"""The default configuration identifier, used as the directory name for
storing configuration files.
"""
def set(settings):
"""Set the module-level :class:`Settings` instance. """
mod = sys.modules[__name__]
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.filePath = settings.filePath
mod.readAll = settings.readAll
mod.listFiles = settings.listFiles
mod.clear = settings.clear
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__`.
"""
set(Settings(*args, **kwargs))
@contextlib.contextmanager
def use(settings):
"""Temporarily replace the module-level :class:`Settings` object
with the given one.
"""
mod = sys.modules[__name__]
old = getattr(mod, 'settings', None)
try:
set(settings)
yield
finally:
if old is not None:
set(old)
# These are all overwritten by
# the initialise function.
def read(name, default=None):
return default
def write(*args, **kwargs):
pass
def delete(*args, **kwargs):
pass
def readFile(*args, **kwargs):
pass
@contextlib.contextmanager
def writeFile(*args, **kwargs):
yield
def deleteFile(*args, **kwargs):
pass
def filePath(*args, **kwargs):
pass
def readAll(*args, **kwarg):
return {}
def listFiles(*args, **kwarg):
return []
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).
"""
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.
:arg cfgdir: Store configuration settings in this directory,
instead of the default.
:arg writeOnExit: If ``True`` (the default), an ``atexit`` function
is registered, which calls :meth:`writeConfigFile`.
"""
if cfgdir is None:
cfgdir = self.__getConfigDir(cfgid)
self.__configID = cfgid
self.__configDir = cfgdir
self.__config = self.__readConfigFile()
if writeOnExit:
atexit.register(self.writeConfigFile)
@property
def configID(self):
"""Returns the configuration identifier. """
return self.__configID
@property
def configDir(self):
"""Returns the location of the configuration directory. """
return self.__configDir
def read(self, name, default=None):
"""Reads a setting with the given ``name``, return ``default`` if
there is no setting called ``name``.
"""
log.debug('Reading {}/{}'.format(self.__configID, name))
return self.__config.get(name, default)
def write(self, name, value):
"""Writes the given ``value`` to the given file ``path``. """
log.debug('Writing {}/{}: {}'.format(self.__configID, name, value))
self.__config[name] = value
def delete(self, name):
"""Delete the setting with the given ``name``. """
log.debug('Deleting {}/{}'.format(self.__configID, name))
self.__config.pop(name, None)
def readFile(self, path, mode='t'):
"""Reads and returns the contents of the given file ``path``.
Returns ``None`` if the path does not exist.
:arg mode: ``'t'`` for text mode, or ``'b'`` for binary.
"""
mode = 'r' + mode
path = self.filePath(path)
if op.exists(path):
with open(path, mode) as f:
return f.read()
else:
return None
@contextlib.contextmanager
def writeFile(self, path, mode='t'):
"""Write to the given file ``path``. This function is intended
to be used as a context manager. For example::
with settings.writeFile('mydata.txt') as f:
f.write('data\\n')
An alternate method of writing to a file is via :meth:`filePath`,
e.g.::
fname = settings.filePath('mydata.txt')
with open(fname, 'wt') as f:
f.write('data\\n')
However using ``writeFile`` has the advantage that any intermediate
directories will be created if they don't already exist.
"""
mode = 'w' + mode
path = self.filePath(path)
pathdir = op.dirname(path)
if not op.exists(pathdir):
os.makedirs(pathdir)
with open(path, mode) as f:
yield f
def deleteFile(self, path):
"""Deletes the given file ``path``. """
path = self.filePath(path)
if op.exists(path):
os.remove(path)
def filePath(self, path):
"""Converts the given ``path`` to an absolute path. Note that there
is no guarantee that the returned file path (or its containing
directory) exists.
"""
path = self.__fixPath(path)
path = op.join(self.__configDir, path)
return path
def readAll(self, pattern=None):
"""Returns all settings with names that match the given glob-style
pattern.
"""
if pattern is None:
return copy.deepcopy(self.__config)
keys = fnmatch.filter(self.__config.keys(), pattern)
vals = [copy.deepcopy(self.__config[k]) for k in keys]
return dict(zip(keys, vals))
def listFiles(self, pattern=None):
"""Returns a list of all stored settings files which match the given
glob-style pattern. If a pattern is not given, all files are returned.
"""
allFiles = []
if pattern is not None:
pattern = self.__fixPath(pattern)
for dirpath, dirnames, filenames in os.walk(self.__configDir):
dirpath = op.relpath(dirpath, self.__configDir)
filenames = [op.join(dirpath, fn) for fn in filenames]
if pattern is None:
allFiles.extend(filenames)
else:
allFiles.extend(fnmatch.filter(filenames, pattern))
return allFiles
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. Only works for paths which use ``'/'`` as the path
separator.
"""
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 OSError:
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 __readConfigFile(self):
"""Called by :meth:`__init__`. Reads any settings that were stored
in a file, and returns them in a dictionary.
"""
configFile = op.join(self.__configDir, 'config.pkl')
log.debug('Reading {} configuration from: {}'.format(
self.__configID, configFile))
try:
with open(configFile, 'rb') as f:
return pickle.load(f)
except (IOError, pickle.UnpicklingError, EOFError):
log.debug('Unable to load stored {} configuration file '
'{}'.format(self.__configID, configFile),
exc_info=True)
return {}
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, protocol=2)
except (IOError, pickle.PicklingError, EOFError):
log.warning('Unable to save {} configuration file '
'{}'.format(self.__configID, configFile),
exc_info=True)