#
# Copyright (C) 2012 Niek Linnenbank
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Bouwer configuration layer
Configuration allows the user to influence the execution
of the build automation system. A user can create and modify
a (set of) configuration(s) using any available configuration frontend.
For example, the :class:`.LineConfig` plugin allows the user to change
configuration items via the command line.
"""
import os
import sys
import logging
import inspect
import shlex
import json
import collections
import bouwer.util
[docs]class Config(object):
"""
Represents a single generic configuration item
"""
def __init__(self, name, value, path = None, **keywords):
"""
Constructor
"""
self.name = name
self._value = value
self._parent = None
self._path = path
self._keywords = keywords
self.update(value)
[docs] def get_key(self, key, default = None):
""" Wrapper for the :obj:`dict.get` function """
# TODO: why do we need this function???
#return self._interpolate(self._keywords.get(key, default))
try:
return self.__getitem__(key)
except KeyError:
return default
[docs] def keys(self):
""" Retrieve a list of keyword keys """
return self._keywords.keys()
def __getitem__(self, key):
"""
Implements the conf_item['keyword'] mechanism.
"""
try:
return self._interpolate(self._keywords[key])
except KeyError as e:
if self._parent:
return self._parent[key]
else:
raise e
def _interpolate(self, text):
"""
Substitute ${ITEMNAME} with the value of a Configuration item.
"""
# Only process strings.
if type(text) is not str:
return text
offset = 0
output = ""
saved_idx = 0
# TODO: is there an easier way in python???
while True:
# Start of the item name
idx_start = text.find('${', saved_idx)
if idx_start == -1:
break
# End of the item name
idx_end = text.find('}', idx_start)
if idx_end == -1:
break
# Get item
length = idx_end - idx_start
item = Configuration.Instance().get(text[idx_start+2 : idx_start+length])
# Append to the output
output += text[ saved_idx : saved_idx + (idx_start-saved_idx) ]
output += str(item.value())
saved_idx = idx_end + 1
# Append the last part
output += text[ saved_idx : ]
return output
[docs] def satisfied(self, tree = None):
"""
See if we are satisfied with all our dependencies in `tree`
If no `tree` is specified, the currently active tree will be searched
"""
if tree is None:
tree = Configuration.Instance().active_tree
# See if our dependencies are met.
for dep in self.get_key('depends', []):
if tree.get(dep) is not None and not tree.get(dep).satisfied(tree):
return False
# If we are in a list, then we must be selected to satisfy.
if self.get_key('in_list', False):
lst = tree.get(self.get_key('in_list'))
return lst.value(tree) == self.name
# TODO: hack
if type(self._value) is bool:
return self._value
else:
return True
[docs] def value(self, tree = None): # TODO: do we need the tree argument???
"""
Retrieve the current value of the configuration item
"""
# TODO: do we need the tree parameter???
if self._value is None and self._parent:
return self._parent.value()
else:
return self._value
[docs] def update(self, value):
"""
Assign a new `value` to the configuration item
"""
self._value = value
[docs] def add_dependency(self, item_name):
"""
Introduce a new dependency on `item_name`
"""
if 'depends' not in self._keywords:
self._keywords['depends'] = []
if item_name not in self._keywords['depends']:
self._keywords['depends'].append(item_name)
[docs] def serialize(self, tree):
"""
Return serializable representation of the item
"""
return dict(name = self.name,
type = self.__class__.__name__,
value = self.value(tree),
keywords = self._keywords,
path = self._path,
parent = str(self._parent.name) if self._parent else None )
def __str__(self):
""" String representation """
return str(self.value())
def __repr__(self):
""" Interactive representation """
return self.name
[docs]class ConfigBool(Config):
"""
Boolean configuration item
This type of configuration item can only be `True` or `False`.
"""
def __init__(self, name, value, path = None, **keywords):
"""
Constructor
"""
super(ConfigBool, self).__init__(name, bouwer.util.str2bool(value), path, **keywords)
# List items are always true.
if 'in_list' in keywords:
self._value = True
[docs] def value(self, tree = None):
"""
Retrieve our value, also taking dependencies into account.
"""
if tree is None:
tree = Configuration.Instance().active_tree
return self._value and self.satisfied(tree)
[docs] def update(self, value):
"""
Update the `value` of this boolean item
"""
super(ConfigBool, self).update(bouwer.util.str2bool(value))
[docs]class ConfigString(Config):
"""
String configuration item
This type of configuration item contains any string value,
which must be of the basic type `str`. Empty strings are allowed.
"""
[docs] def update(self, value):
"""
Update the `value` of this string item
"""
if type(value) is not str:
raise Exception('value must be a string')
else:
super(ConfigString, self).update(value)
[docs]class ConfigList(Config):
"""
List configuration item
"""
#def __init__(self, name, value = None, **keywords):
# """
# Constructor
# """
# super(ConfigList, self).__init__(name, value, **keywords)
[docs] def add(self, item):
"""
Add an option to the list
"""
self._keywords.setdefault('options', []).append(item.name)
# TODO: refix this
#def update(self, value):
# """
# Update the selected item in this list
# """
#
# if value not in self.get_key('options'):
# raise Exception("item " + str(value) + " not in list " + self.name)
# else:
# super(ConfigList, self).update(value)
[docs]class ConfigInt:
"""
Integer configuration item
"""
pass
[docs]class ConfigFloat:
"""
Floating point number configuration item
"""
pass
[docs]class ConfigTri:
"""
Tristate configuration item
"""
pass
[docs]class ConfigTree(ConfigBool):
"""
Tree configuration items can contain other configuration items
"""
def __init__(self, name, value, path = None, parent = None, **keywords):
"""
Constructor
"""
super(ConfigTree, self).__init__(name, value, ".", **keywords)
self.subitems = collections.OrderedDict()
self._parent = parent
[docs] def add(self, item, path = None):
"""
Introduce a new :class:`.Config` item to the tree
"""
# TODO: here we must set the parent correctly....
# if the item is not in this tree, look for the parent of us (the tree).
# if our parent has the item, make the parent of the new item that item in our parent tree...
# TODO: watch out... are subdirectories added in the correct sequence?
# we want to avoid the scenario where we need to re-update all the parents again.
# Every item in the tree contains a list with items
if item.name not in self.subitems:
self.subitems[item.name] = []
if self._parent:
item._parent = self._parent.get(item.name)
# TODO: this assumes items are added in order of directory hierarchy... is this true???
else:
item._parent = self.subitems[item.name][-1]
# Add to the subitems dict
if path is None:
path = Configuration.Instance().active_dir
item._path = path
self.subitems[item.name].append(item)
[docs] def get(self, item_name):
"""
Retrieve item in this tree with the given `item_name`
"""
try:
return getattr(self, item_name)
except AttributeError:
return None
[docs] def satisfied(self, tree = None):
"""
See if we are satisfied with all our dependencies in `tree`
If no `tree` is specified, the currently active tree will be searched
"""
conf = Configuration.Instance()
if tree is None:
tree = conf.active_tree
return (tree is self) or conf.edit_mode
[docs] def value(self, tree = None):
"""
Retrieve value of the tree. Either `True` or `False`.
"""
conf = Configuration.Instance()
if tree is None:
tree = conf.active_tree
return (tree.name == self.name and super(ConfigTree, self).value(self)) or conf.edit_mode
[docs] def get_items_by_path(self):
"""
Return a dictionary with path as key and items as value
"""
ret_dict = collections.OrderedDict()
for item_list in self.subitems.values():
for item in item_list:
ret_dict.setdefault(item._path, []).append(item)
return ret_dict
def __getattr__(self, name):
"""
Implements the `conf.ITEM` mechanism
"""
if name == self.name:
return self
elif name in self.__dict__:
return self.__dict__[name]
conf = Configuration.Instance()
# See if we know this item in this tree
if name in self.__dict__['subitems']:
items = self.__dict__['subitems'][name]
path = conf.active_dir
match_item = None
# Find a matching item
while path and path != '/' and not match_item:
for item in items:
if item._path == path:
match_item = item
break
path = os.path.dirname(path)
# If no match, ask the parent
if not match_item and self._parent:
match_item = self._parent.get(name)
# If still no match, just return the first available
if not match_item:
match_item = items[0]
return match_item
if name in conf.trees:
return conf.trees[name]
elif '_parent' in self.__dict__:
return self.__dict__['_parent'].get(name)
else:
raise AttributeError('no such attribute: ' + str(name))
[docs]class BouwConfigParser:
"""
Parser for Bouwconfig files
"""
CONFIG_MODE = 1
CHOICE_MODE = 2
KEYWORD_MODE = 3
HELP_MODE = 4
TREE_MODE = 5
def __init__(self, conf):
"""
Constructor
"""
# TODO: make these private
self.conf = conf
self.log = logging.getLogger(__name__)
self.helpindent = None
self.name = None
self.item = None
self.mode = self.CONFIG_MODE
self.syntax = { 'config' : self._parse_config,
'choice' : self._parse_choice,
'tree' : self._parse_tree,
'inside' : self._parse_inside,
'keywords' : self._parse_keywords,
'help' : self._parse_help,
'default' : self._parse_default,
'string' : self._parse_string,
'tristate' : self._parse_tristate,
'bool' : self._parse_bool,
'endchoice' : self._parse_endchoice,
'depends' : self._parse_depends }
# TODO: rewrite this. Its too complicated.
[docs] def parse(self, filename):
"""
Parse a Bouwconfig file
"""
self.log.debug('reading `' + filename + '\'')
self.name = None
self.item = None
self.choice = None
self.mode = self.CONFIG_MODE
for line in open(filename).readlines():
if self.mode == self.KEYWORD_MODE:
if line.find('=') == -1:
self.mode = self.CONFIG_MODE
else:
parsed = line.partition('=')
key = parsed[0].strip()
value = parsed[2].strip()
self.item._keywords[key] = value
continue
if self.mode == self.HELP_MODE:
if self.helpindent is None:
self.helpindent = self._get_indent(line)
if line.find(self.helpindent) == -1:
self.mode = self.HELP_MODE
self.helpindex = None
else:
helpstr = line[ len(self.helpindent) : ]
self.item._keywords['help'] += helpstr
continue
self.parsed = shlex.split(line, True)
if len(self.parsed) == 0:
continue
else:
self.syntax[self.parsed[0]](line)
def _get_indent(self, line):
index = 0
for char in line:
if char in (' ', '\t'):
index += 1
else:
break
return line[:index]
def _parse_depends(self, line):
if 'depends' not in self.item._keywords:
self.item._keywords['depends'] = []
self.item._keywords['depends'].append(self.parsed[2])
def _parse_keywords(self, line):
self.mode = self.KEYWORD_MODE
def _parse_config(self, line):
self.name = self.parsed[1]
self.tree = 'DEFAULT'
self.mode = self.CONFIG_MODE
def _parse_choice(self, line):
self.name = self.parsed[1]
self.tree = 'DEFAULT'
self.mode = self.CHOICE_MODE
def _parse_endchoice(self, line):
self.mode = self.CONFIG_MODE
self.choice = None
def _parse_inside(self, line):
self.tree = self.parsed[1]
def _parse_tree(self, line):
self.item = ConfigTree(self.name, True, parent = self.conf.trees['DEFAULT'])
self.conf.put(self.item)
def _parse_default(self, line):
self.item.update(self.parsed[1])
def _parse_string(self, line):
self.item = ConfigString(self.name, '', self.conf.active_dir)
self.conf.put(self.item, self.tree)
def _parse_tristate(self, line):
pass
def _parse_bool(self, line):
if self.mode == self.CHOICE_MODE:
self.item = ConfigList(self.name, None, self.conf.active_dir)
self.choice = self.item
else:
self.item = ConfigBool(self.name, True, self.conf.active_dir)
if self.choice is not None:
self.item._keywords['in_list'] = self.choice.name
self.choice.add(self.item)
self.conf.put(self.item, self.tree)
def _parse_help(self, line):
self.item._keywords['help'] = ''
self.mode = self.HELP_MODE
[docs]class Configuration(bouwer.util.Singleton):
"""
Represents the current configuration
"""
def __init__(self, cli = None):
"""
Constructor
"""
self.cli = cli
self.log = logging.getLogger(__name__)
self.args = cli.args
self.trees = {}
self.parser = BouwConfigParser(self)
self.edit_mode = False
# Find the path to the Bouwer predefined configuration files
curr_file = inspect.getfile(inspect.currentframe())
curr_dir = os.path.dirname(os.path.abspath(curr_file))
base_path = os.path.dirname(os.path.abspath(curr_dir + '..' + os.sep + '..' + os.sep))
self.base_conf = base_path + os.sep + 'config'
# The active tree and directory are used for
# evaluation in the Config class, if needed.
self.active_tree = None
self.active_dir = self.base_conf
# Attempt to load saved config, otherwise reset to predefined.
if not self.load():
self.reset()
# Validate tree
# self._validate()
# Dump configuration for debugging
self.dump()
[docs] def get(self, item_name):
"""
Retrieve item named `item_name` from the active tree
"""
return self.active_tree.get(item_name)
[docs] def put(self, item, tree_name = 'DEFAULT', path = None):
"""
Introduce a new :class:`.Config` object `item`
"""
if isinstance(item, ConfigTree):
self.trees[item.name] = item
else:
self.trees[tree_name].add(item, path)
[docs] def load(self, filename = '.bouwconf'):
"""
Load a saved configuration from the given `filename`
"""
if not os.path.isfile(filename):
return False
try:
contents = open(filename).read()
except IOError as e:
self.log.critical("failed to read configuration file `" +
str(filename) + "':" + str(e))
sys.exit(1)
# Parse the JSON and convert to python dict.
conf_dict = json.loads(contents, cls = bouwer.util.AsciiDecoder)
# Add all items to the configuration
for json_name, json_paths in conf_dict.items():
for json_item in json_paths:
conf_class = getattr(bouwer.config, json_item['type'])
conf_item = conf_class(json_item['name'],
json_item['value'],
**json_item['keywords'])
# set active_dir to path
if 'path' in json_item:
self.active_dir = json_item['path']
# Set parent
#if 'parent' in json_item:
# conf_item._parent = self.trees[json_item['parent']]
if type(conf_item) is ConfigTree:
self.put(conf_item)
self.active_tree = conf_item
else:
self.put(conf_item, json_item['tree'])
return True
[docs] def save(self, filename = '.bouwconf'):
"""
Save the current configuration to `filename`
"""
fp = open(filename, 'w')
# Ordered dict makes sure items added stay in order,
# i.e. ConfigTree's will appear first in the JSON file
conf_dict = collections.OrderedDict()
# First add trees.
for tree in self.trees.values():
conf_dict[tree.name] = [ tree.serialize(tree) ]
# Now the subitems
for tree in self.trees.values():
for subitem_entry in tree.subitems.values():
for subitem in subitem_entry:
if subitem.name not in conf_dict:
conf_dict[subitem.name] = []
item_dict = subitem.serialize(tree)
item_dict['tree'] = tree.name
conf_dict[subitem.name].append(item_dict)
fp.write(json.dumps(conf_dict, ensure_ascii=True, indent=4, separators=(',', ': ')))
fp.write(os.linesep)
fp.close()
[docs] def reset(self):
"""
Reset configuration to the initial predefined state using Bouwconfigs
"""
# Insert the default tree.
self.put(ConfigTree('DEFAULT', True))
self.active_tree = self.trees['DEFAULT']
# Parse all pre-defined configurations from Bouwer
self._scan_dir(self.base_conf)
# Parse all user defined configurations
self._scan_dir('.') #os.getcwd())
# TODO: this should be inside the ConfigTree... not in here...
[docs] def dump(self):
"""
Dump the current configuration to the debug log
"""
for tree_name, tree in self.trees.items():
self._dump_item(tree, tree)
def _validate(self):
"""
Validate & enforce dependencies in all trees.
"""
for tree_name, tree in self.trees.items():
for item_name, paths in tree.subitems.items():
for path_name, item in paths.items():
for dep in item.get_key('depends', []):
# Is it an unknown dependency?
if dep not in tree.subitems and \
dep not in self.trees:
raise Exception('Unknown dependency item ' + dep + ' in ' + item_name)
# TODO: add circular dependency check
def _dump_item(self, item, tree, parent = ''):
"""
Dump a single configuration item
"""
self.log.debug(parent + item.name + ':' + str(item.__class__) + ' = ' + str(item.value(tree)) + ' (@'+str(item._path)+')')
for key in item._keywords:
self.log.debug('\t' + key + ' => ' + str(item._keywords[key]).replace('\n', '.'))
self.log.debug('')
# TODO: this dumping could be improved...
# TODO: perhaps put the dump method in ConfigTree itself?
if isinstance(item, ConfigTree):
for child_item_name, subitems in item.subitems.items():
for child_item in subitems:
self._dump_item(child_item, tree, parent + item.name + '.')
def _scan_dir(self, dirname):
"""
Scans a directory for Bouwconfig files
"""
found = False
# Look for all Bouwconfig's.
for filename in os.listdir(dirname):
# TODO: replace 'Bouwconfig' literal with a constant, e.g. BOUWCONF or something or CONFFILE
if filename.endswith('Bouwconfig'):
self.active_dir = dirname
self.parser.parse(dirname + os.sep + filename)
found = True
# Only scan subdirectories if at least one Bouwconfig found.
if found:
for filename in os.listdir(dirname):
if os.path.isdir(dirname + os.sep + filename):
self._scan_dir(dirname + os.sep + filename)