You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
680 lines
20 KiB
680 lines
20 KiB
# coding=utf-8
|
|
from __future__ import absolute_import, division, print_function, unicode_literals
|
|
from builtins import object
|
|
from collections import OrderedDict
|
|
from glob import glob
|
|
from itertools import chain
|
|
from os import path
|
|
|
|
import io
|
|
|
|
from . _configuration_validator import AppConfigurationValidator
|
|
from . _internal import FileBuffer, NamedObject, ObjectView
|
|
from .. utils import SlimLogger, encode_string
|
|
from .. utils.internal import string
|
|
|
|
|
|
#
|
|
# Copyright © Splunk, Inc. All Rights Reserved.
|
|
|
|
""" app_configuration module
|
|
|
|
The app_configuration_spec module defines this class hierarchy:
|
|
|
|
.. code-block::
|
|
AppConfiguration(ObjectView)
|
|
|
|
|
└-> (attribute, files: AppConfigurationFile* )*
|
|
|
|
|
├-> filename: string
|
|
|
|
|
└-> stanzas: (
|
|
name: string,
|
|
AppConfigurationStanza(NamedObject) )*
|
|
|
|
|
├-> placement: AppConfigurationPlacement
|
|
|
|
|
├-> position: FilePosition
|
|
|
|
|
└-> settings: (
|
|
name: string,
|
|
AppConfigurationSetting)*
|
|
|
|
|
├-> name: string
|
|
|
|
|
├-> value: string
|
|
|
|
|
├-> position: FilePosition
|
|
|
|
|
└-> placement: AppConfigurationPlacement
|
|
""" # pylint: disable=pointless-string-statement
|
|
|
|
|
|
class AppConfiguration(object):
|
|
|
|
def __init__(self, app_root):
|
|
""" Create an AppConfiguration object that holds the results from parse conf.
|
|
|
|
"""
|
|
self._app_root = app_root
|
|
self._files = None
|
|
|
|
# region Special methods
|
|
|
|
def __repr__(self):
|
|
return self.__class__.__name__ + '(app_root=' + repr(self._app_root) + ')'
|
|
|
|
# endregion
|
|
|
|
# region Accessors
|
|
|
|
@property
|
|
def app_root(self):
|
|
return self._app_root
|
|
|
|
def files(self):
|
|
files = self._files
|
|
return (files[name] for name in files)
|
|
|
|
def get(self, file, stanza=None, setting=None): # pylint: disable=redefined-builtin
|
|
if stanza is None and setting is not None:
|
|
raise ValueError('Expected setting to be None because stanza is None')
|
|
try:
|
|
file = self._files[file]
|
|
except KeyError:
|
|
value = None
|
|
else:
|
|
value = file if stanza is None else file.get(stanza, setting)
|
|
return value
|
|
|
|
def get_value(self, file, stanza, setting, default=None): # pylint: disable=redefined-builtin
|
|
try:
|
|
file = self._files[file]
|
|
except KeyError:
|
|
value = _default_value(setting, default)[0]
|
|
else:
|
|
value = file.get_value(stanza, setting, default)
|
|
return value
|
|
|
|
def has(self, file, stanza=None, setting=None): # pylint: disable=redefined-builtin
|
|
if stanza is None and setting is not None:
|
|
raise ValueError('Expected setting to be None because stanza is None')
|
|
try:
|
|
file = self._files[file]
|
|
except KeyError:
|
|
value = False
|
|
else:
|
|
value = True if stanza is None else file.has(stanza, setting)
|
|
return value
|
|
|
|
# endregion
|
|
|
|
# region Methods
|
|
|
|
@classmethod
|
|
def load(cls, app_root):
|
|
# TODO: find the right place to validate that the app_root is a directory that contains an app (here?)
|
|
configuration = cls(app_root)
|
|
configuration._load() # pylint: disable=protected-access
|
|
return configuration
|
|
|
|
def save(self, file, indent=False): # pylint: disable=redefined-builtin
|
|
if isinstance(file, string):
|
|
with io.open(file, encoding='utf-8', mode='w', newline='') as ostream:
|
|
self._save(ostream, indent)
|
|
return
|
|
self._save(file, indent)
|
|
|
|
def to_dict(self):
|
|
files = self._files
|
|
value = OrderedDict(((name, files[name]) for name in files)) # copying protects our internals
|
|
return value
|
|
|
|
# endregion
|
|
|
|
# region Protected
|
|
|
|
def _load(self):
|
|
|
|
app_root = self._app_root
|
|
basename = path.basename
|
|
isdir = path.isdir
|
|
join = path.join
|
|
|
|
directory_names = (n for n in (join(app_root, n) for n in ('default', 'local')) if isdir(n))
|
|
configurations = OrderedDict()
|
|
end = -len('.conf')
|
|
|
|
for filename in chain.from_iterable(sorted(glob(join(d, '*.conf'))) for d in directory_names):
|
|
name = basename(filename)[:end]
|
|
try:
|
|
filenames = configurations[name]
|
|
except KeyError:
|
|
filenames = [filename]
|
|
else:
|
|
filenames.append(filename)
|
|
configurations[name] = filenames
|
|
|
|
files = OrderedDict()
|
|
|
|
for name in configurations:
|
|
configuration_file = AppConfigurationFile(name)
|
|
with AppConfigurationValidator(name, app_root) as validator:
|
|
for filename in configurations[name]:
|
|
configuration_file.load(filename, validator)
|
|
files[name] = configuration_file
|
|
|
|
self._files = files
|
|
|
|
def _save(self, ostream, indent):
|
|
iterencode = ObjectView.iterencode_indent if indent is True else ObjectView.iterencode
|
|
for chunk in iterencode(self):
|
|
ostream.write(string(chunk))
|
|
|
|
# endregion
|
|
pass # pylint: disable=unnecessary-pass
|
|
|
|
|
|
class AppConfigurationFile(NamedObject):
|
|
|
|
def __init__(self, name):
|
|
NamedObject.__init__(self, name)
|
|
self._sections = OrderedDict()
|
|
self._stanzas = OrderedDict()
|
|
|
|
# region Special methods
|
|
|
|
def __repr__(self):
|
|
return self.__class__.__name__ + '(name=' + repr(self._name) + 'stanzas=' + repr(self._stanzas) + ')'
|
|
|
|
def __str__(self):
|
|
return encode_string(self._name)
|
|
|
|
# endregion
|
|
|
|
# region Accessors
|
|
|
|
def get(self, stanza, setting=None):
|
|
try:
|
|
stanza = self._stanzas[stanza]
|
|
except KeyError:
|
|
value = None
|
|
else:
|
|
value = stanza if setting is None else stanza.get(setting)
|
|
return value
|
|
|
|
def get_value(self, stanza, setting, default=None):
|
|
try:
|
|
stanza = self._stanzas[stanza]
|
|
except KeyError:
|
|
value = _default_value(setting, default)[0]
|
|
else:
|
|
value = stanza.get_value(setting, default)
|
|
return value
|
|
|
|
def has(self, stanza, setting=None):
|
|
try:
|
|
stanza = self._stanzas[stanza]
|
|
except KeyError:
|
|
value = False
|
|
else:
|
|
value = True if setting is None else stanza.has(setting)
|
|
return value
|
|
|
|
def sections(self):
|
|
sections = self._sections
|
|
return (sections[name] for name in sections)
|
|
|
|
def stanzas(self):
|
|
stanzas = self._stanzas
|
|
return (stanzas[name] for name in stanzas)
|
|
|
|
# endregion
|
|
|
|
# region Methods
|
|
|
|
def load(self, filename, validator):
|
|
|
|
section = AppConfigurationFile.Section.load(filename, validator)
|
|
stanzas = self._stanzas
|
|
|
|
for section_stanza in section.stanzas():
|
|
name = section_stanza.name
|
|
try:
|
|
stanza = stanzas[name]
|
|
except KeyError:
|
|
stanza = AppConfigurationStanza(name)
|
|
stanzas[name] = stanza
|
|
stanza.add(section_stanza)
|
|
|
|
self._sections[filename] = section
|
|
|
|
def to_dict(self):
|
|
sections = self._sections
|
|
return OrderedDict(((name, sections[name]) for name in sections)) # copying protects our internals
|
|
|
|
# endregion
|
|
|
|
class Section(object):
|
|
|
|
def __init__(self, file_buffer):
|
|
self._file_buffer = file_buffer
|
|
|
|
# region Special methods
|
|
|
|
def __repr__(self):
|
|
name, stanzas = repr(self.name), repr(self._file_buffer.stanzas)
|
|
return self.__class__.__name__ + '(name=' + name + ', stanzas=' + stanzas + ')'
|
|
|
|
def __str__(self):
|
|
return encode_string(self.name)
|
|
|
|
# endregion
|
|
|
|
# region Properties
|
|
|
|
@property
|
|
def name(self):
|
|
return self._file_buffer.filename
|
|
|
|
# endregion
|
|
|
|
# region Accessors
|
|
|
|
def get(self, name):
|
|
return self._file_buffer.setting.get(name)
|
|
|
|
def stanzas(self):
|
|
stanzas = self._file_buffer.stanzas
|
|
return (stanzas[name] for name in stanzas)
|
|
|
|
# endregion
|
|
|
|
# region Methods
|
|
|
|
@classmethod
|
|
def load(cls, filename, validator):
|
|
file_buffer = _AppConfigurationFileBuffer(filename)
|
|
file_buffer.load(validator=validator)
|
|
return cls(file_buffer)
|
|
|
|
def save(self, filename=None):
|
|
self._file_buffer.save(filename)
|
|
|
|
def to_dict(self):
|
|
return OrderedDict(((stanza.name, stanza) for stanza in self.stanzas())) # copying protects our internals
|
|
|
|
# endregion
|
|
pass # pylint: disable=unnecessary-pass
|
|
|
|
Section.__name__ = str('AppConfigurationFile.Section')
|
|
|
|
|
|
class AppConfigurationSetting(NamedObject):
|
|
|
|
def __init__(self, name):
|
|
NamedObject.__init__(self, name)
|
|
self._sections = OrderedDict()
|
|
self._setting = None
|
|
|
|
# region Special methods
|
|
|
|
def __eq__(self, other):
|
|
return isinstance(other, AppConfigurationSetting) and self.value == other.value
|
|
|
|
def __ge__(self, other):
|
|
return not self < other
|
|
|
|
def __gt__(self, other):
|
|
return not (self < other or self == other)
|
|
|
|
def __le__(self, other):
|
|
return self < other or self == other
|
|
|
|
def __lt__(self, other):
|
|
return isinstance(other, AppConfigurationSetting) and self.value < other.value
|
|
|
|
def __ne__(self, other):
|
|
return not self == other
|
|
|
|
def __repr__(self):
|
|
name, placement, position = repr(self._name), repr(self.placement), repr(self.position)
|
|
return self.__class__.__name__ + '(name=' + name + ', placement=' + placement + ', position=' + position + ')'
|
|
|
|
def __str__(self):
|
|
return self._setting.__str__()
|
|
|
|
# endregion
|
|
|
|
# region Properties
|
|
|
|
@property
|
|
def placement(self):
|
|
return self._setting.placement
|
|
|
|
@property
|
|
def position(self):
|
|
return self._setting.position
|
|
|
|
@property
|
|
def value(self):
|
|
return self._setting.value
|
|
|
|
# endregion
|
|
|
|
# region Methods
|
|
|
|
def add(self, section):
|
|
self._sections[section.name] = self._setting = section # last-in section holds the value of this setting
|
|
|
|
def to_dict(self):
|
|
return self._setting.to_dict()
|
|
|
|
# endregion
|
|
|
|
class Section(NamedObject):
|
|
|
|
def __init__(self, name, value, position, placement=None):
|
|
|
|
NamedObject.__init__(self, name)
|
|
self._placement = placement
|
|
self._position = position
|
|
self._value = '' if value is None else value.strip()
|
|
|
|
# region Special methods
|
|
|
|
def __repr__(self):
|
|
property_names = self._property_names
|
|
arguments = (n + '=' + repr(getattr(self, property_names[n])) for n in property_names)
|
|
return self.__class__.__name__ + '(' + ', '.join(arguments) + ')'
|
|
|
|
def __str__(self):
|
|
return self._name + ' = ' + string(self._value).replace('\n', '\\\n')
|
|
|
|
# endregion
|
|
|
|
# region Properties
|
|
|
|
@property
|
|
def position(self):
|
|
return self._position
|
|
|
|
@property
|
|
def placement(self):
|
|
return self._placement
|
|
|
|
@property
|
|
def value(self):
|
|
return self._value
|
|
|
|
# endregion
|
|
|
|
# region Methods
|
|
|
|
def to_dict(self):
|
|
return OrderedDict((('value', self._value), ('placement', self._placement), ('line', self._position.line)))
|
|
|
|
# endregion
|
|
|
|
# region Protected
|
|
|
|
_property_names = OrderedDict(((name, '_' + name) for name in ('name', 'value', 'placement', 'position')))
|
|
|
|
# endregion
|
|
pass # pylint: disable=unnecessary-pass
|
|
|
|
Section.__name__ = str('AppConfigurationSetting.Section')
|
|
|
|
|
|
class AppConfigurationStanza(NamedObject):
|
|
|
|
def __init__(self, name):
|
|
NamedObject.__init__(self, name)
|
|
self._sections = OrderedDict()
|
|
self._settings = OrderedDict()
|
|
self._placement = None
|
|
|
|
# region Special methods
|
|
|
|
def __repr__(self):
|
|
return self.__class__.__name__ + '(name=' + repr(self._name) + ', settings=' + repr(self._settings) + ')'
|
|
|
|
def __str__(self):
|
|
return '[' + self._name.replace('\n', '\\n') + ']'
|
|
|
|
# endregion
|
|
|
|
# region Properties
|
|
|
|
@property
|
|
def placement(self):
|
|
return self._placement
|
|
|
|
# endregion
|
|
|
|
# region Accessors
|
|
|
|
def add(self, section):
|
|
|
|
settings = self._settings
|
|
|
|
for section_setting in section.settings():
|
|
try:
|
|
setting = settings[section_setting.name]
|
|
except KeyError:
|
|
setting = AppConfigurationSetting(section_setting.name)
|
|
settings[setting.name] = setting
|
|
setting.add(section_setting)
|
|
|
|
assert self._placement is None or self._placement == section.placement
|
|
self._sections[section.position.file] = section
|
|
self._placement = section.placement
|
|
|
|
def get(self, setting):
|
|
get = self._settings.get
|
|
if isinstance(setting, tuple):
|
|
item = tuple((get(name) for name in setting))
|
|
else:
|
|
item = get(setting)
|
|
return item
|
|
|
|
def get_value(self, setting, default=None):
|
|
|
|
default, is_tuple = _default_value(setting, default)
|
|
|
|
if is_tuple is True:
|
|
value = [None] * len(setting)
|
|
for i, name in enumerate(setting):
|
|
try:
|
|
value[i] = self._settings[name].value
|
|
except KeyError:
|
|
value[i] = default[i]
|
|
value = tuple(value)
|
|
else:
|
|
name = setting
|
|
try:
|
|
value = self._settings[name].value
|
|
except KeyError:
|
|
value = default
|
|
|
|
return value
|
|
|
|
def has(self, setting):
|
|
return setting in self._settings
|
|
|
|
def settings(self):
|
|
settings = self._settings
|
|
return (settings[name] for name in settings)
|
|
|
|
# endregion
|
|
|
|
# region Methods
|
|
|
|
def to_dict(self):
|
|
settings = self._settings
|
|
OrderedDict((name, settings[name]) for name in self._settings) # copying protects our internals
|
|
|
|
# endregion
|
|
|
|
class Section(NamedObject):
|
|
|
|
def __init__(self, name, placement, position):
|
|
NamedObject.__init__(self, name)
|
|
self._settings = OrderedDict()
|
|
self._placement = placement
|
|
self._position = position
|
|
|
|
# region Special methods
|
|
|
|
def __repr__(self):
|
|
return self.__class__.__name__ + '(name=' + repr(self._name) + ', position=' + repr(self._position) + ')'
|
|
|
|
def __str__(self):
|
|
return '[' + self._name.replace('\n', '\\n') + ']'
|
|
|
|
# endregion
|
|
|
|
# region Properties
|
|
|
|
@property
|
|
def placement(self):
|
|
return self._placement
|
|
|
|
@property
|
|
def position(self):
|
|
return self._position
|
|
|
|
# endregion
|
|
|
|
# region Methods
|
|
|
|
def add(self, setting):
|
|
self._settings[setting.name] = setting
|
|
|
|
def get(self, name):
|
|
return self._settings.get(name)
|
|
|
|
def get_value(self, name, default=None):
|
|
try:
|
|
setting = self._settings[name]
|
|
except KeyError:
|
|
value = default
|
|
else:
|
|
value = setting.name
|
|
return value
|
|
|
|
def settings(self):
|
|
settings = self._settings
|
|
return (settings[name] for name in settings)
|
|
|
|
def to_dict(self):
|
|
return OrderedDict((setting.name, setting) for setting in self.settings()) # copying protects our internals
|
|
|
|
# endregion
|
|
pass # pylint: disable=unnecessary-pass
|
|
|
|
Section.__name__ = str('AppConfigurationStanza.Section')
|
|
|
|
|
|
# region Protected
|
|
|
|
def _default_value(setting, default):
|
|
if not isinstance(setting, tuple):
|
|
return default, False
|
|
if default is None:
|
|
return (None,) * len(setting), True
|
|
if not isinstance(default, tuple):
|
|
raise TypeError('Expected default: tuple, not default: ', type(setting).__name__)
|
|
if len(default) != len(setting):
|
|
raise ValueError('Expected len(default) == len(setting)')
|
|
return tuple(default), True
|
|
|
|
|
|
class _AppConfigurationFileBuffer(FileBuffer):
|
|
|
|
def __init__(self, filename):
|
|
FileBuffer.__init__(self, filename)
|
|
self._stanzas = None
|
|
|
|
# region Properties
|
|
|
|
@property
|
|
def stanzas(self):
|
|
return self._stanzas
|
|
|
|
# endregion
|
|
|
|
# region Protected
|
|
|
|
def _load(self, reader, **kwargs):
|
|
""" Loads or reloads the conf file associated with the current Buffer
|
|
|
|
"""
|
|
match_assignment_statement = self._match_assignment_statement
|
|
skip_whitespace = self._skip_whitespace
|
|
self._stanzas = stanzas = OrderedDict()
|
|
validator = kwargs['validator']
|
|
|
|
current_stanza = None
|
|
validate_setting = None
|
|
|
|
for line in reader:
|
|
try:
|
|
match = skip_whitespace(line)
|
|
start = match.end()
|
|
if start >= len(line):
|
|
# blank line
|
|
item = '\n'
|
|
start = 0
|
|
elif line[start] in ';#':
|
|
# comment
|
|
item = line[start:]
|
|
else:
|
|
line = reader.read_continuation(line)
|
|
if line[start] == '[':
|
|
# stanza where namesakes are merged (by way of the call to stanzas.setdefault)
|
|
item = self._parse_stanza(line, start, reader, validator)
|
|
validate_setting = validator.get(item)
|
|
item = current_stanza = stanzas.setdefault(item.name, item)
|
|
else:
|
|
if current_stanza is None:
|
|
# settings before a stanza get put into the [default] stanza
|
|
current_stanza = AppConfigurationStanza.Section(
|
|
'default', validator.get_placement('default'), reader.position
|
|
)
|
|
validate_setting = validator.get(current_stanza)
|
|
stanzas['default'] = current_stanza
|
|
# setting where namesakes are replaced
|
|
match = match_assignment_statement(line, start)
|
|
if match is None:
|
|
text = encode_string(line.strip())
|
|
raise self._Error('Expected a setting assignment, not ' + text)
|
|
item = AppConfigurationSetting.Section(
|
|
name=match.group(1), value=match.group(2), position=reader.position,
|
|
placement=current_stanza.placement)
|
|
current_stanza.add(item)
|
|
validate_setting(item)
|
|
self._append(item, reader.position, indentation=start)
|
|
except self._Error as error:
|
|
SlimLogger.error(reader.position, ': ', error)
|
|
|
|
def _parse_stanza(self, line, start, reader, validator):
|
|
|
|
start += 1
|
|
match = self._search_right_square_bracket(line, start)
|
|
|
|
if match is None:
|
|
SlimLogger.warning(reader.position, ': missing terminating right square bracket at end of stanza header')
|
|
end = -1
|
|
else:
|
|
end = match.start()
|
|
|
|
name = line[start:end]
|
|
placement = validator.get_placement(name)
|
|
return AppConfigurationStanza.Section(name, placement, reader.position)
|
|
|
|
# endregion
|
|
pass # pylint: disable=unnecessary-pass
|
|
|
|
# endregion
|