frontend.py 36 KB

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