Edgewall Software

source: trunk/trac/config.py

Last change on this file was 17713, checked in by Ryan J Ollos, 3 months ago

1.6dev: Fix typos in comments

Patch by figaro.

Refs #13578.

  • Property svn:eol-style set to native
File size: 36.3 KB
Line 
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
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 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
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
Note: See TracBrowser for help on using the repository browser.