1
0

frontend.py 39 KB

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