diff --git a/python/helpers/py3only/docutils/__init__.py b/python/helpers/py3only/docutils/__init__.py index 0bcc6a4af264..16af4108e6e1 100644 --- a/python/helpers/py3only/docutils/__init__.py +++ b/python/helpers/py3only/docutils/__init__.py @@ -1,4 +1,4 @@ -# $Id: __init__.py 7756 2014-07-06 11:48:05Z grubert $ +# $Id: __init__.py 9649 2024-04-23 18:54:26Z grubert $ # Author: David Goodger # Copyright: This module has been placed in the public domain. @@ -35,8 +35,8 @@ Subpackages: - readers: Context-specific input handlers which understand the data source and manage a parser. -- transforms: Modules used by readers and writers to modify DPS - doctrees. +- transforms: Modules used by readers and writers to modify + the Docutils document tree. - utils: Contains the ``Reporter`` system warning class and miscellaneous utilities used by readers, writers, and transforms. @@ -50,30 +50,85 @@ Subpackages: - writers: Format-specific output translators. """ +from collections import namedtuple + __docformat__ = 'reStructuredText' -__version__ = '0.12' -"""``major.minor.micro`` version number. The micro number is bumped for API -changes, for new functionality, and for interim project releases. The minor -number is bumped whenever there is a significant project release. The major -number will be bumped when the project is feature-complete, and perhaps if -there is a major change in the design.""" +__version__ = '0.21.2' +"""Docutils version identifier (complies with PEP 440):: -__version_details__ = 'release' -"""Extra version details (e.g. 'snapshot 2005-05-29, r3410', 'repository', -'release'), modified automatically & manually.""" + major.minor[.micro][releaselevel[serial]][.dev] -import sys +For version comparison operations, use `__version_info__` (see, below) +rather than parsing the text of `__version__`. -class ApplicationError(Exception): - # Workaround: - # In Python < 2.6, unicode() calls `str` on the - # arg and therefore, e.g., unicode(StandardError(u'\u234')) fails - # with UnicodeDecodeError. - if sys.version_info < (2,6): - def __unicode__(self): - return ', '.join(self.args) +https://docutils.sourceforge.io/docs/dev/policies.html#version-identification +""" +__version_details__ = '' +"""Optional extra version details (e.g. 'snapshot 2005-05-29, r3410'). + +For development and release status, use `__version__ and `__version_info__`. +""" + + +class VersionInfo(namedtuple('VersionInfo', + 'major minor micro releaselevel serial release')): + + def __new__(cls, major=0, minor=0, micro=0, + releaselevel='final', serial=0, release=True): + releaselevels = ('alpha', 'beta', 'candidate', 'final') + if releaselevel not in releaselevels: + raise ValueError('releaselevel must be one of %r.' + % (releaselevels, )) + if releaselevel == 'final': + if not release: + raise ValueError('releaselevel "final" must not be used ' + 'with development versions (leads to wrong ' + 'version ordering of the related __version__') + # cf. https://peps.python.org/pep-0440/#summary-of-permitted-suffixes-and-relative-ordering # noqa + if serial != 0: + raise ValueError('"serial" must be 0 for final releases') + + return super().__new__(cls, major, minor, micro, + releaselevel, serial, release) + + def __lt__(self, other): + if isinstance(other, tuple): + other = VersionInfo(*other) + return tuple.__lt__(self, other) + + def __gt__(self, other): + if isinstance(other, tuple): + other = VersionInfo(*other) + return tuple.__gt__(self, other) + + def __le__(self, other): + if isinstance(other, tuple): + other = VersionInfo(*other) + return tuple.__le__(self, other) + + def __ge__(self, other): + if isinstance(other, tuple): + other = VersionInfo(*other) + return tuple.__ge__(self, other) + + +__version_info__ = VersionInfo( + major=0, + minor=21, + micro=2, + releaselevel='final', # one of 'alpha', 'beta', 'candidate', 'final' + serial=0, # pre-release number (0 for final releases and snapshots) + release=True # True for official releases and pre-releases + ) +"""Comprehensive version information tuple. + +https://docutils.sourceforge.io/docs/dev/policies.html#version-identification +""" + + +class ApplicationError(Exception): pass class DataError(ApplicationError): pass @@ -85,6 +140,17 @@ class SettingsSpec: SettingsSpec subclass objects used by `docutils.frontend.OptionParser`. """ + # TODO: replace settings_specs with a new data structure + # Backwards compatiblity: + # Drop-in components: + # Sphinx supplies settings_spec in the current format in some places + # Myst parser provides a settings_spec tuple + # + # Sphinx reads a settings_spec in order to set a default value + # in writers/html.py:59 + # https://github.com/sphinx-doc/sphinx/blob/4.x/sphinx/writers/html.py + # This should be changed (before retiring the old format) + # to use `settings_default_overrides` instead. settings_spec = () """Runtime settings specification. Override in subclasses. @@ -131,7 +197,7 @@ class SettingsSpec: settings_default_overrides = None """A dictionary of auxiliary defaults, to override defaults for settings - defined in other components. Override in subclasses.""" + defined in other components' `setting_specs`. Override in subclasses.""" relative_path_settings = () """Settings containing filesystem paths. Override in subclasses. @@ -151,18 +217,21 @@ class SettingsSpec: class TransformSpec: - """ Runtime transform specification base class. - TransformSpec subclass objects used by `docutils.transforms.Transformer`. + Provides the interface to register "transforms" and helper functions + to resolve references with a `docutils.transforms.Transformer`. + + https://docutils.sourceforge.io/docs/ref/transforms.html """ def get_transforms(self): """Transforms required by this class. Override in subclasses.""" if self.default_transforms != (): import warnings - warnings.warn('default_transforms attribute deprecated.\n' + warnings.warn('TransformSpec: the "default_transforms" attribute ' + 'will be removed in Docutils 2.0.\n' 'Use get_transforms() method instead.', DeprecationWarning) return list(self.default_transforms) @@ -172,11 +241,13 @@ class TransformSpec: default_transforms = () unknown_reference_resolvers = () - """List of functions to try to resolve unknown references. Unknown - references have a 'refname' attribute which doesn't correspond to any - target in the document. Called when the transforms in - `docutils.tranforms.references` are unable to find a correct target. The - list should contain functions which will try to resolve unknown + """List of functions to try to resolve unknown references. + + Unknown references have a 'refname' attribute which doesn't correspond + to any target in the document. Called when the transforms in + `docutils.transforms.references` are unable to find a correct target. + + The list should contain functions which will try to resolve unknown references, with the following signature:: def reference_resolver(node): @@ -193,7 +264,10 @@ class TransformSpec: reference_resolver.priority = 100 - Override in subclasses.""" + This hook is provided for 3rd party extensions. + Example use case: the `MoinMoin - ReStructured Text Parser` + in ``sandbox/mmgilbe/rst.py``. + """ class Component(SettingsSpec, TransformSpec): @@ -205,7 +279,7 @@ class Component(SettingsSpec, TransformSpec): subclasses.""" supported = () - """Names for this component. Override in subclasses.""" + """Name and aliases for this component. Override in subclasses.""" def supports(self, format): """ diff --git a/python/helpers/py3only/docutils/core.py b/python/helpers/py3only/docutils/core.py index ac26350c12fa..adf807592fa9 100644 --- a/python/helpers/py3only/docutils/core.py +++ b/python/helpers/py3only/docutils/core.py @@ -1,4 +1,4 @@ -# $Id: core.py 7466 2012-06-25 14:56:51Z milde $ +# $Id: core.py 9369 2023-05-02 23:04:27Z milde $ # Author: David Goodger # Copyright: This module has been placed in the public domain. @@ -9,19 +9,22 @@ behavior. For custom behavior (setting component options), create custom component objects first, and pass *them* to ``publish_*``/`Publisher`. See `The Docutils Publisher`_. -.. _The Docutils Publisher: http://docutils.sf.net/docs/api/publisher.html +.. _The Docutils Publisher: + https://docutils.sourceforge.io/docs/api/publisher.html """ __docformat__ = 'reStructuredText' +import locale import pprint +import os import sys +import warnings -import docutils.readers.doctree -from docutils import __version__, __version_details__, SettingsSpec -from docutils import frontend, io, utils, readers, writers +from docutils import (__version__, __version_details__, SettingsSpec, + io, utils, readers, writers) from docutils.frontend import OptionParser -from docutils.utils.error_reporting import ErrorOutput, ErrorString +from docutils.readers import doctree class Publisher: @@ -36,8 +39,9 @@ class Publisher: settings=None): """ Initial setup. If any of `reader`, `parser`, or `writer` are not - specified, the corresponding ``set_...`` method should be called with - a component name (`set_reader` sets the parser as well). + specified, ``set_components()`` or the corresponding ``set_...()`` + method should be called with component names + (`set_reader` sets the parser as well). """ self.document = None @@ -76,7 +80,7 @@ class Publisher: """An object containing Docutils settings as instance attributes. Set by `self.process_command_line()` or `self.get_settings()`.""" - self._stderr = ErrorOutput() + self._stderr = io.ErrorOutput() def set_reader(self, reader_name, parser, parser_name): """Set `self.reader` by name.""" @@ -102,6 +106,9 @@ class Publisher: def setup_option_parser(self, usage=None, description=None, settings_spec=None, config_section=None, **defaults): + warnings.warn('Publisher.setup_option_parser is deprecated, ' + 'and will be removed in Docutils 0.21.', + DeprecationWarning, stacklevel=2) if config_section: if not settings_spec: settings_spec = SettingsSpec() @@ -109,23 +116,33 @@ class Publisher: parts = config_section.split() if len(parts) > 1 and parts[-1] == 'application': settings_spec.config_section_dependencies = ['applications'] - #@@@ Add self.source & self.destination to components in future? - option_parser = OptionParser( + # @@@ Add self.source & self.destination to components in future? + return OptionParser( components=(self.parser, self.reader, self.writer, settings_spec), defaults=defaults, read_config_files=True, usage=usage, description=description) - return option_parser + + def _setup_settings_parser(self, *args, **kwargs): + # Provisional: will change (docutils.frontend.OptionParser will + # be replaced by a parser based on arparse.ArgumentParser) + # and may be removed later. + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', category=DeprecationWarning) + return self.setup_option_parser(*args, **kwargs) def get_settings(self, usage=None, description=None, settings_spec=None, config_section=None, **defaults): """ - Set and return default settings (overrides in `defaults` dict). + Return settings from components and config files. - Set components first (`self.set_reader` & `self.set_writer`). - Explicitly setting `self.settings` disables command line option - processing from `self.publish()`. + Please set components first (`self.set_reader` & `self.set_writer`). + Use keyword arguments to override component defaults + (before updating from configuration files). + + Calling this function also sets `self.settings` which makes + `self.publish()` skip parsing command line options. """ - option_parser = self.setup_option_parser( + option_parser = self._setup_settings_parser( usage, description, settings_spec, config_section, **defaults) self.settings = option_parser.get_default_values() return self.settings @@ -134,7 +151,7 @@ class Publisher: settings_overrides, config_section): if self.settings is None: - defaults = (settings_overrides or {}).copy() + defaults = settings_overrides.copy() if settings_overrides else {} # Propagate exceptions by default when used programmatically: defaults.setdefault('traceback', True) self.get_settings(settings_spec=settings_spec, @@ -145,20 +162,17 @@ class Publisher: settings_spec=None, config_section=None, **defaults): """ - Pass an empty list to `argv` to avoid reading `sys.argv` (the - default). + Parse command line arguments and set ``self.settings``. + + Pass an empty sequence to `argv` to avoid reading `sys.argv` + (the default behaviour). Set components first (`self.set_reader` & `self.set_writer`). """ - option_parser = self.setup_option_parser( + option_parser = self._setup_settings_parser( usage, description, settings_spec, config_section, **defaults) if argv is None: argv = sys.argv[1:] - # converting to Unicode (Python 3 does this automatically): - if sys.version_info < (3,0): - # TODO: make this failsafe and reversible? - argv_encoding = (frontend.locale_encoding or 'ascii') - argv = [a.decode(argv_encoding) for a in argv] self.settings = option_parser.parse_args(argv) def set_io(self, source_path=None, destination_path=None): @@ -172,24 +186,26 @@ class Publisher: source_path = self.settings._source else: self.settings._source = source_path - # Raise IOError instead of system exit with `tracback == True` - # TODO: change io.FileInput's default behaviour and remove this hack - try: - self.source = self.source_class( - source=source, source_path=source_path, - encoding=self.settings.input_encoding) - except TypeError: - self.source = self.source_class( - source=source, source_path=source_path, - encoding=self.settings.input_encoding) + self.source = self.source_class( + source=source, source_path=source_path, + encoding=self.settings.input_encoding, + error_handler=self.settings.input_encoding_error_handler) def set_destination(self, destination=None, destination_path=None): if destination_path is None: - destination_path = self.settings._destination - else: - self.settings._destination = destination_path + if (self.settings.output and self.settings._destination + and self.settings.output != self.settings._destination): + raise SystemExit('The positional argument is ' + 'obsoleted by the --output option. ' + 'You cannot use them together.') + if self.settings.output == '-': # means stdout + self.settings.output = None + destination_path = (self.settings.output + or self.settings._destination) + self.settings._destination = destination_path self.destination = self.destination_class( - destination=destination, destination_path=destination_path, + destination=destination, + destination_path=destination_path, encoding=self.settings.output_encoding, error_handler=self.settings.output_encoding_error_handler) @@ -214,18 +230,19 @@ class Publisher: argv, usage, description, settings_spec, config_section, **(settings_overrides or {})) self.set_io() + self.prompt() self.document = self.reader.read(self.source, self.parser, self.settings) self.apply_transforms() output = self.writer.write(self.document, self.destination) self.writer.assemble_parts() except SystemExit as error: - exit = 1 + exit = True exit_status = error.code except Exception as error: if not self.settings: # exception too early to report nicely raise - if self.settings.traceback: # Propagate exceptions? + if self.settings.traceback: # Propagate exceptions? self.debugging_dumps() raise self.report_Exception(error) @@ -251,8 +268,8 @@ class Publisher: print(pprint.pformat(self.document.__dict__), file=self._stderr) if self.settings.dump_transforms: print('\n::: Transforms applied:', file=self._stderr) - print((' (priority, transform class, ' - 'pending node details, keyword args)'), file=self._stderr) + print(' (priority, transform class, pending node details, ' + 'keyword args)', file=self._stderr) print(pprint.pformat( [(priority, '%s.%s' % (xclass.__module__, xclass.__name__), pending and pending.details, kwargs) @@ -263,6 +280,28 @@ class Publisher: print(self.document.pformat().encode( 'raw_unicode_escape'), file=self._stderr) + def prompt(self): + """Print info and prompt when waiting for input from a terminal.""" + try: + if not (self.source.isatty() and self._stderr.isatty()): + return + except AttributeError: + return + eot_key = 'Ctrl+Z' if os.name == 'nt' else 'Ctrl+D' + in_format = '' + out_format = 'useful formats' + try: + in_format = self.parser.supported[0] + out_format = self.writer.supported[0] + except (AttributeError, IndexError): + pass + print(f'Docutils {__version__} \n' + f'converting "{in_format}" into "{out_format}".\n' + f'Call with option "--help" for more info.\n' + f'.. Waiting for source text (finish with {eot_key} ' + 'on an empty line):', + file=self._stderr) + def report_Exception(self, error): if isinstance(error, utils.SystemMessage): self.report_SystemMessage(error) @@ -270,25 +309,25 @@ class Publisher: self.report_UnicodeError(error) elif isinstance(error, io.InputError): self._stderr.write('Unable to open source file for reading:\n' - ' %s\n' % ErrorString(error)) + ' %s\n' % io.error_string(error)) elif isinstance(error, io.OutputError): self._stderr.write( 'Unable to open destination file for writing:\n' - ' %s\n' % ErrorString(error)) + ' %s\n' % io.error_string(error)) else: - print('%s' % ErrorString(error), file=self._stderr) - print(("""\ + print('%s' % io.error_string(error), file=self._stderr) + print(f"""\ Exiting due to error. Use "--traceback" to diagnose. -Please report errors to . -Include "--traceback" output, Docutils version (%s [%s]), -Python version (%s), your OS type & version, and the -command line used.""" % (__version__, __version_details__, - sys.version.split()[0])), file=self._stderr) +Please report errors to . +Include "--traceback" output, Docutils version ({__version__}\ +{f' [{__version_details__}]' if __version_details__ else ''}), +Python version ({sys.version.split()[0]}), your OS type & version, \ +and the command line used.""", file=self._stderr) def report_SystemMessage(self, error): - print(('Exiting due to level-%s (%s) system message.' - % (error.level, - utils.Reporter.levels[error.level])), file=self._stderr) + print('Exiting due to level-%s (%s) system message.' % ( + error.level, utils.Reporter.levels[error.level]), + file=self._stderr) def report_UnicodeError(self, error): data = error.object[error.start:error.end] @@ -309,23 +348,34 @@ command line used.""" % (__version__, __version_details__, '\n' 'Exiting due to error. Use "--traceback" to diagnose.\n' 'If the advice above doesn\'t eliminate the error,\n' - 'please report it to .\n' + 'please report it to .\n' 'Include "--traceback" output, Docutils version (%s),\n' 'Python version (%s), your OS type & version, and the\n' 'command line used.\n' - % (ErrorString(error), + % (io.error_string(error), self.settings.output_encoding, data.encode('ascii', 'xmlcharrefreplace'), data.encode('ascii', 'backslashreplace'), self.settings.output_encoding_error_handler, __version__, sys.version.split()[0])) -default_usage = '%prog [options] [ []]' -default_description = ('Reads from (default is stdin) and writes to ' - ' (default is stdout). See ' - ' for ' - 'the full reference.') +default_usage = '%prog [options] [ []]' +default_description = ( + 'Reads from (default is stdin) ' + 'and writes to (default is stdout). ' + 'See https://docutils.sourceforge.io/docs/user/config.html ' + 'for a detailed settings reference.') + + +# TODO: or not to do? cf. https://clig.dev/#help +# +# Display output on success, but keep it brief. +# Provide a -q option to suppress all non-essential output. +# +# Chain several args as input and use --output or redirection for output: +# argparser.add_argument('source', nargs='+') +# def publish_cmdline(reader=None, reader_name='standalone', parser=None, parser_name='restructuredtext', writer=None, writer_name='pseudoxml', @@ -335,10 +385,11 @@ def publish_cmdline(reader=None, reader_name='standalone', usage=default_usage, description=default_description): """ Set up & run a `Publisher` for command-line-based file I/O (input and - output file paths taken automatically from the command line). Return the - encoded string output also. + output file paths taken automatically from the command line). + Also return the output as `str` or `bytes` (for binary output document + formats). - Parameters: see `publish_programmatically` for the remainder. + Parameters: see `publish_programmatically()` for the remainder. - `argv`: Command-line argument list to use instead of ``sys.argv[1:]``. - `usage`: Usage string, output if there's a problem parsing the command @@ -346,13 +397,14 @@ def publish_cmdline(reader=None, reader_name='standalone', - `description`: Program description, output for the "--help" option (along with command-line option descriptions). """ - pub = Publisher(reader, parser, writer, settings=settings) - pub.set_components(reader_name, parser_name, writer_name) - output = pub.publish( + publisher = Publisher(reader, parser, writer, settings=settings) + publisher.set_components(reader_name, parser_name, writer_name) + output = publisher.publish( argv, usage, description, settings_spec, settings_overrides, config_section=config_section, enable_exit_status=enable_exit_status) return output + def publish_file(source=None, source_path=None, destination=None, destination_path=None, reader=None, reader_name='standalone', @@ -362,11 +414,12 @@ def publish_file(source=None, source_path=None, config_section=None, enable_exit_status=False): """ Set up & run a `Publisher` for programmatic use with file-like I/O. - Return the encoded string output also. + Also return the output as `str` or `bytes` (for binary output document + formats). - Parameters: see `publish_programmatically`. + Parameters: see `publish_programmatically()`. """ - output, pub = publish_programmatically( + output, publisher = publish_programmatically( source_class=io.FileInput, source=source, source_path=source_path, destination_class=io.FileOutput, destination=destination, destination_path=destination_path, @@ -379,6 +432,7 @@ def publish_file(source=None, source_path=None, enable_exit_status=enable_exit_status) return output + def publish_string(source, source_path=None, destination_path=None, reader=None, reader_name='standalone', parser=None, parser_name='restructuredtext', @@ -387,22 +441,23 @@ def publish_string(source, source_path=None, destination_path=None, settings_overrides=None, config_section=None, enable_exit_status=False): """ - Set up & run a `Publisher` for programmatic use with string I/O. Return - the encoded string or Unicode string output. + Set up & run a `Publisher` for programmatic use with string I/O. - For encoded string output, be sure to set the 'output_encoding' setting to - the desired encoding. Set it to 'unicode' for unencoded Unicode string - output. Here's one way:: + Accepts a `bytes` or `str` instance as `source`. - publish_string(..., settings_overrides={'output_encoding': 'unicode'}) + The output is encoded according to the `output_encoding`_ setting; + the return value is a `bytes` instance (unless `output_encoding`_ is + "unicode", cf. `docutils.io.StringOutput.write()`). - Similarly for Unicode string input (`source`):: + Parameters: see `publish_programmatically()`. - publish_string(..., settings_overrides={'input_encoding': 'unicode'}) + This function is provisional because in Python 3 name and behaviour + no longer match. - Parameters: see `publish_programmatically`. + .. _output_encoding: + https://docutils.sourceforge.io/docs/user/config.html#output-encoding """ - output, pub = publish_programmatically( + output, publisher = publish_programmatically( source_class=io.StringInput, source=source, source_path=source_path, destination_class=io.StringOutput, destination=None, destination_path=destination_path, @@ -415,6 +470,7 @@ def publish_string(source, source_path=None, destination_path=None, enable_exit_status=enable_exit_status) return output + def publish_parts(source, source_path=None, source_class=io.StringInput, destination_path=None, reader=None, reader_name='standalone', @@ -425,18 +481,21 @@ def publish_parts(source, source_path=None, source_class=io.StringInput, enable_exit_status=False): """ Set up & run a `Publisher`, and return a dictionary of document parts. - Dictionary keys are the names of parts, and values are Unicode strings; - encoding is up to the client. For programmatic use with string I/O. - For encoded string input, be sure to set the 'input_encoding' setting to - the desired encoding. Set it to 'unicode' for unencoded Unicode string - input. Here's how:: + Dictionary keys are the names of parts. + Dictionary values are `str` instances; encoding is up to the client, + e.g.:: - publish_parts(..., settings_overrides={'input_encoding': 'unicode'}) + parts = publish_parts(...) + body = parts['body'].encode(parts['encoding'], parts['errors']) - Parameters: see `publish_programmatically`. + See the `API documentation`__ for details on the provided parts. + + Parameters: see `publish_programmatically()`. + + __ https://docutils.sourceforge.io/docs/api/publisher.html#publish-parts """ - output, pub = publish_programmatically( + output, publisher = publish_programmatically( source=source, source_path=source_path, source_class=source_class, destination_class=io.StringOutput, destination=None, destination_path=destination_path, @@ -447,7 +506,8 @@ def publish_parts(source, source_path=None, source_class=io.StringInput, settings_overrides=settings_overrides, config_section=config_section, enable_exit_status=enable_exit_status) - return pub.writer.parts + return publisher.writer.parts + def publish_doctree(source, source_path=None, source_class=io.StringInput, @@ -457,28 +517,23 @@ def publish_doctree(source, source_path=None, settings_overrides=None, config_section=None, enable_exit_status=False): """ - Set up & run a `Publisher` for programmatic use with string I/O. - Return the document tree. + Set up & run a `Publisher` for programmatic use. Return a document tree. - For encoded string input, be sure to set the 'input_encoding' setting to - the desired encoding. Set it to 'unicode' for unencoded Unicode string - input. Here's one way:: - - publish_doctree(..., settings_overrides={'input_encoding': 'unicode'}) - - Parameters: see `publish_programmatically`. + Parameters: see `publish_programmatically()`. """ - pub = Publisher(reader=reader, parser=parser, writer=None, - settings=settings, - source_class=source_class, - destination_class=io.NullOutput) - pub.set_components(reader_name, parser_name, 'null') - pub.process_programmatic_settings( - settings_spec, settings_overrides, config_section) - pub.set_source(source, source_path) - pub.set_destination(None, None) - output = pub.publish(enable_exit_status=enable_exit_status) - return pub.document + _output, publisher = publish_programmatically( + source=source, source_path=source_path, + source_class=source_class, + destination=None, destination_path=None, + destination_class=io.NullOutput, + reader=reader, reader_name=reader_name, + parser=parser, parser_name=parser_name, + writer=None, writer_name='null', + settings=settings, settings_spec=settings_spec, + settings_overrides=settings_overrides, config_section=config_section, + enable_exit_status=enable_exit_status) + return publisher.document + def publish_from_doctree(document, destination_path=None, writer=None, writer_name='pseudoxml', @@ -486,57 +541,59 @@ def publish_from_doctree(document, destination_path=None, settings_overrides=None, config_section=None, enable_exit_status=False): """ - Set up & run a `Publisher` to render from an existing document - tree data structure, for programmatic use with string I/O. Return - the encoded string output. + Set up & run a `Publisher` to render from an existing document tree + data structure. For programmatic use with string output + (`bytes` or `str`, cf. `publish_string()`). - Note that document.settings is overridden; if you want to use the settings - of the original `document`, pass settings=document.settings. + Note that ``document.settings`` is overridden; if you want to use the + settings of the original `document`, pass ``settings=document.settings``. - Also, new document.transformer and document.reporter objects are + Also, new `document.transformer` and `document.reporter` objects are generated. - For encoded string output, be sure to set the 'output_encoding' setting to - the desired encoding. Set it to 'unicode' for unencoded Unicode string - output. Here's one way:: - - publish_from_doctree( - ..., settings_overrides={'output_encoding': 'unicode'}) - Parameters: `document` is a `docutils.nodes.document` object, an existing document tree. - Other parameters: see `publish_programmatically`. + Other parameters: see `publish_programmatically()`. + + This function is provisional because in Python 3 name and behaviour + of the `io.StringOutput` class no longer match. """ - reader = docutils.readers.doctree.Reader(parser_name='null') - pub = Publisher(reader, None, writer, - source=io.DocTreeInput(document), - destination_class=io.StringOutput, settings=settings) + reader = doctree.Reader(parser_name='null') + publisher = Publisher(reader, None, writer, + source=io.DocTreeInput(document), + destination_class=io.StringOutput, + settings=settings) if not writer and writer_name: - pub.set_writer(writer_name) - pub.process_programmatic_settings( + publisher.set_writer(writer_name) + publisher.process_programmatic_settings( settings_spec, settings_overrides, config_section) - pub.set_destination(None, destination_path) - return pub.publish(enable_exit_status=enable_exit_status) + publisher.set_destination(None, destination_path) + return publisher.publish(enable_exit_status=enable_exit_status) + def publish_cmdline_to_binary(reader=None, reader_name='standalone', - parser=None, parser_name='restructuredtext', - writer=None, writer_name='pseudoxml', - settings=None, settings_spec=None, - settings_overrides=None, config_section=None, - enable_exit_status=True, argv=None, - usage=default_usage, description=default_description, - destination=None, destination_class=io.BinaryFileOutput - ): + parser=None, parser_name='restructuredtext', + writer=None, writer_name='pseudoxml', + settings=None, + settings_spec=None, + settings_overrides=None, + config_section=None, + enable_exit_status=True, + argv=None, + usage=default_usage, + description=default_description, + destination=None, + destination_class=io.BinaryFileOutput): """ Set up & run a `Publisher` for command-line-based file I/O (input and - output file paths taken automatically from the command line). Return the - encoded string output also. + output file paths taken automatically from the command line). + Also return the output as `bytes`. This is just like publish_cmdline, except that it uses io.BinaryFileOutput instead of io.FileOutput. - Parameters: see `publish_programmatically` for the remainder. + Parameters: see `publish_programmatically()` for the remainder. - `argv`: Command-line argument list to use instead of ``sys.argv[1:]``. - `usage`: Usage string, output if there's a problem parsing the command @@ -544,14 +601,15 @@ def publish_cmdline_to_binary(reader=None, reader_name='standalone', - `description`: Program description, output for the "--help" option (along with command-line option descriptions). """ - pub = Publisher(reader, parser, writer, settings=settings, - destination_class=destination_class) - pub.set_components(reader_name, parser_name, writer_name) - output = pub.publish( + publisher = Publisher(reader, parser, writer, settings=settings, + destination_class=destination_class) + publisher.set_components(reader_name, parser_name, writer_name) + output = publisher.publish( argv, usage, description, settings_spec, settings_overrides, config_section=config_section, enable_exit_status=enable_exit_status) return output + def publish_programmatically(source_class, source, source_path, destination_class, destination, destination_path, reader, reader_name, @@ -561,13 +619,15 @@ def publish_programmatically(source_class, source, source_path, settings_overrides, config_section, enable_exit_status): """ - Set up & run a `Publisher` for custom programmatic use. Return the - encoded string output and the Publisher object. + Set up & run a `Publisher` for custom programmatic use. + + Return the output (as `str` or `bytes`, depending on `destination_class`, + writer, and the "output_encoding" setting) and the Publisher object. Applications should not need to call this function directly. If it does seem to be necessary to call this function directly, please write to the Docutils-develop mailing list - . + . Parameters: @@ -581,18 +641,17 @@ def publish_programmatically(source_class, source, source_path, (`source_path` is opened). If neither `source` nor `source_path` are supplied, `sys.stdin` is used. - - If `source_class` is `io.StringInput` **required**: The input - string, either an encoded 8-bit string (set the - 'input_encoding' setting to the correct encoding) or a Unicode - string (set the 'input_encoding' setting to 'unicode'). + - If `source_class` is `io.StringInput` **required**: + The input as either a `bytes` object (ensure the 'input_encoding' + setting matches its encoding) or a `str` object. * `source_path`: Type depends on `source_class`: - `io.FileInput`: Path to the input file, opened if no `source` supplied. - - `io.StringInput`: Optional. Path to the file or object that produced - `source`. Only used for diagnostic output. + - `io.StringInput`: Optional. Path to the file or name of the + object that produced `source`. Only used for diagnostic output. * `destination_class` **required**: The class for dynamically created destination objects. Typically `io.FileOutput` or `io.StringOutput`. @@ -652,13 +711,70 @@ def publish_programmatically(source_class, source, source_path, * `enable_exit_status`: Boolean; enable exit status at end of processing? """ - pub = Publisher(reader, parser, writer, settings=settings, - source_class=source_class, - destination_class=destination_class) - pub.set_components(reader_name, parser_name, writer_name) - pub.process_programmatic_settings( + publisher = Publisher(reader, parser, writer, settings=settings, + source_class=source_class, + destination_class=destination_class) + publisher.set_components(reader_name, parser_name, writer_name) + publisher.process_programmatic_settings( settings_spec, settings_overrides, config_section) - pub.set_source(source, source_path) - pub.set_destination(destination, destination_path) - output = pub.publish(enable_exit_status=enable_exit_status) - return output, pub + publisher.set_source(source, source_path) + publisher.set_destination(destination, destination_path) + output = publisher.publish(enable_exit_status=enable_exit_status) + return output, publisher + + +# "Entry points" with functionality of the "tools/rst2*.py" scripts +# cf. https://packaging.python.org/en/latest/specifications/entry-points/ + +def rst2something(writer, documenttype, doc_path=''): + # Helper function for the common parts of rst2... + # writer: writer name + # documenttype: output document type + # doc_path: documentation path (relative to the documentation root) + description = ( + f'Generate {documenttype} documents ' + 'from standalone reStructuredText sources ' + f'. ' + + default_description) + locale.setlocale(locale.LC_ALL, '') + publish_cmdline(writer_name=writer, description=description) + + +def rst2html(): + rst2something('html', 'HTML', 'user/html.html#html') + + +def rst2html4(): + rst2something('html4', 'XHTML 1.1', 'user/html.html#html4css1') + + +def rst2html5(): + rst2something('html5', 'HTML5', 'user/html.html#html5-polyglot') + + +def rst2latex(): + rst2something('latex', 'LaTeX', 'user/latex.html') + + +def rst2man(): + rst2something('manpage', 'Unix manual (troff)', 'user/manpage.html') + + +def rst2odt(): + rst2something('odt', 'OpenDocument text (ODT)', 'user/odt.html') + + +def rst2pseudoxml(): + rst2something('pseudoxml', 'pseudo-XML (test)', 'ref/doctree.html') + + +def rst2s5(): + rst2something('s5', 'S5 HTML slideshow', 'user/slide-shows.html') + + +def rst2xetex(): + rst2something('xetex', 'LaTeX (XeLaTeX/LuaLaTeX)', 'user/latex.html') + + +def rst2xml(): + rst2something('xml', 'Docutils-native XML', 'ref/doctree.html') diff --git a/python/helpers/py3only/docutils/examples.py b/python/helpers/py3only/docutils/examples.py index 395dbbf14fc2..c27ab70d9171 100644 --- a/python/helpers/py3only/docutils/examples.py +++ b/python/helpers/py3only/docutils/examples.py @@ -1,4 +1,4 @@ -# $Id: examples.py 7320 2012-01-19 22:33:02Z milde $ +# $Id: examples.py 9026 2022-03-04 15:57:13Z milde $ # Author: David Goodger # Copyright: This module has been placed in the public domain. @@ -49,6 +49,7 @@ def html_parts(input_string, source_path=None, destination_path=None, writer_name='html', settings_overrides=overrides) return parts + def html_body(input_string, source_path=None, destination_path=None, input_encoding='unicode', output_encoding='unicode', doctitle=True, initial_header_level=1): @@ -72,6 +73,7 @@ def html_body(input_string, source_path=None, destination_path=None, fragment = fragment.encode(output_encoding) return fragment + def internals(input_string, source_path=None, destination_path=None, input_encoding='unicode', settings_overrides=None): """ diff --git a/python/helpers/py3only/docutils/frontend.py b/python/helpers/py3only/docutils/frontend.py index 0fdfa639a382..2499c628cdc1 100644 --- a/python/helpers/py3only/docutils/frontend.py +++ b/python/helpers/py3only/docutils/frontend.py @@ -1,48 +1,70 @@ -# $Id: frontend.py 7584 2013-01-01 20:00:21Z milde $ +# $Id: frontend.py 9540 2024-02-17 10:36:59Z 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: -* Option callbacks: `store_multiple`, `read_config_file`. -* Setting validators: `validate_encoding`, - `validate_encoding_error_handler`, - `validate_encoding_and_error_handler`, - `validate_boolean`, `validate_ternary`, `validate_threshold`, - `validate_colon_separated_string_list`, - `validate_comma_separated_string_list`, - `validate_dependency_file`. -* `make_paths_absolute`. -* SettingSpec manipulation: `filter_settings_spec`. +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. """ __docformat__ = 'reStructuredText' + import codecs +import configparser import optparse +from optparse import SUPPRESS_HELP import os import os.path +from pathlib import Path import sys import warnings -from optparse import SUPPRESS_HELP - -import configparser as CP import docutils -import docutils.nodes -import docutils.utils -from docutils.utils.error_reporting import locale_encoding, ErrorOutput, ErrorString +from docutils import io, utils def store_multiple(option, opt, value, parser, *args, **kwargs): @@ -54,30 +76,45 @@ def store_multiple(option, opt, value, parser, *args, **kwargs): """ for attribute in args: setattr(parser.values, attribute, None) - for key, value in list(kwargs.items()): + for key, value in kwargs.items(): setattr(parser.values, key, value) + def read_config_file(option, opt, value, parser): """ Read a configuration file during option processing. (Option callback.) """ try: new_settings = parser.get_config_file_settings(value) - except ValueError as error: - parser.error(error) + except ValueError as err: + parser.error(err) parser.values.update(new_settings, parser) -def validate_encoding(setting, value, option_parser, + +def validate_encoding(setting, value=None, option_parser=None, config_parser=None, config_section=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 == '': + return None # allow overwriting a config file value try: codecs.lookup(value) except LookupError: raise LookupError('setting "%s": unknown encoding: "%s"' - % (setting, value)) + % (setting, value)) return value -def validate_encoding_error_handler(setting, value, option_parser, + +def validate_encoding_error_handler(setting, value=None, option_parser=None, config_parser=None, config_section=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 try: codecs.lookup_error(value) except LookupError: @@ -88,17 +125,16 @@ def validate_encoding_error_handler(setting, value, option_parser, 'the Python ``codecs`` module)' % value) return value + def validate_encoding_and_error_handler( setting, value, option_parser, config_parser=None, config_section=None): """ Side-effect: if an error handler is included in the value, it is inserted - into the appropriate place as if it was a separate setting/option. + into the appropriate place as if it were a separate setting/option. """ if ':' in value: encoding, handler = value.split(':') - validate_encoding_error_handler( - setting + '_error_handler', handler, option_parser, - config_parser, config_section) + validate_encoding_error_handler(handler) if config_parser: config_parser.set(config_section, setting + '_error_handler', handler) @@ -106,56 +142,87 @@ def validate_encoding_and_error_handler( setattr(option_parser.values, setting + '_error_handler', handler) else: encoding = value - validate_encoding(setting, encoding, option_parser, - config_parser, config_section) - return encoding + return validate_encoding(encoding) -def validate_boolean(setting, value, option_parser, + +def validate_boolean(setting, value=None, option_parser=None, config_parser=None, config_section=None): """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 option_parser.booleans[value.strip().lower()] + return OptionParser.booleans[value.strip().lower()] except KeyError: raise LookupError('unknown boolean value: "%s"' % value) -def validate_ternary(setting, value, option_parser, + +def validate_ternary(setting, value=None, option_parser=None, config_parser=None, config_section=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 option_parser.booleans[value.strip().lower()] + return OptionParser.booleans[value.strip().lower()] except KeyError: return value -def validate_nonnegative_int(setting, value, option_parser, + +def validate_nonnegative_int(setting, value=None, option_parser=None, config_parser=None, config_section=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 value = int(value) if value < 0: raise ValueError('negative value; must be positive or zero') return value -def validate_threshold(setting, value, option_parser, + +def validate_threshold(setting, value=None, option_parser=None, config_parser=None, config_section=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 try: return int(value) except ValueError: try: - return option_parser.thresholds[value.lower()] + return OptionParser.thresholds[value.lower()] except (KeyError, AttributeError): raise LookupError('unknown threshold: %r.' % value) + def validate_colon_separated_string_list( - setting, value, option_parser, config_parser=None, config_section=None): + setting, value=None, option_parser=None, + config_parser=None, config_section=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 not isinstance(value, list): value = value.split(':') else: @@ -163,12 +230,19 @@ def validate_colon_separated_string_list( value.extend(last.split(':')) return value -def validate_comma_separated_list(setting, value, option_parser, - config_parser=None, config_section=None): + +def validate_comma_separated_list(setting, value=None, option_parser=None, + config_parser=None, config_section=None): """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`. """ - # `value` is already a ``list`` when given as command line option - # and "action" is "append" and ``unicode`` or ``str`` else. + 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` @@ -178,8 +252,50 @@ def validate_comma_separated_list(setting, value, option_parser, value.extend(items) return value -def validate_url_trailing_slash( - setting, value, option_parser, config_parser=None, config_section=None): + +def validate_math_output(setting, value=None, option_parser=None, + config_parser=None, config_section=None): + """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, value=None, option_parser=None, + config_parser=None, config_section=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 not value: return './' elif value.endswith('/'): @@ -187,26 +303,80 @@ def validate_url_trailing_slash( else: return value + '/' -def validate_dependency_file(setting, value, option_parser, - config_parser=None, config_section=None): - try: - return docutils.utils.DependencyList(value) - except IOError: - return docutils.utils.DependencyList(None) -def validate_strip_class(setting, value, option_parser, +def validate_dependency_file(setting, value=None, option_parser=None, + config_parser=None, config_section=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 + try: + return utils.DependencyList(value) + except OSError: + # TODO: warn/info? + return utils.DependencyList(None) + + +def validate_strip_class(setting, value=None, option_parser=None, config_parser=None, config_section=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 # value is a comma separated string list: - value = validate_comma_separated_list(setting, value, option_parser, - config_parser, config_section) + 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?)' + raise ValueError('Invalid class value %r (perhaps %r?)' % (cls, normalized)) return value + +def validate_smartquotes_locales(setting, value=None, option_parser=None, + config_parser=None, config_section=None): + """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, keys, base_path=None): """ Interpret filesystem path settings relative to the `base_path` given. @@ -215,26 +385,31 @@ def make_paths_absolute(pathdict, keys, base_path=None): `OptionParser.relative_path_settings`. """ if base_path is None: - base_path = os.getcwd() # type(base_path) == unicode - # to allow combining non-ASCII cwd with unicode values in `pathdict` + base_path = Path.cwd() + else: + base_path = Path(base_path) for key in keys: if key in pathdict: value = pathdict[key] if isinstance(value, list): - value = [make_one_path_absolute(base_path, path) - for path in value] + value = [str((base_path/path).resolve()) for path in value] elif value: - value = make_one_path_absolute(base_path, value) + value = str((base_path/value).resolve()) pathdict[key] = value + def make_one_path_absolute(base_path, path): + # 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, *exclude, **replace): """Return a copy of `settings_spec` excluding/replacing some settings. - `settings_spec` is a tuple of configuration settings with a structure - described for docutils.SettingsSpec.settings_spec. + `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. @@ -248,11 +423,10 @@ def filter_settings_spec(settings_spec, *exclude, **replace): # opt_spec is ("", [