1# -*- coding: utf-8 -*- 2# 3# Copyright (C) 2005-2021 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 15import copy 16import os.path 17import re 18from configparser import (ConfigParser, NoOptionError, NoSectionError, 19 ParsingError) 20 21from trac.admin import AdminCommandError, IAdminCommandProvider 22from trac.core import Component, ExtensionPoint, TracError, implements 23from trac.util import AtomicFile, as_bool 24from trac.util.compat import wait_for_file_mtime_change 25from trac.util.html import tag 26from trac.util.text import cleandoc, printout, to_unicode, to_utf8 27from 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 37def _getint(value): 38 return int(value or 0) 39 40 41def _getfloat(value): 42 return float(value or 0.0) 43 44 45def _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 61def _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 69class 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 79class 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 136class 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 wll 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 430class 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 610def _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 631class 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 670class 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 744class 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 756class 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 770class 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 784class 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 811class 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 843class 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 854class 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 882class 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 930class 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 983def 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 1013