| 1 | # -*- coding: utf-8 -*-
|
|---|
| 2 | #
|
|---|
| 3 | # Copyright (C) 2005-2023 Edgewall Software
|
|---|
| 4 | # Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
|
|---|
| 5 | # All rights reserved.
|
|---|
| 6 | #
|
|---|
| 7 | # This software is licensed as described in the file COPYING, which
|
|---|
| 8 | # you should have received as part of this distribution. The terms
|
|---|
| 9 | # are also available at https://trac.edgewall.org/wiki/TracLicense.
|
|---|
| 10 | #
|
|---|
| 11 | # This software consists of voluntary contributions made by many
|
|---|
| 12 | # individuals. For the exact contribution history, see the revision
|
|---|
| 13 | # history and logs, available at https://trac.edgewall.org/log/.
|
|---|
| 14 |
|
|---|
| 15 | import copy
|
|---|
| 16 | import os.path
|
|---|
| 17 | import re
|
|---|
| 18 | from configparser import (ConfigParser, NoOptionError, NoSectionError,
|
|---|
| 19 | ParsingError)
|
|---|
| 20 |
|
|---|
| 21 | from trac.admin import AdminCommandError, IAdminCommandProvider
|
|---|
| 22 | from trac.core import Component, ExtensionPoint, TracError, implements
|
|---|
| 23 | from trac.util import AtomicFile, as_bool
|
|---|
| 24 | from trac.util.compat import wait_for_file_mtime_change
|
|---|
| 25 | from trac.util.html import tag
|
|---|
| 26 | from trac.util.text import cleandoc, printout, to_unicode, to_utf8
|
|---|
| 27 | from trac.util.translation import N_, _, dgettext, tag_
|
|---|
| 28 |
|
|---|
| 29 | __all__ = ['Configuration', 'ConfigSection', 'Option', 'BoolOption',
|
|---|
| 30 | 'IntOption', 'FloatOption', 'ListOption', 'ChoiceOption',
|
|---|
| 31 | 'PathOption', 'ExtensionOption', 'OrderedExtensionsOption',
|
|---|
| 32 | 'ConfigurationError']
|
|---|
| 33 |
|
|---|
| 34 | _use_default = object()
|
|---|
| 35 |
|
|---|
| 36 |
|
|---|
| 37 | def _getint(value):
|
|---|
| 38 | return int(value or 0)
|
|---|
| 39 |
|
|---|
| 40 |
|
|---|
| 41 | def _getfloat(value):
|
|---|
| 42 | return float(value or 0.0)
|
|---|
| 43 |
|
|---|
| 44 |
|
|---|
| 45 | def _getlist(value, sep, keep_empty):
|
|---|
| 46 | if not value:
|
|---|
| 47 | return []
|
|---|
| 48 | if isinstance(value, str):
|
|---|
| 49 | if isinstance(sep, (list, tuple)):
|
|---|
| 50 | splitted = re.split('|'.join(map(re.escape, sep)), value)
|
|---|
| 51 | else:
|
|---|
| 52 | splitted = value.split(sep)
|
|---|
| 53 | items = [item.strip() for item in splitted]
|
|---|
| 54 | else:
|
|---|
| 55 | items = list(value)
|
|---|
| 56 | if not keep_empty:
|
|---|
| 57 | items = [item for item in items if item not in (None, '')]
|
|---|
| 58 | return items
|
|---|
| 59 |
|
|---|
| 60 |
|
|---|
| 61 | def _getdoc(option_or_section):
|
|---|
| 62 | doc = to_unicode(option_or_section.__doc__)
|
|---|
| 63 | if doc:
|
|---|
| 64 | doc = dgettext(option_or_section.doc_domain, doc,
|
|---|
| 65 | **(option_or_section.doc_args or {}))
|
|---|
| 66 | return doc
|
|---|
| 67 |
|
|---|
| 68 |
|
|---|
| 69 | class ConfigurationError(TracError):
|
|---|
| 70 | """Exception raised when a value in the configuration file is not valid."""
|
|---|
| 71 | title = N_("Configuration Error")
|
|---|
| 72 |
|
|---|
| 73 | def __init__(self, message=None, title=None, show_traceback=False):
|
|---|
| 74 | if message is None:
|
|---|
| 75 | message = _("Look in the Trac log for more information.")
|
|---|
| 76 | super().__init__(message, title, show_traceback)
|
|---|
| 77 |
|
|---|
| 78 |
|
|---|
| 79 | class UnicodeConfigParser(ConfigParser):
|
|---|
| 80 | """A Unicode-aware version of ConfigParser. Arguments are encoded to
|
|---|
| 81 | UTF-8 and return values are decoded from UTF-8.
|
|---|
| 82 | """
|
|---|
| 83 |
|
|---|
| 84 | # All of the methods of ConfigParser are overridden except
|
|---|
| 85 | # `getboolean`, `getint`, `getfloat`, `defaults`, `read`, `readfp`,
|
|---|
| 86 | # `optionxform` and `write`. `getboolean`, `getint` and `getfloat`
|
|---|
| 87 | # call `get`, so it isn't necessary to reimplement them.
|
|---|
| 88 | # The base class `RawConfigParser` doesn't inherit from `object`
|
|---|
| 89 | # so we can't use `super`.
|
|---|
| 90 |
|
|---|
| 91 | def __init__(self, ignorecase_option=True, **kwargs):
|
|---|
| 92 | self._ignorecase_option = ignorecase_option
|
|---|
| 93 | kwargs.setdefault('interpolation', None)
|
|---|
| 94 | ConfigParser.__init__(self, **kwargs)
|
|---|
| 95 |
|
|---|
| 96 | def optionxform(self, option):
|
|---|
| 97 | if self._ignorecase_option:
|
|---|
| 98 | option = option.lower()
|
|---|
| 99 | return option
|
|---|
| 100 |
|
|---|
| 101 | def get(self, section, option, raw=False, vars=None,
|
|---|
| 102 | fallback=_use_default):
|
|---|
| 103 | try:
|
|---|
| 104 | return ConfigParser.get(self, section, option, raw=raw, vars=vars)
|
|---|
| 105 | except (NoSectionError, NoOptionError):
|
|---|
| 106 | if fallback is _use_default:
|
|---|
| 107 | raise
|
|---|
| 108 | return fallback
|
|---|
| 109 |
|
|---|
| 110 | def items(self, section, raw=False, vars=None, fallback=_use_default):
|
|---|
| 111 | try:
|
|---|
| 112 | return ConfigParser.items(self, section, raw=raw, vars=vars)
|
|---|
| 113 | except NoSectionError:
|
|---|
| 114 | if fallback is _use_default:
|
|---|
| 115 | raise
|
|---|
| 116 | return fallback
|
|---|
| 117 |
|
|---|
| 118 | def set(self, section, option, value=None):
|
|---|
| 119 | value_str = to_unicode(value if value is not None else '')
|
|---|
| 120 | ConfigParser.set(self, section, option, value_str)
|
|---|
| 121 |
|
|---|
| 122 | def read(self, filename, encoding='utf-8'):
|
|---|
| 123 | return ConfigParser.read(self, filename, encoding)
|
|---|
| 124 |
|
|---|
| 125 | def __copy__(self):
|
|---|
| 126 | parser = self.__class__()
|
|---|
| 127 | parser._sections = copy.copy(self._sections)
|
|---|
| 128 | return parser
|
|---|
| 129 |
|
|---|
| 130 | def __deepcopy__(self, memo):
|
|---|
| 131 | parser = self.__class__()
|
|---|
| 132 | parser._sections = copy.deepcopy(self._sections)
|
|---|
| 133 | return parser
|
|---|
| 134 |
|
|---|
| 135 |
|
|---|
| 136 | class Configuration(object):
|
|---|
| 137 | """Thin layer over `ConfigParser` from the Python standard library.
|
|---|
| 138 |
|
|---|
| 139 | In addition to providing some convenience methods, the class remembers
|
|---|
| 140 | the last modification time of the configuration file, and reparses it
|
|---|
| 141 | when the file has changed.
|
|---|
| 142 | """
|
|---|
| 143 | def __init__(self, filename, params={}):
|
|---|
| 144 | self.filename = filename
|
|---|
| 145 | self.parser = UnicodeConfigParser()
|
|---|
| 146 | self._pristine_parser = None
|
|---|
| 147 | self.parents = []
|
|---|
| 148 | self._lastmtime = 0
|
|---|
| 149 | self._sections = {}
|
|---|
| 150 | self.parse_if_needed(force=True)
|
|---|
| 151 |
|
|---|
| 152 | def __repr__(self):
|
|---|
| 153 | return '<%s %r>' % (self.__class__.__name__, self.filename)
|
|---|
| 154 |
|
|---|
| 155 | def __contains__(self, name):
|
|---|
| 156 | """Return whether the configuration contains a section of the given
|
|---|
| 157 | name.
|
|---|
| 158 | """
|
|---|
| 159 | return name in self.sections()
|
|---|
| 160 |
|
|---|
| 161 | def __getitem__(self, name):
|
|---|
| 162 | """Return the configuration section with the specified name."""
|
|---|
| 163 | if name not in self._sections:
|
|---|
| 164 | self._sections[name] = Section(self, name)
|
|---|
| 165 | return self._sections[name]
|
|---|
| 166 |
|
|---|
| 167 | def __delitem__(self, name):
|
|---|
| 168 | self._sections.pop(name, None)
|
|---|
| 169 | self.parser.remove_section(name)
|
|---|
| 170 |
|
|---|
| 171 | @property
|
|---|
| 172 | def exists(self):
|
|---|
| 173 | """Return boolean indicating configuration file existence.
|
|---|
| 174 |
|
|---|
| 175 | :since: 1.0.11
|
|---|
| 176 | """
|
|---|
| 177 | return os.path.isfile(self.filename)
|
|---|
| 178 |
|
|---|
| 179 | def get(self, section, key, default=''):
|
|---|
| 180 | """Return the value of the specified option.
|
|---|
| 181 |
|
|---|
| 182 | Valid default input is a string. Returns a string.
|
|---|
| 183 | """
|
|---|
| 184 | return self[section].get(key, default)
|
|---|
| 185 |
|
|---|
| 186 | def getbool(self, section, key, default=''):
|
|---|
| 187 | """Return the specified option as boolean value.
|
|---|
| 188 |
|
|---|
| 189 | If the value of the option is one of "yes", "true", "enabled", "on",
|
|---|
| 190 | or "1", this method will return `True`, otherwise `False`.
|
|---|
| 191 |
|
|---|
| 192 | Valid default input is a string or a bool. Returns a bool.
|
|---|
| 193 | """
|
|---|
| 194 | return self[section].getbool(key, default)
|
|---|
| 195 |
|
|---|
| 196 | def getint(self, section, key, default=''):
|
|---|
| 197 | """Return the value of the specified option as integer.
|
|---|
| 198 |
|
|---|
| 199 | If the specified option can not be converted to an integer, a
|
|---|
| 200 | `ConfigurationError` exception is raised.
|
|---|
| 201 |
|
|---|
| 202 | Valid default input is a string or an int. Returns an int.
|
|---|
| 203 | """
|
|---|
| 204 | return self[section].getint(key, default)
|
|---|
| 205 |
|
|---|
| 206 | def getfloat(self, section, key, default=''):
|
|---|
| 207 | """Return the value of the specified option as float.
|
|---|
| 208 |
|
|---|
| 209 | If the specified option can not be converted to a float, a
|
|---|
| 210 | `ConfigurationError` exception is raised.
|
|---|
| 211 |
|
|---|
| 212 | Valid default input is a string, float or int. Returns a float.
|
|---|
| 213 | """
|
|---|
| 214 | return self[section].getfloat(key, default)
|
|---|
| 215 |
|
|---|
| 216 | def getlist(self, section, key, default='', sep=',', keep_empty=False):
|
|---|
| 217 | """Return a list of values that have been specified as a single
|
|---|
| 218 | comma-separated option.
|
|---|
| 219 |
|
|---|
| 220 | A different separator can be specified using the `sep` parameter. The
|
|---|
| 221 | `sep` parameter can specify multiple values using a list or a tuple.
|
|---|
| 222 | If the `keep_empty` parameter is set to `True`, empty elements are
|
|---|
| 223 | included in the list.
|
|---|
| 224 |
|
|---|
| 225 | Valid default input is a string or a list. Returns a string.
|
|---|
| 226 | """
|
|---|
| 227 | return self[section].getlist(key, default, sep, keep_empty)
|
|---|
| 228 |
|
|---|
| 229 | def getpath(self, section, key, default=''):
|
|---|
| 230 | """Return a configuration value as an absolute path.
|
|---|
| 231 |
|
|---|
| 232 | Relative paths are resolved relative to the location of this
|
|---|
| 233 | configuration file.
|
|---|
| 234 |
|
|---|
| 235 | Valid default input is a string. Returns a normalized path.
|
|---|
| 236 | """
|
|---|
| 237 | return self[section].getpath(key, default)
|
|---|
| 238 |
|
|---|
| 239 | def set(self, section, key, value):
|
|---|
| 240 | """Change a configuration value.
|
|---|
| 241 |
|
|---|
| 242 | These changes are not persistent unless saved with `save()`.
|
|---|
| 243 | """
|
|---|
| 244 | self[section].set(key, value)
|
|---|
| 245 |
|
|---|
| 246 | def defaults(self, compmgr=None):
|
|---|
| 247 | """Returns a dictionary of the default configuration values.
|
|---|
| 248 |
|
|---|
| 249 | If `compmgr` is specified, return only options declared in components
|
|---|
| 250 | that are enabled in the given `ComponentManager`.
|
|---|
| 251 | """
|
|---|
| 252 | defaults = {}
|
|---|
| 253 | for (section, key), option in \
|
|---|
| 254 | Option.get_registry(compmgr).items():
|
|---|
| 255 | defaults.setdefault(section, {})[key] = \
|
|---|
| 256 | option.dumps(option.default)
|
|---|
| 257 | return defaults
|
|---|
| 258 |
|
|---|
| 259 | def options(self, section, compmgr=None):
|
|---|
| 260 | """Return a list of `(name, value)` tuples for every option in the
|
|---|
| 261 | specified section.
|
|---|
| 262 |
|
|---|
| 263 | This includes options that have default values that haven't been
|
|---|
| 264 | overridden. If `compmgr` is specified, only return default option
|
|---|
| 265 | values for components that are enabled in the given
|
|---|
| 266 | `ComponentManager`.
|
|---|
| 267 | """
|
|---|
| 268 | return self[section].options(compmgr)
|
|---|
| 269 |
|
|---|
| 270 | def remove(self, section, key=None):
|
|---|
| 271 | """Remove the specified option or section."""
|
|---|
| 272 | if key:
|
|---|
| 273 | self[section].remove(key)
|
|---|
| 274 | else:
|
|---|
| 275 | del self[section]
|
|---|
| 276 |
|
|---|
| 277 | def sections(self, compmgr=None, defaults=True, empty=False):
|
|---|
| 278 | """Return a list of section names.
|
|---|
| 279 |
|
|---|
| 280 | If `compmgr` is specified, only the section names corresponding to
|
|---|
| 281 | options declared in components that are enabled in the given
|
|---|
| 282 | `ComponentManager` are returned.
|
|---|
| 283 |
|
|---|
| 284 | :param empty: If `True`, include sections from the registry that
|
|---|
| 285 | contain no options.
|
|---|
| 286 | """
|
|---|
| 287 | sections = set(self.parser.sections())
|
|---|
| 288 | for parent in self.parents:
|
|---|
| 289 | sections.update(parent.sections(compmgr, defaults=False))
|
|---|
| 290 | if defaults:
|
|---|
| 291 | sections.update(self.defaults(compmgr))
|
|---|
| 292 | if empty:
|
|---|
| 293 | sections.update(ConfigSection.get_registry(compmgr))
|
|---|
| 294 | return sorted(sections)
|
|---|
| 295 |
|
|---|
| 296 | def has_option(self, section, option, defaults=True):
|
|---|
| 297 | """Returns True if option exists in section in either the project
|
|---|
| 298 | trac.ini or one of the parents, or is available through the Option
|
|---|
| 299 | registry.
|
|---|
| 300 | """
|
|---|
| 301 | return self[section].contains(option, defaults)
|
|---|
| 302 |
|
|---|
| 303 | def save(self):
|
|---|
| 304 | """Write the configuration options to the primary file."""
|
|---|
| 305 |
|
|---|
| 306 | all_options = {}
|
|---|
| 307 | for (section, name), option in Option.get_registry().items():
|
|---|
| 308 | all_options.setdefault(section, {})[name] = option
|
|---|
| 309 |
|
|---|
| 310 | def normalize(section, name, value):
|
|---|
| 311 | option = all_options.get(section, {}).get(name)
|
|---|
| 312 | return option.normalize(value) if option else value
|
|---|
| 313 |
|
|---|
| 314 | sections = []
|
|---|
| 315 | for section in self.sections():
|
|---|
| 316 | options = []
|
|---|
| 317 | for option in self[section]:
|
|---|
| 318 | default = None
|
|---|
| 319 | for parent in self.parents:
|
|---|
| 320 | if parent.has_option(section, option, defaults=False):
|
|---|
| 321 | default = normalize(section, option,
|
|---|
| 322 | parent.get(section, option))
|
|---|
| 323 | break
|
|---|
| 324 | if self.parser.has_option(section, option):
|
|---|
| 325 | current = normalize(section, option,
|
|---|
| 326 | self.parser.get(section, option))
|
|---|
| 327 | if current != default:
|
|---|
| 328 | options.append((option, current))
|
|---|
| 329 | if options:
|
|---|
| 330 | sections.append((section, sorted(options)))
|
|---|
| 331 |
|
|---|
| 332 | # Prepare new file contents to write to disk.
|
|---|
| 333 | parser = UnicodeConfigParser()
|
|---|
| 334 | for section, options in sections:
|
|---|
| 335 | parser.add_section(section)
|
|---|
| 336 | for key, val in options:
|
|---|
| 337 | parser.set(section, key, val)
|
|---|
| 338 |
|
|---|
| 339 | try:
|
|---|
| 340 | self._write(parser)
|
|---|
| 341 | except Exception:
|
|---|
| 342 | # Revert all changes to avoid inconsistencies
|
|---|
| 343 | self.parser = copy.deepcopy(self._pristine_parser)
|
|---|
| 344 | raise
|
|---|
| 345 | else:
|
|---|
| 346 | self._pristine_parser = copy.deepcopy(self.parser)
|
|---|
| 347 |
|
|---|
| 348 | def parse_if_needed(self, force=False):
|
|---|
| 349 | if not self.filename or not self.exists:
|
|---|
| 350 | return False
|
|---|
| 351 |
|
|---|
| 352 | changed = False
|
|---|
| 353 | modtime = os.path.getmtime(self.filename)
|
|---|
| 354 | if force or modtime != self._lastmtime:
|
|---|
| 355 | self.parser = UnicodeConfigParser()
|
|---|
| 356 | try:
|
|---|
| 357 | if not self.parser.read(self.filename):
|
|---|
| 358 | raise TracError(_("Error reading '%(file)s', make sure "
|
|---|
| 359 | "it is readable.", file=self.filename))
|
|---|
| 360 | except ParsingError as e:
|
|---|
| 361 | raise TracError(e) from e
|
|---|
| 362 | self._lastmtime = modtime
|
|---|
| 363 | self._pristine_parser = copy.deepcopy(self.parser)
|
|---|
| 364 | changed = True
|
|---|
| 365 |
|
|---|
| 366 | if changed:
|
|---|
| 367 | self.parents = self._get_parents()
|
|---|
| 368 | else:
|
|---|
| 369 | for parent in self.parents:
|
|---|
| 370 | changed |= parent.parse_if_needed(force=force)
|
|---|
| 371 |
|
|---|
| 372 | if changed:
|
|---|
| 373 | self._sections = {}
|
|---|
| 374 | return changed
|
|---|
| 375 |
|
|---|
| 376 | def touch(self):
|
|---|
| 377 | if self.filename and self.exists \
|
|---|
| 378 | and os.access(self.filename, os.W_OK):
|
|---|
| 379 | wait_for_file_mtime_change(self.filename)
|
|---|
| 380 |
|
|---|
| 381 | def set_defaults(self, compmgr=None, component=None):
|
|---|
| 382 | """Retrieve all default values and store them explicitly in the
|
|---|
| 383 | configuration, so that they can be saved to file.
|
|---|
| 384 |
|
|---|
| 385 | Values already set in the configuration are not overwritten.
|
|---|
| 386 | """
|
|---|
| 387 | def set_option_default(option):
|
|---|
| 388 | section = option.section
|
|---|
| 389 | name = option.name
|
|---|
| 390 | if not self.has_option(section, name, defaults=False):
|
|---|
| 391 | value = option.dumps(option.default)
|
|---|
| 392 | self.set(section, name, value)
|
|---|
| 393 |
|
|---|
| 394 | if component:
|
|---|
| 395 | if component.endswith('.*'):
|
|---|
| 396 | component = component[:-2]
|
|---|
| 397 | component = component.lower().split('.')
|
|---|
| 398 | from trac.core import ComponentMeta
|
|---|
| 399 | for cls in ComponentMeta._components:
|
|---|
| 400 | clsname = (cls.__module__ + '.' + cls.__name__).lower() \
|
|---|
| 401 | .split('.')
|
|---|
| 402 | if clsname[:len(component)] == component:
|
|---|
| 403 | for option in cls.__dict__.values():
|
|---|
| 404 | if isinstance(option, Option):
|
|---|
| 405 | set_option_default(option)
|
|---|
| 406 | else:
|
|---|
| 407 | for option in Option.get_registry(compmgr).values():
|
|---|
| 408 | set_option_default(option)
|
|---|
| 409 |
|
|---|
| 410 | def _get_parents(self):
|
|---|
| 411 | _parents = []
|
|---|
| 412 | if self.parser.has_option('inherit', 'file'):
|
|---|
| 413 | for filename in self.parser.get('inherit', 'file').split(','):
|
|---|
| 414 | filename = filename.strip()
|
|---|
| 415 | if not os.path.isabs(filename):
|
|---|
| 416 | filename = os.path.join(os.path.dirname(self.filename),
|
|---|
| 417 | filename)
|
|---|
| 418 | _parents.append(Configuration(filename))
|
|---|
| 419 | return _parents
|
|---|
| 420 |
|
|---|
| 421 | def _write(self, parser):
|
|---|
| 422 | if not self.filename:
|
|---|
| 423 | return
|
|---|
| 424 | wait_for_file_mtime_change(self.filename)
|
|---|
| 425 | with AtomicFile(self.filename, 'w') as fd:
|
|---|
| 426 | fd.writelines(['# -*- coding: utf-8 -*-\n', '\n'])
|
|---|
| 427 | parser.write(fd)
|
|---|
| 428 |
|
|---|
| 429 |
|
|---|
| 430 | class Section(object):
|
|---|
| 431 | """Proxy for a specific configuration section.
|
|---|
| 432 |
|
|---|
| 433 | Objects of this class should not be instantiated directly.
|
|---|
| 434 | """
|
|---|
| 435 | __slots__ = ['config', 'name', '_cache']
|
|---|
| 436 |
|
|---|
| 437 | def __init__(self, config, name):
|
|---|
| 438 | self.config = config
|
|---|
| 439 | self.name = name
|
|---|
| 440 | self._cache = {}
|
|---|
| 441 |
|
|---|
| 442 | def __repr__(self):
|
|---|
| 443 | return '<%s [%s]>' % (self.__class__.__name__, self.name)
|
|---|
| 444 |
|
|---|
| 445 | def contains(self, key, defaults=True):
|
|---|
| 446 | if self.config.parser.has_option(self.name, key):
|
|---|
| 447 | return True
|
|---|
| 448 | for parent in self.config.parents:
|
|---|
| 449 | if parent[self.name].contains(key, defaults=False):
|
|---|
| 450 | return True
|
|---|
| 451 | return defaults and (self.name, key) in Option.registry
|
|---|
| 452 |
|
|---|
| 453 | __contains__ = contains
|
|---|
| 454 |
|
|---|
| 455 | def iterate(self, compmgr=None, defaults=True):
|
|---|
| 456 | """Iterate over the options in this section.
|
|---|
| 457 |
|
|---|
| 458 | If `compmgr` is specified, only return default option values for
|
|---|
| 459 | components that are enabled in the given `ComponentManager`.
|
|---|
| 460 | """
|
|---|
| 461 | options = set()
|
|---|
| 462 | if self.config.parser.has_section(self.name):
|
|---|
| 463 | for option in self.config.parser.options(self.name):
|
|---|
| 464 | options.add(option.lower())
|
|---|
| 465 | yield option
|
|---|
| 466 | for parent in self.config.parents:
|
|---|
| 467 | for option in parent[self.name].iterate(defaults=False):
|
|---|
| 468 | loption = option.lower()
|
|---|
| 469 | if loption not in options:
|
|---|
| 470 | options.add(loption)
|
|---|
| 471 | yield option
|
|---|
| 472 | if defaults:
|
|---|
| 473 | for section, option in Option.get_registry(compmgr).keys():
|
|---|
| 474 | if section == self.name and option.lower() not in options:
|
|---|
| 475 | yield option
|
|---|
| 476 |
|
|---|
| 477 | __iter__ = iterate
|
|---|
| 478 |
|
|---|
| 479 | def get(self, key, default=''):
|
|---|
| 480 | """Return the value of the specified option.
|
|---|
| 481 |
|
|---|
| 482 | Valid default input is a string. Returns a string.
|
|---|
| 483 | """
|
|---|
| 484 | cached = self._cache.get(key, _use_default)
|
|---|
| 485 | if cached is not _use_default:
|
|---|
| 486 | return cached
|
|---|
| 487 | if self.config.parser.has_option(self.name, key):
|
|---|
| 488 | value = self.config.parser.get(self.name, key)
|
|---|
| 489 | else:
|
|---|
| 490 | for parent in self.config.parents:
|
|---|
| 491 | value = parent[self.name].get(key, _use_default)
|
|---|
| 492 | if value is not _use_default:
|
|---|
| 493 | break
|
|---|
| 494 | else:
|
|---|
| 495 | if default is not _use_default:
|
|---|
| 496 | option = Option.registry.get((self.name, key))
|
|---|
| 497 | value = option.dumps(option.default) if option \
|
|---|
| 498 | else _use_default
|
|---|
| 499 | else:
|
|---|
| 500 | value = _use_default
|
|---|
| 501 | if value is _use_default:
|
|---|
| 502 | return default
|
|---|
| 503 | self._cache[key] = value
|
|---|
| 504 | return value
|
|---|
| 505 |
|
|---|
| 506 | def getbool(self, key, default=''):
|
|---|
| 507 | """Return the value of the specified option as boolean.
|
|---|
| 508 |
|
|---|
| 509 | This method returns `True` if the option value is one of "yes",
|
|---|
| 510 | "true", "enabled", "on", or non-zero numbers, ignoring case.
|
|---|
| 511 | Otherwise `False` is returned.
|
|---|
| 512 |
|
|---|
| 513 | Valid default input is a string or a bool. Returns a bool.
|
|---|
| 514 | """
|
|---|
| 515 | return as_bool(self.get(key, default))
|
|---|
| 516 |
|
|---|
| 517 | def getint(self, key, default=''):
|
|---|
| 518 | """Return the value of the specified option as integer.
|
|---|
| 519 |
|
|---|
| 520 | If the specified option can not be converted to an integer, a
|
|---|
| 521 | `ConfigurationError` exception is raised.
|
|---|
| 522 |
|
|---|
| 523 | Valid default input is a string or an int. Returns an int.
|
|---|
| 524 | """
|
|---|
| 525 | value = self.get(key, default)
|
|---|
| 526 | try:
|
|---|
| 527 | return _getint(value)
|
|---|
| 528 | except ValueError:
|
|---|
| 529 | raise ConfigurationError(
|
|---|
| 530 | _('[%(section)s] %(entry)s: expected integer,'
|
|---|
| 531 | ' got %(value)s', section=self.name, entry=key,
|
|---|
| 532 | value=repr(value)))
|
|---|
| 533 |
|
|---|
| 534 | def getfloat(self, key, default=''):
|
|---|
| 535 | """Return the value of the specified option as float.
|
|---|
| 536 |
|
|---|
| 537 | If the specified option can not be converted to a float, a
|
|---|
| 538 | `ConfigurationError` exception is raised.
|
|---|
| 539 |
|
|---|
| 540 | Valid default input is a string, float or int. Returns a float.
|
|---|
| 541 | """
|
|---|
| 542 | value = self.get(key, default)
|
|---|
| 543 | try:
|
|---|
| 544 | return _getfloat(value)
|
|---|
| 545 | except ValueError:
|
|---|
| 546 | raise ConfigurationError(
|
|---|
| 547 | _('[%(section)s] %(entry)s: expected float,'
|
|---|
| 548 | ' got %(value)s', section=self.name, entry=key,
|
|---|
| 549 | value=repr(value)))
|
|---|
| 550 |
|
|---|
| 551 | def getlist(self, key, default='', sep=',', keep_empty=True):
|
|---|
| 552 | """Return a list of values that have been specified as a single
|
|---|
| 553 | comma-separated option.
|
|---|
| 554 |
|
|---|
| 555 | A different separator can be specified using the `sep` parameter. The
|
|---|
| 556 | `sep` parameter can specify multiple values using a list or a tuple.
|
|---|
| 557 | If the `keep_empty` parameter is set to `True`, empty elements are
|
|---|
| 558 | included in the list.
|
|---|
| 559 |
|
|---|
| 560 | Valid default input is a string or a list. Returns a list.
|
|---|
| 561 | """
|
|---|
| 562 | return _getlist(self.get(key, default), sep, keep_empty)
|
|---|
| 563 |
|
|---|
| 564 | def getpath(self, key, default=''):
|
|---|
| 565 | """Return the value of the specified option as a path, relative to
|
|---|
| 566 | the location of this configuration file.
|
|---|
| 567 |
|
|---|
| 568 | Valid default input is a string. Returns a normalized path.
|
|---|
| 569 | """
|
|---|
| 570 | path = self.get(key, default)
|
|---|
| 571 | if not path:
|
|---|
| 572 | return default
|
|---|
| 573 | if not os.path.isabs(path):
|
|---|
| 574 | path = os.path.join(os.path.dirname(self.config.filename), path)
|
|---|
| 575 | return os.path.normcase(os.path.realpath(path))
|
|---|
| 576 |
|
|---|
| 577 | def options(self, compmgr=None):
|
|---|
| 578 | """Return `(key, value)` tuples for every option in the section.
|
|---|
| 579 |
|
|---|
| 580 | This includes options that have default values that haven't been
|
|---|
| 581 | overridden. If `compmgr` is specified, only return default option
|
|---|
| 582 | values for components that are enabled in the given `ComponentManager`.
|
|---|
| 583 | """
|
|---|
| 584 | for key in self.iterate(compmgr):
|
|---|
| 585 | yield key, self.get(key)
|
|---|
| 586 |
|
|---|
| 587 | def set(self, key, value):
|
|---|
| 588 | """Change a configuration value.
|
|---|
| 589 |
|
|---|
| 590 | These changes are not persistent unless saved with `save()`.
|
|---|
| 591 | """
|
|---|
| 592 | self._cache.pop(key, None)
|
|---|
| 593 | if not self.config.parser.has_section(self.name):
|
|---|
| 594 | self.config.parser.add_section(self.name)
|
|---|
| 595 | return self.config.parser.set(self.name, key, value)
|
|---|
| 596 |
|
|---|
| 597 | def remove(self, key):
|
|---|
| 598 | """Delete a key from this section.
|
|---|
| 599 |
|
|---|
| 600 | Like for `set()`, the changes won't persist until `save()` gets
|
|---|
| 601 | called.
|
|---|
| 602 | """
|
|---|
| 603 | self._cache.pop(key, None)
|
|---|
| 604 | if self.config.parser.has_section(self.name):
|
|---|
| 605 | self.config.parser.remove_option(self.name, key)
|
|---|
| 606 | if not self.config.parser.options(self.name):
|
|---|
| 607 | del self.config[self.name]
|
|---|
| 608 |
|
|---|
| 609 |
|
|---|
| 610 | def _get_registry(cls, compmgr=None):
|
|---|
| 611 | """Return the descriptor registry.
|
|---|
| 612 |
|
|---|
| 613 | If `compmgr` is specified, only return descriptors for components that
|
|---|
| 614 | are enabled in the given `ComponentManager`.
|
|---|
| 615 | """
|
|---|
| 616 | if compmgr is None:
|
|---|
| 617 | return cls.registry
|
|---|
| 618 |
|
|---|
| 619 | from trac.core import ComponentMeta
|
|---|
| 620 | components = {}
|
|---|
| 621 | for comp in ComponentMeta._components:
|
|---|
| 622 | for attr in comp.__dict__.values():
|
|---|
| 623 | if isinstance(attr, cls):
|
|---|
| 624 | components[attr] = comp
|
|---|
| 625 |
|
|---|
| 626 | return dict(each for each in cls.registry.items()
|
|---|
| 627 | if each[1] not in components
|
|---|
| 628 | or compmgr.is_enabled(components[each[1]]))
|
|---|
| 629 |
|
|---|
| 630 |
|
|---|
| 631 | class ConfigSection(object):
|
|---|
| 632 | """Descriptor for configuration sections."""
|
|---|
| 633 |
|
|---|
| 634 | registry = {}
|
|---|
| 635 |
|
|---|
| 636 | @staticmethod
|
|---|
| 637 | def get_registry(compmgr=None):
|
|---|
| 638 | """Return the section registry, as a `dict` mapping section names to
|
|---|
| 639 | `ConfigSection` objects.
|
|---|
| 640 |
|
|---|
| 641 | If `compmgr` is specified, only return sections for components that
|
|---|
| 642 | are enabled in the given `ComponentManager`.
|
|---|
| 643 | """
|
|---|
| 644 | return _get_registry(ConfigSection, compmgr)
|
|---|
| 645 |
|
|---|
| 646 | def __init__(self, name, doc, doc_domain='tracini', doc_args=None):
|
|---|
| 647 | """Create the configuration section."""
|
|---|
| 648 | self.name = name
|
|---|
| 649 | self.registry[self.name] = self
|
|---|
| 650 | self.__doc__ = cleandoc(doc)
|
|---|
| 651 | self.doc_domain = doc_domain
|
|---|
| 652 | self.doc_args = doc_args
|
|---|
| 653 |
|
|---|
| 654 | def __get__(self, instance, owner):
|
|---|
| 655 | if instance is None:
|
|---|
| 656 | return self
|
|---|
| 657 | config = getattr(instance, 'config', None)
|
|---|
| 658 | if config and isinstance(config, Configuration):
|
|---|
| 659 | return config[self.name]
|
|---|
| 660 |
|
|---|
| 661 | def __repr__(self):
|
|---|
| 662 | return '<%s [%s]>' % (self.__class__.__name__, self.name)
|
|---|
| 663 |
|
|---|
| 664 | @property
|
|---|
| 665 | def doc(self):
|
|---|
| 666 | """Return localized document of the section"""
|
|---|
| 667 | return _getdoc(self)
|
|---|
| 668 |
|
|---|
| 669 |
|
|---|
| 670 | class Option(object):
|
|---|
| 671 | """Descriptor for configuration options."""
|
|---|
| 672 |
|
|---|
| 673 | registry = {}
|
|---|
| 674 |
|
|---|
| 675 | def accessor(self, section, name, default):
|
|---|
| 676 | return section.get(name, default)
|
|---|
| 677 |
|
|---|
| 678 | @staticmethod
|
|---|
| 679 | def get_registry(compmgr=None):
|
|---|
| 680 | """Return the option registry, as a `dict` mapping `(section, key)`
|
|---|
| 681 | tuples to `Option` objects.
|
|---|
| 682 |
|
|---|
| 683 | If `compmgr` is specified, only return options for components that are
|
|---|
| 684 | enabled in the given `ComponentManager`.
|
|---|
| 685 | """
|
|---|
| 686 | return _get_registry(Option, compmgr)
|
|---|
| 687 |
|
|---|
| 688 | def __init__(self, section, name, default=None, doc='',
|
|---|
| 689 | doc_domain='tracini', doc_args=None):
|
|---|
| 690 | """Create the configuration option.
|
|---|
| 691 |
|
|---|
| 692 | :param section: the name of the configuration section this option
|
|---|
| 693 | belongs to
|
|---|
| 694 | :param name: the name of the option
|
|---|
| 695 | :param default: the default value for the option
|
|---|
| 696 | :param doc: documentation of the option
|
|---|
| 697 | """
|
|---|
| 698 | self.section = section
|
|---|
| 699 | self.name = name
|
|---|
| 700 | self.default = self.normalize(default)
|
|---|
| 701 | self.registry[(self.section, self.name)] = self
|
|---|
| 702 | self.__doc__ = cleandoc(doc)
|
|---|
| 703 | self.doc_domain = doc_domain
|
|---|
| 704 | self.doc_args = doc_args
|
|---|
| 705 |
|
|---|
| 706 | def __get__(self, instance, owner):
|
|---|
| 707 | if instance is None:
|
|---|
| 708 | return self
|
|---|
| 709 | config = getattr(instance, 'config', None)
|
|---|
| 710 | if config and isinstance(config, Configuration):
|
|---|
| 711 | section = config[self.section]
|
|---|
| 712 | value = self.accessor(section, self.name, self.default)
|
|---|
| 713 | return value
|
|---|
| 714 |
|
|---|
| 715 | def __set__(self, instance, value):
|
|---|
| 716 | raise AttributeError(_("Setting attribute is not allowed."))
|
|---|
| 717 |
|
|---|
| 718 | def __repr__(self):
|
|---|
| 719 | return '<%s [%s] %r>' % (self.__class__.__name__, self.section,
|
|---|
| 720 | self.name)
|
|---|
| 721 |
|
|---|
| 722 | @property
|
|---|
| 723 | def doc(self):
|
|---|
| 724 | """Return localized document of the option"""
|
|---|
| 725 | return _getdoc(self)
|
|---|
| 726 |
|
|---|
| 727 | def dumps(self, value):
|
|---|
| 728 | """Return the value as a string to write to a trac.ini file"""
|
|---|
| 729 | if value is None:
|
|---|
| 730 | return ''
|
|---|
| 731 | if value is True:
|
|---|
| 732 | return 'enabled'
|
|---|
| 733 | if value is False:
|
|---|
| 734 | return 'disabled'
|
|---|
| 735 | if isinstance(value, str):
|
|---|
| 736 | return value
|
|---|
| 737 | return to_unicode(value)
|
|---|
| 738 |
|
|---|
| 739 | def normalize(self, value):
|
|---|
| 740 | """Normalize the given value to write to a trac.ini file"""
|
|---|
| 741 | return self.dumps(value)
|
|---|
| 742 |
|
|---|
| 743 |
|
|---|
| 744 | class BoolOption(Option):
|
|---|
| 745 | """Descriptor for boolean configuration options."""
|
|---|
| 746 |
|
|---|
| 747 | def accessor(self, section, name, default):
|
|---|
| 748 | return section.getbool(name, default)
|
|---|
| 749 |
|
|---|
| 750 | def normalize(self, value):
|
|---|
| 751 | if value not in (True, False):
|
|---|
| 752 | value = as_bool(value)
|
|---|
| 753 | return self.dumps(value)
|
|---|
| 754 |
|
|---|
| 755 |
|
|---|
| 756 | class IntOption(Option):
|
|---|
| 757 | """Descriptor for integer configuration options."""
|
|---|
| 758 |
|
|---|
| 759 | def accessor(self, section, name, default):
|
|---|
| 760 | return section.getint(name, default)
|
|---|
| 761 |
|
|---|
| 762 | def normalize(self, value):
|
|---|
| 763 | try:
|
|---|
| 764 | value = _getint(value)
|
|---|
| 765 | except ValueError:
|
|---|
| 766 | pass
|
|---|
| 767 | return self.dumps(value)
|
|---|
| 768 |
|
|---|
| 769 |
|
|---|
| 770 | class FloatOption(Option):
|
|---|
| 771 | """Descriptor for float configuration options."""
|
|---|
| 772 |
|
|---|
| 773 | def accessor(self, section, name, default):
|
|---|
| 774 | return section.getfloat(name, default)
|
|---|
| 775 |
|
|---|
| 776 | def normalize(self, value):
|
|---|
| 777 | try:
|
|---|
| 778 | value = _getfloat(value)
|
|---|
| 779 | except ValueError:
|
|---|
| 780 | pass
|
|---|
| 781 | return self.dumps(value)
|
|---|
| 782 |
|
|---|
| 783 |
|
|---|
| 784 | class ListOption(Option):
|
|---|
| 785 | """Descriptor for configuration options that contain multiple values
|
|---|
| 786 | separated by a specific character.
|
|---|
| 787 | """
|
|---|
| 788 |
|
|---|
| 789 | def __init__(self, section, name, default=None, sep=',', keep_empty=False,
|
|---|
| 790 | doc='', doc_domain='tracini', doc_args=None):
|
|---|
| 791 | self.sep = sep
|
|---|
| 792 | self.keep_empty = keep_empty
|
|---|
| 793 | Option.__init__(self, section, name, default, doc, doc_domain,
|
|---|
| 794 | doc_args)
|
|---|
| 795 |
|
|---|
| 796 | def accessor(self, section, name, default):
|
|---|
| 797 | return section.getlist(name, default, self.sep, self.keep_empty)
|
|---|
| 798 |
|
|---|
| 799 | def dumps(self, value):
|
|---|
| 800 | if isinstance(value, (list, tuple)):
|
|---|
| 801 | sep = self.sep
|
|---|
| 802 | if isinstance(sep, (list, tuple)):
|
|---|
| 803 | sep = sep[0]
|
|---|
| 804 | return sep.join(Option.dumps(self, v) or '' for v in value)
|
|---|
| 805 | return Option.dumps(self, value)
|
|---|
| 806 |
|
|---|
| 807 | def normalize(self, value):
|
|---|
| 808 | return self.dumps(_getlist(value, self.sep, self.keep_empty))
|
|---|
| 809 |
|
|---|
| 810 |
|
|---|
| 811 | class ChoiceOption(Option):
|
|---|
| 812 | """Descriptor for configuration options providing a choice among a list
|
|---|
| 813 | of items.
|
|---|
| 814 |
|
|---|
| 815 | The default value is the first choice in the list.
|
|---|
| 816 | """
|
|---|
| 817 |
|
|---|
| 818 | def __init__(self, section, name, choices, doc='', doc_domain='tracini',
|
|---|
| 819 | doc_args=None, case_sensitive=True):
|
|---|
| 820 | Option.__init__(self, section, name, to_unicode(choices[0]), doc,
|
|---|
| 821 | doc_domain, doc_args)
|
|---|
| 822 | self.choices = list({to_unicode(c).strip() for c in choices})
|
|---|
| 823 | self.case_sensitive = case_sensitive
|
|---|
| 824 |
|
|---|
| 825 | def accessor(self, section, name, default):
|
|---|
| 826 | value = section.get(name, default)
|
|---|
| 827 | choices = self.choices[:]
|
|---|
| 828 | if not self.case_sensitive:
|
|---|
| 829 | choices = [c.lower() for c in choices]
|
|---|
| 830 | value = value.lower()
|
|---|
| 831 | try:
|
|---|
| 832 | idx = choices.index(value)
|
|---|
| 833 | except ValueError:
|
|---|
| 834 | raise ConfigurationError(
|
|---|
| 835 | _('[%(section)s] %(entry)s: expected one of '
|
|---|
| 836 | '(%(choices)s), got %(value)s',
|
|---|
| 837 | section=section.name, entry=name, value=repr(value),
|
|---|
| 838 | choices=', '.join('"%s"' % c
|
|---|
| 839 | for c in sorted(self.choices))))
|
|---|
| 840 | return self.choices[idx]
|
|---|
| 841 |
|
|---|
| 842 |
|
|---|
| 843 | class PathOption(Option):
|
|---|
| 844 | """Descriptor for file system path configuration options.
|
|---|
| 845 |
|
|---|
| 846 | Relative paths are resolved to absolute paths using the directory
|
|---|
| 847 | containing the configuration file as the reference.
|
|---|
| 848 | """
|
|---|
| 849 |
|
|---|
| 850 | def accessor(self, section, name, default):
|
|---|
| 851 | return section.getpath(name, default)
|
|---|
| 852 |
|
|---|
| 853 |
|
|---|
| 854 | class ExtensionOption(Option):
|
|---|
| 855 | """Name of a component implementing `interface`. Raises a
|
|---|
| 856 | `ConfigurationError` if the component cannot be found in the list of
|
|---|
| 857 | active components implementing the interface."""
|
|---|
| 858 |
|
|---|
| 859 | def __init__(self, section, name, interface, default=None, doc='',
|
|---|
| 860 | doc_domain='tracini', doc_args=None):
|
|---|
| 861 | Option.__init__(self, section, name, default, doc, doc_domain,
|
|---|
| 862 | doc_args)
|
|---|
| 863 | self.xtnpt = ExtensionPoint(interface)
|
|---|
| 864 |
|
|---|
| 865 | def __get__(self, instance, owner):
|
|---|
| 866 | if instance is None:
|
|---|
| 867 | return self
|
|---|
| 868 | value = Option.__get__(self, instance, owner)
|
|---|
| 869 | for impl in self.xtnpt.extensions(instance):
|
|---|
| 870 | if impl.__class__.__name__ == value:
|
|---|
| 871 | return impl
|
|---|
| 872 | raise ConfigurationError(
|
|---|
| 873 | tag_("Cannot find an implementation of the %(interface)s "
|
|---|
| 874 | "interface named %(implementation)s. Please check "
|
|---|
| 875 | "that the Component is enabled or update the option "
|
|---|
| 876 | "%(option)s in trac.ini.",
|
|---|
| 877 | interface=tag.code(self.xtnpt.interface.__name__),
|
|---|
| 878 | implementation=tag.code(value),
|
|---|
| 879 | option=tag.code("[%s] %s" % (self.section, self.name))))
|
|---|
| 880 |
|
|---|
| 881 |
|
|---|
| 882 | class OrderedExtensionsOption(ListOption):
|
|---|
| 883 | """A comma separated, ordered, list of components implementing
|
|---|
| 884 | `interface`. Can be empty.
|
|---|
| 885 |
|
|---|
| 886 | If `include_missing` is true (the default) all components implementing the
|
|---|
| 887 | interface are returned, with those specified by the option ordered first.
|
|---|
| 888 | """
|
|---|
| 889 |
|
|---|
| 890 | def __init__(self, section, name, interface, default=None,
|
|---|
| 891 | include_missing=True, doc='', doc_domain='tracini',
|
|---|
| 892 | doc_args=None):
|
|---|
| 893 | ListOption.__init__(self, section, name, default, doc=doc,
|
|---|
| 894 | doc_domain=doc_domain, doc_args=doc_args)
|
|---|
| 895 | self.xtnpt = ExtensionPoint(interface)
|
|---|
| 896 | self.include_missing = include_missing
|
|---|
| 897 |
|
|---|
| 898 | def __get__(self, instance, owner):
|
|---|
| 899 | if instance is None:
|
|---|
| 900 | return self
|
|---|
| 901 | order = ListOption.__get__(self, instance, owner)
|
|---|
| 902 | components = []
|
|---|
| 903 | implementing_classes = []
|
|---|
| 904 | for impl in self.xtnpt.extensions(instance):
|
|---|
| 905 | implementing_classes.append(impl.__class__.__name__)
|
|---|
| 906 | if self.include_missing or impl.__class__.__name__ in order:
|
|---|
| 907 | components.append(impl)
|
|---|
| 908 | not_found = sorted(set(order) - set(implementing_classes))
|
|---|
| 909 | if not_found:
|
|---|
| 910 | raise ConfigurationError(
|
|---|
| 911 | tag_("Cannot find implementation(s) of the %(interface)s "
|
|---|
| 912 | "interface named %(implementation)s. Please check "
|
|---|
| 913 | "that the Component is enabled or update the option "
|
|---|
| 914 | "%(option)s in trac.ini.",
|
|---|
| 915 | interface=tag.code(self.xtnpt.interface.__name__),
|
|---|
| 916 | implementation=tag(
|
|---|
| 917 | (', ' if idx != 0 else None, tag.code(impl))
|
|---|
| 918 | for idx, impl in enumerate(not_found)),
|
|---|
| 919 | option=tag.code("[%s] %s" % (self.section, self.name))))
|
|---|
| 920 |
|
|---|
| 921 | def key(impl):
|
|---|
| 922 | name = impl.__class__.__name__
|
|---|
| 923 | if name in order:
|
|---|
| 924 | return 0, order.index(name)
|
|---|
| 925 | else:
|
|---|
| 926 | return 1, components.index(impl)
|
|---|
| 927 | return sorted(components, key=key)
|
|---|
| 928 |
|
|---|
| 929 |
|
|---|
| 930 | class ConfigurationAdmin(Component):
|
|---|
| 931 | """trac-admin command provider for trac.ini administration."""
|
|---|
| 932 |
|
|---|
| 933 | implements(IAdminCommandProvider)
|
|---|
| 934 |
|
|---|
| 935 | # IAdminCommandProvider methods
|
|---|
| 936 |
|
|---|
| 937 | def get_admin_commands(self):
|
|---|
| 938 | yield ('config get', '<section> <option>',
|
|---|
| 939 | 'Get the value of the given option in "trac.ini"',
|
|---|
| 940 | self._complete_config, self._do_get)
|
|---|
| 941 | yield ('config remove', '<section> [<option>]',
|
|---|
| 942 | 'Remove the specified option or section from "trac.ini"',
|
|---|
| 943 | self._complete_config, self._do_remove)
|
|---|
| 944 | yield ('config set', '<section> <option> <value>',
|
|---|
| 945 | 'Set the value for the given option in "trac.ini"',
|
|---|
| 946 | self._complete_config, self._do_set)
|
|---|
| 947 |
|
|---|
| 948 | def _complete_config(self, args):
|
|---|
| 949 | if len(args) == 1:
|
|---|
| 950 | return self.config.sections(empty=True)
|
|---|
| 951 | elif len(args) == 2:
|
|---|
| 952 | return [name for (name, value) in self.config[args[0]].options()]
|
|---|
| 953 |
|
|---|
| 954 | def _do_get(self, section, option):
|
|---|
| 955 | if not self.config.has_option(section, option):
|
|---|
| 956 | raise AdminCommandError(
|
|---|
| 957 | _("Option '%(option)s' doesn't exist in section"
|
|---|
| 958 | " '%(section)s'", option=option, section=section))
|
|---|
| 959 | printout(self.config.get(section, option))
|
|---|
| 960 |
|
|---|
| 961 | def _do_set(self, section, option, value):
|
|---|
| 962 | self.config.set(section, option, value)
|
|---|
| 963 | if section == 'components' and as_bool(value):
|
|---|
| 964 | self.config.set_defaults(component=option)
|
|---|
| 965 | self.config.save()
|
|---|
| 966 | if section == 'inherit' and option == 'file':
|
|---|
| 967 | self.config.parse_if_needed(force=True) # Full reload
|
|---|
| 968 |
|
|---|
| 969 | def _do_remove(self, section, option=None):
|
|---|
| 970 | if option and not self.config.has_option(section, option):
|
|---|
| 971 | raise AdminCommandError(
|
|---|
| 972 | _("Option '%(option)s' doesn't exist in section"
|
|---|
| 973 | " '%(section)s'", option=option, section=section))
|
|---|
| 974 | elif section not in self.config:
|
|---|
| 975 | raise AdminCommandError(
|
|---|
| 976 | _("Section '%(section)s' doesn't exist", section=section))
|
|---|
| 977 | self.config.remove(section, option)
|
|---|
| 978 | self.config.save()
|
|---|
| 979 | if section == 'inherit' and option == 'file':
|
|---|
| 980 | self.config.parse_if_needed(force=True) # Full reload
|
|---|
| 981 |
|
|---|
| 982 |
|
|---|
| 983 | def get_configinfo(env):
|
|---|
| 984 | """Returns a list of dictionaries containing the `name` and `options`
|
|---|
| 985 | of each configuration section. The value of `options` is a list of
|
|---|
| 986 | dictionaries containing the `name`, `value` and `modified` state of
|
|---|
| 987 | each configuration option. The `modified` value is True if the value
|
|---|
| 988 | differs from its default.
|
|---|
| 989 |
|
|---|
| 990 | :since: version 1.1.2
|
|---|
| 991 | """
|
|---|
| 992 | all_options = {}
|
|---|
| 993 | for (section, name), option in \
|
|---|
| 994 | Option.get_registry(env.compmgr).items():
|
|---|
| 995 | all_options.setdefault(section, {})[name] = option
|
|---|
| 996 | sections = []
|
|---|
| 997 | for section in env.config.sections(env.compmgr):
|
|---|
| 998 | options = []
|
|---|
| 999 | for name, value in env.config.options(section, env.compmgr):
|
|---|
| 1000 | registered = all_options.get(section, {}).get(name)
|
|---|
| 1001 | if registered:
|
|---|
| 1002 | default = registered.default
|
|---|
| 1003 | normalized = registered.normalize(value)
|
|---|
| 1004 | else:
|
|---|
| 1005 | default = ''
|
|---|
| 1006 | normalized = str(value)
|
|---|
| 1007 | options.append({'name': name, 'value': value,
|
|---|
| 1008 | 'modified': normalized != default})
|
|---|
| 1009 | options.sort(key=lambda o: o['name'])
|
|---|
| 1010 | sections.append({'name': section, 'options': options})
|
|---|
| 1011 | sections.sort(key=lambda s: s['name'])
|
|---|
| 1012 | return sections
|
|---|