352 lines
13 KiB
Python
352 lines
13 KiB
Python
|
# coding=utf-8
|
||
|
"""Types for creating section definitions.
|
||
|
|
||
|
A section definition consists of a subclass of ``StaticSection``, on which any
|
||
|
number of subclasses of ``BaseValidated`` (a few common ones of which are
|
||
|
available in this module) are assigned as attributes. These descriptors define
|
||
|
how to read values from, and write values to, the config file.
|
||
|
|
||
|
As an example, if one wanted to define the ``[spam]`` section as having an
|
||
|
``eggs`` option, which contains a list of values, they could do this:
|
||
|
|
||
|
>>> class SpamSection(StaticSection):
|
||
|
... eggs = ListAttribute('eggs')
|
||
|
...
|
||
|
>>> SpamSection(config, 'spam')
|
||
|
>>> print(config.spam.eggs)
|
||
|
[]
|
||
|
>>> config.spam.eggs = ['goose', 'turkey', 'duck', 'chicken', 'quail']
|
||
|
>>> print(config.spam.eggs)
|
||
|
['goose', 'turkey', 'duck', 'chicken', 'quail']
|
||
|
>>> config.spam.eggs = 'herring'
|
||
|
Traceback (most recent call last):
|
||
|
...
|
||
|
ValueError: ListAttribute value must be a list.
|
||
|
"""
|
||
|
|
||
|
from __future__ import unicode_literals, absolute_import, print_function, division
|
||
|
import os.path
|
||
|
import sys
|
||
|
from tools import get_input
|
||
|
|
||
|
try:
|
||
|
import configparser
|
||
|
except ImportError:
|
||
|
import ConfigParser as configparser
|
||
|
|
||
|
if sys.version_info.major >= 3:
|
||
|
unicode = str
|
||
|
basestring = (str, bytes)
|
||
|
|
||
|
|
||
|
class NO_DEFAULT(object):
|
||
|
"""A special value to indicate that there should be no default."""
|
||
|
|
||
|
|
||
|
class StaticSection(object):
|
||
|
"""A configuration section with parsed and validated settings.
|
||
|
|
||
|
This class is intended to be subclassed with added ``ValidatedAttribute``\s.
|
||
|
"""
|
||
|
def __init__(self, config, section_name, validate=True):
|
||
|
if not config.parser.has_section(section_name):
|
||
|
config.parser.add_section(section_name)
|
||
|
self._parent = config
|
||
|
self._parser = config.parser
|
||
|
self._section_name = section_name
|
||
|
for value in dir(self):
|
||
|
try:
|
||
|
getattr(self, value)
|
||
|
except ValueError as e:
|
||
|
raise ValueError(
|
||
|
'Invalid value for {}.{}: {}'.format(section_name, value,
|
||
|
str(e))
|
||
|
)
|
||
|
except AttributeError:
|
||
|
if validate:
|
||
|
raise ValueError(
|
||
|
'Missing required value for {}.{}'.format(section_name,
|
||
|
value)
|
||
|
)
|
||
|
|
||
|
def configure_setting(self, name, prompt, default=NO_DEFAULT):
|
||
|
"""Return a validated value for this attribute from the terminal.
|
||
|
|
||
|
``prompt`` will be the docstring of the attribute if not given.
|
||
|
|
||
|
If ``default`` is passed, it will be used if no value is given by the
|
||
|
user. If it is not passed, the current value of the setting, or the
|
||
|
default value if it's unset, will be used. Note that if ``default`` is
|
||
|
passed, the current value of the setting will be ignored, even if it is
|
||
|
not the attribute's default.
|
||
|
"""
|
||
|
clazz = getattr(self.__class__, name)
|
||
|
if default is NO_DEFAULT:
|
||
|
try:
|
||
|
default = getattr(self, name)
|
||
|
except AttributeError:
|
||
|
pass
|
||
|
except ValueError:
|
||
|
print('The configured value for this option was invalid.')
|
||
|
if clazz.default is not NO_DEFAULT:
|
||
|
default = clazz.default
|
||
|
while True:
|
||
|
try:
|
||
|
value = clazz.configure(prompt, default, self._parent, self._section_name)
|
||
|
except ValueError as exc:
|
||
|
print(exc)
|
||
|
else:
|
||
|
break
|
||
|
setattr(self, name, value)
|
||
|
|
||
|
|
||
|
class BaseValidated(object):
|
||
|
"""The base type for a descriptor in a ``StaticSection``."""
|
||
|
def __init__(self, name, default=None):
|
||
|
"""
|
||
|
``name`` is the name of the setting in the section.
|
||
|
``default`` is the value to be returned if the setting is not set. If
|
||
|
not given, AttributeError will be raised instead.
|
||
|
"""
|
||
|
self.name = name
|
||
|
self.default = default
|
||
|
|
||
|
def configure(self, prompt, default, parent, section_name):
|
||
|
"""With the prompt and default, parse and return a value from terminal.
|
||
|
"""
|
||
|
if default is not NO_DEFAULT and default is not None:
|
||
|
prompt = '{} [{}]'.format(prompt, default)
|
||
|
value = get_input(prompt + ' ')
|
||
|
if not value and default is NO_DEFAULT:
|
||
|
raise ValueError("You must provide a value for this option.")
|
||
|
value = value or default
|
||
|
return self.parse(value)
|
||
|
|
||
|
def serialize(self, value):
|
||
|
"""Take some object, and return the string to be saved to the file.
|
||
|
|
||
|
Must be implemented in subclasses.
|
||
|
"""
|
||
|
raise NotImplemented("Serialize method must be implemented in subclass")
|
||
|
|
||
|
def parse(self, value):
|
||
|
"""Take a string from the file, and return the appropriate object.
|
||
|
|
||
|
Must be implemented in subclasses."""
|
||
|
raise NotImplemented("Parse method must be implemented in subclass")
|
||
|
|
||
|
def __get__(self, instance, owner=None):
|
||
|
if instance is None:
|
||
|
# If instance is None, we're getting from a section class, not an
|
||
|
# instance of a session class. It makes the wizard code simpler
|
||
|
# (and is really just more intuitive) to return the descriptor
|
||
|
# instance here.
|
||
|
return self
|
||
|
|
||
|
if instance._parser.has_option(instance._section_name, self.name):
|
||
|
value = instance._parser.get(instance._section_name, self.name)
|
||
|
else:
|
||
|
if self.default is not NO_DEFAULT:
|
||
|
return self.default
|
||
|
raise AttributeError(
|
||
|
"Missing required value for {}.{}".format(
|
||
|
instance._section_name, self.name
|
||
|
)
|
||
|
)
|
||
|
return self.parse(value)
|
||
|
|
||
|
def __set__(self, instance, value):
|
||
|
if value is None:
|
||
|
instance._parser.remove_option(instance._section_name, self.name)
|
||
|
return
|
||
|
value = self.serialize(value)
|
||
|
instance._parser.set(instance._section_name, self.name, value)
|
||
|
|
||
|
def __delete__(self, instance):
|
||
|
instance._parser.remove_option(instance._section_name, self.name)
|
||
|
|
||
|
|
||
|
def _parse_boolean(value):
|
||
|
if value is True or value == 1:
|
||
|
return value
|
||
|
if isinstance(value, basestring):
|
||
|
return value.lower() in ['1', 'yes', 'y', 'true', 'on']
|
||
|
return bool(value)
|
||
|
|
||
|
|
||
|
def _serialize_boolean(value):
|
||
|
return 'true' if _parse_boolean(value) else 'false'
|
||
|
|
||
|
|
||
|
class ValidatedAttribute(BaseValidated):
|
||
|
def __init__(self, name, parse=None, serialize=None, default=None):
|
||
|
"""A descriptor for settings in a ``StaticSection``
|
||
|
|
||
|
``parse`` is the function to be used to read the string and create the
|
||
|
appropriate object. If not given, return the string as-is.
|
||
|
``serialize`` takes an object, and returns the value to be written to
|
||
|
the file. If not given, defaults to ``unicode``.
|
||
|
"""
|
||
|
self.name = name
|
||
|
if parse == bool:
|
||
|
parse = _parse_boolean
|
||
|
if not serialize or serialize == bool:
|
||
|
serialize = _serialize_boolean
|
||
|
self.parse = parse or self.parse
|
||
|
self.serialize = serialize or self.serialize
|
||
|
self.default = default
|
||
|
|
||
|
def serialize(self, value):
|
||
|
return unicode(value)
|
||
|
|
||
|
def parse(self, value):
|
||
|
return value
|
||
|
|
||
|
def configure(self, prompt, default, parent, section_name):
|
||
|
if self.parse == _parse_boolean:
|
||
|
prompt += ' (y/n)'
|
||
|
default = 'y' if default else 'n'
|
||
|
return super(ValidatedAttribute, self).configure(prompt, default, parent, section_name)
|
||
|
|
||
|
|
||
|
class ListAttribute(BaseValidated):
|
||
|
"""A config attribute containing a list of string values.
|
||
|
|
||
|
Values are saved to the file as a comma-separated list. It does not
|
||
|
currently support commas within items in the list. By default, the spaces
|
||
|
before and after each item are stripped; you can override this by passing
|
||
|
``strip=False``."""
|
||
|
def __init__(self, name, strip=True, default=None):
|
||
|
default = default or []
|
||
|
super(ListAttribute, self).__init__(name, default=default)
|
||
|
self.strip = strip
|
||
|
|
||
|
def parse(self, value):
|
||
|
value = value.split(',')
|
||
|
if self.strip:
|
||
|
return [v.strip() for v in value]
|
||
|
else:
|
||
|
return value
|
||
|
|
||
|
def serialize(self, value):
|
||
|
if not isinstance(value, (list, set)):
|
||
|
raise ValueError('ListAttribute value must be a list.')
|
||
|
return ','.join(value)
|
||
|
|
||
|
def configure(self, prompt, default, parent, section_name):
|
||
|
each_prompt = '?'
|
||
|
if isinstance(prompt, tuple):
|
||
|
each_prompt = prompt[1]
|
||
|
prompt = prompt[0]
|
||
|
|
||
|
if default is not NO_DEFAULT:
|
||
|
default = ','.join(default)
|
||
|
prompt = '{} [{}]'.format(prompt, default)
|
||
|
else:
|
||
|
default = ''
|
||
|
print(prompt)
|
||
|
values = []
|
||
|
value = get_input(each_prompt + ' ') or default
|
||
|
while value:
|
||
|
values.append(value)
|
||
|
value = get_input(each_prompt + ' ')
|
||
|
return self.parse(','.join(values))
|
||
|
|
||
|
|
||
|
class ChoiceAttribute(BaseValidated):
|
||
|
"""A config attribute which must be one of a set group of options.
|
||
|
|
||
|
Currently, the choices can only be strings."""
|
||
|
def __init__(self, name, choices, default=None):
|
||
|
super(ChoiceAttribute, self).__init__(name, default=default)
|
||
|
self.choices = choices
|
||
|
|
||
|
def parse(self, value):
|
||
|
if value in self.choices:
|
||
|
return value
|
||
|
else:
|
||
|
raise ValueError('Value must be in {}'.format(self.choices))
|
||
|
|
||
|
def serialize(self, value):
|
||
|
if value in self.choices:
|
||
|
return value
|
||
|
else:
|
||
|
raise ValueError('Value must be in {}'.format(self.choices))
|
||
|
|
||
|
|
||
|
class FilenameAttribute(BaseValidated):
|
||
|
"""A config attribute which must be a file or directory."""
|
||
|
def __init__(self, name, relative=True, directory=False, default=None):
|
||
|
"""
|
||
|
``relative`` is whether the path should be relative to the location
|
||
|
of the config file (absolute paths will still be absolute). If
|
||
|
``directory`` is True, the path must indicate a directory, rather than
|
||
|
a file.
|
||
|
"""
|
||
|
super(FilenameAttribute, self).__init__(name, default=default)
|
||
|
self.relative = relative
|
||
|
self.directory = directory
|
||
|
|
||
|
def __get__(self, instance, owner=None):
|
||
|
if instance is None:
|
||
|
return self
|
||
|
if instance._parser.has_option(instance._section_name, self.name):
|
||
|
value = instance._parser.get(instance._section_name, self.name)
|
||
|
else:
|
||
|
if self.default is not NO_DEFAULT:
|
||
|
value = self.default
|
||
|
else:
|
||
|
raise AttributeError(
|
||
|
"Missing required value for {}.{}".format(
|
||
|
instance._section_name, self.name
|
||
|
)
|
||
|
)
|
||
|
main_config = instance._parent
|
||
|
this_section = getattr(main_config, instance._section_name)
|
||
|
return self.parse(main_config, this_section, value)
|
||
|
|
||
|
def __set__(self, instance, value):
|
||
|
main_config = instance._parent
|
||
|
this_section = getattr(main_config, instance._section_name)
|
||
|
value = self.serialize(main_config, this_section, value)
|
||
|
instance._parser.set(instance._section_name, self.name, value)
|
||
|
|
||
|
def configure(self, prompt, default, parent, section_name):
|
||
|
"""With the prompt and default, parse and return a value from terminal.
|
||
|
"""
|
||
|
if default is not NO_DEFAULT and default is not None:
|
||
|
prompt = '{} [{}]'.format(prompt, default)
|
||
|
value = get_input(prompt + ' ')
|
||
|
if not value and default is NO_DEFAULT:
|
||
|
raise ValueError("You must provide a value for this option.")
|
||
|
value = value or default
|
||
|
return self.parse(parent, section_name, value)
|
||
|
|
||
|
def parse(self, main_config, this_section, value):
|
||
|
if value is None:
|
||
|
return
|
||
|
|
||
|
value = os.path.expanduser(value)
|
||
|
|
||
|
if not os.path.isabs(value):
|
||
|
if not self.relative:
|
||
|
raise ValueError("Value must be an absolute path.")
|
||
|
value = os.path.join(main_config.homedir, value)
|
||
|
|
||
|
if self.directory and not os.path.isdir(value):
|
||
|
try:
|
||
|
os.makedirs(value)
|
||
|
except OSError:
|
||
|
raise ValueError(
|
||
|
"Value must be an existing or creatable directory.")
|
||
|
if not self.directory and not os.path.isfile(value):
|
||
|
try:
|
||
|
open(value, 'w').close()
|
||
|
except OSError:
|
||
|
raise ValueError("Value must be an existant or creatable file.")
|
||
|
return value
|
||
|
|
||
|
def serialize(self, main_config, this_section, value):
|
||
|
self.parse(main_config, this_section, value)
|
||
|
return value # So that it's still relative
|