# $Id: frontend.py 9945 2024-10-04 12:21:52Z milde $ # Author: David Goodger # Copyright: This module has been placed in the public domain. """ Command-line and common processing for Docutils front-end tools. This module is provisional. Major changes will happen with the switch from the deprecated "optparse" module to "arparse". Applications should use the high-level API provided by `docutils.core`. See https://docutils.sourceforge.io/docs/api/runtime-settings.html. Exports the following classes: * `OptionParser`: Standard Docutils command-line processing. Deprecated. Will be replaced by an ArgumentParser. * `Option`: Customized version of `optparse.Option`; validation support. Deprecated. Will be removed. * `Values`: Runtime settings; objects are simple structs (``object.attribute``). Supports cumulative list settings (attributes). Deprecated. Will be removed. * `ConfigParser`: Standard Docutils config file processing. Provisional. Details will change. Also exports the following functions: Interface function: `get_default_settings()`. New in 0.19. Option callbacks: `store_multiple()`, `read_config_file()`. Deprecated. Setting validators: `validate_encoding()`, `validate_encoding_error_handler()`, `validate_encoding_and_error_handler()`, `validate_boolean()`, `validate_ternary()`, `validate_nonnegative_int()`, `validate_threshold()`, `validate_colon_separated_string_list()`, `validate_comma_separated_list()`, `validate_url_trailing_slash()`, `validate_dependency_file()`, `validate_strip_class()` `validate_smartquotes_locales()`. Provisional. Misc: `make_paths_absolute()`, `filter_settings_spec()`. Provisional. """ from __future__ import annotations __docformat__ = 'reStructuredText' import codecs import configparser import optparse import os import os.path import sys import warnings from optparse import SUPPRESS_HELP from pathlib import Path from typing import TYPE_CHECKING import docutils from docutils import io, utils if TYPE_CHECKING: from collections.abc import Iterable, Mapping, Sequence from typing import Any, ClassVar, Literal, Protocol from docutils import SettingsSpec, _OptionTuple, _SettingsSpecTuple from docutils.io import StrPath class _OptionValidator(Protocol): def __call__( self, setting: str, value: str | None, option_parser: OptionParser, /, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> Any: ... def store_multiple(option: optparse.Option, opt: str, value: Any, parser: OptionParser, *args: str, **kwargs: Any, ) -> None: """ Store multiple values in `parser.values`. (Option callback.) Store `None` for each attribute named in `args`, and store the value for each key (attribute name) in `kwargs`. """ for attribute in args: setattr(parser.values, attribute, None) for key, value in kwargs.items(): setattr(parser.values, key, value) def read_config_file(option: optparse.Option, opt: str, value: Any, parser: OptionParser, ) -> None: """ Read a configuration file during option processing. (Option callback.) """ try: new_settings = parser.get_config_file_settings(value) except ValueError as err: parser.error(err) parser.values.update(new_settings, parser) def validate_encoding(setting: str, value: str | None = None, option_parser: OptionParser | None = None, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> str | None: # All arguments except `value` are ignored # (kept for compatibility with "optparse" module). # If there is only one positional argument, it is interpreted as `value`. if value is None: value = setting if value == '': warnings.warn('Input encoding detection will be removed ' 'in Docutils 1.0.', DeprecationWarning, stacklevel=2) return None try: codecs.lookup(value) except LookupError: prefix = f'setting "{setting}":' if setting else '' raise LookupError(f'{prefix} unknown encoding: "{value}"') return value def validate_encoding_error_handler( setting: str, value: str | None = None, option_parser: OptionParser | None = None, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> str: # All arguments except `value` are ignored # (kept for compatibility with "optparse" module). # If there is only one positional argument, it is interpreted as `value`. if value is None: value = setting try: codecs.lookup_error(value) except LookupError: raise LookupError( 'unknown encoding error handler: "%s" (choices: ' '"strict", "ignore", "replace", "backslashreplace", ' '"xmlcharrefreplace", and possibly others; see documentation for ' 'the Python ``codecs`` module)' % value) return value def validate_encoding_and_error_handler( setting: str, value: str | None = None, option_parser: OptionParser | None = None, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> str: """Check/normalize encoding settings Side-effect: if an error handler is included in the value, it is inserted into the appropriate place as if it were a separate setting/option. All arguments except `value` are ignored (kept for compatibility with "optparse" module). If there is only one positional argument, it is interpreted as `value`. """ if ':' in value: encoding, handler = value.split(':') validate_encoding_error_handler(handler) if config_parser: config_parser.set(config_section, setting + '_error_handler', handler) else: setattr(option_parser.values, setting + '_error_handler', handler) else: encoding = value return validate_encoding(encoding) def validate_boolean(setting: str | bool, value: str | None = None, option_parser: OptionParser | None = None, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> bool: """Check/normalize boolean settings: :True: '1', 'on', 'yes', 'true' :False: '0', 'off', 'no','false', '' All arguments except `value` are ignored (kept for compatibility with "optparse" module). If there is only one positional argument, it is interpreted as `value`. """ if value is None: value = setting if isinstance(value, bool): return value try: return OptionParser.booleans[value.strip().lower()] except KeyError: raise LookupError('unknown boolean value: "%s"' % value) def validate_ternary(setting: str | bool, value: str | None = None, option_parser: OptionParser | None = None, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> str | bool | None: """Check/normalize three-value settings: :True: '1', 'on', 'yes', 'true' :False: '0', 'off', 'no','false', '' :any other value: returned as-is. All arguments except `value` are ignored (kept for compatibility with "optparse" module). If there is only one positional argument, it is interpreted as `value`. """ if value is None: value = setting if isinstance(value, bool) or value is None: return value try: return OptionParser.booleans[value.strip().lower()] except KeyError: return value def validate_nonnegative_int(setting: str | int, value: str | None = None, option_parser: OptionParser | None = None, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> int: # All arguments except `value` are ignored # (kept for compatibility with "optparse" module). # If there is only one positional argument, it is interpreted as `value`. if value is None: value = setting value = int(value) if value < 0: raise ValueError('negative value; must be positive or zero') return value def validate_threshold(setting: str | int, value: str | None = None, option_parser: OptionParser | None = None, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> int: # All arguments except `value` are ignored # (kept for compatibility with "optparse" module). # If there is only one positional argument, it is interpreted as `value`. if value is None: value = setting try: return int(value) except ValueError: try: return OptionParser.thresholds[value.lower()] except (KeyError, AttributeError): raise LookupError('unknown threshold: %r.' % value) def validate_colon_separated_string_list( setting: str | list[str], value: str | None = None, option_parser: OptionParser | None = None, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> list[str]: # All arguments except `value` are ignored # (kept for compatibility with "optparse" module). # If there is only one positional argument, it is interpreted as `value`. if value is None: value = setting if not isinstance(value, list): value = value.split(':') else: last = value.pop() value.extend(last.split(':')) return value def validate_comma_separated_list( setting: str | list[str], value: str | None = None, option_parser: OptionParser | None = None, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> list[str]: """Check/normalize list arguments (split at "," and strip whitespace). All arguments except `value` are ignored (kept for compatibility with "optparse" module). If there is only one positional argument, it is interpreted as `value`. """ if value is None: value = setting # `value` may be ``bytes``, ``str``, or a ``list`` (when given as # command line option and "action" is "append"). if not isinstance(value, list): value = [value] # this function is called for every option added to `value` # -> split the last item and append the result: last = value.pop() items = [i.strip(' \t\n') for i in last.split(',') if i.strip(' \t\n')] value.extend(items) return value def validate_math_output(setting: str, value: str | None = None, option_parser: OptionParser | None = None, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> tuple[()] | tuple[str, str]: """Check "math-output" setting, return list with "format" and "options". See also https://docutils.sourceforge.io/docs/user/config.html#math-output Argument list for compatibility with "optparse" module. All arguments except `value` are ignored. If there is only one positional argument, it is interpreted as `value`. """ if value is None: value = setting formats = ('html', 'latex', 'mathml', 'mathjax') tex2mathml_converters = ('', 'latexml', 'ttm', 'blahtexml', 'pandoc') if not value: return () values = value.split(maxsplit=1) format = values[0].lower() try: options = values[1] except IndexError: options = '' if format not in formats: raise LookupError(f'Unknown math output format: "{value}",\n' f' choose from {formats}.') if format == 'mathml': converter = options.lower() if converter not in tex2mathml_converters: raise LookupError(f'MathML converter "{options}" not supported,\n' f' choose from {tex2mathml_converters}.') options = converter return format, options def validate_url_trailing_slash(setting: str | None, value: str | None = None, option_parser: OptionParser | None = None, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> str: # All arguments except `value` are ignored # (kept for compatibility with "optparse" module). # If there is only one positional argument, it is interpreted as `value`. if value is None: value = setting if not value: return './' elif value.endswith('/'): return value else: return value + '/' def validate_dependency_file(setting: str | None, value: str | None = None, option_parser: OptionParser | None = None, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> utils.DependencyList: # All arguments except `value` are ignored # (kept for compatibility with "optparse" module). # If there is only one positional argument, it is interpreted as `value`. if value is None: value = setting try: return utils.DependencyList(value) except OSError: # TODO: warn/info? return utils.DependencyList(None) def validate_strip_class(setting: str, value: str | None = None, option_parser: OptionParser | None = None, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> list[str]: # All arguments except `value` are ignored # (kept for compatibility with "optparse" module). # If there is only one positional argument, it is interpreted as `value`. if value is None: value = setting # value is a comma separated string list: value = validate_comma_separated_list(value) # validate list elements: for cls in value: normalized = docutils.nodes.make_id(cls) if cls != normalized: raise ValueError('Invalid class value %r (perhaps %r?)' % (cls, normalized)) return value def validate_smartquotes_locales( setting: str | list[str | tuple[str, str]], value: str | None = None, option_parser: OptionParser | None = None, config_parser: ConfigParser | None = None, config_section: str | None = None, ) -> list[tuple[str, Sequence[str]]]: """Check/normalize a comma separated list of smart quote definitions. Return a list of (language-tag, quotes) string tuples. All arguments except `value` are ignored (kept for compatibility with "optparse" module). If there is only one positional argument, it is interpreted as `value`. """ if value is None: value = setting # value is a comma separated string list: value = validate_comma_separated_list(value) # validate list elements lc_quotes = [] for item in value: try: lang, quotes = item.split(':', 1) except AttributeError: # this function is called for every option added to `value` # -> ignore if already a tuple: lc_quotes.append(item) continue except ValueError: raise ValueError('Invalid value "%s".' ' Format is ":".' % item.encode('ascii', 'backslashreplace')) # parse colon separated string list: quotes = quotes.strip() multichar_quotes = quotes.split(':') if len(multichar_quotes) == 4: quotes = multichar_quotes elif len(quotes) != 4: raise ValueError('Invalid value "%s". Please specify 4 quotes\n' ' (primary open/close; secondary open/close).' % item.encode('ascii', 'backslashreplace')) lc_quotes.append((lang, quotes)) return lc_quotes def make_paths_absolute(pathdict: dict[str, list[StrPath] | StrPath], keys: tuple[str], base_path: StrPath | None = None, ) -> None: """ Interpret filesystem path settings relative to the `base_path` given. Paths are values in `pathdict` whose keys are in `keys`. Get `keys` from `OptionParser.relative_path_settings`. """ if base_path is None: base_path = Path.cwd() else: base_path = Path(base_path) if sys.platform == 'win32' and sys.version_info[:2] <= (3, 9): base_path = base_path.absolute() for key in keys: if key in pathdict: value = pathdict[key] if isinstance(value, (list, tuple)): value = [str((base_path/path).resolve()) for path in value] elif value: value = str((base_path/value).resolve()) pathdict[key] = value def make_one_path_absolute(base_path: StrPath, path: StrPath) -> str: # deprecated, will be removed warnings.warn('frontend.make_one_path_absolute() will be removed ' 'in Docutils 0.23.', DeprecationWarning, stacklevel=2) return os.path.abspath(os.path.join(base_path, path)) def filter_settings_spec(settings_spec: _SettingsSpecTuple, *exclude: str, **replace: _OptionTuple, ) -> _SettingsSpecTuple: """Return a copy of `settings_spec` excluding/replacing some settings. `settings_spec` is a tuple of configuration settings (cf. `docutils.SettingsSpec.settings_spec`). Optional positional arguments are names of to-be-excluded settings. Keyword arguments are option specification replacements. (See the html4strict writer for an example.) """ settings = list(settings_spec) # every third item is a sequence of option tuples for i in range(2, len(settings), 3): newopts: list[_OptionTuple] = [] for opt_spec in settings[i]: # opt_spec is ("", [