mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-06-10 02:29:43 +00:00
489 lines
15 KiB
Python
489 lines
15 KiB
Python
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
r"""
|
|
This file defines classes for representing config data/settings.
|
|
|
|
Config data is modeled as key-value pairs. Keys are grouped together into named
|
|
sections. Individual config settings (options) have metadata associated with
|
|
them. This metadata includes type, default value, valid values, etc.
|
|
|
|
The main interface to config data is the ConfigSettings class. 1 or more
|
|
ConfigProvider classes are associated with ConfigSettings and define what
|
|
settings are available.
|
|
|
|
Descriptions of individual config options can be translated to multiple
|
|
languages using gettext. Each option has associated with it a domain and locale
|
|
directory. By default, the domain is the section the option is in and the
|
|
locale directory is the "locale" directory beneath the directory containing the
|
|
module that defines it.
|
|
|
|
People implementing ConfigProvider instances are expected to define a complete
|
|
gettext .po and .mo file for the en-US locale. You can use the gettext-provided
|
|
msgfmt binary to perform this conversion. Generation of the original .po file
|
|
can be done via the write_pot() of ConfigSettings.
|
|
"""
|
|
|
|
from __future__ import absolute_import, unicode_literals
|
|
|
|
import collections
|
|
import gettext
|
|
import os
|
|
import sys
|
|
|
|
if sys.version_info[0] == 3:
|
|
from configparser import RawConfigParser
|
|
str_type = str
|
|
else:
|
|
from ConfigParser import RawConfigParser
|
|
str_type = basestring
|
|
|
|
|
|
class ConfigType(object):
|
|
"""Abstract base class for config values."""
|
|
|
|
@staticmethod
|
|
def validate(value):
|
|
"""Validates a Python value conforms to this type.
|
|
|
|
Raises a TypeError or ValueError if it doesn't conform. Does not do
|
|
anything if the value is valid.
|
|
"""
|
|
|
|
@staticmethod
|
|
def from_config(config, section, option):
|
|
"""Obtain the value of this type from a RawConfigParser.
|
|
|
|
Receives a RawConfigParser instance, a str section name, and the str
|
|
option in that section to retrieve.
|
|
|
|
The implementation may assume the option exists in the RawConfigParser
|
|
instance.
|
|
|
|
Implementations are not expected to validate the value. But, they
|
|
should return the appropriate Python type.
|
|
"""
|
|
|
|
@staticmethod
|
|
def to_config(value):
|
|
return value
|
|
|
|
|
|
class StringType(ConfigType):
|
|
@staticmethod
|
|
def validate(value):
|
|
if not isinstance(value, str_type):
|
|
raise TypeError()
|
|
|
|
@staticmethod
|
|
def from_config(config, section, option):
|
|
return config.get(section, option)
|
|
|
|
|
|
class BooleanType(ConfigType):
|
|
@staticmethod
|
|
def validate(value):
|
|
if not isinstance(value, bool):
|
|
raise TypeError()
|
|
|
|
@staticmethod
|
|
def from_config(config, section, option):
|
|
return config.getboolean(section, option)
|
|
|
|
@staticmethod
|
|
def to_config(value):
|
|
return 'true' if value else 'false'
|
|
|
|
|
|
class IntegerType(ConfigType):
|
|
@staticmethod
|
|
def validate(value):
|
|
if not isinstance(value, int):
|
|
raise TypeError()
|
|
|
|
@staticmethod
|
|
def from_config(config, section, option):
|
|
return config.getint(section, option)
|
|
|
|
|
|
class PositiveIntegerType(IntegerType):
|
|
@staticmethod
|
|
def validate(value):
|
|
if not isinstance(value, int):
|
|
raise TypeError()
|
|
|
|
if value < 0:
|
|
raise ValueError()
|
|
|
|
|
|
class PathType(StringType):
|
|
@staticmethod
|
|
def validate(value):
|
|
if not isinstance(value, str_type):
|
|
raise TypeError()
|
|
|
|
@staticmethod
|
|
def from_config(config, section, option):
|
|
return config.get(section, option)
|
|
|
|
|
|
class AbsolutePathType(PathType):
|
|
@staticmethod
|
|
def validate(value):
|
|
if not isinstance(value, str_type):
|
|
raise TypeError()
|
|
|
|
if not os.path.isabs(value):
|
|
raise ValueError()
|
|
|
|
|
|
class RelativePathType(PathType):
|
|
@staticmethod
|
|
def validate(value):
|
|
if not isinstance(value, str_type):
|
|
raise TypeError()
|
|
|
|
if os.path.isabs(value):
|
|
raise ValueError()
|
|
|
|
|
|
class DefaultValue(object):
|
|
pass
|
|
|
|
|
|
class ConfigProvider(object):
|
|
"""Abstract base class for an object providing config settings.
|
|
|
|
Classes implementing this interface expose configurable settings. Settings
|
|
are typically only relevant to that component itself. But, nothing says
|
|
settings can't be shared by multiple components.
|
|
"""
|
|
|
|
@classmethod
|
|
def register_settings(cls):
|
|
"""Registers config settings.
|
|
|
|
This is called automatically. Child classes should likely not touch it.
|
|
See _register_settings() instead.
|
|
"""
|
|
if hasattr(cls, '_settings_registered'):
|
|
return
|
|
|
|
cls._settings_registered = True
|
|
|
|
cls.config_settings = {}
|
|
|
|
ourdir = os.path.dirname(__file__)
|
|
cls.config_settings_locale_directory = os.path.join(ourdir, 'locale')
|
|
|
|
cls._register_settings()
|
|
|
|
@classmethod
|
|
def _register_settings(cls):
|
|
"""The actual implementation of register_settings().
|
|
|
|
This is what child classes should implement. They should not touch
|
|
register_settings().
|
|
|
|
Implementations typically make 1 or more calls to _register_setting().
|
|
"""
|
|
raise NotImplemented('%s must implement _register_settings.' %
|
|
__name__)
|
|
|
|
@classmethod
|
|
def register_setting(cls, section, option, type_cls, default=DefaultValue,
|
|
choices=None, domain=None):
|
|
"""Register a config setting with this type.
|
|
|
|
This is a convenience method to populate available settings. It is
|
|
typically called in the class's _register_settings() implementation.
|
|
|
|
Each setting must have:
|
|
|
|
section -- str section to which the setting belongs. This is how
|
|
settings are grouped.
|
|
|
|
option -- str id for the setting. This must be unique within the
|
|
section it appears.
|
|
|
|
type -- a ConfigType-derived type defining the type of the setting.
|
|
|
|
Each setting has the following optional parameters:
|
|
|
|
default -- The default value for the setting. If None (the default)
|
|
there is no default.
|
|
|
|
choices -- A set of values this setting can hold. Values not in
|
|
this set are invalid.
|
|
|
|
domain -- Translation domain for this setting. By default, the
|
|
domain is the same as the section name.
|
|
"""
|
|
if not section in cls.config_settings:
|
|
cls.config_settings[section] = {}
|
|
|
|
if option in cls.config_settings[section]:
|
|
raise Exception('Setting has already been registered: %s.%s' % (
|
|
section, option))
|
|
|
|
domain = domain if domain is not None else section
|
|
|
|
meta = {
|
|
'short': '%s.short' % option,
|
|
'full': '%s.full' % option,
|
|
'type_cls': type_cls,
|
|
'domain': domain,
|
|
'localedir': cls.config_settings_locale_directory,
|
|
}
|
|
|
|
if default != DefaultValue:
|
|
meta['default'] = default
|
|
|
|
if choices is not None:
|
|
meta['choices'] = choices
|
|
|
|
cls.config_settings[section][option] = meta
|
|
|
|
|
|
class ConfigSettings(collections.Mapping):
|
|
"""Interface for configuration settings.
|
|
|
|
This is the main interface to the configuration.
|
|
|
|
A configuration is a collection of sections. Each section contains
|
|
key-value pairs.
|
|
|
|
When an instance is created, the caller first registers ConfigProvider
|
|
instances with it. This tells the ConfigSettings what individual settings
|
|
are available and defines extra metadata associated with those settings.
|
|
This is used for validation, etc.
|
|
|
|
Once ConfigProvider instances are registered, a config is populated. It can
|
|
be loaded from files or populated by hand.
|
|
|
|
ConfigSettings instances are accessed like dictionaries or by using
|
|
attributes. e.g. the section "foo" is accessed through either
|
|
settings.foo or settings['foo'].
|
|
|
|
Sections are modeled by the ConfigSection class which is defined inside
|
|
this one. They look just like dicts or classes with attributes. To access
|
|
the "bar" option in the "foo" section:
|
|
|
|
value = settings.foo.bar
|
|
value = settings['foo']['bar']
|
|
value = settings.foo['bar']
|
|
|
|
Assignment is similar:
|
|
|
|
settings.foo.bar = value
|
|
settings['foo']['bar'] = value
|
|
settings['foo'].bar = value
|
|
|
|
You can even delete user-assigned values:
|
|
|
|
del settings.foo.bar
|
|
del settings['foo']['bar']
|
|
|
|
If there is a default, it will be returned.
|
|
|
|
When settings are mutated, they are validated against the registered
|
|
providers. Setting unknown settings or setting values to illegal values
|
|
will result in exceptions being raised.
|
|
"""
|
|
|
|
class ConfigSection(collections.MutableMapping, object):
|
|
"""Represents an individual config section."""
|
|
def __init__(self, config, name, settings):
|
|
object.__setattr__(self, '_config', config)
|
|
object.__setattr__(self, '_name', name)
|
|
object.__setattr__(self, '_settings', settings)
|
|
|
|
# MutableMapping interface
|
|
def __len__(self):
|
|
return len(self._settings)
|
|
|
|
def __iter__(self):
|
|
return iter(self._settings.keys())
|
|
|
|
def __contains__(self, k):
|
|
return k in self._settings
|
|
|
|
def __getitem__(self, k):
|
|
if k not in self._settings:
|
|
raise KeyError('Option not registered with provider: %s' % k)
|
|
|
|
meta = self._settings[k]
|
|
|
|
if self._config.has_option(self._name, k):
|
|
return meta['type_cls'].from_config(self._config, self._name, k)
|
|
|
|
if not 'default' in meta:
|
|
raise KeyError('No default value registered: %s' % k)
|
|
|
|
return meta['default']
|
|
|
|
def __setitem__(self, k, v):
|
|
if k not in self._settings:
|
|
raise KeyError('Option not registered with provider: %s' % k)
|
|
|
|
meta = self._settings[k]
|
|
|
|
meta['type_cls'].validate(v)
|
|
|
|
if not self._config.has_section(self._name):
|
|
self._config.add_section(self._name)
|
|
|
|
self._config.set(self._name, k, meta['type_cls'].to_config(v))
|
|
|
|
def __delitem__(self, k):
|
|
self._config.remove_option(self._name, k)
|
|
|
|
# Prune empty sections.
|
|
if not len(self._config.options(self._name)):
|
|
self._config.remove_section(self._name)
|
|
|
|
def __getattr__(self, k):
|
|
return self.__getitem__(k)
|
|
|
|
def __setattr__(self, k, v):
|
|
self.__setitem__(k, v)
|
|
|
|
def __delattr__(self, k):
|
|
self.__delitem__(k)
|
|
|
|
|
|
def __init__(self):
|
|
self._config = RawConfigParser()
|
|
|
|
self._settings = {}
|
|
self._sections = {}
|
|
self._finalized = False
|
|
self._loaded_filenames = set()
|
|
|
|
def load_file(self, filename):
|
|
self.load_files([filename])
|
|
|
|
def load_files(self, filenames):
|
|
"""Load a config from files specified by their paths.
|
|
|
|
Files are loaded in the order given. Subsequent files will overwrite
|
|
values from previous files. If a file does not exist, it will be
|
|
ignored.
|
|
"""
|
|
filtered = [f for f in filenames if os.path.exists(f)]
|
|
|
|
fps = [open(f, 'rt') for f in filtered]
|
|
self.load_fps(fps)
|
|
self._loaded_filenames.update(set(filtered))
|
|
for fp in fps:
|
|
fp.close()
|
|
|
|
def load_fps(self, fps):
|
|
"""Load config data by reading file objects."""
|
|
|
|
for fp in fps:
|
|
self._config.readfp(fp)
|
|
|
|
def loaded_files(self):
|
|
return self._loaded_filenames
|
|
|
|
def write(self, fh):
|
|
"""Write the config to a file object."""
|
|
self._config.write(fh)
|
|
|
|
def validate(self):
|
|
"""Ensure that the current config passes validation.
|
|
|
|
This is a generator of tuples describing any validation errors. The
|
|
elements of the tuple are:
|
|
|
|
(bool) True if error is fatal. False if just a warning.
|
|
(str) Type of validation issue. Can be one of ('unknown-section',
|
|
'missing-required', 'type-error')
|
|
"""
|
|
|
|
def register_provider(self, provider):
|
|
"""Register a ConfigProvider with this settings interface."""
|
|
|
|
if self._finalized:
|
|
raise Exception('Providers cannot be registered after finalized.')
|
|
|
|
provider.register_settings()
|
|
|
|
for section_name, settings in provider.config_settings.items():
|
|
section = self._settings.get(section_name, {})
|
|
|
|
for k, v in settings.items():
|
|
if k in section:
|
|
raise Exception('Setting already registered: %s.%s' %
|
|
section_name, k)
|
|
|
|
section[k] = v
|
|
|
|
self._settings[section_name] = section
|
|
|
|
def write_pot(self, fh):
|
|
"""Write a pot gettext translation file."""
|
|
|
|
for section in sorted(self):
|
|
fh.write('# Section %s\n\n' % section)
|
|
for option in sorted(self[section]):
|
|
fh.write('msgid "%s.%s.short"\n' % (section, option))
|
|
fh.write('msgstr ""\n\n')
|
|
|
|
fh.write('msgid "%s.%s.full"\n' % (section, option))
|
|
fh.write('msgstr ""\n\n')
|
|
|
|
fh.write('# End of section %s\n\n' % section)
|
|
|
|
def option_help(self, section, option):
|
|
"""Obtain the translated help messages for an option."""
|
|
|
|
meta = self[section]._settings[option]
|
|
|
|
# Providers should always have an en-US translation. If they don't,
|
|
# they are coded wrong and this will raise.
|
|
default = gettext.translation(meta['domain'], meta['localedir'],
|
|
['en-US'])
|
|
|
|
t = gettext.translation(meta['domain'], meta['localedir'],
|
|
fallback=True)
|
|
t.add_fallback(default)
|
|
|
|
short = t.ugettext('%s.%s.short' % (section, option))
|
|
full = t.ugettext('%s.%s.full' % (section, option))
|
|
|
|
return (short, full)
|
|
|
|
def _finalize(self):
|
|
if self._finalized:
|
|
return
|
|
|
|
for section, settings in self._settings.items():
|
|
s = ConfigSettings.ConfigSection(self._config, section, settings)
|
|
self._sections[section] = s
|
|
|
|
self._finalized = True
|
|
|
|
# Mapping interface.
|
|
def __len__(self):
|
|
return len(self._settings)
|
|
|
|
def __iter__(self):
|
|
self._finalize()
|
|
|
|
return iter(self._sections.keys())
|
|
|
|
def __contains__(self, k):
|
|
return k in self._settings
|
|
|
|
def __getitem__(self, k):
|
|
self._finalize()
|
|
|
|
return self._sections[k]
|
|
|
|
# Allow attribute access because it looks nice.
|
|
def __getattr__(self, k):
|
|
return self.__getitem__(k)
|