sopel/config/__init__.py
2018-01-18 23:43:27 -05:00

258 lines
8.0 KiB
Python
Executable File

#! /usr/bin/env python3
# -*- 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
"""
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
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, str):
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!")