diff --git a/Doc/conf.py b/Doc/conf.py index 1aeecaeb3073f5..2bf10038341d2d 100644 --- a/Doc/conf.py +++ b/Doc/conf.py @@ -23,7 +23,6 @@ # Our custom Sphinx extensions are found in Doc/Tools/extensions/ extensions = [ 'audit_events', - 'availability', 'c_annotations', 'changes', 'glossary_search', diff --git a/Doc/tools/extensions/availability.py b/Doc/tools/extensions/availability.py deleted file mode 100644 index 47833fdcb87590..00000000000000 --- a/Doc/tools/extensions/availability.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Support for documenting platform availability""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from docutils import nodes -from sphinx import addnodes -from sphinx.util import logging -from sphinx.util.docutils import SphinxDirective - -if TYPE_CHECKING: - from sphinx.application import Sphinx - from sphinx.util.typing import ExtensionMetadata - -logger = logging.getLogger("availability") - -# known platform, libc, and threading implementations -_PLATFORMS = frozenset({ - "AIX", - "Android", - "BSD", - "DragonFlyBSD", - "Emscripten", - "FreeBSD", - "GNU/kFreeBSD", - "iOS", - "Linux", - "macOS", - "NetBSD", - "OpenBSD", - "POSIX", - "Solaris", - "Unix", - "VxWorks", - "WASI", - "Windows", -}) -_LIBC = frozenset({ - "BSD libc", - "glibc", - "musl", -}) -_THREADING = frozenset({ - # POSIX platforms with pthreads - "pthreads", -}) -KNOWN_PLATFORMS = _PLATFORMS | _LIBC | _THREADING - - -class Availability(SphinxDirective): - has_content = True - required_arguments = 1 - optional_arguments = 0 - final_argument_whitespace = True - - def run(self) -> list[nodes.container]: - title = "Availability" - refnode = addnodes.pending_xref( - title, - nodes.inline(title, title, classes=["xref", "std", "std-ref"]), - refdoc=self.env.docname, - refdomain="std", - refexplicit=True, - reftarget="availability", - reftype="ref", - refwarn=True, - ) - sep = nodes.Text(": ") - parsed, msgs = self.state.inline_text(self.arguments[0], self.lineno) - pnode = nodes.paragraph(title, "", refnode, sep, *parsed, *msgs) - self.set_source_info(pnode) - cnode = nodes.container("", pnode, classes=["availability"]) - self.set_source_info(cnode) - if self.content: - self.state.nested_parse(self.content, self.content_offset, cnode) - self.parse_platforms() - - return [cnode] - - def parse_platforms(self) -> dict[str, str | bool]: - """Parse platform information from arguments - - Arguments is a comma-separated string of platforms. A platform may - be prefixed with "not " to indicate that a feature is not available. - - Example:: - - .. availability:: Windows, Linux >= 4.2, not WASI - - Arguments like "Linux >= 3.17 with glibc >= 2.27" are currently not - parsed into separate tokens. - """ - platforms = {} - for arg in self.arguments[0].rstrip(".").split(","): - arg = arg.strip() - platform, _, version = arg.partition(" >= ") - if platform.startswith("not "): - version = False - platform = platform.removeprefix("not ") - elif not version: - version = True - platforms[platform] = version - - if unknown := set(platforms).difference(KNOWN_PLATFORMS): - logger.warning( - "Unknown platform%s or syntax '%s' in '.. availability:: %s', " - "see %s:KNOWN_PLATFORMS for a set of known platforms.", - "s" if len(platforms) != 1 else "", - " ".join(sorted(unknown)), - self.arguments[0], - __file__, - ) - - return platforms - - -def setup(app: Sphinx) -> ExtensionMetadata: - app.add_directive("availability", Availability) - - return { - "version": "1.0", - "parallel_read_safe": True, - "parallel_write_safe": True, - } diff --git a/Doc/tools/extensions/pyspecific.py b/Doc/tools/extensions/pyspecific.py index f363dfd4216929..72a6eeb9a257ff 100644 --- a/Doc/tools/extensions/pyspecific.py +++ b/Doc/tools/extensions/pyspecific.py @@ -12,20 +12,28 @@ import re import io from os import getenv, path +from time import asctime +from pprint import pformat from docutils import nodes +from docutils.io import StringOutput from docutils.parsers.rst import directives -from docutils.utils import unescape +from docutils.utils import new_document, unescape from sphinx import addnodes +from sphinx.builders import Builder +from sphinx.domains.changeset import VersionChange, versionlabels, versionlabel_classes from sphinx.domains.python import PyFunction, PyMethod, PyModule from sphinx.locale import _ as sphinx_gettext +from sphinx.util import logging from sphinx.util.docutils import SphinxDirective +from sphinx.writers.text import TextWriter, TextTranslator +from sphinx.util.display import status_iterator ISSUE_URI = 'https://bugs.python.org/issue?@action=redirect&bpo=%s' GH_ISSUE_URI = 'https://github.com/python/cpython/issues/%s' # Used in conf.py and updated here by python/release-tools/run_release.py -SOURCE_URI = 'https://github.com/python/cpython/tree/main/%s' +SOURCE_URI = 'https://github.com/python/cpython/tree/3.12/%s' # monkey-patch reST parser to disable alphabetic and roman enumerated lists from docutils.parsers.rst.states import Body @@ -34,6 +42,16 @@ Body.enum.converters['lowerroman'] = \ Body.enum.converters['upperroman'] = lambda x: None +# monkey-patch the productionlist directive to allow hyphens in group names +# https://github.com/sphinx-doc/sphinx/issues/11854 +from sphinx.domains import std + +std.token_re = re.compile(r'`((~?[\w-]*:)?\w+)`') + +# backport :no-index: +PyModule.option_spec['no-index'] = directives.flag + + # Support for marking up and linking to bugs.python.org issues def issue_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): @@ -90,6 +108,106 @@ def run(self): return [pnode] +# Support for documenting platform availability + +class Availability(SphinxDirective): + + has_content = True + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + + # known platform, libc, and threading implementations + known_platforms = frozenset({ + "AIX", "Android", "BSD", "DragonFlyBSD", "Emscripten", "FreeBSD", + "GNU/kFreeBSD", "Linux", "NetBSD", "OpenBSD", "POSIX", "Solaris", + "Unix", "VxWorks", "WASI", "Windows", "macOS", "iOS", + # libc + "BSD libc", "glibc", "musl", + # POSIX platforms with pthreads + "pthreads", + }) + + def run(self): + availability_ref = ':ref:`Availability `: ' + avail_nodes, avail_msgs = self.state.inline_text( + availability_ref + self.arguments[0], + self.lineno) + pnode = nodes.paragraph(availability_ref + self.arguments[0], + '', *avail_nodes, *avail_msgs) + self.set_source_info(pnode) + cnode = nodes.container("", pnode, classes=["availability"]) + self.set_source_info(cnode) + if self.content: + self.state.nested_parse(self.content, self.content_offset, cnode) + self.parse_platforms() + + return [cnode] + + def parse_platforms(self): + """Parse platform information from arguments + + Arguments is a comma-separated string of platforms. A platform may + be prefixed with "not " to indicate that a feature is not available. + + Example:: + + .. availability:: Windows, Linux >= 4.2, not WASI + + Arguments like "Linux >= 3.17 with glibc >= 2.27" are currently not + parsed into separate tokens. + """ + platforms = {} + for arg in self.arguments[0].rstrip(".").split(","): + arg = arg.strip() + platform, _, version = arg.partition(" >= ") + if platform.startswith("not "): + version = False + platform = platform[4:] + elif not version: + version = True + platforms[platform] = version + + unknown = set(platforms).difference(self.known_platforms) + if unknown: + cls = type(self) + logger = logging.getLogger(cls.__qualname__) + logger.warning( + f"Unknown platform(s) or syntax '{' '.join(sorted(unknown))}' " + f"in '.. availability:: {self.arguments[0]}', see " + f"{__file__}:{cls.__qualname__}.known_platforms for a set " + "known platforms." + ) + + return platforms + + +# Support for documenting decorators + +class PyDecoratorMixin(object): + def handle_signature(self, sig, signode): + ret = super(PyDecoratorMixin, self).handle_signature(sig, signode) + signode.insert(0, addnodes.desc_addname('@', '@')) + return ret + + def needs_arglist(self): + return False + + +class PyDecoratorFunction(PyDecoratorMixin, PyFunction): + def run(self): + # a decorator function is a function after all + self.name = 'py:function' + return PyFunction.run(self) + + +# TODO: Use sphinx.domains.python.PyDecoratorMethod when possible +class PyDecoratorMethod(PyDecoratorMixin, PyMethod): + def run(self): + self.name = 'py:method' + return PyMethod.run(self) + + class PyCoroutineMixin(object): def handle_signature(self, sig, signode): ret = super(PyCoroutineMixin, self).handle_signature(sig, signode) @@ -141,6 +259,38 @@ def run(self): return PyMethod.run(self) +# Support for documenting version of removal in deprecations + +class DeprecatedRemoved(VersionChange): + required_arguments = 2 + + _deprecated_label = sphinx_gettext('Deprecated since version %s, will be removed in version %s') + _removed_label = sphinx_gettext('Deprecated since version %s, removed in version %s') + + def run(self): + # Replace the first two arguments (deprecated version and removed version) + # with a single tuple of both versions. + version_deprecated = self.arguments[0] + version_removed = self.arguments.pop(1) + self.arguments[0] = version_deprecated, version_removed + + # Set the label based on if we have reached the removal version + current_version = tuple(map(int, self.config.version.split('.'))) + removed_version = tuple(map(int, version_removed.split('.'))) + if current_version < removed_version: + versionlabels[self.name] = self._deprecated_label + versionlabel_classes[self.name] = 'deprecated' + else: + versionlabels[self.name] = self._removed_label + versionlabel_classes[self.name] = 'removed' + try: + return super().run() + finally: + # reset versionlabels and versionlabel_classes + versionlabels[self.name] = '' + versionlabel_classes[self.name] = '' + + # Support for including Misc/NEWS issue_re = re.compile('(?:[Ii]ssue #|bpo-)([0-9]+)', re.I) @@ -181,6 +331,69 @@ def run(self): return [] +# Support for building "topic help" for pydoc + +pydoc_topic_labels = [ + 'assert', 'assignment', 'async', 'atom-identifiers', 'atom-literals', + 'attribute-access', 'attribute-references', 'augassign', 'await', + 'binary', 'bitwise', 'bltin-code-objects', 'bltin-ellipsis-object', + 'bltin-null-object', 'bltin-type-objects', 'booleans', + 'break', 'callable-types', 'calls', 'class', 'comparisons', 'compound', + 'context-managers', 'continue', 'conversions', 'customization', 'debugger', + 'del', 'dict', 'dynamic-features', 'else', 'exceptions', 'execmodel', + 'exprlists', 'floating', 'for', 'formatstrings', 'function', 'global', + 'id-classes', 'identifiers', 'if', 'imaginary', 'import', 'in', 'integers', + 'lambda', 'lists', 'naming', 'nonlocal', 'numbers', 'numeric-types', + 'objects', 'operator-summary', 'pass', 'power', 'raise', 'return', + 'sequence-types', 'shifting', 'slicings', 'specialattrs', 'specialnames', + 'string-methods', 'strings', 'subscriptions', 'truth', 'try', 'types', + 'typesfunctions', 'typesmapping', 'typesmethods', 'typesmodules', + 'typesseq', 'typesseq-mutable', 'unary', 'while', 'with', 'yield' +] + + +class PydocTopicsBuilder(Builder): + name = 'pydoc-topics' + + default_translator_class = TextTranslator + + def init(self): + self.topics = {} + self.secnumbers = {} + + def get_outdated_docs(self): + return 'all pydoc topics' + + def get_target_uri(self, docname, typ=None): + return '' # no URIs + + def write(self, *ignored): + writer = TextWriter(self) + for label in status_iterator(pydoc_topic_labels, + 'building topics... ', + length=len(pydoc_topic_labels)): + if label not in self.env.domaindata['std']['labels']: + self.env.logger.warning(f'label {label!r} not in documentation') + continue + docname, labelid, sectname = self.env.domaindata['std']['labels'][label] + doctree = self.env.get_and_resolve_doctree(docname, self) + document = new_document('
') + document.append(doctree.ids[labelid]) + destination = StringOutput(encoding='utf-8') + writer.write(document, destination) + self.topics[label] = writer.output + + def finish(self): + f = open(path.join(self.outdir, 'topics.py'), 'wb') + try: + f.write('# -*- coding: utf-8 -*-\n'.encode('utf-8')) + f.write(('# Autogenerated by Sphinx on %s\n' % asctime()).encode('utf-8')) + f.write('# as part of the release process.\n'.encode('utf-8')) + f.write(('topics = ' + pformat(self.topics) + '\n').encode('utf-8')) + finally: + f.close() + + # Support for documenting Opcodes opcode_sig_re = re.compile(r'(\w+(?:\+\d)?)(?:\s*\((.*)\))?') @@ -260,9 +473,15 @@ def setup(app): app.add_role('issue', issue_role) app.add_role('gh', gh_issue_role) app.add_directive('impl-detail', ImplementationDetail) + app.add_directive('availability', Availability) + app.add_directive('deprecated-removed', DeprecatedRemoved) + app.add_builder(PydocTopicsBuilder) app.add_object_type('opcode', 'opcode', '%s (opcode)', parse_opcode_signature) app.add_object_type('pdbcommand', 'pdbcmd', '%s (pdb command)', parse_pdb_command) app.add_object_type('monitoring-event', 'monitoring-event', '%s (monitoring event)', parse_monitoring_event) + app.add_object_type('2to3fixer', '2to3fixer', '%s (2to3 fixer)') + app.add_directive_to_domain('py', 'decorator', PyDecoratorFunction) + app.add_directive_to_domain('py', 'decoratormethod', PyDecoratorMethod) app.add_directive_to_domain('py', 'coroutinefunction', PyCoroutineFunction) app.add_directive_to_domain('py', 'coroutinemethod', PyCoroutineMethod) app.add_directive_to_domain('py', 'awaitablefunction', PyAwaitableFunction)