# $Id: frontend.py 10196 2025-08-07 06:35:37Z 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 transition from the
"optparse" module to "arparse" in Docutils 2.0 or later.
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. To be removed.
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
import docutils
from docutils import io, utils
TYPE_CHECKING = False
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`.
Deprecated. Will be removed with the switch to from optparse to argparse.
"""
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.)
Deprecated. Will be removed with the switch to from optparse to argparse.
"""
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 and the '
'special encoding values None and "" become invalid '
'in Docutils 1.0.', FutureWarning, 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 2.0 or later.',
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 ("", [