edbob/edbob/configuration.py

393 lines
14 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
################################################################################
#
# edbob -- Pythonic Software Framework
# Copyright © 2010-2012 Lance Edgar
#
# This file is part of edbob.
#
# edbob is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# edbob is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
``edbob.configuration`` -- Configuration
"""
import os
import os.path
import sys
import ConfigParser
import logging
import logging.config
import edbob
from edbob import exceptions
__all__ = ['AppConfigParser']
log = logging.getLogger(__name__)
class AppConfigParser(ConfigParser.SafeConfigParser):
"""
Subclass of ``ConfigParser.SafeConfigParser``, with some conveniences
added.
"""
def __init__(self, appname, *args, **kwargs):
ConfigParser.SafeConfigParser.__init__(self, *args, **kwargs)
self.appname = appname
self.paths_attempted = []
self.paths_loaded = []
def clear(self):
"""
Completely clears the contents of the config instance.
"""
for section in self.sections():
self.remove_section(section)
del self.paths_attempted[:]
del self.paths_loaded[:]
def configure_logging(self):
"""
Saves the current (possibly cascaded) configuration to a temporary
file, and passes that to ``logging.config.fileConfig()``.
"""
if self.getboolean('edbob', 'basic_logging', default=False):
edbob.basic_logging()
if self.getboolean('edbob', 'configure_logging', default=False):
path = edbob.temp_path(suffix='.conf')
self.save(path)
try:
logging.config.fileConfig(path, disable_existing_loggers=False)
except ConfigParser.NoSectionError:
pass
os.remove(path)
log.debug("Configured logging")
def get(self, section, option, raw=False, vars=None, default=None):
"""
Overridden version of ``ConfigParser.SafeConfigParser.get()``; this one
adds the ``default`` keyword parameter and will return it instead of
raising an error when the option doesn't exist.
"""
if self.has_option(section, option):
return ConfigParser.SafeConfigParser.get(self, section, option, raw, vars)
return default
def getboolean(self, section, option, default=None):
"""
Overriddes base class method to allow for a default.
"""
try:
val = ConfigParser.SafeConfigParser.getboolean(self, section, option)
except AttributeError:
return default
return val
def get_dict(self, section):
"""
Convenience method which returns a dictionary of options contained
within the given section.
"""
d = {}
for opt in self.options(section):
d[opt] = self.get(section, opt)
return d
def get_user_dir(self, create=False):
"""
Returns a path to the "preferred" user-level folder, in which
additional config files (etc.) may be placed as needed. This
essentially returns a platform-specific variation of ``~/.appname``.
If ``create`` is ``True``, then the folder will be created if it does
not already exist.
"""
path = os.path.expanduser('~/.%s' % self.appname)
if sys.platform == 'win32':
from win32com.shell import shell, shellcon
path = os.path.join(
shell.SHGetSpecialFolderPath(0, shellcon.CSIDL_APPDATA),
self.appname)
if create and not os.path.exists(path):
os.mkdir(path)
return path
def get_user_file(self, filename, create=False):
"""
Returns a full path to a user-level config file location. This is
obtained by first calling :meth:`get_user_dir()` and then joining the
result with ``filename``.
The ``create`` argument will be passed directly to
:meth:`get_user_dir()`, and may be used to ensure the user-level folder
exists.
"""
return os.path.join(self.get_user_dir(create=create), filename)
def options(self, section):
"""
Overridden version of ``ConfigParser.SafeConfigParser.options()``.
This one doesn't raise an error if ``section`` doesn't exist, but
instead returns an empty list.
"""
if not self.has_section(section):
return []
return ConfigParser.SafeConfigParser.options(self, section)
def read(self, paths, recurse=True):
r"""
.. highlight:: ini
Overrides the ``RawConfigParser`` method by implementing the following
logic:
Prior to actually reading the contents of the file(s) specified by
``paths`` into the current config instance, a recursive algorithm will
inspect the config found in the file(s) to see if additional config
file(s) are to be included. All config files, whether specified
directly by ``paths`` or indirectly by way of primary configuration,
are finally read into the current config instance in the proper order
so that cascading works as expected.
If you pass ``recurse=False`` to this method then none of the magical
inclusion logic will happen at all.
Note that when a config file indicates that another file(s) is to be
included, the referenced file will be read into this config instance
*before* the original (primary) file is read into it. A convenient
setup then could be to maintain a "site-wide" config file, shared on
the network, including something like this::
# //file-server/share/edbob/site.conf
#
# This file contains settings relevant to all machines on the
# network. Mail and logging configuration at least would be good
# candidates for inclusion here.
[edbob.mail]
smtp.server = mail.example.com
# smtp.username = user
# smtp.password = pass
sender.default = noreply@example.com
recipients.default = ['tech-support@example.com']
[loggers]
keys = root, edbob
# ...etc. The bulk of logging configuration would go here.
Then a config file local to a particular machine could look something
like this::
# C:\ProgramData\edbob\edbob.conf
#
# This file contains settings specific to the local machine.
[edbob]
include_config = [r'\\file-server\share\edbob\site.conf']
# Add any local app config here, e.g. connection details for a
# database, etc.
# All logging config is inherited from the site-wide file, except
# we'll override the output file location so that it remains local.
# And maybe we need the level bumped up while we troubleshoot
# something.
[handler_file]
args = (r'C:\ProgramData\edbob\edbob.log', 'a')
[logger_edbob]
level = DEBUG
There is no right way to do this of course; the need should drive the
method. Since recursion is used, there is also no real limit to how
you go about it. A config file specific to a particular app on a
particular machine can further include a config file specific to the
local user on that machine, which in turn can include a file specific
to the local machine generally, which could then include one or more
site-wide files, etc. Or the "most specific" (initially read; primary)
config file could indicate which other files to include for every level
of that, in which case recursion would be less necessary (though still
technically used).
"""
if isinstance(paths, basestring):
paths = [paths]
for path in paths:
self.read_path(path, recurse=recurse)
return self.paths_loaded
def read_path(self, path, recurse=True):
"""
.. highlight:: ini
Reads a "single" config file into the instance. If ``recurse`` is ``True``,
*and* the config file references other "parent" config file(s), then the
parent(s) are read also in recursive fashion.
"Parent" config file paths may be specified in this way::
[edbob]
include_config = [
r'\\file-server\share\edbob\site.conf',
r'C:\ProgramData\edbob\special-stuff.conf',
]
See :meth:`read()` for more information.
"""
if path in self.paths_attempted:
return
self.paths_attempted.append(path)
log.debug("Reading config file: %s" % path)
if not os.path.exists(path):
log.debug("File doesn't exist")
return
config = ConfigParser.SafeConfigParser(dict(
here=os.path.abspath(os.path.dirname(path))))
if not config.read(path):
log.debug("Read failed")
return
include = None
if recurse:
if (config.has_section('edbob') and
config.has_option('edbob', 'include_config')):
include = config.get('edbob', 'include_config')
if include:
for p in eval(include):
self.read_path(os.path.abspath(p))
ConfigParser.SafeConfigParser.read(self, path)
if include:
self.remove_option('edbob', 'include_config')
self.paths_loaded.append(path)
log.info("Read config file: %s" % path)
def require(self, section, option, msg=None):
"""
Convenience method which will raise an exception if the given option
does not exist. ``msg`` can be used to override (some of) the error
text.
"""
value = self.get(section, option)
if value:
return value
raise exceptions.ConfigError(section, option, msg)
def save(self, filename, create_dir=True):
"""
Saves the current config contents to a file. Optionally can create the
parent folder(s) as necessary.
"""
config_folder = os.path.dirname(filename)
if create_dir and not os.path.exists(config_folder):
os.makedirs(config_folder)
config_file = open(filename, 'w')
self.write(config_file)
config_file.close()
def set(self, section, option, value):
"""
Overrides ``ConfigParser.SafeConfigParser.set()`` so that ``section``
is created if it doesn't already exist, instead of raising an error.
"""
if not self.has_section(section):
self.add_section(section)
ConfigParser.SafeConfigParser.set(self, section, option, value)
def default_system_paths(appname):
r"""
Returns a list of default system-level config file paths for the given
``appname``, according to ``sys.platform``.
For example, assuming an app name of ``'rattail'``, the following would be
returned:
``win32``:
* ``<COMMON_APPDATA>\rattail.conf``
* ``<COMMON_APPDATA>\rattail\rattail.conf``
Any other platform:
* ``/etc/rattail.conf``
* ``/etc/rattail/rattail.conf``
* ``/usr/local/etc/rattail.conf``
* ``/usr/local/etc/rattail/rattail.conf``
"""
if sys.platform == 'win32':
from win32com.shell import shell, shellcon
return [
os.path.join(shell.SHGetSpecialFolderPath(
0, shellcon.CSIDL_COMMON_APPDATA), '%s.conf' % appname),
os.path.join(shell.SHGetSpecialFolderPath(
0, shellcon.CSIDL_COMMON_APPDATA), appname, '%s.conf' % appname),
]
return [
'/etc/%s.conf' % appname,
'/etc/%s/%s.conf' % (appname, appname),
'/usr/local/etc/%s.conf' % appname,
'/usr/local/etc/%s/%s.conf' % (appname, appname),
]
def default_user_paths(appname):
r"""
Returns a list of default user-level config file paths for the given
``appname``, according to ``sys.platform``.
For example, assuming an app name of ``'rattail'``, the following would be
returned:
``win32``:
* ``<APPDATA>\rattail.conf``
* ``<APPDATA>\rattail\rattail.conf``
Any other platform:
* ``~/.rattail.conf``
* ``~/.rattail/rattail.conf``
"""
if sys.platform == 'win32':
from win32com.shell import shell, shellcon
return [
os.path.join(shell.SHGetSpecialFolderPath(
0, shellcon.CSIDL_APPDATA), '%s.conf' % appname),
os.path.join(shell.SHGetSpecialFolderPath(
0, shellcon.CSIDL_APPDATA), appname, '%s.conf' % appname),
]
return [
os.path.expanduser('~/.%s.conf' % appname),
os.path.expanduser('~/.%s/%s.conf' % (appname, appname)),
]