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