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