blob: 3d47abf9683e7c3ebf781f4cd4a38aedc4166449 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Configuration objects for Bloodhound product environments"""
__all__ = 'Configuration', 'Section'
import os.path
from trac.config import Configuration, ConfigurationError, Option, \
OrderedExtensionsOption, Section, _use_default
from trac.resource import ResourceNotFound
from trac.util.text import to_unicode
from multiproduct.model import ProductSetting
from multiproduct.perm import MultiproductPermissionPolicy
class Configuration(Configuration):
"""Product-aware settings repository equivalent to instances of
`trac.config.Configuration` (and thus `ConfigParser` from the
Python Standard Library) but retrieving configuration values
from the database.
"""
def __init__(self, env, product, parents=None):
"""Initialize configuration object with an instance of
`trac.env.Environment` and product prefix.
Optionally it is possible to inherit settings from parent
Configuration objects. Environment's configuration will not
be added to parents list.
"""
self.env = env
self.product = to_unicode(product)
self._sections = {}
self._setup_parents(parents)
def __getitem__(self, name):
"""Return the configuration section with the specified name.
"""
if name not in self._sections:
self._sections[name] = Section(self, name)
return self._sections[name]
def sections(self, compmgr=None, defaults=True):
"""Return a list of section names.
If `compmgr` is specified, only the section names corresponding to
options declared in components that are enabled in the given
`ComponentManager` are returned.
"""
sections = set(to_unicode(s) \
for s in ProductSetting.get_sections(self.env, self.product))
for parent in self.parents:
sections.update(parent.sections(compmgr, defaults=False))
if defaults:
sections.update(self.defaults(compmgr))
return sorted(sections)
def has_option(self, section, option, defaults=True):
"""Returns True if option exists in section in either the project
trac.ini or one of the parents, or is available through the Option
registry.
(since Trac 0.11)
"""
if ProductSetting.exists(self.env, self.product, section, option):
return True
for parent in self.parents:
if parent.has_option(section, option, defaults=False):
return True
return defaults and (section, option) in Option.registry
def save(self):
"""Nothing to do.
Notice: Opposite to Trac's Configuration objects Bloodhound's
product configuration objects commit changes to the database
immediately. Thus there's no much to do in this method.
"""
def parse_if_needed(self, force=False):
"""Just invalidate options cache.
Notice: Opposite to Trac's Configuration objects Bloodhound's
product configuration objects commit changes to the database
immediately. Thus there's no much to do in this method.
"""
for section in self.sections():
self[section]._cache.clear()
def touch(self):
pass
def set_defaults(self, compmgr=None):
"""Retrieve all default values and store them explicitly in the
configuration, so that they can be saved to file.
Values already set in the configuration are not overridden.
"""
for section, default_options in self.defaults(compmgr).items():
for name, value in default_options.items():
if not ProductSetting.exists(self.env, self.product,
section, name):
if any(parent[section].contains(name, defaults=False)
for parent in self.parents):
value = None
self.set(section, name, value)
# Helper methods
def _setup_parents(self, parents=None):
"""Inherit configuration from parent `Configuration` instances.
If there's a value set to 'file' option in 'inherit' section then
it will be considered as a list of paths to .ini files
that will be added to parents list as well.
"""
from trac import config
self.parents = (parents or [])
for filename in self.get('inherit', 'file').split(','):
filename = Section._normalize_path(filename.strip(), self.env)
self.parents.append(config.Configuration(filename))
class Section(Section):
"""Proxy for a specific configuration section.
Objects of this class should not be instantiated directly.
"""
__slots__ = ['config', 'name', 'overridden', '_cache']
@staticmethod
def optionxform(optionstr):
return to_unicode(optionstr.lower());
def __init__(self, config, name):
self.config = config
self.name = to_unicode(name)
self.overridden = {}
self._cache = {}
@property
def env(self):
return self.config.env
@property
def product(self):
return self.config.product
def contains(self, key, defaults=True):
key = self.optionxform(key)
if ProductSetting.exists(self.env, self.product, self.name, key):
return True
for parent in self.config.parents:
if parent[self.name].contains(key, defaults=False):
return True
return defaults and Option.registry.has_key((self.name, key))
__contains__ = contains
def iterate(self, compmgr=None, defaults=True):
"""Iterate over the options in this section.
If `compmgr` is specified, only return default option values for
components that are enabled in the given `ComponentManager`.
"""
options = set()
name_str = self.name
for setting in ProductSetting.select(self.env,
where={'product':self.product, 'section':name_str}):
option = self.optionxform(setting.option)
options.add(option)
yield option
for parent in self.config.parents:
for option in parent[self.name].iterate(defaults=False):
loption = self.optionxform(option)
if loption not in options:
options.add(loption)
yield option
if defaults:
for section, option in Option.get_registry(compmgr).keys():
if section == self.name and \
self.optionxform(option) not in options:
yield option
__iter__ = iterate
def __repr__(self):
return '<%s [%s , %s]>' % (self.__class__.__name__, \
self.product, self.name)
def get(self, key, default=''):
"""Return the value of the specified option.
Valid default input is a string. Returns a string.
"""
key = self.optionxform(key)
cached = self._cache.get(key, _use_default)
if cached is not _use_default:
return cached
name_str = self.name
key_str = to_unicode(key)
settings = ProductSetting.select(self.env,
where={'product':self.product, 'section':name_str,
'option':key_str})
if len(settings) > 0:
value = settings[0].value
else:
for parent in self.config.parents:
value = parent[self.name].get(key, _use_default)
if value is not _use_default:
break
else:
if default is not _use_default:
option = Option.registry.get((self.name, key))
value = option.default if option else _use_default
else:
value = _use_default
if value is _use_default:
return default
if not value:
value = u''
elif isinstance(value, basestring):
value = to_unicode(value)
self._cache[key] = value
return value
def getpath(self, key, default=''):
"""Return a configuration value as an absolute path.
Relative paths are resolved relative to `conf` subfolder
of the target global environment. This approach is consistent
with TracIni path resolution.
Valid default input is a string. Returns a normalized path.
(enabled since Trac 0.11.5)
"""
path = self.get(key, default)
if not path:
return default
return self._normalize_path(path, self.env)
def remove(self, key):
"""Delete a key from this section.
Like for `set()`, the changes won't persist until `save()` gets called.
"""
key_str = self.optionxform(key)
option_key = {
'product' : self.product,
'section' : self.name,
'option' : key_str,
}
try:
setting = ProductSetting(self.env, keys=option_key)
except ResourceNotFound:
self.env.log.warning("No record for product option %s", option_key)
else:
self._cache.pop(key, None)
setting.delete()
self.env.log.info("Removing product option %s", option_key)
def set(self, key, value):
"""Change a configuration value.
These changes will be persistent right away.
"""
key_str = self.optionxform(key)
value_str = to_unicode(value)
self._cache.pop(key_str, None)
option_key = {
'product' : self.product,
'section' : self.name,
'option' : key_str,
}
try:
setting = ProductSetting(self.env, option_key)
except ResourceNotFound:
if value is not None:
# Insert new record in the database
setting = ProductSetting(self.env)
setting._data.update(option_key)
setting._data['value'] = value_str
self.env.log.debug('Writing option %s', setting._data)
setting.insert()
else:
if value is None:
# Delete existing record from the database
# FIXME : Why bother with setting overriden
self.overridden[key] = True
setting.delete()
else:
# Update existing record
setting._data['value'] = value
setting.update()
# Helper methods
@staticmethod
def _normalize_path(path, env):
if not os.path.isabs(path):
path = os.path.join(env.path, 'conf', path)
return os.path.normcase(os.path.realpath(path))
#--------------------
# Option override classes
#--------------------
class ProductPermissionPolicyOption(OrderedExtensionsOption):
"""Prepend an instance of `multiproduct.perm.MultiproductPermissionPolicy`
"""
def __get__(self, instance, owner):
# FIXME: Better handling of recursive imports
from multiproduct.env import ProductEnvironment
if instance is None:
return self
components = OrderedExtensionsOption.__get__(self, instance, owner)
env = getattr(instance, 'env', None)
return [MultiproductPermissionPolicy(env)] + components \
if isinstance(env, ProductEnvironment) \
else components