264 lines
9.6 KiB
Python
264 lines
9.6 KiB
Python
|
# coding=utf-8
|
||
|
"""
|
||
|
The config object provides a simplified to access Sopel's configuration file.
|
||
|
The sections of the file are attributes of the object, and the keys in the
|
||
|
section are attributes of that. So, for example, the ``eggs`` attribute in the
|
||
|
``[spam]`` section can be accessed from ``config.spam.eggs``.
|
||
|
|
||
|
Section definitions (see "Section configuration sections" below) can be added
|
||
|
to the config object with ``define_section``. When this is done, only the
|
||
|
defined keys will be available. A section can not be given more than one
|
||
|
definition. The ``[core]`` section is defined with ``CoreSection`` when the
|
||
|
object is initialized.
|
||
|
|
||
|
.. versionadded:: 6.0.0
|
||
|
"""
|
||
|
# Copyright 2012-2015, Elsie Powell, embolalia.com
|
||
|
# Copyright © 2012, Elad Alfassa <elad@fedoraproject.org>
|
||
|
# Licensed under the Eiffel Forum License 2.
|
||
|
|
||
|
from __future__ import unicode_literals, absolute_import, print_function, division
|
||
|
|
||
|
from tools import iteritems, stderr
|
||
|
import tools
|
||
|
from tools import get_input
|
||
|
import loader
|
||
|
import os
|
||
|
import sys
|
||
|
if sys.version_info.major < 3:
|
||
|
import ConfigParser
|
||
|
else:
|
||
|
basestring = str
|
||
|
import configparser as ConfigParser
|
||
|
import config.core_section
|
||
|
from config.types import StaticSection
|
||
|
|
||
|
|
||
|
class ConfigurationError(Exception):
|
||
|
""" Exception type for configuration errors """
|
||
|
|
||
|
def __init__(self, value):
|
||
|
self.value = value
|
||
|
|
||
|
def __str__(self):
|
||
|
return 'ConfigurationError: %s' % self.value
|
||
|
|
||
|
|
||
|
class Config(object):
|
||
|
def __init__(self, filename, validate=True):
|
||
|
"""The bot's configuration.
|
||
|
|
||
|
The given filename will be associated with the configuration, and is
|
||
|
the file which will be written if write() is called. If load is not
|
||
|
given or True, the configuration object will load the attributes from
|
||
|
the file at filename.
|
||
|
|
||
|
A few default values will be set here if they are not defined in the
|
||
|
config file, or a config file is not loaded. They are documented below.
|
||
|
"""
|
||
|
self.filename = filename
|
||
|
"""The config object's associated file, as noted above."""
|
||
|
self.parser = ConfigParser.RawConfigParser(allow_no_value=True)
|
||
|
self.parser.read(self.filename)
|
||
|
self.define_section('core', config.core_section.CoreSection,
|
||
|
validate=validate)
|
||
|
self.get = self.parser.get
|
||
|
|
||
|
@property
|
||
|
def homedir(self):
|
||
|
"""An alias to config.core.homedir"""
|
||
|
# Technically it's the other way around, so we can bootstrap filename
|
||
|
# attributes in the core section, but whatever.
|
||
|
configured = None
|
||
|
if self.parser.has_option('core', 'homedir'):
|
||
|
configured = self.parser.get('core', 'homedir')
|
||
|
if configured:
|
||
|
return configured
|
||
|
else:
|
||
|
return os.path.dirname(self.filename)
|
||
|
|
||
|
def save(self):
|
||
|
"""Save all changes to the config file."""
|
||
|
cfgfile = open(self.filename, 'w')
|
||
|
self.parser.write(cfgfile)
|
||
|
cfgfile.flush()
|
||
|
cfgfile.close()
|
||
|
|
||
|
def add_section(self, name):
|
||
|
"""Add a section to the config file.
|
||
|
|
||
|
Returns ``False`` if already exists.
|
||
|
"""
|
||
|
try:
|
||
|
return self.parser.add_section(name)
|
||
|
except ConfigParser.DuplicateSectionError:
|
||
|
return False
|
||
|
|
||
|
def define_section(self, name, cls_, validate=True):
|
||
|
"""Define the available settings in a section.
|
||
|
|
||
|
``cls_`` must be a subclass of ``StaticSection``. If the section has
|
||
|
already been defined with a different class, ValueError is raised.
|
||
|
|
||
|
If ``validate`` is True, the section's values will be validated, and an
|
||
|
exception raised if they are invalid. This is desirable in a module's
|
||
|
setup function, for example, but might not be in the configure function.
|
||
|
"""
|
||
|
if not issubclass(cls_, StaticSection):
|
||
|
raise ValueError("Class must be a subclass of StaticSection.")
|
||
|
current = getattr(self, name, None)
|
||
|
current_name = str(current.__class__)
|
||
|
new_name = str(cls_)
|
||
|
if (current is not None and not isinstance(current, self.ConfigSection)
|
||
|
and not current_name == new_name):
|
||
|
raise ValueError(
|
||
|
"Can not re-define class for section from {} to {}.".format(
|
||
|
current_name, new_name)
|
||
|
)
|
||
|
setattr(self, name, cls_(self, name, validate=validate))
|
||
|
|
||
|
class ConfigSection(object):
|
||
|
|
||
|
"""Represents a section of the config file.
|
||
|
|
||
|
Contains all keys in thesection as attributes.
|
||
|
|
||
|
"""
|
||
|
|
||
|
def __init__(self, name, items, parent):
|
||
|
object.__setattr__(self, '_name', name)
|
||
|
object.__setattr__(self, '_parent', parent)
|
||
|
for item in items:
|
||
|
value = item[1].strip()
|
||
|
if not value.lower() == 'none':
|
||
|
if value.lower() == 'false':
|
||
|
value = False
|
||
|
object.__setattr__(self, item[0], value)
|
||
|
|
||
|
def __getattr__(self, name):
|
||
|
return None
|
||
|
|
||
|
def __setattr__(self, name, value):
|
||
|
object.__setattr__(self, name, value)
|
||
|
if type(value) is list:
|
||
|
value = ','.join(value)
|
||
|
self._parent.parser.set(self._name, name, value)
|
||
|
|
||
|
def get_list(self, name):
|
||
|
value = getattr(self, name)
|
||
|
if not value:
|
||
|
return []
|
||
|
if isinstance(value, basestring):
|
||
|
value = value.split(',')
|
||
|
# Keep the split value, so we don't have to keep doing this
|
||
|
setattr(self, name, value)
|
||
|
return value
|
||
|
|
||
|
def __getattr__(self, name):
|
||
|
if name in self.parser.sections():
|
||
|
items = self.parser.items(name)
|
||
|
section = self.ConfigSection(name, items, self) # Return a section
|
||
|
setattr(self, name, section)
|
||
|
return section
|
||
|
else:
|
||
|
raise AttributeError("%r object has no attribute %r"
|
||
|
% (type(self).__name__, name))
|
||
|
|
||
|
def option(self, question, default=False):
|
||
|
"""Ask "y/n" and return the corresponding boolean answer.
|
||
|
|
||
|
Show user in terminal a "y/n" prompt, and return true or false based on
|
||
|
the response. If default is passed as true, the default will be shown
|
||
|
as ``[y]``, else it will be ``[n]``. ``question`` should be phrased as
|
||
|
a question, but without a question mark at the end.
|
||
|
|
||
|
"""
|
||
|
d = 'n'
|
||
|
if default:
|
||
|
d = 'y'
|
||
|
ans = get_input(question + ' (y/n)? [' + d + '] ')
|
||
|
if not ans:
|
||
|
ans = d
|
||
|
return ans.lower() == 'y'
|
||
|
|
||
|
def _modules(self):
|
||
|
home = os.getcwd()
|
||
|
modules_dir = os.path.join(home, 'modules')
|
||
|
filenames = sopel.loader.enumerate_modules(self)
|
||
|
os.sys.path.insert(0, modules_dir)
|
||
|
for name, mod_spec in iteritems(filenames):
|
||
|
path, type_ = mod_spec
|
||
|
try:
|
||
|
module, _ = sopel.loader.load_module(name, path, type_)
|
||
|
except Exception as e:
|
||
|
filename, lineno = sopel.tools.get_raising_file_and_line()
|
||
|
rel_path = os.path.relpath(filename, os.path.dirname(__file__))
|
||
|
raising_stmt = "%s:%d" % (rel_path, lineno)
|
||
|
stderr("Error loading %s: %s (%s)" % (name, e, raising_stmt))
|
||
|
else:
|
||
|
if hasattr(module, 'configure'):
|
||
|
prompt = name + ' module'
|
||
|
if module.__doc__:
|
||
|
doc = module.__doc__.split('\n', 1)[0]
|
||
|
if doc:
|
||
|
prompt = doc
|
||
|
prompt = 'Configure {} (y/n)? [n]'.format(prompt)
|
||
|
do_configure = get_input(prompt)
|
||
|
do_configure = do_configure and do_configure.lower() == 'y'
|
||
|
if do_configure:
|
||
|
module.configure(self)
|
||
|
self.save()
|
||
|
|
||
|
|
||
|
def _wizard(section, config=None):
|
||
|
dotdir = os.path.expanduser('~/.sopel')
|
||
|
configpath = os.path.join(dotdir, (config or 'default') + '.cfg')
|
||
|
if section == 'all':
|
||
|
_create_config(configpath)
|
||
|
elif section == 'mod':
|
||
|
_check_dir(False)
|
||
|
if not os.path.isfile(configpath):
|
||
|
print("No config file found." +
|
||
|
" Please make one before configuring these options.")
|
||
|
sys.exit(1)
|
||
|
config = Config(configpath, validate=False)
|
||
|
config._modules()
|
||
|
|
||
|
|
||
|
def _check_dir(create=True):
|
||
|
dotdir = os.path.join(os.path.expanduser('~'), '.sopel')
|
||
|
if not os.path.isdir(dotdir):
|
||
|
if create:
|
||
|
print('Creating a config directory at ~/.sopel...')
|
||
|
try:
|
||
|
os.makedirs(dotdir)
|
||
|
except Exception as e:
|
||
|
print('There was a problem creating %s:' % dotdir, file=sys.stderr)
|
||
|
print('%s, %s' % (e.__class__, str(e)), file=sys.stderr)
|
||
|
print('Please fix this and then run Sopel again.', file=sys.stderr)
|
||
|
sys.exit(1)
|
||
|
else:
|
||
|
print("No config file found. Please make one before configuring these options.")
|
||
|
sys.exit(1)
|
||
|
|
||
|
|
||
|
def _create_config(configpath):
|
||
|
_check_dir()
|
||
|
print("Please answer the following questions" +
|
||
|
" to create your configuration file:\n")
|
||
|
try:
|
||
|
config = Config(configpath, validate=False)
|
||
|
sopel.config.core_section.configure(config)
|
||
|
if config.option(
|
||
|
'Would you like to see if there are any modules'
|
||
|
' that need configuring'
|
||
|
):
|
||
|
config._modules()
|
||
|
config.save()
|
||
|
except Exception:
|
||
|
print("Encountered an error while writing the config file." +
|
||
|
" This shouldn't happen. Check permissions.")
|
||
|
raise
|
||
|
sys.exit(1)
|
||
|
print("Config file written sucessfully!")
|