Fix bug 2896512 and add some more test cases.
[docutils.git] / docutils / frontend.py
blob33608f50afec7bdac9ed4ccb77a970f3aef478f0
1 # $Id$
2 # Author: David Goodger <goodger@python.org>
3 # Copyright: This module has been placed in the public domain.
5 """
6 Command-line and common processing for Docutils front-end tools.
8 Exports the following classes:
10 * `OptionParser`: Standard Docutils command-line processing.
11 * `Option`: Customized version of `optparse.Option`; validation support.
12 * `Values`: Runtime settings; objects are simple structs
13 (``object.attribute``). Supports cumulative list settings (attributes).
14 * `ConfigParser`: Standard Docutils config file processing.
16 Also exports the following functions:
18 * Option callbacks: `store_multiple`, `read_config_file`.
19 * Setting validators: `validate_encoding`,
20 `validate_encoding_error_handler`,
21 `validate_encoding_and_error_handler`, `validate_boolean`,
22 `validate_threshold`, `validate_colon_separated_string_list`,
23 `validate_dependency_file`.
24 * `make_paths_absolute`.
25 * SettingSpec manipulation: `filter_settings_spec`.
26 """
28 __docformat__ = 'reStructuredText'
30 import os
31 import os.path
32 import sys
33 import warnings
34 import ConfigParser as CP
35 import codecs
36 import docutils
37 import docutils.utils
38 import docutils.nodes
39 import optparse
40 from optparse import SUPPRESS_HELP
43 def store_multiple(option, opt, value, parser, *args, **kwargs):
44 """
45 Store multiple values in `parser.values`. (Option callback.)
47 Store `None` for each attribute named in `args`, and store the value for
48 each key (attribute name) in `kwargs`.
49 """
50 for attribute in args:
51 setattr(parser.values, attribute, None)
52 for key, value in kwargs.items():
53 setattr(parser.values, key, value)
55 def read_config_file(option, opt, value, parser):
56 """
57 Read a configuration file during option processing. (Option callback.)
58 """
59 try:
60 new_settings = parser.get_config_file_settings(value)
61 except ValueError, error:
62 parser.error(error)
63 parser.values.update(new_settings, parser)
65 def validate_encoding(setting, value, option_parser,
66 config_parser=None, config_section=None):
67 try:
68 codecs.lookup(value)
69 except LookupError:
70 raise (LookupError('setting "%s": unknown encoding: "%s"'
71 % (setting, value)),
72 None, sys.exc_info()[2])
73 return value
75 def validate_encoding_error_handler(setting, value, option_parser,
76 config_parser=None, config_section=None):
77 try:
78 codecs.lookup_error(value)
79 except AttributeError: # TODO: remove (only needed prior to Python 2.3)
80 if value not in ('strict', 'ignore', 'replace', 'xmlcharrefreplace'):
81 raise (LookupError(
82 'unknown encoding error handler: "%s" (choices: '
83 '"strict", "ignore", "replace", or "xmlcharrefreplace")' % value),
84 None, sys.exc_info()[2])
85 except LookupError:
86 raise (LookupError(
87 'unknown encoding error handler: "%s" (choices: '
88 '"strict", "ignore", "replace", "backslashreplace", '
89 '"xmlcharrefreplace", and possibly others; see documentation for '
90 'the Python ``codecs`` module)' % value),
91 None, sys.exc_info()[2])
92 return value
94 def validate_encoding_and_error_handler(
95 setting, value, option_parser, config_parser=None, config_section=None):
96 """
97 Side-effect: if an error handler is included in the value, it is inserted
98 into the appropriate place as if it was a separate setting/option.
99 """
100 if ':' in value:
101 encoding, handler = value.split(':')
102 validate_encoding_error_handler(
103 setting + '_error_handler', handler, option_parser,
104 config_parser, config_section)
105 if config_parser:
106 config_parser.set(config_section, setting + '_error_handler',
107 handler)
108 else:
109 setattr(option_parser.values, setting + '_error_handler', handler)
110 else:
111 encoding = value
112 validate_encoding(setting, encoding, option_parser,
113 config_parser, config_section)
114 return encoding
116 def validate_boolean(setting, value, option_parser,
117 config_parser=None, config_section=None):
118 if isinstance(value, unicode):
119 try:
120 return option_parser.booleans[value.strip().lower()]
121 except KeyError:
122 raise (LookupError('unknown boolean value: "%s"' % value),
123 None, sys.exc_info()[2])
124 return value
126 def validate_nonnegative_int(setting, value, option_parser,
127 config_parser=None, config_section=None):
128 value = int(value)
129 if value < 0:
130 raise ValueError('negative value; must be positive or zero')
131 return value
133 def validate_threshold(setting, value, option_parser,
134 config_parser=None, config_section=None):
135 try:
136 return int(value)
137 except ValueError:
138 try:
139 return option_parser.thresholds[value.lower()]
140 except (KeyError, AttributeError):
141 raise (LookupError('unknown threshold: %r.' % value),
142 None, sys.exc_info[2])
144 def validate_colon_separated_string_list(
145 setting, value, option_parser, config_parser=None, config_section=None):
146 if isinstance(value, unicode):
147 value = value.split(':')
148 else:
149 last = value.pop()
150 value.extend(last.split(':'))
151 return value
153 def validate_url_trailing_slash(
154 setting, value, option_parser, config_parser=None, config_section=None):
155 if not value:
156 return './'
157 elif value.endswith('/'):
158 return value
159 else:
160 return value + '/'
162 def validate_dependency_file(setting, value, option_parser,
163 config_parser=None, config_section=None):
164 try:
165 return docutils.utils.DependencyList(value)
166 except IOError:
167 return docutils.utils.DependencyList(None)
169 def validate_strip_class(setting, value, option_parser,
170 config_parser=None, config_section=None):
171 if config_parser: # validate all values
172 class_values = value
173 else: # just validate the latest value
174 class_values = [value[-1]]
175 for class_value in class_values:
176 normalized = docutils.nodes.make_id(class_value)
177 if class_value != normalized:
178 raise ValueError('invalid class value %r (perhaps %r?)'
179 % (class_value, normalized))
180 return value
182 def make_paths_absolute(pathdict, keys, base_path=None):
184 Interpret filesystem path settings relative to the `base_path` given.
186 Paths are values in `pathdict` whose keys are in `keys`. Get `keys` from
187 `OptionParser.relative_path_settings`.
189 if base_path is None:
190 base_path = os.getcwd()
191 for key in keys:
192 if key in pathdict:
193 value = pathdict[key]
194 if isinstance(value, list):
195 value = [make_one_path_absolute(base_path, path)
196 for path in value]
197 elif value:
198 value = make_one_path_absolute(base_path, value)
199 pathdict[key] = value
201 def make_one_path_absolute(base_path, path):
202 return os.path.abspath(os.path.join(base_path, path))
204 def filter_settings_spec(settings_spec, *exclude, **replace):
205 """Return a copy of `setting_spec` excluding/replacing some settings.
207 `setting_spec` is a tuple of configuration settings with a structure
208 described for docutils.SettingsSpec.settings_spec.
210 Optional positional arguments are names of to-be-excluded settings.
211 Keyword arguments are option specification replacements.
212 (See the html4strict writer for an example.)
214 settings = list(settings_spec)
215 # every third item is a sequence of option tuples
216 for i in range(2, len(settings), 3):
217 newopts = []
218 for opt_spec in settings[i]:
219 # opt_spec is ("<help>", [<option strings>], {<keyword args>})
220 opt_name = [opt_string[2:].replace('-', '_')
221 for opt_string in opt_spec[1]
222 if opt_string.startswith('--')
223 ][0]
224 if opt_name in exclude:
225 continue
226 if opt_name in replace.keys():
227 newopts.append(replace[opt_name])
228 else:
229 newopts.append(opt_spec)
230 settings[i] = tuple(newopts)
231 return tuple(settings)
234 class Values(optparse.Values):
237 Updates list attributes by extension rather than by replacement.
238 Works in conjunction with the `OptionParser.lists` instance attribute.
241 def __init__(self, *args, **kwargs):
242 optparse.Values.__init__(self, *args, **kwargs)
243 if (not hasattr(self, 'record_dependencies')
244 or self.record_dependencies is None):
245 # Set up dependency list, in case it is needed.
246 self.record_dependencies = docutils.utils.DependencyList()
248 def update(self, other_dict, option_parser):
249 if isinstance(other_dict, Values):
250 other_dict = other_dict.__dict__
251 other_dict = other_dict.copy()
252 for setting in option_parser.lists.keys():
253 if (hasattr(self, setting) and setting in other_dict):
254 value = getattr(self, setting)
255 if value:
256 value += other_dict[setting]
257 del other_dict[setting]
258 self._update_loose(other_dict)
260 def copy(self):
261 """Return a shallow copy of `self`."""
262 return self.__class__(defaults=self.__dict__)
265 class Option(optparse.Option):
267 ATTRS = optparse.Option.ATTRS + ['validator', 'overrides']
269 def process(self, opt, value, values, parser):
271 Call the validator function on applicable settings and
272 evaluate the 'overrides' option.
273 Extends `optparse.Option.process`.
275 result = optparse.Option.process(self, opt, value, values, parser)
276 setting = self.dest
277 if setting:
278 if self.validator:
279 value = getattr(values, setting)
280 try:
281 new_value = self.validator(setting, value, parser)
282 except Exception, error:
283 raise (optparse.OptionValueError(
284 'Error in option "%s":\n %s: %s'
285 % (opt, error.__class__.__name__, error)),
286 None, sys.exc_info()[2])
287 setattr(values, setting, new_value)
288 if self.overrides:
289 setattr(values, self.overrides, None)
290 return result
293 class OptionParser(optparse.OptionParser, docutils.SettingsSpec):
296 Parser for command-line and library use. The `settings_spec`
297 specification here and in other Docutils components are merged to build
298 the set of command-line options and runtime settings for this process.
300 Common settings (defined below) and component-specific settings must not
301 conflict. Short options are reserved for common settings, and components
302 are restrict to using long options.
305 standard_config_files = [
306 '/etc/docutils.conf', # system-wide
307 './docutils.conf', # project-specific
308 '~/.docutils'] # user-specific
309 """Docutils configuration files, using ConfigParser syntax. Filenames
310 will be tilde-expanded later. Later files override earlier ones."""
312 threshold_choices = 'info 1 warning 2 error 3 severe 4 none 5'.split()
313 """Possible inputs for for --report and --halt threshold values."""
315 thresholds = {'info': 1, 'warning': 2, 'error': 3, 'severe': 4, 'none': 5}
316 """Lookup table for --report and --halt threshold values."""
318 booleans={'1': 1, 'on': 1, 'yes': 1, 'true': 1,
319 '0': 0, 'off': 0, 'no': 0, 'false': 0, '': 0}
320 """Lookup table for boolean configuration file settings."""
322 try:
323 default_error_encoding = sys.stderr.encoding or 'ascii'
324 except AttributeError:
325 default_error_encoding = 'ascii'
327 default_error_encoding_error_handler = 'backslashreplace'
329 settings_spec = (
330 'General Docutils Options',
331 None,
332 (('Specify the document title as metadata.',
333 ['--title'], {}),
334 ('Include a "Generated by Docutils" credit and link.',
335 ['--generator', '-g'], {'action': 'store_true',
336 'validator': validate_boolean}),
337 ('Do not include a generator credit.',
338 ['--no-generator'], {'action': 'store_false', 'dest': 'generator'}),
339 ('Include the date at the end of the document (UTC).',
340 ['--date', '-d'], {'action': 'store_const', 'const': '%Y-%m-%d',
341 'dest': 'datestamp'}),
342 ('Include the time & date (UTC).',
343 ['--time', '-t'], {'action': 'store_const',
344 'const': '%Y-%m-%d %H:%M UTC',
345 'dest': 'datestamp'}),
346 ('Do not include a datestamp of any kind.',
347 ['--no-datestamp'], {'action': 'store_const', 'const': None,
348 'dest': 'datestamp'}),
349 ('Include a "View document source" link.',
350 ['--source-link', '-s'], {'action': 'store_true',
351 'validator': validate_boolean}),
352 ('Use <URL> for a source link; implies --source-link.',
353 ['--source-url'], {'metavar': '<URL>'}),
354 ('Do not include a "View document source" link.',
355 ['--no-source-link'],
356 {'action': 'callback', 'callback': store_multiple,
357 'callback_args': ('source_link', 'source_url')}),
358 ('Link from section headers to TOC entries. (default)',
359 ['--toc-entry-backlinks'],
360 {'dest': 'toc_backlinks', 'action': 'store_const', 'const': 'entry',
361 'default': 'entry'}),
362 ('Link from section headers to the top of the TOC.',
363 ['--toc-top-backlinks'],
364 {'dest': 'toc_backlinks', 'action': 'store_const', 'const': 'top'}),
365 ('Disable backlinks to the table of contents.',
366 ['--no-toc-backlinks'],
367 {'dest': 'toc_backlinks', 'action': 'store_false'}),
368 ('Link from footnotes/citations to references. (default)',
369 ['--footnote-backlinks'],
370 {'action': 'store_true', 'default': 1,
371 'validator': validate_boolean}),
372 ('Disable backlinks from footnotes and citations.',
373 ['--no-footnote-backlinks'],
374 {'dest': 'footnote_backlinks', 'action': 'store_false'}),
375 ('Enable section numbering by Docutils. (default)',
376 ['--section-numbering'],
377 {'action': 'store_true', 'dest': 'sectnum_xform',
378 'default': 1, 'validator': validate_boolean}),
379 ('Disable section numbering by Docutils.',
380 ['--no-section-numbering'],
381 {'action': 'store_false', 'dest': 'sectnum_xform'}),
382 ('Remove comment elements from the document tree.',
383 ['--strip-comments'],
384 {'action': 'store_true', 'validator': validate_boolean}),
385 ('Leave comment elements in the document tree. (default)',
386 ['--leave-comments'],
387 {'action': 'store_false', 'dest': 'strip_comments'}),
388 ('Remove all elements with classes="<class>" from the document tree. '
389 'Warning: potentially dangerous; use with caution. '
390 '(Multiple-use option.)',
391 ['--strip-elements-with-class'],
392 {'action': 'append', 'dest': 'strip_elements_with_classes',
393 'metavar': '<class>', 'validator': validate_strip_class}),
394 ('Remove all classes="<class>" attributes from elements in the '
395 'document tree. Warning: potentially dangerous; use with caution. '
396 '(Multiple-use option.)',
397 ['--strip-class'],
398 {'action': 'append', 'dest': 'strip_classes',
399 'metavar': '<class>', 'validator': validate_strip_class}),
400 ('Report system messages at or higher than <level>: "info" or "1", '
401 '"warning"/"2" (default), "error"/"3", "severe"/"4", "none"/"5"',
402 ['--report', '-r'], {'choices': threshold_choices, 'default': 2,
403 'dest': 'report_level', 'metavar': '<level>',
404 'validator': validate_threshold}),
405 ('Report all system messages. (Same as "--report=1".)',
406 ['--verbose', '-v'], {'action': 'store_const', 'const': 1,
407 'dest': 'report_level'}),
408 ('Report no system messages. (Same as "--report=5".)',
409 ['--quiet', '-q'], {'action': 'store_const', 'const': 5,
410 'dest': 'report_level'}),
411 ('Halt execution at system messages at or above <level>. '
412 'Levels as in --report. Default: 4 (severe).',
413 ['--halt'], {'choices': threshold_choices, 'dest': 'halt_level',
414 'default': 4, 'metavar': '<level>',
415 'validator': validate_threshold}),
416 ('Halt at the slightest problem. Same as "--halt=info".',
417 ['--strict'], {'action': 'store_const', 'const': 1,
418 'dest': 'halt_level'}),
419 ('Enable a non-zero exit status for non-halting system messages at '
420 'or above <level>. Default: 5 (disabled).',
421 ['--exit-status'], {'choices': threshold_choices,
422 'dest': 'exit_status_level',
423 'default': 5, 'metavar': '<level>',
424 'validator': validate_threshold}),
425 ('Enable debug-level system messages and diagnostics.',
426 ['--debug'], {'action': 'store_true', 'validator': validate_boolean}),
427 ('Disable debug output. (default)',
428 ['--no-debug'], {'action': 'store_false', 'dest': 'debug'}),
429 ('Send the output of system messages to <file>.',
430 ['--warnings'], {'dest': 'warning_stream', 'metavar': '<file>'}),
431 ('Enable Python tracebacks when Docutils is halted.',
432 ['--traceback'], {'action': 'store_true', 'default': None,
433 'validator': validate_boolean}),
434 ('Disable Python tracebacks. (default)',
435 ['--no-traceback'], {'dest': 'traceback', 'action': 'store_false'}),
436 ('Specify the encoding and optionally the '
437 'error handler of input text. Default: <locale-dependent>:strict.',
438 ['--input-encoding', '-i'],
439 {'metavar': '<name[:handler]>',
440 'validator': validate_encoding_and_error_handler}),
441 ('Specify the error handler for undecodable characters. '
442 'Choices: "strict" (default), "ignore", and "replace".',
443 ['--input-encoding-error-handler'],
444 {'default': 'strict', 'validator': validate_encoding_error_handler}),
445 ('Specify the text encoding and optionally the error handler for '
446 'output. Default: UTF-8:strict.',
447 ['--output-encoding', '-o'],
448 {'metavar': '<name[:handler]>', 'default': 'utf-8',
449 'validator': validate_encoding_and_error_handler}),
450 ('Specify error handler for unencodable output characters; '
451 '"strict" (default), "ignore", "replace", '
452 '"xmlcharrefreplace", "backslashreplace".',
453 ['--output-encoding-error-handler'],
454 {'default': 'strict', 'validator': validate_encoding_error_handler}),
455 ('Specify text encoding and error handler for error output. '
456 'Default: %s:%s.'
457 % (default_error_encoding, default_error_encoding_error_handler),
458 ['--error-encoding', '-e'],
459 {'metavar': '<name[:handler]>', 'default': default_error_encoding,
460 'validator': validate_encoding_and_error_handler}),
461 ('Specify the error handler for unencodable characters in '
462 'error output. Default: %s.'
463 % default_error_encoding_error_handler,
464 ['--error-encoding-error-handler'],
465 {'default': default_error_encoding_error_handler,
466 'validator': validate_encoding_error_handler}),
467 ('Specify the language (as 2-letter code). Default: en.',
468 ['--language', '-l'], {'dest': 'language_code', 'default': 'en',
469 'metavar': '<name>'}),
470 ('Write output file dependencies to <file>.',
471 ['--record-dependencies'],
472 {'metavar': '<file>', 'validator': validate_dependency_file,
473 'default': None}), # default set in Values class
474 ('Read configuration settings from <file>, if it exists.',
475 ['--config'], {'metavar': '<file>', 'type': 'string',
476 'action': 'callback', 'callback': read_config_file}),
477 ("Show this program's version number and exit.",
478 ['--version', '-V'], {'action': 'version'}),
479 ('Show this help message and exit.',
480 ['--help', '-h'], {'action': 'help'}),
481 # Typically not useful for non-programmatical use:
482 (SUPPRESS_HELP, ['--id-prefix'], {'default': ''}),
483 (SUPPRESS_HELP, ['--auto-id-prefix'], {'default': 'id'}),
484 # Hidden options, for development use only:
485 (SUPPRESS_HELP, ['--dump-settings'], {'action': 'store_true'}),
486 (SUPPRESS_HELP, ['--dump-internals'], {'action': 'store_true'}),
487 (SUPPRESS_HELP, ['--dump-transforms'], {'action': 'store_true'}),
488 (SUPPRESS_HELP, ['--dump-pseudo-xml'], {'action': 'store_true'}),
489 (SUPPRESS_HELP, ['--expose-internal-attribute'],
490 {'action': 'append', 'dest': 'expose_internals',
491 'validator': validate_colon_separated_string_list}),
492 (SUPPRESS_HELP, ['--strict-visitor'], {'action': 'store_true'}),
494 """Runtime settings and command-line options common to all Docutils front
495 ends. Setting specs specific to individual Docutils components are also
496 used (see `populate_from_components()`)."""
498 settings_defaults = {'_disable_config': None,
499 '_source': None,
500 '_destination': None,
501 '_config_files': None}
502 """Defaults for settings that don't have command-line option equivalents."""
504 relative_path_settings = ('warning_stream',)
506 config_section = 'general'
508 version_template = ('%%prog (Docutils %s [%s], Python %s, on %s)'
509 % (docutils.__version__, docutils.__version_details__,
510 sys.version.split()[0], sys.platform))
511 """Default version message."""
513 def __init__(self, components=(), defaults=None, read_config_files=None,
514 *args, **kwargs):
516 `components` is a list of Docutils components each containing a
517 ``.settings_spec`` attribute. `defaults` is a mapping of setting
518 default overrides.
521 self.lists = {}
522 """Set of list-type settings."""
524 self.config_files = []
525 """List of paths of applied configuration files."""
527 optparse.OptionParser.__init__(
528 self, option_class=Option, add_help_option=None,
529 formatter=optparse.TitledHelpFormatter(width=78),
530 *args, **kwargs)
531 if not self.version:
532 self.version = self.version_template
533 # Make an instance copy (it will be modified):
534 self.relative_path_settings = list(self.relative_path_settings)
535 self.components = (self,) + tuple(components)
536 self.populate_from_components(self.components)
537 self.set_defaults_from_dict(defaults or {})
538 if read_config_files and not self.defaults['_disable_config']:
539 try:
540 config_settings = self.get_standard_config_settings()
541 except ValueError, error:
542 self.error(error)
543 self.set_defaults_from_dict(config_settings.__dict__)
545 def populate_from_components(self, components):
547 For each component, first populate from the `SettingsSpec.settings_spec`
548 structure, then from the `SettingsSpec.settings_defaults` dictionary.
549 After all components have been processed, check for and populate from
550 each component's `SettingsSpec.settings_default_overrides` dictionary.
552 for component in components:
553 if component is None:
554 continue
555 settings_spec = component.settings_spec
556 self.relative_path_settings.extend(
557 component.relative_path_settings)
558 for i in range(0, len(settings_spec), 3):
559 title, description, option_spec = settings_spec[i:i+3]
560 if title:
561 group = optparse.OptionGroup(self, title, description)
562 self.add_option_group(group)
563 else:
564 group = self # single options
565 for (help_text, option_strings, kwargs) in option_spec:
566 option = group.add_option(help=help_text, *option_strings,
567 **kwargs)
568 if kwargs.get('action') == 'append':
569 self.lists[option.dest] = 1
570 if component.settings_defaults:
571 self.defaults.update(component.settings_defaults)
572 for component in components:
573 if component and component.settings_default_overrides:
574 self.defaults.update(component.settings_default_overrides)
576 def get_standard_config_files(self):
577 """Return list of config files, from environment or standard."""
578 try:
579 config_files = os.environ['DOCUTILSCONFIG'].split(os.pathsep)
580 except KeyError:
581 config_files = self.standard_config_files
583 # If 'HOME' is not set, expandvars() requires the 'pwd' module which is
584 # not available under certain environments, for example, within
585 # mod_python. The publisher ends up in here, and we need to publish
586 # from within mod_python. Therefore we need to avoid expanding when we
587 # are in those environments.
588 expand = os.path.expanduser
589 if 'HOME' not in os.environ:
590 try:
591 import pwd
592 except ImportError:
593 expand = lambda x: x
594 return [expand(f) for f in config_files if f.strip()]
596 def get_standard_config_settings(self):
597 settings = Values()
598 for filename in self.get_standard_config_files():
599 settings.update(self.get_config_file_settings(filename), self)
600 return settings
602 def get_config_file_settings(self, config_file):
603 """Returns a dictionary containing appropriate config file settings."""
604 parser = ConfigParser()
605 parser.read(config_file, self)
606 self.config_files.extend(parser._files)
607 base_path = os.path.dirname(config_file)
608 applied = {}
609 settings = Values()
610 for component in self.components:
611 if not component:
612 continue
613 for section in (tuple(component.config_section_dependencies or ())
614 + (component.config_section,)):
615 if section in applied:
616 continue
617 applied[section] = 1
618 settings.update(parser.get_section(section), self)
619 make_paths_absolute(
620 settings.__dict__, self.relative_path_settings, base_path)
621 return settings.__dict__
623 def check_values(self, values, args):
624 """Store positional arguments as runtime settings."""
625 values._source, values._destination = self.check_args(args)
626 make_paths_absolute(values.__dict__, self.relative_path_settings,
627 os.getcwd())
628 values._config_files = self.config_files
629 return values
631 def check_args(self, args):
632 source = destination = None
633 if args:
634 source = args.pop(0)
635 if source == '-': # means stdin
636 source = None
637 if args:
638 destination = args.pop(0)
639 if destination == '-': # means stdout
640 destination = None
641 if args:
642 self.error('Maximum 2 arguments allowed.')
643 if source and source == destination:
644 self.error('Do not specify the same file for both source and '
645 'destination. It will clobber the source file.')
646 return source, destination
648 def set_defaults_from_dict(self, defaults):
649 self.defaults.update(defaults)
651 def get_default_values(self):
652 """Needed to get custom `Values` instances."""
653 defaults = Values(self.defaults)
654 defaults._config_files = self.config_files
655 return defaults
657 def get_option_by_dest(self, dest):
659 Get an option by its dest.
661 If you're supplying a dest which is shared by several options,
662 it is undefined which option of those is returned.
664 A KeyError is raised if there is no option with the supplied
665 dest.
667 for group in self.option_groups + [self]:
668 for option in group.option_list:
669 if option.dest == dest:
670 return option
671 raise KeyError('No option with dest == %r.' % dest)
674 class ConfigParser(CP.ConfigParser):
676 old_settings = {
677 'pep_stylesheet': ('pep_html writer', 'stylesheet'),
678 'pep_stylesheet_path': ('pep_html writer', 'stylesheet_path'),
679 'pep_template': ('pep_html writer', 'template')}
680 """{old setting: (new section, new setting)} mapping, used by
681 `handle_old_config`, to convert settings from the old [options] section."""
683 old_warning = """
684 The "[option]" section is deprecated. Support for old-format configuration
685 files may be removed in a future Docutils release. Please revise your
686 configuration files. See <http://docutils.sf.net/docs/user/config.html>,
687 section "Old-Format Configuration Files".
690 not_utf8_error = """\
691 Unable to read configuration file "%s": content not encoded as UTF-8.
692 Skipping "%s" configuration file.
695 def __init__(self, *args, **kwargs):
696 CP.ConfigParser.__init__(self, *args, **kwargs)
698 self._files = []
699 """List of paths of configuration files read."""
701 def read(self, filenames, option_parser):
702 if type(filenames) in (str, unicode):
703 filenames = [filenames]
704 for filename in filenames:
705 try:
706 # Config files must be UTF-8-encoded:
707 fp = codecs.open(filename, 'r', 'utf-8')
708 except IOError:
709 continue
710 try:
711 CP.ConfigParser.readfp(self, fp, filename)
712 except UnicodeDecodeError:
713 sys.stderr.write(self.not_utf8_error % (filename, filename))
714 fp.close()
715 continue
716 fp.close()
717 self._files.append(filename)
718 if self.has_section('options'):
719 self.handle_old_config(filename)
720 self.validate_settings(filename, option_parser)
722 def handle_old_config(self, filename):
723 warnings.warn_explicit(self.old_warning, ConfigDeprecationWarning,
724 filename, 0)
725 options = self.get_section('options')
726 if not self.has_section('general'):
727 self.add_section('general')
728 for key, value in options.items():
729 if key in self.old_settings:
730 section, setting = self.old_settings[key]
731 if not self.has_section(section):
732 self.add_section(section)
733 else:
734 section = 'general'
735 setting = key
736 if not self.has_option(section, setting):
737 self.set(section, setting, value)
738 self.remove_section('options')
740 def validate_settings(self, filename, option_parser):
742 Call the validator function and implement overrides on all applicable
743 settings.
745 for section in self.sections():
746 for setting in self.options(section):
747 try:
748 option = option_parser.get_option_by_dest(setting)
749 except KeyError:
750 continue
751 if option.validator:
752 value = self.get(section, setting, raw=1)
753 try:
754 new_value = option.validator(
755 setting, value, option_parser,
756 config_parser=self, config_section=section)
757 except Exception, error:
758 raise (ValueError(
759 'Error in config file "%s", section "[%s]":\n'
760 ' %s: %s\n %s = %s'
761 % (filename, section, error.__class__.__name__,
762 error, setting, value)), None, sys.exc_info()[2])
763 self.set(section, setting, new_value)
764 if option.overrides:
765 self.set(section, option.overrides, None)
767 def optionxform(self, optionstr):
769 Transform '-' to '_' so the cmdline form of option names can be used.
771 return optionstr.lower().replace('-', '_')
773 def get_section(self, section):
775 Return a given section as a dictionary (empty if the section
776 doesn't exist).
778 section_dict = {}
779 if self.has_section(section):
780 for option in self.options(section):
781 section_dict[option] = self.get(section, option, raw=1)
782 return section_dict
785 class ConfigDeprecationWarning(DeprecationWarning):
786 """Warning for deprecated configuration file features."""