PK7V9gNN!sphinx_toolbox/github/__init__.py#!/usr/bin/env python3 # # __init__.py r""" Sphinx domain for GitHub.com, and related utilities. .. versionadded:: 2.4.0 .. extensions:: sphinx_toolbox.github Configuration -------------- .. latex:vspace:: -10px .. confval:: github_username :type: :class:`str` :required: True The username of the GitHub account that owns the repository this documentation corresponds to. .. confval:: github_repository :type: :class:`str` :required: True The GitHub repository this documentation corresponds to. Usage ------ .. latex:vspace:: -10px .. rst:role:: github:issue Role which shows a link to the given issue on GitHub. If the issue exists, the link has a tooltip that shows the title of the issue. **Example** .. rest-example:: :github:issue:`1` You can also reference an issue in a different repository by adding the repository name inside ``<>``. .. rest-example:: :github:issue:`7680 ` .. rst:role:: github:pull Role which shows a link to the given pull request on GitHub. If the pull requests exists, the link has a tooltip that shows the title of the pull requests. **Example** .. rest-example:: :github:pull:`2` You can also reference a pull request in a different repository by adding the repository name inside ``<>``. .. rest-example:: :github:pull:`7671 ` .. rst:role:: github:repo Role which shows a link to the given repository on GitHub. **Example** .. rest-example:: :github:repo:`sphinx-toolbox/sphinx-toolbox` You can also use a different label for the link:. .. rest-example:: See more in the :github:repo:`pytest repository `. .. rst:role:: github:user Role which shows a link to the given user on GitHub. **Example** .. rest-example:: :github:user:`domdfcoding` You can also use a different label for the link:. .. rest-example:: See more of my :github:user:`repositories `. .. latex:clearpage:: .. rst:role:: github:org Role which shows a link to the given organization on GitHub. **Example** .. rest-example:: :github:org:`sphinx-toolbox` You can also use a different label for the link:. .. rest-example:: See more repositories in the :github:org:`pytest-dev org `. Caching ----------- HTTP requests to obtain issue/pull request titles are cached for four hours. To clear the cache manually, run: .. prompt:: bash python3 -m sphinx_toolbox API Reference --------------- Enable this extension from your extension's setup function like so: .. code-block:: def setup(app: Sphinx) -> Dict[str, Any]: app.setup_extension('sphinx_toolbox.github') return {} This will guarantee that the following values will be available via :attr:`app.config `: * **github_username** (:class:`str`\) -- The username of the GitHub account that owns the repository this documentation corresponds to. * **github_repository** (:class:`str`\) -- The GitHub repository this documentation corresponds to. * **github_url** (:class:`apeye.requests_url.RequestsURL`\) -- The complete URL of the repository on GitHub. * **github_source_url** (:class:`~apeye.requests_url.RequestsURL`\) -- The base URL for the source code on GitHub. * **github_issues_url** (:class:`~apeye.requests_url.RequestsURL`\) -- The base URL for the issues on GitHub. * **github_pull_url** (:class:`~apeye.requests_url.RequestsURL`\) -- The base URL for the pull requests on GitHub. If the user has not provided either ``github_username`` or ``github_repository`` a :exc:`~.MissingOptionError` will be raised. .. latex:clearpage:: """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # 3rd party from sphinx.application import Sphinx from sphinx.domains import Domain from sphinx.environment import BuildEnvironment # this package from sphinx_toolbox.config import MissingOptionError, ToolboxConfig from sphinx_toolbox.github.issues import ( IssueNode, _depart_issue_node_latex, _visit_issue_node_latex, depart_issue_node, issue_role, pull_role, visit_issue_node ) from sphinx_toolbox.github.repos_and_users import ( GitHubObjectLinkNode, _depart_github_object_link_node_latex, _visit_github_object_link_node_latex, depart_github_object_link_node, repository_role, user_role, visit_github_object_link_node ) from sphinx_toolbox.utils import SphinxExtMetadata, make_github_url, metadata_add_version _ = BuildEnvironment __all__ = ("GitHubDomain", "validate_config", "setup") class GitHubDomain(Domain): """ Sphinx domain for `GitHub.com `_. """ name = "github" label = "GitHub" roles = { "issue": issue_role, # type: ignore[dict-item] "pull": pull_role, # type: ignore[dict-item] "user": user_role, # type: ignore[dict-item] "org": user_role, # type: ignore[dict-item] "repo": repository_role, # type: ignore[dict-item] } def validate_config(app: Sphinx, config: ToolboxConfig) -> None: """ Validate the provided configuration values. See :class:`~sphinx_toolbox.config.ToolboxConfig` for a list of the configuration values. :param app: The Sphinx application. :param config: :type config: :class:`~sphinx.config.Config` """ if not config.github_username: raise MissingOptionError("The 'github_username' option is required.") else: config.github_username = str(config.github_username) if not config.github_repository: raise MissingOptionError("The 'github_repository' option is required.") else: config.github_repository = str(config.github_repository) config.github_url = make_github_url(config.github_username, config.github_repository) config.github_source_url = config.github_url / "blob" / "master" config.github_issues_url = config.github_url / "issues" config.github_pull_url = config.github_url / "pull" @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.github`. .. versionadded:: 1.0.0 :param app: The Sphinx application. """ app.connect("config-inited", validate_config, priority=850) app.add_config_value("github_username", None, "env", types=[str]) app.add_config_value("github_repository", None, "env", types=[str]) app.add_domain(GitHubDomain) # Custom node for issues and PRs app.add_node( IssueNode, html=(visit_issue_node, depart_issue_node), latex=(_visit_issue_node_latex, _depart_issue_node_latex) ) app.add_node( GitHubObjectLinkNode, html=(visit_github_object_link_node, depart_github_object_link_node), latex=(_visit_github_object_link_node_latex, _depart_github_object_link_node_latex) ) return {"parallel_read_safe": True} PK7V?7 * *sphinx_toolbox/github/issues.py#!/usr/bin/env python3 # # issues.py """ Roles and nodes for GitHub issues and Pull Requests. """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Based on pyspecific.py from the Python documentation. # Copyright 2008-2014 by Georg Brandl. # Licensed under the PSF License 2.0 # # Parts of the docstrings based on https://docutils.sourceforge.io/docs/howto/rst-roles.html # # stdlib import warnings import xml.sax.saxutils from typing import Any, Dict, List, Optional, Tuple, Union # 3rd party import requests # nodep from apeye.url import URL from bs4 import BeautifulSoup # type: ignore[import] from docutils import nodes from docutils.nodes import system_message from docutils.parsers.rst.states import Inliner from sphinx.util.nodes import split_explicit_title from sphinx.writers.html import HTMLTranslator from sphinx.writers.latex import LaTeXTranslator # this package from sphinx_toolbox.cache import cache from sphinx_toolbox.utils import make_github_url __all__ = ( "IssueNode", "IssueNodeWithName", "issue_role", "pull_role", "visit_issue_node", "depart_issue_node", "get_issue_title", ) class IssueNode(nodes.reference): """ Docutils Node to represent a link to a GitHub *Issue* or *Pull Request*. :param issue_number: The number of the issue or pull request. :param refuri: The URL of the issue / pull request on GitHub. """ has_tooltip: bool issue_number: int issue_url: str def __init__( self, issue_number: Union[str, int], refuri: Union[str, URL], **kwargs, ): self.has_tooltip = False self.issue_number = int(issue_number) self.issue_url = str(refuri) source = f"#{issue_number}" super().__init__(source, source, refuri=self.issue_url) @property def _copy_kwargs(self): # pragma: no cover # noqa: MAN002 return {"issue_number": self.issue_number, "refuri": self.issue_url} def copy(self) -> "IssueNode": # pragma: no cover """ Return a copy of the :class:`sphinx_toolbox.github.issues.IssueNode`. """ # This was required to stop some breakage, but it doesn't seem to run during the tests. obj = self.__class__(**self._copy_kwargs) obj.document = self.document obj.has_tooltip = self.has_tooltip obj.line = self.line return obj class IssueNodeWithName(IssueNode): """ Docutils Node to represent a link to a GitHub *Issue* or *Pull Request*, with the repository name shown. .. versionadded:: 2.4.0 :param repo_name: The full name of the repository, in the form ``owner/name``. :param issue_number: The number of the issue or pull request. :param refuri: The URL of the issue / pull request on GitHub. """ repo_name: str def __init__( self, repo_name: str, issue_number: Union[str, int], refuri: Union[str, URL], **kwargs, ): self.has_tooltip = False self.issue_number = int(issue_number) self.issue_url = str(refuri) self.repo_name = str(repo_name) source = f"{repo_name}#{issue_number}" nodes.reference.__init__(self, source, source, refuri=self.issue_url) @property def _copy_kwargs(self) -> Dict[str, Any]: # pragma: no cover return {"repo_name": self.repo_name, "issue_number": self.issue_number, "refuri": self.issue_url} def issue_role( name: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: Dict[str, Any] = {}, content: List[str] = [] ) -> Tuple[List[IssueNode], List[system_message]]: """ Adds a link to the given issue on GitHub. :param name: The local name of the interpreted role, the role name actually used in the document. :param rawtext: A string containing the entire interpreted text input, including the role and markup. :param text: The interpreted text content. :param lineno: The line number where the interpreted text begins. :param inliner: The :class:`docutils.parsers.rst.states.Inliner` object that called :func:`~.issue_role`. It contains the several attributes useful for error reporting and document tree access. :param options: A dictionary of directive options for customization (from the ``role`` directive), to be interpreted by the function. Used for additional attributes for the generated elements and other functionality. :param content: A list of strings, the directive content for customization (from the ``role`` directive). To be interpreted by the function. :return: A list containing the created node, and a list containing any messages generated during the function. .. latex:clearpage:: """ has_t, issue_number, repository = split_explicit_title(text) issue_number = nodes.unescape(issue_number) messages: List[system_message] = [] refnode: IssueNode if has_t: repository_parts = nodes.unescape(repository).split('/') if len(repository_parts) != 2: warning_message = inliner.document.reporter.warning( f"Invalid repository '{repository}' for issue #{issue_number}.", ) messages.append(warning_message) else: refnode = IssueNodeWithName( repo_name=repository, issue_number=issue_number, refuri=make_github_url(*repository_parts) / "issues" / str(int(issue_number)), ) return [refnode], messages issues_url = inliner.document.settings.env.app.config.github_issues_url refnode = IssueNode(issue_number=issue_number, refuri=issues_url / str(int(issue_number))) return [refnode], messages def pull_role( name: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: Dict[str, Any] = {}, content: List[str] = [] ) -> Tuple[List[IssueNode], List[system_message]]: """ Adds a link to the given pulll request on GitHub. :param name: The local name of the interpreted role, the role name actually used in the document. :param rawtext: A string containing the entire interpreted text input, including the role and markup. :param text: The interpreted text content. :param lineno: The line number where the interpreted text begins. :param inliner: The :class:`docutils.parsers.rst.states.Inliner` object that called :func:`~.pull_role`. It contains the several attributes useful for error reporting and document tree access. :param options: A dictionary of directive options for customization (from the ``role`` directive), to be interpreted by the function. Used for additional attributes for the generated elements and other functionality. :param content: A list of strings, the directive content for customization (from the ``role`` directive). To be interpreted by the function. :return: A list containing the created node, and a list containing any messages generated during the function. """ has_t, issue_number, repository = split_explicit_title(text) issue_number = nodes.unescape(issue_number) messages: List[system_message] = [] refnode: IssueNode if has_t: repository_parts = nodes.unescape(repository).split('/') if len(repository_parts) != 2: warning_message = inliner.document.reporter.warning( f"Invalid repository '{repository}' for pull request #{issue_number}." ) messages.append(warning_message) else: refnode = IssueNodeWithName( repo_name=repository, issue_number=issue_number, refuri=make_github_url(*repository_parts) / "pull" / str(int(issue_number)), ) return [refnode], messages pull_url = inliner.document.settings.env.app.config.github_pull_url refnode = IssueNode(issue_number=issue_number, refuri=pull_url / str(int(issue_number))) return [refnode], messages def visit_issue_node(translator: HTMLTranslator, node: IssueNode) -> None: """ Visit an :class:`~.IssueNode`. If the node points to a valid issue / pull request, add a tooltip giving the title of the issue / pull request and a hyperlink to the page on GitHub. :param translator: :param node: The node being visited. """ issue_title = get_issue_title(node.issue_url) if issue_title: node.has_tooltip = True translator.body.append(f'') translator.visit_reference(node) else: warnings.warn(f"Issue/Pull Request #{node.issue_number} not found.") def depart_issue_node(translator: HTMLTranslator, node: IssueNode) -> None: """ Depart an :class:`~.IssueNode`. :param translator: :param node: The node being visited. """ if node.has_tooltip: translator.depart_reference(node) translator.body.append("") def _visit_issue_node_latex(translator: LaTeXTranslator, node: IssueNode) -> None: """ Visit an :class:`~.IssueNode`. If the node points to a valid issue / pull request, add a tooltip giving the title of the issue / pull request and a hyperlink to the page on GitHub. :param translator: :param node: The node being visited. """ node.children = node.children[:1] translator.visit_reference(node) def _depart_issue_node_latex(translator: LaTeXTranslator, node: IssueNode) -> None: """ Depart an :class:`~.IssueNode`. :param translator: :param node: The node being visited. """ translator.depart_reference(node) def get_issue_title(issue_url: str) -> Optional[str]: """ Returns the title of the issue with the given url, or :py:obj:`None` if the issue isn't found. :param issue_url: """ # noqa: D400 try: r = cache.session.get(issue_url, timeout=30) except requests.exceptions.RequestException: return None if r.status_code == 200: soup = BeautifulSoup(r.content, "html5lib") try: content = soup.find_all("span", attrs={"class": "js-issue-title"})[0].text except IndexError: # As of 13 Jan 2023 GitHub seems to use a bidirectional text tag instead content = soup.find_all("bdi", attrs={"class": "js-issue-title"})[0].text content = xml.sax.saxutils.escape(content).replace('"', """) return content.strip() return None PK7Vo  (sphinx_toolbox/github/repos_and_users.py#!/usr/bin/env python3 # # repos_and_users.py """ Roles and nodes for referencing GitHub repositories and organizations. """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts of the docstrings based on https://docutils.sourceforge.io/docs/howto/rst-roles.html # # stdlib from typing import Any, Dict, List, Tuple, Union # 3rd party from apeye.url import URL from docutils import nodes from docutils.nodes import system_message from docutils.parsers.rst.states import Inliner from sphinx.util.nodes import split_explicit_title from sphinx.writers.html import HTMLTranslator from sphinx.writers.latex import LaTeXTranslator # this package from sphinx_toolbox.utils import GITHUB_COM, make_github_url __all__ = ( "GitHubObjectLinkNode", "repository_role", "user_role", "visit_github_object_link_node", "depart_github_object_link_node", ) class GitHubObjectLinkNode(nodes.reference): """ Docutils Node to represent a link to a GitHub repository. :param repo_name: The full name of the repository, in the form ``owner/name``. :param refuri: The URL of the issue / pull request on GitHub. .. clearpage:: """ name: str url: str def __init__( self, name: str, refuri: Union[str, URL], **kwargs, ): self.name = str(name) self.url = str(refuri) super().__init__(self.name, self.name, refuri=self.url) def copy(self) -> "GitHubObjectLinkNode": # pragma: no cover """ Return a copy of the :class:`sphinx_toolbox.github.repos.GitHubObjectLinkNode`. """ # This was required to stop some breakage, but it doesn't seem to run during the tests. obj = self.__class__(self.name, self.url) obj.document = self.document obj.source = self.source obj.line = self.line return obj def repository_role( name: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: Dict[str, Any] = {}, content: List[str] = [] ) -> Tuple[List[nodes.reference], List[system_message]]: """ Adds a link to the given repository on GitHub. :param name: The local name of the interpreted role, the role name actually used in the document. :param rawtext: A string containing the entire interpreted text input, including the role and markup. :param text: The interpreted text content. :param lineno: The line number where the interpreted text begins. :param inliner: The :class:`docutils.parsers.rst.states.Inliner` object that called :func:`~.repository_role`. It contains the several attributes useful for error reporting and document tree access. :param options: A dictionary of directive options for customization (from the ``role`` directive), to be interpreted by the function. Used for additional attributes for the generated elements and other functionality. :param content: A list of strings, the directive content for customization (from the ``role`` directive). To be interpreted by the function. :return: A list containing the created node, and a list containing any messages generated during the function. """ has_t, text, repo_name = split_explicit_title(text) repo_name = nodes.unescape(repo_name) repository_parts = nodes.unescape(repo_name).split('/') if len(repository_parts) != 2: return [], [inliner.document.reporter.warning(f"Invalid repository '{repo_name}'.")] # refnode: nodes.reference if has_t: refnode = nodes.reference( text, text, refuri=str(make_github_url(*repository_parts)), ) else: refnode = GitHubObjectLinkNode( name=repo_name, refuri=make_github_url(*repository_parts), ) return [refnode], [] def user_role( name: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: Dict[str, Any] = {}, content: List[str] = [] ) -> Tuple[List[nodes.reference], List[system_message]]: """ Adds a link to the given user / organization on GitHub. :param name: The local name of the interpreted role, the role name actually used in the document. :param rawtext: A string containing the entire interpreted text input, including the role and markup. :param text: The interpreted text content. :param lineno: The line number where the interpreted text begins. :param inliner: The :class:`docutils.parsers.rst.states.Inliner` object that called :func:`~.user_role`. It contains the several attributes useful for error reporting and document tree access. :param options: A dictionary of directive options for customization (from the ``role`` directive), to be interpreted by the function. Used for additional attributes for the generated elements and other functionality. :param content: A list of strings, the directive content for customization (from the ``role`` directive). To be interpreted by the function. :return: A list containing the created node, and a list containing any messages generated during the function. .. clearpage:: """ has_t, text, username = split_explicit_title(text) username = nodes.unescape(username) messages: List[system_message] = [] if has_t: refnode = nodes.reference( text, text, refuri=str(GITHUB_COM / username), ) else: refnode = GitHubObjectLinkNode( name=f"@{username}", refuri=GITHUB_COM / username, ) return [refnode], messages def visit_github_object_link_node(translator: HTMLTranslator, node: GitHubObjectLinkNode) -> None: """ Visit a :class:`~.GitHubObjectLinkNode`. :param translator: :param node: The node being visited. """ translator.body.append(f'') translator.visit_reference(node) def depart_github_object_link_node(translator: HTMLTranslator, node: GitHubObjectLinkNode) -> None: """ Depart an :class:`~.GitHubObjectLinkNode`. :param translator: :param node: The node being visited. """ translator.depart_reference(node) translator.body.append("") def _visit_github_object_link_node_latex(translator: LaTeXTranslator, node: GitHubObjectLinkNode) -> None: """ Visit a :class:`~.GitHubObjectLinkNode`. :param translator: :param node: The node being visited. """ node.children = node.children[:1] translator.visit_reference(node) def _depart_github_object_link_node_latex(translator: LaTeXTranslator, node: GitHubObjectLinkNode) -> None: """ Depart an :class:`~.GitHubObjectLinkNode`. :param translator: :param node: The node being visited. """ translator.depart_reference(node) PK7Vݝ-G-G sphinx_toolbox/latex/__init__.py#!/usr/bin/env python3 # # latex.py r""" Sphinx utilities for LaTeX builders. .. versionadded:: 2.8.0 In addition to the developer API (see below), :mod:`sphinx_toolbox.latex` configures Sphinx and LaTeX to correctly handle symbol footnotes. .. versionchanged:: 2.12.0 Sphinx is also configured to respect ``.. only:: html`` etc. directives surrounding toctree directives when determining the overall toctree depth. .. extensions:: sphinx_toolbox.latex .. latex:vspace:: -5px Example Footnotes -------------------- .. latex:vspace:: -10px | Hello [1]_ | Goodbye [2]_ | Symbol [*]_ | Another Symbol [*]_ | Number Again [3]_ | Symbol 3 [*]_ | Symbol 4 [*]_ | Symbol 5 [*]_ | Symbol 6 [*]_ | Symbol 7 [*]_ | Symbol 8 [*]_ | Symbol 9 [*]_ .. latex:vspace:: 20px .. [1] One .. [2] Two .. [*] Buckle my shoe .. [*] The second symbol .. [3] The number after the symbol .. [*] Symbol 3 .. [*] Symbol 4 .. [*] Symbol 5 .. [*] Symbol 6 .. [*] Symbol 7 .. [*] Symbol 8 .. [*] Symbol 9 Usage ------- .. latex:vspace:: -10px .. rst:directive:: latex:samepage samepage Configures LaTeX to make all content within this directive appear on the same page. This can be useful to avoid awkward page breaks. This directive has no effect with non-LaTeX builders. .. versionadded:: 2.9.0 .. rst:directive:: latex:clearpage clearpage Configures LaTeX to start a new page. This directive has no effect with non-LaTeX builders. .. versionadded:: 2.10.0 .. seealso:: :rst:dir:`latex:cleardoublepage` .. rst:directive:: latex:cleardoublepage cleardoublepage Configures LaTeX to start a new page. In a two-sided printing it also makes the next page a right-hand (odd-numbered) page, inserting a blank page if necessary. This directive has no effect with non-LaTeX builders. .. versionadded:: 2.10.0 .. seealso:: :rst:dir:`latex:clearpage` .. rst:directive:: .. latex:vspace:: space Configures LaTeX to add or remove vertical space. The value for ``space`` is passed verbatim to the ``\vspace{}`` command. Ensure you pass a valid value. This directive has no effect with non-LaTeX builders. .. versionadded:: 2.11.0 .. latex:clearpage:: API Reference ---------------- .. latex:vspace:: -20px """ # # Copyright © 2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # PatchedLaTeXBuilder based on Sphinx # Copyright (c) 2007-2020 by the Sphinx team. # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib import re from os.path import join as joinpath from textwrap import dedent from typing import Any, List, Optional, cast # 3rd party import sphinx from docutils import nodes from docutils.frontend import OptionParser from docutils.transforms.references import Footnotes from domdf_python_tools.paths import PathPlus from domdf_python_tools.stringlist import DelimitedList from sphinx import addnodes from sphinx.application import Sphinx from sphinx.builders.latex import LaTeXBuilder from sphinx.domains import Domain from sphinx.environment import BuildEnvironment from sphinx.locale import __ from sphinx.util.docutils import SphinxDirective, SphinxFileOutput from sphinx.util.nodes import process_only_nodes from sphinx.writers.latex import LaTeXTranslator, LaTeXWriter # this package from sphinx_toolbox.utils import Config, SphinxExtMetadata, metadata_add_version try: # 3rd party from sphinx.util.display import progress_message # type: ignore[import] except ImportError: # 3rd party from sphinx.util import progress_message _ = BuildEnvironment __all__ = ( "use_package", "visit_footnote", "depart_footnote", "SamepageDirective", "ClearPageDirective", "ClearDoublePageDirective", "VSpaceDirective", "LaTeXDomain", "replace_unknown_unicode", "better_header_layout", "configure", "setup", ) footmisc_symbols = ['0', *Footnotes.symbols] # footmisc_symbols = ['0', '*', '†', '‡', '§', '¶', '‖', "**", "††", "‡‡"] def visit_footnote(translator: LaTeXTranslator, node: nodes.footnote) -> None: """ Visit a :class:`docutils.nodes.footnote` node with the LaTeX translator. Unlike the default ``visit_footnote`` function, this one handles footnotes using symbols. .. versionadded:: 2.8.0 :param translator: :param node: """ translator.in_footnote += 1 footnote_id = str(cast(nodes.label, node[0]).astext()) if not translator.in_parsed_literal: translator.body.append("%\n") if not footnote_id.isnumeric() and footnote_id in footmisc_symbols: footnote_id = str(footmisc_symbols.index(footnote_id)) translator.body.append(r"\renewcommand\thefootnote{\thesymbolfootnote}") translator.body.append(rf"\begin{{footnote}}[{footnote_id}]") translator.body.append("\\sphinxAtStartFootnote\n") def depart_footnote(translator: LaTeXTranslator, node: nodes.footnote) -> None: """ Depart a :class:`docutils.nodes.footnote` node with the LaTeX translator. .. versionadded:: 2.8.0 :param translator: :param node: """ if not translator.in_parsed_literal: translator.body.append("%\n") translator.body.append(r"\end{footnote}") translator.body.append(r"\renewcommand\thefootnote{\thenumberfootnote}") translator.in_footnote -= 1 def use_package(package: str, config: Config, *args: str, **kwargs: str) -> None: r""" Configure LaTeX to use the given package. The ``\usepackage`` entry is added to the :py:obj:`sphinx.config.Config.latex_elements` ``["preamble"]`` attribute. :param package: :param config: :param \*args: Passed as options to the LaTeX package. :param \*\*kwargs: Passed as named options to the LaTeX package. """ options: DelimitedList[str] = DelimitedList() options.extend(args) options.extend(map("{}={}".format, kwargs.items())) use_string = rf"\usepackage[{options:,}]{{{package}}}" if not hasattr(config, "latex_elements") or not config.latex_elements: # pragma: no cover config.latex_elements = {} latex_preamble = config.latex_elements.get("preamble", '') if use_string not in latex_preamble: config.latex_elements["preamble"] = f"{latex_preamble}\n{use_string}" class SamepageDirective(SphinxDirective): """ Directive which configures LaTeX to make all content within this directive appear on the same page. This can be useful to avoid awkward page breaks. This directive has no effect with non-LaTeX builders. .. versionadded:: 2.9.0 """ has_content = True def run(self) -> List[nodes.Node]: """ Process the content of the directive. """ content_node = nodes.container(rawsource='\n'.join(self.content)) self.state.nested_parse(self.content, self.content_offset, content_node) return [ nodes.raw('', r"\par\begin{samepage}", format="latex"), content_node, nodes.raw('', r"\end{samepage}\par", format="latex"), ] class ClearPageDirective(SphinxDirective): """ Directive which configures LaTeX to start a new page. This directive has no effect with non-LaTeX builders. .. versionadded:: 2.10.0 .. seealso:: :class:`~.ClearDoublePageDirective` """ has_content = True def run(self) -> List[nodes.Node]: """ Process the content of the directive. """ return [nodes.raw('', r"\clearpage", format="latex")] class ClearDoublePageDirective(SphinxDirective): """ Directive which configures LaTeX to start a new page. In a two-sided printing it also makes the next page a right-hand (odd-numbered) page, inserting a blank page if necessary. This directive has no effect with non-LaTeX builders. .. versionadded:: 2.10.0 .. seealso:: :class:`~.ClearPageDirective` """ has_content = True def run(self) -> List[nodes.Node]: """ Process the content of the directive. """ return [nodes.raw('', r"\cleardoublepage", format="latex")] class VSpaceDirective(SphinxDirective): """ Directive which configures LaTeX to add or remove vertical space. This directive has no effect with non-LaTeX builders. .. versionadded:: 2.11.0 """ required_arguments = 1 # the space def run(self) -> List[nodes.Node]: """ Process the content of the directive. """ return [nodes.raw('', f"\n\\vspace{{{self.arguments[0]}}}\n", format="latex")] class LaTeXDomain(Domain): """ Domain containing various LaTeX-specific directives. .. versionadded:: 2.11.0 """ name = "latex" label = "LaTeX" directives = { "samepage": SamepageDirective, "clearpage": ClearPageDirective, "cleardoublepage": ClearDoublePageDirective, "vspace": VSpaceDirective, } def replace_unknown_unicode(app: Sphinx, exception: Optional[Exception] = None) -> None: r""" Replaces certain unknown unicode characters in the Sphinx LaTeX output with the best equivalents. .. only:: html The mapping is as follows: * ♠ -- \spadesuit * ♥ -- \heartsuit * ♦ -- \diamondsuit * ♣ -- \clubsuit * Zero width space -- \hspace{0pt} * μ -- \textmu * ≡ -- \equiv (new in version 2.11.0) * ≈ -- \approx (new in version 2.12.0) * ≥ -- \geq (new in version 2.13.0) * ≤ -- \leq (new in version 2.13.0) This function can be hooked into the :event:`build-finished` event as follows: .. code-block:: python app.connect("build-finished", replace_unknown_unicode) .. versionadded:: 2.9.0 :param app: The Sphinx application. :param exception: Any exception which occurred and caused Sphinx to abort. """ if exception: # pragma: no cover return if app.builder is None or app.builder.name.lower() != "latex": return builder = cast(LaTeXBuilder, app.builder) output_file = PathPlus(builder.outdir) / f"{builder.titles[0][1].lower()}.tex" output_content = output_file.read_text() output_content = output_content.replace('♠', r' $\spadesuit$ ') output_content = output_content.replace('♥', r' $\heartsuit$ ') output_content = output_content.replace('♦', r' $\diamondsuit$ ') output_content = output_content.replace('♣', r' $\clubsuit$ ') output_content = output_content.replace('\u200b', r'\hspace{0pt}') # Zero width space output_content = output_content.replace('μ', r"\textmu{}") output_content = output_content.replace('≡', r" $\equiv$ ") output_content = output_content.replace('≈', r" $\approx$ ") output_content = output_content.replace('≥', r" $\geq$ ") output_content = output_content.replace('≤', r" $\leq$ ") output_file.write_clean(output_content) def better_header_layout( config: Config, space_before: int = 10, space_after: int = 20, ) -> None: """ Makes LaTeX chapter names lowercase, and adjusts the spacing above and below the chapter name. .. versionadded:: 2.10.0 :param config: The Sphinx configuration object. :param space_before: The space, in pixels, before the chapter name. :param space_after: The space, in pixels, after the chapter name. """ begin = "% begin st better header layout" end = "% end st better header layout" commands = rf""" {begin} \makeatletter \renewcommand{{\DOCH}}{{% \mghrulefill{{\RW}}\par\nobreak \CNV\FmN{{\@chapapp}}\par\nobreak \CNoV\TheAlphaChapter\par\nobreak \vskip -1\baselineskip\vskip 5pt\mghrulefill{{\RW}}\par\nobreak \vskip 10\p@ }} \renewcommand{{\DOTI}}[1]{{% \CTV\FmTi{{#1}}\par\nobreak \vskip {space_before}\p@ }} \renewcommand{{\DOTIS}}[1]{{% \CTV\FmTi{{#1}}\par\nobreak \vskip {space_after}\p@ }} \makeatother {end} """ if not hasattr(config, "latex_elements") or not config.latex_elements: # pragma: no cover config.latex_elements = {} latex_preamble = config.latex_elements.get("preamble", '') if begin in latex_preamble: config.latex_elements["preamble"] = re.sub( f"{begin}.*{end}", dedent(commands), latex_preamble, count=1, flags=re.DOTALL, ) else: config.latex_elements["preamble"] = f"{latex_preamble}\n{dedent(commands)}" config.latex_elements["fncychap"] = "\\usepackage[Bjarne]{fncychap}\n\\ChNameAsIs\n\\ChTitleAsIs\n" class PatchedLaTeXBuilder(LaTeXBuilder): """ Patched version of Sphinx's LaTeX builder which skips toctrees under ``.. only:: html`` etc. directives when determining the max toctree depth. The default behaviour uses the ``:maxdepth:`` option of the first toctree, irrespective of whether the toctree will exist with the LaTeX builder. """ # noqa: D400 # TODO: respect toctree caption for LaTeX table of contents too # \addto\captionsenglish{\renewcommand{\contentsname}{Documentation}} def write(self, *ignored: Any) -> None: assert self.env is not None docwriter = LaTeXWriter(self) docsettings: Any = OptionParser( defaults=self.env.settings, components=(docwriter, ), read_config_files=True, ).get_default_values() if sphinx.version_info <= (4, 0): # 3rd party from sphinx.builders.latex import patch_settings # type: ignore[attr-defined] patch_settings(docsettings) self.init_document_data() self.write_stylesheet() for entry in self.document_data: docname, targetname, title, author, themename = entry[:5] theme = self.themes.get(themename) toctree_only = False if len(entry) > 5: toctree_only = entry[5] destination = SphinxFileOutput( destination_path=joinpath(self.outdir, targetname), encoding="utf-8", overwrite_if_changed=True ) with progress_message(__("processing %s") % targetname): doctree = self.env.get_doctree(docname) process_only_nodes(doctree, self.tags) if hasattr(doctree, "findall"): toctree = next(doctree.findall(addnodes.toctree), None) # type: ignore[attr-defined] else: toctree = next(iter(doctree.traverse(addnodes.toctree)), None) if toctree and toctree.get("maxdepth") > 0: tocdepth = toctree.get("maxdepth") else: tocdepth = None doctree = self.assemble_doctree( docname, toctree_only, appendices=(self.config.latex_appendices if theme.name != "howto" else []) ) doctree["docclass"] = theme.docclass doctree["contentsname"] = self.get_contentsname(docname) doctree["tocdepth"] = tocdepth self.post_process_images(doctree) self.update_doc_context(title, author, theme) if hasattr(self, "update_context"): # pragma: no cover # Only present in newer Sphinx versions self.update_context() with progress_message(__("writing")): # pylint: disable=loop-invariant-statement docsettings._author = author docsettings._title = title docsettings._contentsname = doctree["contentsname"] docsettings._docname = docname docsettings._docclass = theme.name doctree.settings = docsettings docwriter.theme = theme docwriter.write(doctree, destination) # pylint: enable=loop-invariant-statement def configure(app: Sphinx, config: Config) -> None: """ Configure :mod:`sphinx_toolbox.latex`. :param app: The Sphinx application. :param config: """ if not hasattr(config, "latex_elements") or not config.latex_elements: # pragma: no cover config.latex_elements = {} latex_preamble = config.latex_elements.get("preamble", '') command_string = r"\newcommand\thesymbolfootnote{\fnsymbol{footnote}}\let\thenumberfootnote\thefootnote" if command_string not in latex_preamble: config.latex_elements["preamble"] = f"{latex_preamble}\n{command_string}" @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.latex`. .. versionadded:: 2.8.0 :param app: The Sphinx application. """ app.add_node(nodes.footnote, latex=(visit_footnote, depart_footnote), override=True) app.add_directive("samepage", SamepageDirective) app.add_directive("clearpage", ClearPageDirective) app.add_directive("cleardoublepage", ClearDoublePageDirective) app.add_domain(LaTeXDomain) app.add_builder(PatchedLaTeXBuilder, override=True) app.connect("config-inited", configure) return {} PK7V@%%sphinx_toolbox/latex/layout.py#!/usr/bin/env python3 # # layout.py r""" Makes minor adjustments to the LaTeX layout. * Increases the whitespace above function signatures by 5px, to prevent the function visually merging with the previous one. * Remove unnecessary indentation and allow "raggedright" for the fields in the body of functions, which prevents ugly whitespace and line breaks. * Disables justification for function signatures. This is a backport of changes from Sphinx 4 added in :github:pull:`8997 `. .. versionadded:: 2.12.0 * With Sphinx 3.5, doesn't add ``\sphinxAtStartPar`` before every paragraph. The change in :github:issue:`8781 ` was to solve an issue with *tables*, but it isn't clear why it then gets added for *every* paragraph so this extension removes it. .. versionadded:: 2.13.0 * Configures hyperref to apply correct page numbering to the frontmatter. .. versionadded:: 2.14.0 * Optionally, configures the ``needspace`` package. The :confval:`needspace_amount` option can be set in ``conf.py`` to add the ``\needspace{}`` command before each ``addnodes.desc`` node (i.e. a function or class description). The amount of space is set by the ``needspace_amount`` option, e.g.: .. code-block:: python needspace_amount = r"4\baselineskip" .. versionadded:: 3.0.0 .. confval:: needspace_amount :type: :class:`str` :default: :py:obj:`None` Sets the value for the ``\needspace{}`` command. Values should be in the form ``r"4\baselineskip"``. .. versionadded:: 3.0.0 .. versionadded:: 2.10.0 .. versionchanged:: 3.0.0 Moved from :mod:`sphinx_toolbox.tweaks.latex_layout`. .. latex:clearpage:: .. extensions:: sphinx_toolbox.latex.layout ----- """ # # Copyright © 2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # 3rd party from docutils import nodes from domdf_python_tools.stringlist import StringList from sphinx import addnodes from sphinx.application import Sphinx from sphinx.builders.latex.nodes import footnotetext from sphinx.config import Config from sphinx.writers.latex import LaTeXTranslator # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ("setup", ) def visit_desc(translator: LaTeXTranslator, node: addnodes.desc) -> None: translator.body.append('\n\n\\vspace{5px}') needspace_amount = getattr(translator.config, "needspace_amount") if needspace_amount: translator.body.append(f"\\needspace{{{needspace_amount}}}") if "sphinxcontrib.toctree_plus" in translator.config.extensions: # 3rd party from sphinxcontrib import toctree_plus # type: ignore[import] # nodep toctree_plus.visit_desc(translator, node) else: LaTeXTranslator.visit_desc(translator, node) def visit_field_list(translator: LaTeXTranslator, node: nodes.field_list) -> None: translator.body.append('\\vspace{10px}\\begin{flushleft}\\begin{description}\n') if translator.table: # pragma: no cover translator.table.has_problematic = True def depart_field_list(translator: LaTeXTranslator, node: nodes.field_list) -> None: translator.body.append('\\end{description}\\end{flushleft}\\vspace{10px}\n') def configure(app: Sphinx, config: Config) -> None: """ Configure Sphinx Extension. :param app: The Sphinx application. :param config: """ if not hasattr(config, "latex_elements"): # pragma: no cover config.latex_elements = {} # type: ignore[attr-defined] latex_elements = (config.latex_elements or {}) latex_preamble = latex_elements.get("preamble", '') # Backported from Sphinx 4 # See https://github.com/sphinx-doc/sphinx/pull/8997 config.latex_elements["preamble"] = '\n'.join([ latex_preamble, r"\makeatletter", '', r"\renewcommand{\py@sigparams}[2]{%", r" \parbox[t]{\py@argswidth}{\raggedright #1\sphinxcode{)}#2\strut}%", " % final strut is to help get correct vertical separation in case of multi-line", " % box with the item contents.", '}', r"\makeatother", ]) config.latex_elements["hyperref"] = '\n'.join([ r"% Include hyperref last.", r"\usepackage[pdfpagelabels,hyperindex,hyperfigures]{hyperref}", r"% Fix anchor placement for figures with captions.", r"\usepackage{hypcap}% it must be loaded after hyperref.", ]) config.latex_elements["maketitle"] = '\n'.join([ r"\begingroup", r"\let\oldthepage\thepage", r"\renewcommand{\thepage}{T\oldthepage}", config.latex_elements.get("maketitle", r"\sphinxmaketitle"), r"\endgroup" ]) needspace_amount = getattr(config, "needspace_amount") if needspace_amount: latex_extrapackages = StringList(latex_elements.get("extrapackages", '')) latex_extrapackages.append(r"\usepackage{needspace}") latex_elements["extrapackages"] = str(latex_extrapackages) def visit_paragraph(translator: LaTeXTranslator, node: nodes.paragraph) -> None: index = node.parent.index(node) if ( index > 0 and isinstance(node.parent, nodes.compound) and not isinstance(node.parent[index - 1], nodes.paragraph) and not isinstance(node.parent[index - 1], nodes.compound) ): # insert blank line, if the paragraph follows a non-paragraph node in a compound translator.body.append("\\noindent\n") elif index == 1 and isinstance(node.parent, (nodes.footnote, footnotetext)): # don't insert blank line, if the paragraph is second child of a footnote # (first one is label node) pass else: # Sphinx 3.5 adds \sphinxAtStartPar here, but I don't see what it gains. translator.body.append('\n') @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.latex.layout`. :param app: The Sphinx application. """ app.connect("config-inited", configure, priority=500) app.add_node(addnodes.desc, latex=(visit_desc, LaTeXTranslator.depart_desc), override=True) app.add_node(nodes.field_list, latex=(visit_field_list, depart_field_list), override=True) app.add_node(nodes.paragraph, latex=(visit_paragraph, LaTeXTranslator.depart_paragraph), override=True) app.add_config_value("needspace_amount", default=None, rebuild="latex", types=[str]) return {"parallel_read_safe": True} PK7VK (sphinx_toolbox/latex/succinct_seealso.py#!/usr/bin/env python3 # # succinct_seealso.py """ Sphinx extension which customises :rst:dir:`seealso` directives to be on one line with the LaTeX builder. .. extensions:: sphinx_toolbox.latex.succinct_seealso .. versionadded:: 3.0.0 """ # # Copyright © 2022 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # 3rd party from sphinx import addnodes from sphinx.application import Sphinx from sphinx.locale import admonitionlabels from sphinx.writers.latex import LaTeXTranslator __all__ = ("setup", ) # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version def visit_seealso(translator: LaTeXTranslator, node: addnodes.seealso) -> None: """ Visit an :class:`addnodes.seealso`` node. :param translator: :param node: """ # translator.body.append('\n\n\\begin{description}\\item[{%s:}] \\leavevmode' % admonitionlabels['seealso']) # translator.body.append('\n\n\\sphinxstrong{%s:} ' % admonitionlabels["seealso"]) if len(node) > 1: LaTeXTranslator.visit_seealso(translator, node) else: translator.body.append('\n\n\\sphinxstrong{%s:} ' % admonitionlabels["seealso"]) def depart_seealso(translator: LaTeXTranslator, node: addnodes.seealso) -> None: """ Depart an :class:`addnodes.seealso`` node. :param translator: :param node: """ if len(node) > 1: LaTeXTranslator.depart_seealso(translator, node) else: # translator.body.append("\\end{description}\n\n") translator.body.append("\n\n") @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.latex.succinct_seealso`. :param app: The Sphinx application. """ app.add_node(addnodes.seealso, latex=(visit_seealso, depart_seealso), override=True) return {"parallel_read_safe": True} PK7Vcsphinx_toolbox/latex/toc.py#!/usr/bin/env python3 # # toc.py """ Adjusts the default LaTeX output as follows: * The captions from ``toctree`` directives are converted into document parts. * The PDF outline has the correct hierarchy, including having the indices as top-level elements. .. versionadded:: 2.1.0 .. versionchanged:: 3.0.0 Moved from :mod:`sphinx_toolbox.tweaks.latex_toc`. .. latex:clearpage:: .. extensions:: sphinx_toolbox.latex.toc ----- """ # noqa: D400 # # Copyright © 2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib from typing import List # 3rd party import sphinx.directives.other import sphinx.writers.latex from docutils import nodes from sphinx.application import Sphinx # this package from sphinx_toolbox.latex import use_package from sphinx_toolbox.utils import Config, SphinxExtMetadata, metadata_add_version __all__ = ("setup", ) nest_bookmark_level_part = "\\bookmarksetupnext{{level=part}}\n" class latex_toc(nodes.raw): pass class LaTeXTranslator(sphinx.writers.latex.LaTeXTranslator): def generate_indices(self) -> str: super_output = super().generate_indices() if not super_output: return nest_bookmark_level_part return '\n'.join([ nest_bookmark_level_part, *super_output.splitlines(), '', nest_bookmark_level_part, ]) def visit_latex_toc(self, node: latex_toc) -> None: if not self.is_inline(node): self.body.append('\n') if "latex" in node.get("format", '').split(): self.body.append(f"\\{self.sectionnames[self.sectionlevel]}{{{node.astext()}}}") if not self.is_inline(node): self.body.append('\n') raise nodes.SkipNode def depart_latex_toc(self, node: latex_toc) -> None: # pragma: no cover pass class LatexTocTreeDirective(sphinx.directives.other.TocTree): def run(self) -> List[nodes.Node]: """ Process the content of the directive. """ assert self.env.app.builder is not None output: List[nodes.Node] = [] caption = self.options.get("caption") if ( caption is not None and "hidden" not in self.options and self.env.app.builder.format.lower() == "latex" and self.env.docname == self.env.config.master_doc ): output.append(latex_toc(text=caption, format="latex")) output.extend(super().run()) return output def configure(app: Sphinx, config: Config) -> None: """ Configure :mod:`sphinx_toolbox.latex.toc`. :param app: The Sphinx application. :param config: """ use_package("bookmark", config) def purge_outdated(app: Sphinx, env, added, changed, removed) -> List[str]: # noqa: MAN001 return [env.config.master_doc] @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.tweaks.latex_toc`. :param app: The Sphinx application. """ app.connect("env-get-outdated", purge_outdated) app.connect("config-inited", configure) app.add_directive("toctree", LatexTocTreeDirective, override=True) app.set_translator("latex", LaTeXTranslator, override=True) return {"parallel_read_safe": True} PK7Vj5Fzz'sphinx_toolbox/more_autodoc/__init__.py#!/usr/bin/env python3 # # __init__.py """ Extensions to :mod:`sphinx.ext.autodoc`. .. versionadded:: 0.6.0 """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts based on https://github.com/sphinx-doc/sphinx # | Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file). # | BSD Licensed # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib from typing import TYPE_CHECKING, Any, List, Optional, Tuple # 3rd party from docutils.statemachine import StringList from sphinx.application import Sphinx from sphinx.ext.autodoc import Documenter # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version if TYPE_CHECKING: # 3rd party from sphinx.ext.autodoc import ObjectMembers ObjectMembers = ObjectMembers else: ObjectMembers = List[Tuple[str, Any]] __all__ = ("setup", ) def _documenter_add_content( self: Documenter, more_content: Optional[StringList], no_docstring: bool = False, ) -> None: """ Add content from docstrings, attribute documentation and user. """ # set sourcename and add content from attribute documentation sourcename = self.get_sourcename() if self.analyzer: attr_docs = self.analyzer.find_attr_docs() if self.objpath: key = ('.'.join(self.objpath[:-1]), self.objpath[-1]) if key in attr_docs: no_docstring = True # make a copy of docstring for attributes to avoid cache # the change of autodoc-process-docstring event. docstrings = [list(attr_docs[key])] for i, line in enumerate(self.process_doc(docstrings)): self.add_line(line, sourcename, i) # add content from docstrings if not no_docstring: docstrings = self.get_doc() or [] if docstrings is None: # Do not call autodoc-process-docstring on get_doc() returns None. pass else: if not docstrings: # append at least a dummy docstring, so that the event # autodoc-process-docstring is fired and can add some # content if desired docstrings.append([]) for i, line in enumerate(self.process_doc(docstrings)): self.add_line(line, sourcename, i) # add additional content (e.g. from document), if present if more_content: for line, src in zip(more_content.data, more_content.items): self.add_line(line, src[0], src[1]) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autodoc`. :param app: The Sphinx application. """ # Setup sub-extensions app.setup_extension("sphinx_toolbox.more_autodoc.augment_defaults") app.setup_extension("sphinx_toolbox.more_autodoc.autoprotocol") app.setup_extension("sphinx_toolbox.more_autodoc.autotypeddict") app.setup_extension("sphinx_toolbox.more_autodoc.autonamedtuple") app.setup_extension("sphinx_toolbox.more_autodoc.genericalias") app.setup_extension("sphinx_toolbox.more_autodoc.typehints") app.setup_extension("sphinx_toolbox.more_autodoc.variables") app.setup_extension("sphinx_toolbox.more_autodoc.sourcelink") app.setup_extension("sphinx_toolbox.more_autodoc.no_docstring") app.setup_extension("sphinx_toolbox.more_autodoc.regex") app.setup_extension("sphinx_toolbox.more_autodoc.typevars") app.setup_extension("sphinx_toolbox.more_autodoc.overloads") app.setup_extension("sphinx_toolbox.more_autodoc.generic_bases") return {"parallel_read_safe": True} PK7Vv7'XX/sphinx_toolbox/more_autodoc/augment_defaults.py#!/usr/bin/env python3 # # augment_defaults.py """ Sphinx's autodoc module allows for default options to be set, and allows for those defaults to be disabled for an ``:auto*:`` directive and different values given instead. However, it does not appear to be possible to augment the defaults, such as to globally exclude certain members and then exclude additional members of a single class. This module monkeypatches in that behaviour. .. extensions:: sphinx_toolbox.more_autodoc.augment_defaults .. versionchanged:: 0.6.0 Moved from :mod:`sphinx_toolbox.autodoc_augment_defaults`. """ # noqa: D400 # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Based on Sphinx # Copyright (c) 2007-2020 by the Sphinx team. # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib from typing import Dict, List, Type # 3rd party import autodocsumm # type: ignore[import] import sphinx.ext.autodoc.directive from docutils.utils import assemble_option_dict from sphinx.application import Sphinx from sphinx.errors import ExtensionError from sphinx.ext.autodoc import Documenter, Options # this package import sphinx_toolbox import sphinx_toolbox.more_autosummary from sphinx_toolbox.utils import Config, SphinxExtMetadata __all__ = ("process_documenter_options", "setup") def process_documenter_options( documenter: Type[Documenter], config: Config, options: Dict, ) -> Options: """ Recognize options of Documenter from user input. :param documenter: :param config: :param options: """ for name in sphinx.ext.autodoc.directive.AUTODOC_DEFAULT_OPTIONS: if name not in documenter.option_spec: # pragma: no cover continue else: negated = options.pop("no-" + name, True) is None if name in config.autodoc_default_options and not negated: # pylint: disable=loop-invariant-statement default_value = config.autodoc_default_options[name] existing_value = options.get(name, None) values: List[str] = [v for v in [default_value, existing_value] if v not in {None, True, False}] if values: options[name] = ','.join(values) else: options[name] = None # pragma: no cover # pylint: enable=loop-invariant-statement return Options(assemble_option_dict(options.items(), documenter.option_spec)) def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autodoc.augment_defaults`. :param app: The Sphinx application. """ if "sphinx.ext.autodoc" in app.extensions: raise ExtensionError( "'sphinx_toolbox.more_autodoc.augment_defaults' " "must be loaded before 'sphinx.ext.autodoc'." ) sphinx.ext.autodoc.directive.process_documenter_options = process_documenter_options # type: ignore[assignment] autodocsumm.process_documenter_options = process_documenter_options sphinx_toolbox.more_autosummary.process_documenter_options = process_documenter_options # type: ignore[assignment] app.setup_extension("sphinx.ext.autodoc") return {"version": sphinx_toolbox.__version__, "parallel_read_safe": True} PK7V+Qm@@-sphinx_toolbox/more_autodoc/autonamedtuple.py#!/usr/bin/env python3 # # autonamedtuple.py r""" A Sphinx directive for documenting :class:`NamedTuples ` in Python. .. versionadded:: 0.8.0 .. extensions:: sphinx_toolbox.more_autodoc.autonamedtuple .. versionchanged:: 1.5.0 ``__new__`` methods are documented regardless of other exclusion settings if the annotations differ from the namedtuple itself. Usage --------- .. rst:directive:: autonamedtuple Directive to automatically document a :class:`typing.NamedTuple` or :func:`collections.namedtuple`. The output is based on the :rst:dir:`autoclass` directive. The list of parameters and the attributes are replaced by a list of Fields, combining the types and docstrings from the class docstring individual attributes. These will always be shown regardless of the state of the ``:members:`` option. Otherwise the directive behaves the same as :rst:dir:`autoclass`, and takes all of its arguments. See https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html for further information. .. versionadded:: 0.8.0 .. rst:role:: namedtuple Role which provides a cross-reference to the documentation generated by :rst:dir:`autonamedtuple`. .. rst:role:: namedtuple-field Role which provides a cross-reference to a field in the namedtuple documentation. .. versionadded:: 3.0.0 .. note:: Due to limitations in docutils cross-references for all fields in a namedtuple will point to the top of the fields list, rather than the individial fields. .. seealso:: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html :bold-title:`Examples` .. literalinclude:: ../../../autonamedtuple_demo.py :language: python :tab-width: 4 :linenos: :lines: 1-59 .. rest-example:: .. automodule:: autonamedtuple_demo :no-autosummary: :exclude-members: Movie .. autonamedtuple:: autonamedtuple_demo.Movie This function takes a single argument, the :namedtuple:`~.Movie` to watch. The name of the movie can be accessed with the :namedtuple-field:`~.Movie.name` attribute. API Reference --------------- """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts based on https://github.com/sphinx-doc/sphinx # | Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file). # | BSD Licensed # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib import inspect import re import sys import textwrap from textwrap import dedent from typing import Any, Dict, List, Optional, Tuple, Type, cast, get_type_hints # 3rd party from docutils import nodes from docutils.statemachine import StringList from sphinx import addnodes from sphinx.application import Sphinx from sphinx.domains import ObjType from sphinx.domains.python import PyAttribute, PyClasslike, PythonDomain, PyXRefRole from sphinx.ext.autodoc import ClassDocumenter, Documenter, Options from sphinx.ext.autodoc.directive import DocumenterBridge from sphinx.locale import _ from sphinx.pycode import ModuleAnalyzer from sphinx.util.nodes import make_id # this package from sphinx_toolbox.more_autodoc import ObjectMembers, _documenter_add_content from sphinx_toolbox.more_autodoc.typehints import format_annotation from sphinx_toolbox.utils import ( Param, SphinxExtMetadata, add_fallback_css_class, add_nbsp_substitution, allow_subclass_add, baseclass_is_private, is_namedtuple, metadata_add_version, parse_parameters ) __all__ = ("NamedTupleDocumenter", "setup") class NamedTupleDocumenter(ClassDocumenter): r""" Sphinx autodoc :class:`~sphinx.ext.autodoc.Documenter` for documenting :class:`typing.NamedTuple`\s. .. versionadded:: 0.8.0 .. versionchanged:: 0.1.0 Will no longer attempt to find attribute docstrings from other namedtuple classes. """ # noqa: D400 objtype = "namedtuple" directivetype = "namedtuple" priority = 20 object: Type # noqa: A003 # pylint: disable=redefined-builtin field_alias_re = re.compile("Alias for field number [0-9]+") def __init__(self, directive: DocumenterBridge, name: str, indent: str = '') -> None: super().__init__(directive, name, indent) self.options = Options(self.options.copy()) @classmethod def can_document_member( cls, member: Any, membername: str, isattr: bool, parent: Any, ) -> bool: """ Called to see if a member can be documented by this documenter. :param member: The member being checked. :param membername: The name of the member. :param isattr: :param parent: The parent of the member. """ return is_namedtuple(member) def add_content(self, more_content: Optional[StringList], no_docstring: bool = True) -> None: r""" Add extra content (from docstrings, attribute docs etc.), but not the :class:`typing.NamedTuple`\'s docstring. :param more_content: :param no_docstring: """ # noqa: D400 _documenter_add_content(self, more_content, True) # set sourcename and add content from attribute documentation sourcename = self.get_sourcename() params, pre_output, post_output = self._get_docstring() for line in pre_output: self.add_line(line, sourcename) def _get_docstring(self) -> Tuple[Dict[str, Param], List[str], List[str]]: """ Returns params, pre_output, post_output. """ # Size varies depending on docutils config tab_size = self.env.app.config.docutils_tab_width if self.object.__doc__: docstring = dedent(self.object.__doc__).expandtabs(tab_size).split('\n') elif "show-inheritance" not in self.options: docstring = [":class:`typing.NamedTuple`."] else: docstring = [''] # pylint: disable=W8301 docstring = list(self.process_doc([docstring])) return parse_parameters(docstring, tab_size=tab_size) def add_directive_header(self, sig: str) -> None: """ Add the directive's header, and the inheritance information if the ``:show-inheritance:`` flag set. :param sig: The NamedTuple's signature. """ super().add_directive_header(sig) if "show-inheritance" not in self.options: return acceptable_bases = { " Bases: :class:`tuple`", " Bases: :py:class:`tuple`", " Bases: :class:`tuple`, :class:`typing.Generic`", " Bases: :py:class:`tuple`, :py:class:`typing.Generic`", " Bases: :class:`NamedTuple`", " Bases: :py:class:`NamedTuple`", } # TODO: support generic bases for multiple inheritance if self.directive.result[-1] in acceptable_bases or baseclass_is_private(self.object): # TODO: multiple inheritance if getattr(self.env.config, "generic_bases_fully_qualified", False): # Might not be present; extension might not be enabled prefix = '' else: prefix = '~' if hasattr(self.object, "__annotations__"): self.directive.result[-1] = f" Bases: :class:`{prefix}typing.NamedTuple`" else: self.directive.result[-1] = f" Bases: :func:`{prefix}collections.namedtuple`" def filter_members( self, members: ObjectMembers, want_all: bool, ) -> List[Tuple[str, Any, bool]]: """ Filter the list of members to always include ``__new__`` if it has a different signature to the tuple. :param members: :param want_all: """ all_hints = get_type_hints(self.object) class_hints = {k: all_hints[k] for k in self.object._fields if k in all_hints} # TODO: need a better way to partially resolve type hints, and warn about failures new_hints = get_type_hints( self.object.__new__, globalns=sys.modules[self.object.__module__].__dict__, localns=self.object.__dict__, # type: ignore[arg-type] ) # Stock NamedTuples don't have these, but customised collections.namedtuple or hand-rolled classes may if "cls" in new_hints: new_hints.pop("cls") if "return" in new_hints: new_hints.pop("return") if class_hints and new_hints and class_hints != new_hints: #: __new__ has a different signature or different annotations def unskip_new(app, what, name, obj, skip, options) -> Optional[bool]: # noqa: MAN001 if name == "__new__": return False return None listener_id = self.env.app.connect("autodoc-skip-member", unskip_new) members_ = super().filter_members(members, want_all) self.env.app.disconnect(listener_id) return members_ else: return super().filter_members(members, want_all) def sort_members( self, documenters: List[Tuple[Documenter, bool]], order: str, ) -> List[Tuple[Documenter, bool]]: r""" Sort the :class:`typing.NamedTuple`\'s members. :param documenters: :param order: """ # The documenters for the fields and methods, in the desired order # The fields will be in bysource order regardless of the order option documenters = super().sort_members(documenters, order) # Size varies depending on docutils config a_tab = ' ' * self.env.app.config.docutils_tab_width # Mapping of member names to docstrings (as list of strings) member_docstrings: Dict[str, List[str]] try: namedtuple_source = textwrap.dedent(inspect.getsource(self.object)) # Mapping of member names to docstrings (as list of strings) member_docstrings = { k[1]: v for k, v in ModuleAnalyzer.for_string(namedtuple_source, self.object.__module__ ).find_attr_docs().items() } except (TypeError, OSError): member_docstrings = {} # set sourcename and add content from attribute documentation sourcename = self.get_sourcename() params, pre_output, post_output = self._get_docstring() self.add_line('', sourcename) self.add_line(":Fields:", sourcename) # TODO: support for default_values self.add_line('', sourcename) fields = self.object._fields fields_body = [] for pos, field in enumerate(fields): doc: List[str] = [''] # pylint: disable=W8201 arg_type: str = '' # Prefer doc from class docstring if field in params: doc, arg_type = params.pop(field).values() # type: ignore[assignment] # Otherwise use attribute docstring if not ''.join(doc).strip() and field in member_docstrings: doc = member_docstrings[field] # Fallback to namedtuple's default docstring if not ''.join(doc).strip(): doc = [getattr(self.object, field).__doc__] # pylint: disable=W8301 # Prefer annotations over docstring types type_hints = get_type_hints(self.object) if type_hints: if field in type_hints: arg_type = format_annotation(type_hints[field]) self.add_line(f"{a_tab}.. namedtuple-field:: {field}", sourcename) self.add_line('', sourcename) # field_entry = [f"{a_tab}{pos})", "|nbsp|", f"**{field}**"] field_entry = [f"{a_tab}{pos}) \u00A0**{field}**"] if arg_type: field_entry.append(f"({arg_type}\\)") field_entry.append("--") field_entry.extend(doc) if self.field_alias_re.match(getattr(self.object, field).__doc__ or ''): getattr(self.object, field).__doc__ = ' '.join(doc) fields_body.append(' '.join(field_entry)) fields_body.append('') for line in fields_body: self.add_line(line, sourcename) self.add_line('', sourcename) for line in post_output: self.add_line(line, sourcename) self.add_line('', sourcename) # Stop a duplicate set of parameters being added with `autodoc_typehints = 'both'` self.env.temp_data["annotations"] = {} # Remove documenters corresponding to fields and return the rest return [d for d in documenters if d[0].name.split('.')[-1] not in fields] class _PyNamedTuplelike(PyClasslike): """ Description of a namedtuple-like object. """ def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str: if self.objtype == "namedtuple": return _("%s (namedtuple in %s)") % (name_cls[0], modname) else: return super().get_index_text(modname, name_cls) class _PyNamedTupleField(PyAttribute): """ Index entry and target for a namedtuple field. """ def run(self) -> List[nodes.Node]: if ':' in self.name: self.domain, self.objtype = self.name.split(':', 1) else: self.domain, self.objtype = '', self.name self.indexnode = addnodes.index(entries=[]) modname = self.options.get("module", self.env.ref_context.get("py:module")) classname = self.env.ref_context.get("py:class") name = self.arguments[0] full_name = f"{classname}.{name}" fullname = (modname + '.' if modname else '') + full_name node_id = make_id(self.env, self.state.document, '', fullname) node = nodes.target('', '', ids=[node_id]) node.document = self.state.document if "noindex" not in self.options: # only add target and index entry if this is the first # description of the object with this name in this desc block domain = cast(PythonDomain, self.env.get_domain("py")) domain.note_object(fullname, self.objtype, node_id, location=node) if "noindexentry" not in self.options: key = name[0].upper() pair = [ # pylint: disable=W8301 _("%s (namedtuple in %s)") % (classname, modname), _("%s (namedtuple field)") % name, ] self.indexnode["entries"].append(("pair", "; ".join(pair), node_id, '', key)) return [self.indexnode, node] @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autodoc.autonamedtuple`. .. versionadded:: 0.8.0 :param app: The Sphinx application. """ # Hack to get the docutils tab size, as there doesn't appear to be any other way app.setup_extension("sphinx_toolbox.tweaks.tabsize") app.registry.domains["py"].object_types["namedtuple"] = ObjType(_("namedtuple"), "namedtuple", "class", "obj") app.add_directive_to_domain("py", "namedtuple", _PyNamedTuplelike) app.add_role_to_domain("py", "namedtuple", PyXRefRole()) app.add_directive_to_domain("py", "namedtuple-field", _PyNamedTupleField) app.add_role_to_domain("py", "namedtuple-field", PyXRefRole()) app.registry.domains["py"].object_types["namedtuple-field"] = ObjType( _("namedtuple-field"), "namedtuple-field", "attr", "obj", ) app.connect("object-description-transform", add_fallback_css_class({"namedtuple": "class"})) allow_subclass_add(app, NamedTupleDocumenter) app.connect("config-inited", lambda _, config: add_nbsp_substitution(config)) return {"parallel_read_safe": True} PK7V6gid-d-+sphinx_toolbox/more_autodoc/autoprotocol.py#!/usr/bin/env python3 # # autoprotocol.py r""" A Sphinx directive for documenting :class:`Protocols ` in Python. .. versionadded:: 0.2.0 .. extensions:: sphinx_toolbox.more_autodoc.autoprotocol .. versionchanged:: 0.6.0 Moved from :mod:`sphinx_toolbox.autoprotocol`. .. versionchanged:: 2.13.0 Added support for generic bases, such as ``class SupportsAbs(Protocol[T_co]): ...``. Usage ------- .. latex:vspace:: -20px .. rst:directive:: autoprotocol Directive to automatically document a :class:`typing.Protocol`. The output is based on the :rst:dir:`autoclass` directive, but with a few differences: * Private members are always excluded. * Special members (dunder methods) are always included. * Undocumented members are always included. The following options from :rst:dir:`autoclass` are available: .. rst:directive:option:: noindex :type: flag Do not generate index entries for the documented object (and all autodocumented members). .. rst:directive:option:: member-order :type: string Override the global value of :any:`sphinx:autodoc_member_order` for one directive. .. rst:directive:option:: show-inheritance :type: flag Inserts a list of base classes just below the protocol's signature. .. rst:role:: protocol Role which provides a cross-reference to the documentation generated by :rst:dir:`autoprotocol`. .. latex:vspace:: 5px .. seealso:: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html .. latex:clearpage:: :bold-title:`Examples:` .. literalinclude:: ../../../autoprotocol_demo.py :language: python :tab-width: 4 :lines: 1-31 :linenos: .. rest-example:: .. automodule:: autoprotocol_demo :members: :no-autosummary: :exclude-members: HasGreaterThan .. autoprotocol:: autoprotocol_demo.HasGreaterThan The objects being sorted must implement the :protocol:`~.HasGreaterThan` protocol. .. latex:vspace:: 30px API Reference -------------- """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts based on https://github.com/sphinx-doc/sphinx # | Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file). # | BSD Licensed # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib import sys from typing import Any, Callable, Dict, List, Optional, Tuple # 3rd party from docutils.statemachine import StringList from sphinx.application import Sphinx from sphinx.domains import ObjType from sphinx.domains.python import PyClasslike, PyXRefRole from sphinx.ext.autodoc import ( INSTANCEATTR, ClassDocumenter, Documenter, Options, exclude_members_option, member_order_option ) from sphinx.ext.autodoc.directive import DocumenterBridge from sphinx.locale import _ from sphinx.util.inspect import getdoc, safe_getattr # this package from sphinx_toolbox.more_autodoc import ObjectMembers, _documenter_add_content from sphinx_toolbox.more_autodoc.generic_bases import _add_generic_bases from sphinx_toolbox.utils import ( SphinxExtMetadata, add_fallback_css_class, allow_subclass_add, filter_members_warning, flag, metadata_add_version ) if sys.version_info < (3, 8): # pragma: no cover (>=py38) # 3rd party from typing_extensions import _ProtocolMeta # type: ignore[attr-defined] else: # pragma: no cover (`_." ) class ProtocolDocumenter(ClassDocumenter): r""" Sphinx autodoc :class:`~sphinx.ext.autodoc.Documenter` for documenting :class:`typing.Protocol`\s. """ # noqa: D400 objtype = "protocol" directivetype = "protocol" priority = 20 option_spec: Dict[str, Callable] = { "noindex": flag, "member-order": member_order_option, "show-inheritance": flag, "exclude-protocol-members": exclude_members_option, } globally_excluded_methods = { "__module__", "__new__", "__init__", "__subclasshook__", "__doc__", "__tree_hash__", "__extra__", "__orig_bases__", "__origin__", "__parameters__", "__next_in_mro__", "__slots__", "__args__", "__dict__", "__weakref__", "__annotations__", "__abstractmethods__", "__class_getitem__", "__init_subclass__", } def __init__(self, directive: DocumenterBridge, name: str, indent: str = '') -> None: super().__init__(directive, name, indent) self.options = Options(self.options.copy()) @classmethod def can_document_member( cls, member: Any, membername: str, isattr: bool, parent: Any, ) -> bool: """ Called to see if a member can be documented by this documenter. :param member: The member being checked. :param membername: The name of the member. :param isattr: :param parent: The parent of the member. """ # _is_protocol = True return isinstance(member, _ProtocolMeta) def add_directive_header(self, sig: str) -> None: """ Add the directive header. :param sig: """ sourcename = self.get_sourcename() if self.doc_as_attr: self.directivetype = "attribute" Documenter.add_directive_header(self, sig) if self.analyzer and '.'.join(self.objpath) in self.analyzer.finals: self.add_line(" :final:", sourcename) # add inheritance info, if wanted if not self.doc_as_attr and self.options.show_inheritance: _add_generic_bases(self) def format_signature(self, **kwargs: Any) -> str: """ Protocols do not have a signature. """ return '' # pragma: no cover def add_content(self, more_content: Optional[StringList], no_docstring: bool = False) -> None: """ Add the autodocumenter content. :param more_content: :param no_docstring: """ _documenter_add_content(self, more_content, no_docstring) sourcename = self.get_sourcename() if not getdoc(self.object) and "show-inheritance" not in self.options: self.add_line(":class:`typing.Protocol`.", sourcename) self.add_line('', sourcename) if hasattr(self.object, "_is_runtime_protocol") and self.object._is_runtime_protocol: self.add_line(runtime_message, sourcename) self.add_line('', sourcename) self.add_line( "Classes that implement this protocol must have the following methods / attributes:", sourcename ) self.add_line('', sourcename) def document_members(self, all_members: bool = False) -> None: """ Generate reST for member documentation. All members are always documented. """ super().document_members(True) def filter_members( self, members: ObjectMembers, want_all: bool, ) -> List[Tuple[str, Any, bool]]: """ Filter the given member list. :param members: :param want_all: """ ret = [] # process members and determine which to skip for (membername, member) in members: # if isattr is True, the member is documented as an attribute if safe_getattr(member, "__sphinx_mock__", False): # mocked module or object keep = False # pragma: no cover elif ( self.options.get("exclude-protocol-members", []) and membername in self.options["exclude-protocol-members"] ): # remove members given by exclude-protocol-members keep = False # pragma: no cover elif membername.startswith('_') and not (membername.startswith("__") and membername.endswith("__")): keep = False elif membername not in self.globally_excluded_methods: # Magic method you wouldn't overload, or private method. if membername in dir(self.object.__base__): keep = member is not getattr(self.object.__base__, membername) else: keep = True else: keep = False # give the user a chance to decide whether this member # should be skipped if self.env.app: # let extensions preprocess docstrings try: # pylint: disable=R8203 skip_user = self.env.app.emit_firstresult( "autodoc-skip-member", self.objtype, membername, member, not keep, self.options, ) if skip_user is not None: keep = not skip_user except Exception as exc: filter_members_warning(member, exc) keep = False if keep: ret.append((membername, member, member is INSTANCEATTR)) return ret class _PyProtocollike(PyClasslike): """ Description of a Protocol-like object. """ def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str: if self.objtype == "protocol": return _("%s (protocol in %s)") % (name_cls[0], modname) else: return super().get_index_text(modname, name_cls) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autodoc.autoprotocol`. :param app: The Sphinx application. """ app.registry.domains["py"].object_types["protocol"] = ObjType(_("protocol"), "protocol", "class", "obj") app.add_directive_to_domain("py", "protocol", _PyProtocollike) app.add_role_to_domain("py", "protocol", PyXRefRole()) app.connect("object-description-transform", add_fallback_css_class({"protocol": "class"})) allow_subclass_add(app, ProtocolDocumenter) return {"parallel_read_safe": True} PK7V,LTSA3A3,sphinx_toolbox/more_autodoc/autotypeddict.py#!/usr/bin/env python3 # # autotypeddict.py r""" A Sphinx directive for documenting :class:`TypedDicts ` in Python. Only supports :mod:`typing_extensions`\'s TypedDict until :pull:`700 ` is implemented in CPython. .. versionadded:: 0.5.0 .. extensions:: sphinx_toolbox.more_autodoc.autotypeddict .. versionchanged:: 0.6.0 Moved from :mod:`sphinx_toolbox.autotypeddict`. Usage --------- .. latex:vspace:: -20px .. rst:directive:: autotypeddict Directive to automatically document a :class:`typing.TypedDict`. The output is based on the :rst:dir:`autoclass` directive, but with a few differences: * Private and Special members are always excluded. * Undocumented members are always included. * The default sort order is ``bysource``. The following options are available: .. rst:directive:option:: noindex :type: flag Do not generate index entries for the documented object (and all autodocumented members). .. rst:directive:option:: alphabetical :type: flag Sort the keys alphabetically. By default the keys are listed in the order they were defined. .. rst:directive:option:: show-inheritance :type: flag Inserts a list of base classes just below the TypedDict's signature. .. rst:role:: typeddict Role which provides a cross-reference to the documentation generated by :rst:dir:`autotypeddict`. .. latex:vspace:: 5px .. seealso:: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html .. latex:clearpage:: :bold-title:`Examples:` .. literalinclude:: ../../../autotypeddict_demo.py :language: python :tab-width: 4 :linenos: .. rest-example:: .. automodule:: autotypeddict_demo :no-autosummary: :exclude-members: Movie,AquaticBird,OldStyleAnimal .. autotypeddict:: autotypeddict_demo.Movie This function takes a single argument, the :typeddict:`~.Movie` to watch. .. latex:vspace:: -5px API Reference --------------- .. latex:vspace:: -20px """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts based on https://github.com/sphinx-doc/sphinx # | Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file). # | BSD Licensed # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib from typing import Any, Callable, Dict, List, Tuple, Type, get_type_hints # 3rd party import docutils.statemachine from domdf_python_tools.stringlist import StringList from sphinx.application import Sphinx from sphinx.domains import ObjType from sphinx.domains.python import PyClasslike, PyXRefRole from sphinx.ext.autodoc import INSTANCEATTR, ClassDocumenter, Documenter, Options from sphinx.locale import _ from sphinx.pycode import ModuleAnalyzer from sphinx.util.inspect import safe_getattr # this package from sphinx_toolbox.more_autodoc import ObjectMembers from sphinx_toolbox.more_autodoc.typehints import format_annotation from sphinx_toolbox.utils import ( SphinxExtMetadata, add_fallback_css_class, allow_subclass_add, filter_members_warning, flag, metadata_add_version ) __all__ = ("TypedDictDocumenter", "setup") class TypedDictDocumenter(ClassDocumenter): r""" Sphinx autodoc :class:`~sphinx.ext.autodoc.Documenter` for documenting :class:`typing.TypedDict`\s. """ # noqa: D400 objtype = "typeddict" directivetype = "typeddict" priority = 20 option_spec: Dict[str, Callable] = { "noindex": flag, "alphabetical": flag, "show-inheritance": flag, } def __init__(self, *args: Any) -> None: super().__init__(*args) self.options = Options(self.options.copy()) alphabetical = self.options.get("alphabetical", False) if alphabetical: self.options["member-order"] = "alphabetical" else: self.options["member-order"] = "bysource" for key in {"inherited-members", "special-members"}: # pragma: no cover if key in self.options: del self.options[key] @classmethod def can_document_member( cls, member: Any, membername: str, isattr: bool, parent: Any, ) -> bool: """ Called to see if a member can be documented by this documenter. :param member: The member being checked. :param membername: The name of the member. :param isattr: :param parent: The parent of the member. """ for attr in {"__optional_keys__", "__required_keys__", "__total__"}: if not hasattr(member, attr): return False return True def format_signature(self, **kwargs: Any) -> str: """ Typed Dicts do not have a signature. :rtype: .. latex:clearpage:: """ return '' def add_content(self, more_content: Any, no_docstring: bool = False) -> None: """ Add the autodocumenter content. :param more_content: :param no_docstring: """ if self.doc_as_attr: # pragma: no cover (verbatim from Sphinx) classname = safe_getattr(self.object, "__qualname__", None) if not classname: classname = safe_getattr(self.object, "__name__", None) if classname: module = safe_getattr(self.object, "__module__", None) parentmodule = safe_getattr(self.parent, "__module__", None) if module and module != parentmodule: classname = str(module) + '.' + str(classname) more_content = docutils.statemachine.StringList([_("alias of :class:`%s`") % classname], source='') no_docstring = True # set sourcename and add content from attribute documentation sourcename = self.get_sourcename() if self.analyzer: attr_docs = self.analyzer.find_attr_docs() if self.objpath: key = ('.'.join(self.objpath[:-1]), self.objpath[-1]) if key in attr_docs: no_docstring = True # make a copy of docstring for attributes to avoid cache # the change of autodoc-process-docstring event. docstrings = [list(attr_docs[key])] for i, line in enumerate(self.process_doc(docstrings)): self.add_line(line, sourcename, i) # add content from docstrings if not no_docstring: docstrings = self.get_doc() or [] if not docstrings: # append at least a dummy docstring, so that the event # autodoc-process-docstring is fired and can add some # content if desired docstrings.append([":class:`typing.TypedDict`.", '']) for i, line in enumerate(self.process_doc(docstrings)): self.add_line(line, sourcename, i) # add additional content (e.g. from document), if present if more_content: # pragma: no cover (verbatim from Sphinx) for line, src in zip(more_content.data, more_content.items): self.add_line(line, src[0], src[1]) def document_members(self, all_members: bool = False) -> None: """ Generate reST for member documentation. All members are always documented. """ super().document_members(True) def sort_members( self, documenters: List[Tuple[Documenter, bool]], order: str, ) -> List[Tuple[Documenter, bool]]: """ Sort the TypedDict's members. :param documenters: :param order: """ # The documenters for the keys, in the desired order documenters = super().sort_members(documenters, order) # Mapping of key names to docstrings (as list of strings) docstrings = { k[1]: v for k, v in ModuleAnalyzer.for_module(self.object.__module__).find_attr_docs().items() } required_keys = [] optional_keys = [] types = get_type_hints(self.object) for d in documenters: name = d[0].name.split('.')[-1] if name in self.object.__required_keys__: required_keys.append(name) elif name in self.object.__optional_keys__: optional_keys.append(name) # else: warn user. This shouldn't ever happen, though. sourcename = self.get_sourcename() if required_keys: self.add_line('', sourcename) self.add_line(":Required Keys:", sourcename) self.document_keys(required_keys, types, docstrings) self.add_line('', sourcename) if optional_keys: self.add_line('', sourcename) self.add_line(":Optional Keys:", sourcename) self.document_keys(optional_keys, types, docstrings) self.add_line('', sourcename) return [] def document_keys( self, keys: List[str], types: Dict[str, Type], docstrings: Dict[str, List[str]], ) -> None: """ Document keys in a :class:`typing.TypedDict`. :param keys: List of key names to document. :param types: Mapping of key names to types. :param docstrings: Mapping of key names to docstrings. """ content = StringList() for key in keys: if key in types: key_type = f"({format_annotation(types[key])}) " else: key_type = '' if key in docstrings: content.append(f" * **{key}** {key_type}-- {' '.join(docstrings.get(key, ''))}") else: content.append(f" * **{key}** {key_type}") for line in content: self.add_line(line, self.get_sourcename()) def filter_members( self, members: ObjectMembers, want_all: bool, ) -> List[Tuple[str, Any, bool]]: """ Filter the given member list. :param members: :param want_all: """ ret = [] # process members and determine which to skip for (membername, member) in members: # if isattr is True, the member is documented as an attribute if safe_getattr(member, "__sphinx_mock__", False): # mocked module or object keep = False elif membername.startswith('_'): keep = False else: keep = True # give the user a chance to decide whether this member # should be skipped if self.env.app: # let extensions preprocess docstrings try: # pylint: disable=R8203 skip_user = self.env.app.emit_firstresult( "autodoc-skip-member", self.objtype, membername, member, not keep, self.options, ) if skip_user is not None: keep = not skip_user except Exception as exc: filter_members_warning(member, exc) keep = False if keep: ret.append((membername, member, member is INSTANCEATTR)) return ret class _PyTypedDictlike(PyClasslike): """ Description of a typeddict-like object. """ def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str: if self.objtype == "typeddict": return _("%s (typeddict in %s)") % (name_cls[0], modname) else: return super().get_index_text(modname, name_cls) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autodoc.autotypeddict`. :param app: The Sphinx application. """ app.registry.domains["py"].object_types["typeddict"] = ObjType("typeddict", "typeddict", "class", "obj") app.add_directive_to_domain("py", "typeddict", _PyTypedDictlike) app.add_role_to_domain("py", "typeddict", PyXRefRole()) app.connect("object-description-transform", add_fallback_css_class({"typeddict": "class"})) allow_subclass_add(app, TypedDictDocumenter) return {"parallel_read_safe": True} PK7V_Foo,sphinx_toolbox/more_autodoc/generic_bases.py#!/usr/bin/env python3 # # generic_bases.py r""" Modifies :class:`sphinx.ext.autodoc.ClassDocumenter`\'s ``:show-inheritence:`` option to show generic base classes. This requires a relatively new version of the :mod:`typing` module that implements ``__orig_bases__``. .. versionadded:: 1.5.0 .. extensions:: sphinx_toolbox.more_autodoc.generic_bases Configuration ---------------- .. confval:: generic_bases_fully_qualified :type: :class:`bool` :required: False :default: :py:obj:`False` Determines whether the fully qualified name should be shown for bases. If :py:obj:`False` (the default): .. class:: Foo :noindex: **Bases**: :class:`~typing.List`\[:class:`str`\] If :py:obj:`True`: .. class:: Foo :noindex: **Bases**: :class:`typing.List`\[:class:`str`\] Corresponds to the ``fully_qualified`` argument to :func:`sphinx_toolbox.more_autodoc.typehints.format_annotation`. .. versionadded:: 2.13.0 Example -------- .. autoclass:: sphinx_toolbox.more_autodoc.generic_bases.Example API Reference ----------------- """ # noqa: D400 # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib import sys from typing import List, Tuple # 3rd party from sphinx.application import Sphinx from sphinx.ext.autodoc import Documenter from sphinx.locale import _ from typing_extensions import final # this package from sphinx_toolbox.more_autodoc.typehints import format_annotation from sphinx_toolbox.more_autosummary import PatchedAutoSummClassDocumenter from sphinx_toolbox.utils import SphinxExtMetadata, allow_subclass_add, metadata_add_version if sys.version_info >= (3, 8): # pragma: no cover ( None: """ Add the directive header. :param sig: """ sourcename = self.get_sourcename() if self.doc_as_attr: self.directivetype = "attribute" Documenter.add_directive_header(self, sig) if self.analyzer and '.'.join(self.objpath) in self.analyzer.finals: self.add_line(" :final:", sourcename) # add inheritance info, if wanted if not self.doc_as_attr and self.options.show_inheritance: _add_generic_bases(self) def _add_generic_bases(documenter: Documenter) -> None: """ Add the generic bases to the output of the given Documenter. .. versionadded:: 2.13.0 (undocumented) :param documenter: """ sourcename = documenter.get_sourcename() # add inheritance info, if wanted fully_qualified = getattr(documenter.env.config, "generic_bases_fully_qualified", False) documenter.add_line('', sourcename) bases = [] # pylint: disable=W8301 if ( hasattr(documenter.object, "__orig_bases__") and len(documenter.object.__orig_bases__) and get_origin(documenter.object.__orig_bases__[0]) is documenter.object.__bases__[0] ): # Last condition guards against classes that don't directly subclass a Generic. bases = [format_annotation(b, fully_qualified) for b in documenter.object.__orig_bases__] elif hasattr(documenter.object, "__bases__") and len(documenter.object.__bases__): bases = [format_annotation(b, fully_qualified) for b in documenter.object.__bases__] if bases: bases_string = ", ".join(bases).replace("typing_extensions.", "typing.") documenter.add_line(" " + _("Bases: %s") % bases_string, sourcename) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autodoc.generic_bases`. .. versionadded:: 1.5.0 :param app: The Sphinx application. """ allow_subclass_add(app, GenericBasesClassDocumenter) app.add_config_value( "generic_bases_fully_qualified", default=False, rebuild="env", types=[bool], ) return {"parallel_read_safe": True} class Example(List[Tuple[str, float, List[str]]]): """ An example of :mod:`sphinx_toolbox.more_autodoc.generic_bases`. """ def __init__(self, iterable=()): # pragma: no cover # noqa: MAN001 pass class Example2(Example): """ An example of :mod:`sphinx_toolbox.more_autodoc.generic_bases`. This one does not directly subclass a Generic. """ @final class FinalExample(List[Tuple[str, float, List[str]]]): """ An example of :mod:`sphinx_toolbox.more_autodoc.generic_bases` decorated with ``@final``. """ def __init__(self, iterable=()): # pragma: no cover # noqa: MAN001 pass PK7VrD9+sphinx_toolbox/more_autodoc/genericalias.py#!/usr/bin/env python3 # # genericalias.py """ Documenter for alias, which usually manifest as `type aliases`_. .. _type aliases: https://docs.python.org/3/library/typing.html#type-aliases .. versionadded:: 0.6.0 .. extensions:: sphinx_toolbox.more_autodoc.genericalias .. note:: :mod:`sphinx_toolbox.more_autodoc.genericalias` is only supported on Python 3.7 and above. """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts based on https://github.com/sphinx-doc/sphinx # | Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file). # | BSD Licensed # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib from typing import Any # 3rd party from docutils.statemachine import StringList from sphinx.application import Sphinx from sphinx.ext.autodoc import SUPPRESS, Options from sphinx.ext.autodoc.directive import DocumenterBridge # noqa: F401 from sphinx.locale import _ from sphinx.util import inspect # this package from sphinx_toolbox._data_documenter import DataDocumenter from sphinx_toolbox.more_autodoc.typehints import format_annotation from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ("PrettyGenericAliasDocumenter", "setup") class PrettyGenericAliasDocumenter(DataDocumenter): """ Specialized Documenter subclass for GenericAliases, with prettier output than Sphinx's one. """ objtype = "genericalias" directivetype = "data" priority = DataDocumenter.priority + 2 @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any) -> bool: """ Called to see if a member can be documented by this documenter. """ return inspect.isgenericalias(member) def add_directive_header(self, sig: str) -> None: """ Add the directive header and options to the generated content. """ self.options = Options(self.options) self.options["annotation"] = SUPPRESS super().add_directive_header(sig) def add_content(self, more_content: Any, no_docstring: bool = False) -> None: """ Add the autodocumenter content. :param more_content: :param no_docstring: """ name = format_annotation(self.object) content = StringList([_("Alias of %s") % name], source='') DataDocumenter.add_content(self, content) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autodoc.genericalias`. :param app: The Sphinx application. """ app.setup_extension("sphinx.ext.autodoc") app.add_autodocumenter(PrettyGenericAliasDocumenter, override=True) return {"parallel_read_safe": True} PK7V-w  +sphinx_toolbox/more_autodoc/no_docstring.py#!/usr/bin/env python3 # # no_docstring.py """ Adds the ``:no-docstring:`` option to automodule directives to exclude the docstring from the output. .. versionadded:: 1.0.0 Functions in this module moved from ``sphinx_toolbox.more_autodoc.__init__.py`` .. extensions:: sphinx_toolbox.more_autodoc.no_docstring """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib from types import ModuleType from typing import List # 3rd party import autodocsumm # type: ignore[import] import sphinx.ext.autodoc from sphinx.application import Sphinx # this package from sphinx_toolbox.more_autosummary import PatchedAutoSummModuleDocumenter from sphinx_toolbox.utils import SphinxExtMetadata, flag, metadata_add_version __all__ = ("automodule_add_nodocstring", "no_docstring_process_docstring", "setup") def automodule_add_nodocstring(app: Sphinx) -> None: """ Add the ``:no-docstring:`` option to automodule directives. The option is used to exclude the docstring from the output :param app: The Sphinx application. """ sphinx.ext.autodoc.ModuleDocumenter.option_spec["no-docstring"] = flag autodocsumm.AutoSummModuleDocumenter.option_spec["no-docstring"] = flag PatchedAutoSummModuleDocumenter.option_spec["no-docstring"] = flag app.setup_extension("sphinx.ext.autodoc") app.connect("autodoc-process-docstring", no_docstring_process_docstring, priority=1000) def no_docstring_process_docstring( # noqa: MAN001 app: Sphinx, what, name: str, obj, options, lines: List[str], ) -> None: """ Remove module docstrings if the ``:no-docstring:`` flag was set. :param app: The Sphinx application. :param what: :param name: The name of the object being documented. :param obj: The object being documented. :param options: Mapping of autodoc options to values. :param lines: List of strings representing the current contents of the docstring. """ if isinstance(obj, ModuleType): no_docstring = options.get("no-docstring", False) if no_docstring: lines.clear() @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autodoc.no_docstring`. :param app: The Sphinx application. """ automodule_add_nodocstring(app) return {"parallel_read_safe": True} PK7VΗD9D9(sphinx_toolbox/more_autodoc/overloads.py#!/usr/bin/env python3 # # overloads.py r""" Documenters for functions and methods which display overloads differently. .. versionadded:: 1.4.0 .. extensions:: sphinx_toolbox.more_autodoc.overloads Configuration ---------------- .. latex:vspace:: -20px .. confval:: overloads_location :type: :class:`str` :default: ``'signature'`` The location to display overloads at: * ``'signature'`` -- Display overloads above the function signature. * ``'top'`` -- Display overloads at the top of the docstring, immediately below the signature. * ``'bottom'`` -- Display overloads at the bottom of the docstring, or immediately below the return type. API Reference ---------------- .. latex:vspace:: -20px .. automodulesumm:: sphinx_toolbox.more_autodoc.overloads :autosummary-sections: Classes .. latex:vspace:: -15px .. automodulesumm:: sphinx_toolbox.more_autodoc.overloads :autosummary-sections: Functions """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts based on https://github.com/sphinx-doc/sphinx # | Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file). # | BSD Licensed # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib import re from contextlib import suppress from inspect import Parameter, Signature from types import ModuleType from typing import TYPE_CHECKING, Any, Dict, ForwardRef, List # 3rd party from domdf_python_tools.stringlist import StringList from sphinx.application import Sphinx from sphinx.config import ENUM from sphinx.ext import autodoc from sphinx.ext.autodoc.directive import DocumenterBridge from sphinx.util import inspect from sphinx.util.inspect import evaluate_signature, safe_getattr, stringify_signature # this package from sphinx_toolbox.more_autodoc import _documenter_add_content from sphinx_toolbox.more_autodoc.typehints import _resolve_forwardref, default_preprocessors, format_annotation from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version if TYPE_CHECKING: _OverloadMixinBase = autodoc.ModuleLevelDocumenter else: _OverloadMixinBase = object _ = DocumenterBridge __all__ = ( "OverloadMixin", "FunctionDocumenter", "MethodDocumenter", "setup", ) _return_type_re = re.compile("^:(rtype|return(s)?):") class OverloadMixin(_OverloadMixinBase): """ Mixin class for function and class documenters that changes the appearance of overloaded functions. """ def create_body_overloads(self) -> StringList: """ Create the overloaded implementations for insertion into to the body of the documenter's output. """ output = StringList() formatted_overloads = [] output.blankline() # output.append(":Overloaded Implementations:") output.append(":Overloads:") output.blankline() # Size varies depending on docutils config output.indent_type = ' ' output.indent_size = self.env.app.config.docutils_tab_width if self.analyzer and '.'.join(self.objpath) in self.analyzer.overloads: the_overloads = self.analyzer.overloads.get('.'.join(self.objpath)) assert the_overloads is not None for overload in the_overloads: overload = self.process_overload_signature(overload) buf = [format_annotation(self.object), r"\("] for name, param in overload.parameters.items(): buf.append(f"**{name}**") if param.annotation is not Parameter.empty: buf.append(r"\: ") buf.append(format_annotation(param.annotation)) if param.default is not Parameter.empty: default = param.default # pylint: disable=loop-global-usage if hasattr(inspect, "DefaultValue") and isinstance(default, inspect.DefaultValue): default = default.value # pylint: enable=loop-global-usage buf.append(" = ") buf.append(default) buf.append(r"\, ") if buf[-2][-1] != '`': buf[-1] = r" )" else: buf[-1] = r")" if overload.return_annotation is not Parameter.empty: return_annotation = overload.return_annotation if isinstance(return_annotation, ForwardRef): with suppress(Exception): if isinstance(self.parent, ModuleType): module_name = self.parent.__name__ else: module_name = self.parent.__module__ return_annotation = _resolve_forwardref(return_annotation, module_name) buf.append(" -> ") buf.append(format_annotation(return_annotation)) formatted_overloads.append(''.join(buf)) if len(formatted_overloads) == 1: output.append(formatted_overloads[0]) else: for line in formatted_overloads: output.append(f"* {line}") output.blankline(ensure_single=True) return output return StringList() def process_overload_signature(self, overload: Signature) -> Signature: """ Processes the signature of the given overloaded implementation. :param overload: """ parameters = [] non_overload_sig = inspect.signature(self.object) for param, non_overload_param in zip(overload.parameters.values(), non_overload_sig.parameters.values()): default = param.default if default is not Parameter.empty: for check, preprocessor in default_preprocessors: if check(default): default = preprocessor(default) break if param.annotation is non_overload_param.annotation: annotation = Parameter.empty else: annotation = param.annotation parameters.append(param.replace(default=default, annotation=annotation)) if non_overload_sig.return_annotation is overload.return_annotation: overload = overload.replace(parameters=parameters, return_annotation=Parameter.empty) else: overload = overload.replace(parameters=parameters) return overload def add_content(self, more_content: Any, no_docstring: bool = False) -> None: """ Add content from docstrings, attribute documentation and the user. :param more_content: :param no_docstring: """ if self.env.config.overloads_location == "bottom": def process_docstring( app: Sphinx, what: str, name: str, obj: Any, options: Dict[str, Any], lines: List[str], ) -> None: if callable(obj): seen_return = False n_lines = len(lines) for i, line in enumerate(lines): if not line: continue if line[0].isspace(): continue if _return_type_re.match(line): # pylint: disable=loop-global-usage seen_return = True continue if seen_return and i != n_lines: lines.insert(i, '') for inner_line in reversed(self.create_body_overloads()): # pylint: disable=W8402 lines.insert(i, inner_line) lines.insert(i, '') break else: lines.append('') lines.extend(self.create_body_overloads()) lines.append('') listener_id = self.env.app.connect("autodoc-process-docstring", process_docstring, priority=600) try: _documenter_add_content(self, more_content, no_docstring) finally: self.env.app.disconnect(listener_id) else: _documenter_add_content(self, more_content, no_docstring) class FunctionDocumenter(OverloadMixin, autodoc.FunctionDocumenter): """ Custom :class:`autodoc.FunctionDocumenter ` which renders overloads differently. """ # noqa: D400 def format_signature(self, **kwargs: Any) -> str: """ Format the function's signature, including those for any overloaded implementations. :return: The signature(s), as a multi-line string. """ sigs = [] if self.analyzer and '.'.join(self.objpath) in self.analyzer.overloads: overloaded = True else: overloaded = False sig = super(autodoc.FunctionDocumenter, self).format_signature(**kwargs) sigs.append(sig) if inspect.is_singledispatch_function(self.object): # append signature of singledispatch'ed functions for typ, func in self.object.registry.items(): if typ is object: pass # default implementation. skipped. else: self.annotate_to_first_argument(func, typ) documenter = FunctionDocumenter(self.directive, '') documenter.object = func documenter.objpath = [None] # type: ignore[list-item] sigs.append(documenter.format_signature()) if overloaded: if self.env.config.overloads_location == "signature": the_overloads = self.analyzer.overloads.get('.'.join(self.objpath)) assert the_overloads is not None for overload in the_overloads: sig = stringify_signature(self.process_overload_signature(overload), **kwargs) sigs.append(sig) sig = super(autodoc.FunctionDocumenter, self).format_signature(**kwargs) sigs.append(sig) return '\n'.join(sigs) def add_directive_header(self, sig: str) -> None: """ Add the directive's header. :param sig: """ super().add_directive_header(sig) if self.env.config.overloads_location == "top": for line in self.create_body_overloads(): self.add_line(f"{self.content_indent}{line}", self.get_sourcename()) def process_overload_signature(self, overload: Signature) -> Signature: """ Processes the signature of the given overloaded implementation. :param overload: """ __globals__ = safe_getattr(self.object, "__globals__", {}) overload = evaluate_signature(overload, __globals__) return super().process_overload_signature(overload) class MethodDocumenter(OverloadMixin, autodoc.MethodDocumenter): """ Custom :class:`autodoc.MethodDocumenter ` which renders overloads differently. """ # noqa: D400 def format_signature(self, **kwargs: Any) -> str: """ Format the method's signature, including those for any overloaded implementations. :param kwargs: :return: The signature(s), as a multi-line string. """ sigs = [] if self.analyzer and '.'.join(self.objpath) in self.analyzer.overloads: overloaded = True else: overloaded = False sig = super(autodoc.MethodDocumenter, self).format_signature(**kwargs) sigs.append(sig) meth = self.parent.__dict__.get(self.objpath[-1]) if inspect.is_singledispatch_method(meth): # append signature of singledispatch'ed functions for typ, func in meth.dispatcher.registry.items(): if typ is object: pass # default implementation. skipped. else: self.annotate_to_first_argument(func, typ) documenter = MethodDocumenter(self.directive, '') documenter.parent = self.parent documenter.object = func documenter.objpath = [None] # type: ignore[list-item] sigs.append(documenter.format_signature()) if overloaded: if self.env.config.overloads_location == "signature": the_overloads = self.analyzer.overloads.get('.'.join(self.objpath)) assert the_overloads is not None for overload in the_overloads: sig = stringify_signature(self.process_overload_signature(overload), **kwargs) sigs.append(sig) sig = super(autodoc.MethodDocumenter, self).format_signature(**kwargs) sigs.append(sig) return '\n'.join(sigs) def add_directive_header(self, sig: str) -> None: """ Add the directive's header. :param sig: """ super().add_directive_header(sig) if self.env.config.overloads_location == "top": for line in self.create_body_overloads(): self.add_line(f"{self.content_indent}{line}", self.get_sourcename()) def process_overload_signature(self, overload: Signature) -> Signature: """ Processes the signature of the given overloaded implementation. :param overload: """ __globals__ = safe_getattr(self.object, "__globals__", {}) overload = evaluate_signature(overload, __globals__) if not inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): overload = overload.replace(parameters=list(overload.parameters.values())[1:]) return super().process_overload_signature(overload) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autodoc.overloads`. :param app: The Sphinx application. """ app.add_autodocumenter(FunctionDocumenter, override=True) app.add_autodocumenter(MethodDocumenter, override=True) app.add_config_value( "overloads_location", "signature", "env", ENUM("top", "bottom", "signature"), # top (of body), bottom (of body) ) return {"parallel_read_safe": True} PK7V&ii$sphinx_toolbox/more_autodoc/regex.py#!/usr/bin/env python3 # # regex.py r""" Specialized Documenter for regular expression variables, similar to :rst:dir:`autodata`. .. versionadded:: 1.2.0 .. extensions:: sphinx_toolbox.more_autodoc.regex Usage ------- .. rst:directive:: autoregex Directive to automatically document a regular expression variable. The output is based on the :rst:dir:`autodata` directive, and takes all of its options except ``:annotation:``. .. rst:directive:option:: no-value Don't show the value of the variable. .. rst:directive:option:: value: value :type: string Show this instead of the value taken from the Python source code. .. rst:directive:option:: no-type Don't show the type of the variable. .. rst:directive:option:: no-flags Don't show the flags of the :class:`~typing.Pattern` object. .. rst:directive:option:: flags: flags :type: string Show this instead of the flags taken from the :class:`~typing.Pattern` object. This should be correctly formatted for insertion into reStructuredText, such as ``:py:data:`re.ASCII```. .. versionchanged:: 2.7.0 The flags :py:data:`re.DEBUG` and :py:data:`re.VERBOSE` are now hidden as they don't affect the regex itself. .. latex:clearpage:: .. rst:role:: regex Formats a regular expression with coloured output. .. rest-example:: :regex:`^Hello\s+[Ww]orld[.,](Lovely|Horrible) weather, isn't it (.*)?` .. versionchanged:: 2.11.0 Now generates coloured output with the LaTeX builder. """ # # Copyright © 2020-2022 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts based on https://github.com/sphinx-doc/sphinx # | Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file). # | BSD Licensed # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib import itertools import re import sre_parse from sre_constants import ( ANY, AT, AT_BEGINNING, AT_BEGINNING_STRING, AT_BOUNDARY, AT_END, AT_END_STRING, AT_NON_BOUNDARY, BRANCH, CATEGORY, CATEGORY_DIGIT, CATEGORY_NOT_DIGIT, CATEGORY_NOT_SPACE, CATEGORY_NOT_WORD, CATEGORY_SPACE, CATEGORY_WORD, IN, LITERAL, MAX_REPEAT, MAXREPEAT, MIN_REPEAT, RANGE, SUBPATTERN ) from textwrap import dedent from typing import Any, Callable, List, Optional, Pattern, Tuple # 3rd party import dict2css from docutils import nodes from docutils.nodes import Node, system_message from domdf_python_tools.paths import PathPlus from domdf_python_tools.stringlist import StringList from sphinx.application import Sphinx from sphinx.ext.autodoc import UNINITIALIZED_ATTR, ModuleDocumenter from sphinx.util import texescape from sphinx.util.docutils import SphinxRole from sphinx.writers.html import HTMLTranslator # this package from sphinx_toolbox import _css from sphinx_toolbox.more_autodoc.variables import VariableDocumenter from sphinx_toolbox.utils import Config, SphinxExtMetadata, add_nbsp_substitution, flag, metadata_add_version __all__ = ( "RegexDocumenter", "RegexParser", "TerminalRegexParser", "HTMLRegexParser", "LaTeXRegexParser", "parse_regex_flags", "no_formatting", "span", "latex_textcolor", "copy_asset_files", "setup", ) class RegexDocumenter(VariableDocumenter): """ Specialized Documenter subclass for regex patterns. """ directivetype = "data" objtype = "regex" priority = VariableDocumenter.priority + 1 option_spec = { **VariableDocumenter.option_spec, "no-flag": flag, "flag": str, } del option_spec["type"] del option_spec["annotation"] @classmethod def can_document_member( cls, member: Any, membername: str, isattr: bool, parent: Any, ) -> bool: """ Called to see if a member can be documented by this documenter. :param member: The member being checked. :param membername: The name of the member. :param isattr: :param parent: The parent of the member. """ return isinstance(parent, ModuleDocumenter) and isattr and isinstance(member, Pattern) def add_content(self, more_content: Any, no_docstring: bool = False) -> None: """ Add content from docstrings, attribute documentation and the user. :param more_content: :param no_docstring: """ # set sourcename and add content from attribute documentation sourcename = self.get_sourcename() if self.analyzer: attr_docs = self.analyzer.find_attr_docs() if self.objpath: key = ('.'.join(self.objpath[:-1]), self.objpath[-1]) if key in attr_docs: no_docstring = True # make a copy of docstring for attributes to avoid cache # the change of autodoc-process-docstring event. docstrings = [list(attr_docs[key])] for i, line in enumerate(self.process_doc(docstrings)): self.add_line(line, sourcename, i) # add content from docstrings if not no_docstring: docstrings = self.get_doc() or [] if not docstrings: # append at least a dummy docstring, so that the event # autodoc-process-docstring is fired and can add some # content if desired docstrings.append([]) if docstrings == [["Compiled regular expression objects", '']] or docstrings == [[]]: docstrings = [["Compiled regular expression object.", '']] # pylint: disable=W8301 for i, line in enumerate(self.process_doc(docstrings)): self.add_line(line, sourcename, i) # add additional content (e.g. from document), if present if more_content: for line, src in zip(more_content.data, more_content.items): self.add_line(line, src[0], src[1]) no_value = self.options.get("no-value", False) no_flag = self.options.get("no-flag", False) if self.object is not UNINITIALIZED_ATTR and (not no_value or not no_flag): self.add_line('', sourcename) self.add_line('', sourcename) the_flag: Optional[str] = None if not no_flag: if "flag" in self.options: the_flag = self.options["flag"] else: raw_flags = self.object.flags raw_flags = (raw_flags & ~re.DEBUG) & ~re.VERBOSE the_flag = parse_regex_flags(raw_flags) if no_value and not the_flag: return self.add_line(".. csv-table::", sourcename) self.add_line(" :widths: auto", sourcename) self.add_line(" :stub-columns: 1", sourcename) self.add_line('', sourcename) if not no_value: if "value" in self.options: the_pattern = self.options["value"] else: the_pattern = self.object.pattern the_pattern = the_pattern.replace('`', r"\`") leading_spaces = len(tuple(itertools.takewhile(str.isspace, the_pattern))) trailing_spaces = len(tuple(itertools.takewhile(str.isspace, the_pattern[::-1]))) the_pattern = the_pattern.strip(' ') if leading_spaces > 1: the_pattern = f"[ ]{leading_spaces}{the_pattern}" elif leading_spaces == 1: the_pattern = f"[ ]{the_pattern}" if trailing_spaces > 1: the_pattern += f" {trailing_spaces}" elif trailing_spaces == 1: the_pattern += "[ ]" self.add_line(f' **Pattern**, ":regex:`{the_pattern}`"', sourcename) if the_flag: self.add_line(f" **Flags**, {the_flag}", sourcename) self.add_line('', sourcename) def add_directive_header(self, sig: str) -> None: """ Add the directive's header. :param sig: """ user_no_value = self.options.get("no-value", False) self.options["no-value"] = True super().add_directive_header(sig) self.options["no-value"] = user_no_value def parse_regex_flags(flags: int) -> str: """ Convert regex flags into "bitwise-or'd" Sphinx xrefs. :param flags: """ buf = [] if flags & re.ASCII: buf.append("ASCII") if flags & re.DEBUG: buf.append("DEBUG") if flags & re.IGNORECASE: buf.append("IGNORECASE") if flags & re.LOCALE: buf.append("LOCALE") if flags & re.MULTILINE: buf.append("MULTILINE") if flags & re.DOTALL: buf.append("DOTALL") if flags & re.VERBOSE: buf.append("VERBOSE") return " ``|`` ".join(f":py:data:`re.{x}`" for x in buf) def no_formatting(value: Any) -> str: """ No-op that returns the value as a string. Used for unformatted output. """ return str(value) class RegexParser: r""" Parser for regular expressions that outputs coloured output. The formatting is controlled by the following callable attributes: * ``AT_COLOUR`` -- Used for e.g. :regex:`^\A\b\B\Z$` * ``SUBPATTERN_COLOUR`` -- Used for the parentheses around subpatterns, e.g. :regex:`(Hello) World` * ``IN_COLOUR`` -- Used for the square brackets around character sets, e.g. :regex:`[Hh]ello` * ``REPEAT_COLOUR`` -- Used for repeats, e.g. :regex:`A?B+C*D{2,4}E{5}` * ``REPEAT_BRACE_COLOUR`` -- Used for the braces around numerical repeats. * ``CATEGORY_COLOUR`` -- Used for categories, e.g. :regex:`\d\D\s\D\w\W` * ``BRANCH_COLOUR`` -- Used for branches, e.g. :regex:`(Lovely|Horrible) Weather` * ``LITERAL_COLOUR`` -- Used for literal characters. * ``ANY_COLOUR`` -- Used for the "any" dot. These are all :class:`~typing.Callable`\[[:class:`~typing.Any`], :class:`str`\]. By default no formatting is performed. Subclasses should set these attributes to appropriate functions. """ # Colours AT_COLOUR: Callable[[Any], str] = no_formatting SUBPATTERN_COLOUR: Callable[[Any], str] = no_formatting IN_COLOUR: Callable[[Any], str] = no_formatting REPEAT_COLOUR: Callable[[Any], str] = no_formatting REPEAT_BRACE_COLOUR: Callable[[Any], str] = no_formatting CATEGORY_COLOUR: Callable[[Any], str] = no_formatting BRANCH_COLOUR: Callable[[Any], str] = no_formatting LITERAL_COLOUR: Callable[[Any], str] = no_formatting ANY_COLOUR: Callable[[Any], str] = no_formatting def parse_pattern(self, regex: Pattern) -> str: """ Parse the given regular expression and return the formatted pattern. :param regex: """ buf = [] def _parse_pattern(pattern) -> None: # noqa: MAN001 for what, content in pattern: # print(what, content) if what is AT: if content is AT_BEGINNING: buf.append(type(self).AT_COLOUR('^')) continue elif content is AT_END: buf.append(type(self).AT_COLOUR('$')) continue elif content is AT_BEGINNING_STRING: buf.append(type(self).AT_COLOUR(r"\A")) continue elif content is AT_BOUNDARY: buf.append(type(self).AT_COLOUR(r"\b")) continue elif content is AT_NON_BOUNDARY: buf.append(type(self).AT_COLOUR(r"\B")) continue elif content is AT_END_STRING: buf.append(type(self).AT_COLOUR(r"\Z")) continue if what is SUBPATTERN: buf.append(type(self).SUBPATTERN_COLOUR('(')) group, add_flags, del_flags, subpattern = content # print(group, add_flags, del_flags) _parse_pattern(subpattern) buf.append(type(self).SUBPATTERN_COLOUR(')')) continue if what is LITERAL: # TODO: escape characters that have meaning to avoid ambiguity buf.append(type(self).LITERAL_COLOUR(chr(content))) continue if what is IN: if len(content) > 1 or content[0][0] is RANGE: buf.append(type(self).IN_COLOUR('[')) _parse_pattern(content) if len(content) > 1 or content[0][0] is RANGE: buf.append(type(self).IN_COLOUR(']')) continue if what is MAX_REPEAT or what is MIN_REPEAT: min_, max_, item = content _parse_pattern(item) if min_ == 0 and max_ is MAXREPEAT: buf.append(type(self).REPEAT_COLOUR('*')) elif min_ == 1 and max_ is MAXREPEAT: buf.append(type(self).REPEAT_COLOUR('+')) elif min_ == 0 and max_ == 1: buf.append(type(self).REPEAT_COLOUR('?')) elif min_ == max_: buf.append(type(self).REPEAT_BRACE_COLOUR('{')) buf.append(type(self).REPEAT_COLOUR(str(min_))) buf.append(type(self).REPEAT_BRACE_COLOUR('}')) else: buf.append(type(self).REPEAT_BRACE_COLOUR('{')) buf.append(type(self).REPEAT_COLOUR(str(min_))) buf.append(type(self).LITERAL_COLOUR(',')) buf.append(type(self).REPEAT_COLOUR(str(max_))) buf.append(type(self).REPEAT_BRACE_COLOUR('}')) if what is MIN_REPEAT: buf.append(type(self).REPEAT_COLOUR('?')) continue # # if what is MIN_REPEAT: # min_, max_, item = content # _parse_pattern(item) # print(min_, max_, item) # input(">>>") if what is CATEGORY: if content is CATEGORY_DIGIT: buf.append(type(self).CATEGORY_COLOUR(r"\d")) continue elif content is CATEGORY_NOT_DIGIT: buf.append(type(self).CATEGORY_COLOUR(r"\D")) continue elif content is CATEGORY_SPACE: buf.append(type(self).CATEGORY_COLOUR(r"\s")) continue elif content is CATEGORY_NOT_SPACE: buf.append(type(self).CATEGORY_COLOUR(r"\S")) continue elif content is CATEGORY_WORD: buf.append(type(self).CATEGORY_COLOUR(r"\w")) continue elif content is CATEGORY_NOT_WORD: buf.append(type(self).CATEGORY_COLOUR(r"\W")) continue if what is BRANCH: for branch in content[1]: _parse_pattern(branch) buf.append(type(self).BRANCH_COLOUR('|')) buf.pop(-1) continue if what is RANGE: buf.append(type(self).LITERAL_COLOUR(chr(content[0]))) buf.append(type(self).AT_COLOUR('-')) buf.append(type(self).LITERAL_COLOUR(chr(content[1]))) continue if what is ANY: buf.append(type(self).ANY_COLOUR('.')) continue print(what, content) # pragma: no cover pattern = regex.pattern.replace('\t', r"\t") # Remove leading and trailing spaces from the pattern. They will be added back at the end. leading_spaces = len(tuple(itertools.takewhile(str.isspace, pattern))) trailing_spaces = len(tuple(itertools.takewhile(str.isspace, pattern[::-1]))) pattern = pattern.strip(' ') tokens: List = list(sre_parse.parse(pattern, regex.flags)) # type: ignore[call-overload] if not leading_spaces: while tokens[0] == (LITERAL, ord(' ')): leading_spaces += 1 tokens.pop(0) if not trailing_spaces: while tokens[-1] == (LITERAL, ord(' ')): trailing_spaces += 1 tokens.pop(-1) if leading_spaces: buf.append(type(self).IN_COLOUR('[')) buf.append(type(self).LITERAL_COLOUR(' ')) buf.append(type(self).IN_COLOUR(']')) if leading_spaces > 1: buf.append(type(self).REPEAT_BRACE_COLOUR('{')) buf.append(type(self).REPEAT_COLOUR(str(leading_spaces))) buf.append(type(self).REPEAT_BRACE_COLOUR('}')) _parse_pattern(tokens) if trailing_spaces == 1: buf.append(type(self).IN_COLOUR('[')) buf.append(type(self).LITERAL_COLOUR(' ')) buf.append(type(self).IN_COLOUR(']')) elif trailing_spaces > 1: buf.append(type(self).LITERAL_COLOUR(' ')) buf.append(type(self).REPEAT_BRACE_COLOUR('{')) buf.append(type(self).REPEAT_COLOUR(str(trailing_spaces))) buf.append(type(self).REPEAT_BRACE_COLOUR('}')) return ''.join(buf) def span(css_class: str) -> Callable[[Any], str]: """ Returns a function that wraps a value in a ``span`` tag with the given class. :param css_class: """ def f(value: Any) -> str: return f'{value}' return f def latex_textcolor(colour_name: str) -> Callable[[Any], str]: """ Returns a function that wraps a value in a LaTeX ``textcolor`` command for the given colour. .. versionadded:: 2.11.0 :param colour_name: """ def f(value: Any) -> str: if value == ' ': return "\\enspace" return f'\\textcolor{{{colour_name}}}{{{texescape.escape(value)}}}' return f class HTMLRegexParser(RegexParser): r""" :class:`~.RegexParser` that outputs styled HTML. The formatting is controlled by the following functions, which wrap the character in a ``span`` tag with an appropriate CSS class: * ``AT_COLOUR`` -> ``regex_at`` -- Used for e.g. :regex:`^\A\b\B\Z$` * ``SUBPATTERN_COLOUR`` -> ``regex_subpattern`` -- Used for the parentheses around subpatterns, e.g. :regex:`(Hello) World` * ``IN_COLOUR`` -> ``regex_in`` -- Used for the square brackets around character sets, e.g. :regex:`[Hh]ello` * ``REPEAT_COLOUR`` -> ``regex_repeat`` -- Used for repeats, e.g. :regex:`A?B+C*D{2,4}E{5}` * ``REPEAT_BRACE_COLOUR`` -> ``regex_repeat_brace`` -- Used for the braces around numerical repeats. * ``CATEGORY_COLOUR`` -> ``regex_category`` -- Used for categories, e.g. :regex:`\d\D\s\D\w\W` * ``BRANCH_COLOUR`` -> ``regex_branch`` -- Used for branches, e.g. :regex:`(Lovely|Horrible) Weather` * ``LITERAL_COLOUR`` -> ``regex_literal`` -- Used for literal characters. * ``ANY_COLOUR`` -> ``regex_any`` -- Used for the "any" dot. Additionally, all ``span`` tags the ``regex`` class, and the surrounding ``code`` tag has the following classes: ``docutils literal notranslate regex``. """ # Colours AT_COLOUR = span("regex regex_at") SUBPATTERN_COLOUR = span("regex regex_subpattern") IN_COLOUR = span("regex regex_in") REPEAT_COLOUR = span("regex regex_repeat") REPEAT_BRACE_COLOUR = span("regex regex_repeat_brace") CATEGORY_COLOUR = span("regex regex_category") BRANCH_COLOUR = span("regex regex_branch") LITERAL_COLOUR = span("regex regex_literal") ANY_COLOUR = span("regex regex_any") def parse_pattern(self, regex: Pattern) -> str: """ Parse the given regular expression and return the formatted pattern. :param regex: """ return dedent( f""" {super().parse_pattern(regex)} """ ) class LaTeXRegexParser(RegexParser): r""" :class:`~.RegexParser` that outputs styled LaTeX. The formatting is controlled by the following functions, which wrap the character in a LaTeX ``textcolor`` command for an appropriate colour: * ``AT_COLOUR`` -> ``regex_at`` -- Used for e.g. :regex:`^\A\b\B\Z$` * ``SUBPATTERN_COLOUR`` -> ``regex_subpattern`` -- Used for the parentheses around subpatterns, e.g. :regex:`(Hello) World` * ``IN_COLOUR`` -> ``regex_in`` -- Used for the square brackets around character sets, e.g. :regex:`[Hh]ello` * ``REPEAT_COLOUR`` -> ``regex_repeat`` -- Used for repeats, e.g. :regex:`A?B+C*D{2,4}E{5}` * ``REPEAT_BRACE_COLOUR`` -> ``regex_repeat_brace`` -- Used for the braces around numerical repeats. * ``CATEGORY_COLOUR`` -> ``regex_category`` -- Used for categories, e.g. :regex:`\d\D\s\D\w\W` * ``BRANCH_COLOUR`` -> ``regex_branch`` -- Used for branches, e.g. :regex:`(Lovely|Horrible) Weather` * ``LITERAL_COLOUR`` -> ``regex_literal`` -- Used for literal characters. * ``ANY_COLOUR`` -> ``regex_any`` -- Used for the "any" dot. .. versionadded:: 2.11.0 """ # Colours AT_COLOUR = latex_textcolor("regex_at") SUBPATTERN_COLOUR = latex_textcolor("regex_subpattern") IN_COLOUR = latex_textcolor("regex_in") REPEAT_COLOUR = latex_textcolor("regex_repeat") REPEAT_BRACE_COLOUR = latex_textcolor("regex_repeat_brace") CATEGORY_COLOUR = latex_textcolor("regex_category") BRANCH_COLOUR = latex_textcolor("regex_branch") LITERAL_COLOUR = latex_textcolor("regex_literal") ANY_COLOUR = latex_textcolor("regex_any") def parse_pattern(self, regex: Pattern) -> str: """ Parse the given regular expression and return the formatted pattern. :param regex: """ return f"\\sphinxcode{{\\sphinxupquote{{{super().parse_pattern(regex)}}}}}" class TerminalRegexParser(RegexParser): r""" :class:`~.RegexParser` that outputs ANSI coloured output for the terminal. The formatting is controlled by the following callable attributes, which set ANSI escape codes for the appropriate colour: * ``AT_COLOUR`` -> YELLOW, Used for e.g. :regex:`^\A\b\B\Z$` * ``SUBPATTERN_COLOUR`` -> LIGHTYELLOW_EX, Used for the parentheses around subpatterns, e.g. :regex:`(Hello) World` * ``IN_COLOUR`` -> LIGHTRED_EX, Used for the square brackets around character sets, e.g. :regex:`[Hh]ello` * ``REPEAT_COLOUR`` -> LIGHTBLUE_EX, Used for repeats, e.g. :regex:`A?B+C*D{2,4}E{5}` * ``REPEAT_BRACE_COLOUR`` -> YELLOW, Used for the braces around numerical repeats. * ``CATEGORY_COLOUR`` -> LIGHTYELLOW_EX, Used for categories, e.g. :regex:`\d\D\s\D\w\W` * ``BRANCH_COLOUR`` -> YELLOW, Used for branches, e.g. :regex:`(Lovely|Horrible) Weather` * ``LITERAL_COLOUR`` -> GREEN, Used for literal characters. * ``ANY_COLOUR`` -> YELLOW, Used for the "any" dot. """ # Colours @staticmethod def AT_COLOUR(s: str) -> str: # noqa: D102 return f"\x1b[33m{s}\x1b[39m" @staticmethod def SUBPATTERN_COLOUR(s: str) -> str: # noqa: D102 return f"\x1b[93m{s}\x1b[39m" @staticmethod def IN_COLOUR(s: str) -> str: # noqa: D102 return f"\x1b[91m{s}\x1b[39m" @staticmethod def REPEAT_COLOUR(s: str) -> str: # noqa: D102 return f"\x1b[94m{s}\x1b[39m" @staticmethod def LITERAL_COLOUR(s: str) -> str: # noqa: D102 return f"\x1b[32m{s}\x1b[39m" REPEAT_BRACE_COLOUR = BRANCH_COLOUR = ANY_COLOUR = AT_COLOUR CATEGORY_COLOUR = SUBPATTERN_COLOUR class RegexNode(nodes.literal): """ Docutils Node to show a highlighted regular expression. """ def __init__(self, rawsource: str = '', text: str = '', *children, **attributes) -> None: super().__init__(rawsource, text, *children, **attributes) self.pattern = re.compile(':'.join(rawsource.split(':')[2:])[1:-1]) class Regex(SphinxRole): """ Docutils role to show a highlighted regular expression. """ def run(self) -> Tuple[List[Node], List[system_message]]: """ Process the content of the regex role. """ options = self.options.copy() return [RegexNode(self.rawtext, self.text, **options)], [] def visit_regex_node(translator: HTMLTranslator, node: RegexNode) -> None: """ Visit an :class:`~.RegexNode`. :param translator: :param node: The node being visited. """ translator.body.append(regex_parser.parse_pattern(node.pattern)) def depart_regex_node(translator: HTMLTranslator, node: RegexNode) -> None: """ Depart an :class:`~.RegexNode`. :param translator: :param node: The node being visited. """ translator.body.pop(-1) def visit_regex_node_latex(translator: HTMLTranslator, node: RegexNode) -> None: """ Visit an :class:`~.RegexNode` with the LaTeX builder. .. versionadded:: 2.11.0 :param translator: :param node: The node being visited. """ translator.body.append(latex_regex_parser.parse_pattern(node.pattern)) def depart_regex_node_latex(translator: HTMLTranslator, node: RegexNode) -> None: """ Depart an :class:`~.RegexNode` with the LaTeX builder. .. versionadded:: 2.11.0 :param translator: :param node: The node being visited. """ translator.body.pop(-1) def copy_asset_files(app: Sphinx, exception: Optional[Exception] = None) -> None: """ Copy additional stylesheets into the HTML build directory. :param app: The Sphinx application. :param exception: Any exception which occurred and caused Sphinx to abort. """ if exception: # pragma: no cover return if app.builder is None or app.builder.format.lower() != "html": # pragma: no cover return static_dir = PathPlus(app.outdir) / "_static" static_dir.maybe_make(parents=True) dict2css.dump(_css.regex_styles, static_dir / "regex.css", minify=True) regex_parser = HTMLRegexParser() latex_regex_parser = LaTeXRegexParser() def configure(app: Sphinx, config: Config) -> None: """ Configure :mod:`sphinx_toolbox.code`. .. versionadded:: 2.11.0 :param app: The Sphinx application. :param config: """ latex_elements = getattr(app.config, "latex_elements", {}) latex_preamble = StringList(latex_elements.get("preamble", '')) latex_preamble.blankline() latex_preamble.append(r"\definecolor{regex_literal}{HTML}{696969}") latex_preamble.append(r"\definecolor{regex_at}{HTML}{FF4500}") latex_preamble.append(r"\definecolor{regex_repeat_brace}{HTML}{FF4500}") latex_preamble.append(r"\definecolor{regex_branch}{HTML}{FF4500}") latex_preamble.append(r"\definecolor{regex_subpattern}{HTML}{1e90ff}") latex_preamble.append(r"\definecolor{regex_in}{HTML}{ff8c00}") latex_preamble.append(r"\definecolor{regex_category}{HTML}{8fbc8f}") latex_preamble.append(r"\definecolor{regex_repeat}{HTML}{FF4500}") latex_preamble.append(r"\definecolor{regex_any}{HTML}{FF4500}") latex_elements["preamble"] = str(latex_preamble) app.config.latex_elements = latex_elements # type: ignore[attr-defined] add_nbsp_substitution(config) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autodoc.regex`. :param app: The Sphinx application. """ app.setup_extension("sphinx.ext.autodoc") app.setup_extension("sphinx_toolbox._css") app.connect("config-inited", configure) app.add_autodocumenter(RegexDocumenter) app.add_role("regex", Regex()) app.add_node( RegexNode, html=(visit_regex_node, depart_regex_node), latex=(visit_regex_node_latex, depart_regex_node_latex) ) return {"parallel_read_safe": True} PK7Vd!j)sphinx_toolbox/more_autodoc/sourcelink.py#!/usr/bin/env python3 # # sourcelink.py r""" Show a link to the corresponding source code at the top of :rst:dir:`automodule` output. .. versionadded:: 0.6.0 .. extensions:: sphinx_toolbox.more_autodoc.sourcelink Configuration ---------------- .. raw:: latex \begin{flushleft} :mod:`sphinx_toolbox.more_autodoc.sourcelink` can be configured using the :confval:`autodoc_default_options` option in ``conf.py``, or with the :rst:dir:`:sourcelink: ` option flag to :rst:dir:`automodule`. .. raw:: latex \end{flushleft} .. confval:: autodoc_show_sourcelink :type: :class:`bool` :default: :py:obj:`False` If :py:obj:`True`, shows a link to the corresponding source code at the top of each :rst:dir:`automodule` directive. .. rst:directive:option:: sourcelink When passed as an option flag to an :rst:dir:`automodule` directive, show a link to the corresponding source code at the top of the output *for that module only*. .. latex:vspace:: 5px .. versionchanged:: 1.1.0 Added support for the :rst:dir:`:sourcelink: ` option flag to :rst:dir:`automodule`. .. latex:clearpage:: API Reference -------------- """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib from types import ModuleType from typing import Any, List, Mapping # 3rd party import autodocsumm # type: ignore[import] import sphinx.ext.autodoc from sphinx.application import Sphinx # this package from sphinx_toolbox.more_autosummary import PatchedAutoSummModuleDocumenter from sphinx_toolbox.utils import SphinxExtMetadata, flag, metadata_add_version __all__ = ("sourcelinks_process_docstring", "setup") def sourcelinks_process_docstring( # noqa: MAN001 app: Sphinx, what, name: str, obj, options: Mapping[str, Any], lines: List[str], ) -> None: """ Process the docstring of a module and add a link to the source code if given in the configuration. :param app: The Sphinx application. :param what: :param name: The name of the object being documented. :param obj: The object being documented. :param options: Mapping of autodoc options to values. :param lines: List of strings representing the current contents of the docstring. """ show_sourcelink = options.get("sourcelink", app.config.autodoc_show_sourcelink) if isinstance(obj, ModuleType) and what == "module" and show_sourcelink: if not obj.__file__: return elif obj.__file__.endswith("/__init__.py"): source_target = f"{name.replace('.', '/')}/__init__.py" elif obj.__file__.endswith("\\__init__.py"): source_target = f"{name.replace('.', '/')}/__init__.py" elif obj.__file__.endswith(".py"): source_target = f"{name.replace('.', '/')}.py" else: return lines_to_insert = ( ".. rst-class:: source-link", '', f" **Source code:** :source:`{source_target}`", '', "--------------------", '', ) for line in reversed(lines_to_insert): # pylint: disable=W8402 lines.insert(0, line) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autodoc.sourcelink`. :param app: The Sphinx application. """ sphinx.ext.autodoc.ModuleDocumenter.option_spec["sourcelink"] = flag autodocsumm.AutoSummModuleDocumenter.option_spec["sourcelink"] = flag PatchedAutoSummModuleDocumenter.option_spec["sourcelink"] = flag app.setup_extension("sphinx_toolbox.source") app.setup_extension("sphinx_toolbox._css") app.setup_extension("sphinx.ext.autodoc") app.connect("autodoc-process-docstring", sourcelinks_process_docstring) app.add_config_value("autodoc_show_sourcelink", False, "env", [bool]) return {"parallel_read_safe": True} PK7VC m m(sphinx_toolbox/more_autodoc/typehints.py#!/usr/bin/env python3 # # autodoc_typehints.py r""" | Enhanced version of `sphinx-autodoc-typehints `_. | Copyright (c) Alex Grönholm The changes are: * *None* is formatted as :py:obj:`None` and not ``None``. If `intersphinx `_ is used this will now be a link to the Python documentation. Since :github:pull:`154 ` this feature is now available upstream. * If the signature of the object cannot be read, the signature provided by Sphinx will be used rather than raising an error. This usually occurs for methods of builtin types. * :class:`typing.TypeVar`\s are linked to if they have been included in the documentation. * If a function/method argument has a :class:`module `, :class:`class ` or :py:obj:`function ` object as its default value a better representation will be shown in the signature. For example: .. autofunction:: sphinx_toolbox.more_autodoc.typehints.serialise :noindex: Previously this would have shown the full path to the source file. Now it displays ````. * The ability to hook into the :func:`~.process_docstring` function to edit the object's properties before the annotations are added to the docstring. This is used by `attr-utils `_ to add annotations based on converter functions in `attrs `_ classes. To use this, in your extension's ``setup`` function: .. code-block:: python def setup(app: Sphinx) -> Dict[str, Any]: from sphinx_toolbox.more_autodoc.typehints import docstring_hooks docstring_hooks.append((my_hook, 75)) return {} .. latex:clearpage:: ``my_hook`` is a function that takes the object being documented as its only argument and returns that object after modification. The ``75`` is the priority of the hook: * ``< 20`` runs before ``fget`` functions are extracted from properties * ``< 90`` runs before ``__new__`` functions are extracted from :class:`~typing.NamedTuple`\s. * ``< 100`` runs before ``__init__`` functions are extracted from classes. * Unresolved forward references are handled better. * Many of the built in types from the :mod:`types` module are now formatted and linked to correctly. ----- .. versionadded:: 0.4.0 .. versionchanged:: 0.6.0 Moved from :mod:`sphinx_toolbox.autodoc_typehints`. .. versionchanged:: 0.8.0 Added support for :func:`collections.namedtuple`. ----- .. extensions:: sphinx_toolbox.more_autodoc.typehints .. latex:vspace:: -20px | For configuration information see https://github.com/agronholm/sphinx-autodoc-typehints | In addition, the following configuration value is added by this extension: .. latex:vspace:: -20px .. confval:: hide_none_rtype :type: :class:`bool` :default: :py:obj:`False` Hides return types of :py:obj:`None`. API Reference ----------------- """ # noqa: SXL001 # # Copyright (c) Alex Grönholm # Changes copyright © 2020-2022 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib import inspect import itertools import json import re import sys import types from contextlib import suppress from operator import itemgetter from tempfile import TemporaryDirectory from types import FunctionType, ModuleType from typing import ( Any, AnyStr, Callable, Dict, ForwardRef, List, Mapping, NewType, Optional, Tuple, Type, TypeVar, Union, get_type_hints ) # 3rd party import sphinx.util.typing import sphinx_autodoc_typehints from domdf_python_tools.stringlist import DelimitedList from domdf_python_tools.typing import ( ClassMethodDescriptorType, MethodDescriptorType, MethodWrapperType, WrapperDescriptorType ) from domdf_python_tools.utils import etc from sphinx.application import Sphinx from sphinx.errors import ExtensionError from sphinx.util.inspect import signature as Signature from sphinx.util.inspect import stringify_signature # this package from sphinx_toolbox.utils import ( SphinxExtMetadata, code_repr, escape_trailing__, is_namedtuple, metadata_add_version ) try: # 3rd party import attr except ImportError: # pragma: no cover # attrs is used in a way that it is only required in situations # where it is available to import, so it is fine to do this. pass try: # 3rd party from sphinx_autodoc_typehints import logger as sat_logger # type: ignore[attr-defined] from sphinx_autodoc_typehints import pydata_annotations # type: ignore[attr-defined] except ImportError: # Privatised in 1.14.1 # 3rd party from sphinx_autodoc_typehints import _LOGGER as sat_logger from sphinx_autodoc_typehints import _PYDATA_ANNOTATIONS as pydata_annotations try: # 3rd party from sphinx_autodoc_typehints import _future_annotations_imported except ImportError: def _future_annotations_imported(obj: Any) -> bool: if sys.version_info < (3, 7): # pragma: no cover (py37+) # Only Python ≥ 3.7 supports PEP563. return False _annotations = getattr(inspect.getmodule(obj), "annotations", None) if _annotations is None: return False # Make sure that annotations is imported from __future__ - defined in cpython/Lib/__future__.py # annotations become strings at runtime CO_FUTURE_ANNOTATIONS = 0x100000 if sys.version_info[0:2] == (3, 7) else 0x1000000 return _annotations.compiler_flag == CO_FUTURE_ANNOTATIONS __all__ = ( "ObjectAlias", "Module", "Function", "Class", "process_signature", "process_docstring", "format_annotation", "get_all_type_hints", "docstring_hooks", "setup", "get_annotation_module", "get_annotation_class_name", "get_annotation_args", "backfill_type_hints", "load_args", "split_type_comment_args", "default_preprocessors", "Preprocessor", ) get_annotation_module = sphinx_autodoc_typehints.get_annotation_module get_annotation_class_name = sphinx_autodoc_typehints.get_annotation_class_name get_annotation_args = sphinx_autodoc_typehints.get_annotation_args backfill_type_hints = sphinx_autodoc_typehints.backfill_type_hints load_args = sphinx_autodoc_typehints.load_args split_type_comment_args = sphinx_autodoc_typehints.split_type_comment_args # Demonstration of module default argument in signature def serialise(obj: Any, library=json) -> str: # noqa: MAN001 """ Serialise an object into a JSON string. :param obj: The object to serialise. :param library: The JSON library to use. :no-default library: :return: The JSON string. """ class ObjectAlias: """ Used to represent a module, class, function etc in a Sphinx function/class signature. .. versionadded:: 0.9.0 :param name: The name of the object being aliased. """ _alias_type: str def __init__(self, name: str): self.name: str = name def __repr__(self) -> str: """ Returns a string representation of the :class:`~.ObjectAlias`. """ return f"<{self._alias_type} {self.name!r}>" class Module(ObjectAlias): """ Used to represent a module in a Sphinx function/class signature. :param name: The name of the module. """ _alias_type = "module" class Function(ObjectAlias): """ Used to represent a function in a Sphinx function/class signature. .. versionadded:: 0.9.0 :param name: The name of the function. """ _alias_type = "function" class Class(ObjectAlias): """ Used to represent a class in a Sphinx function/class signature. .. versionadded:: 0.9.0 :param name: The name of the class. """ _alias_type = "class" def format_annotation(annotation: Any, fully_qualified: bool = False) -> str: """ Format a type annotation. :param annotation: :param fully_qualified: Whether the fully qualified name should be shown (e.g. ``typing.List``) or only the object name (e.g. ``List``). :rtype: .. versionchanged:: 2.13.0 Added support for :py:obj:`True` and :py:obj:`False` .. latex:clearpage:: """ prefix = '' if fully_qualified else '~' # Special cases if annotation is None or annotation is type(None): # noqa: E721 return ":py:obj:`None`" elif isinstance(annotation, bool): return f":py:obj:`{annotation}`" elif annotation is Ellipsis: return "..." elif annotation is itertools.cycle: return f":func:`{prefix}itertools.cycle`" elif annotation is types.GetSetDescriptorType: # noqa: E721 return f":py:data:`{prefix}types.GetSetDescriptorType`" elif annotation is types.MemberDescriptorType: # noqa: E721 return f":py:data:`{prefix}types.MemberDescriptorType`" elif annotation is types.MappingProxyType: # noqa: E721 return f":py:class:`{prefix}types.MappingProxyType`" elif annotation is types.ModuleType: # noqa: E721 return f":py:class:`{prefix}types.ModuleType`" elif annotation is types.FunctionType: # noqa: E721 return f":py:data:`{prefix}types.FunctionType`" elif annotation is types.BuiltinFunctionType: # noqa: E721 return f":py:data:`{prefix}types.BuiltinFunctionType`" elif annotation is types.MethodType: # noqa: E721 return f":py:data:`{prefix}types.MethodType`" elif annotation is MethodDescriptorType: return f":py:data:`{prefix}types.MethodDescriptorType`" elif annotation is ClassMethodDescriptorType: return f":py:data:`{prefix}types.ClassMethodDescriptorType`" elif annotation is MethodWrapperType: return f":py:data:`{prefix}types.MethodWrapperType`" elif annotation is WrapperDescriptorType: return f":py:data:`{prefix}types.WrapperDescriptorType`" elif isinstance(annotation, ForwardRef): # Unresolved forward ref return f":py:obj:`{prefix}.{annotation.__forward_arg__}`" elif annotation is type(re.compile('')): # noqa: E721 return f":py:class:`{prefix}typing.Pattern`" elif annotation is TemporaryDirectory: return f":py:obj:`{prefix}tempfile.TemporaryDirectory`" elif sys.version_info >= (3, 10): # pragma: no cover (= (3, 10) and isinstance(annotation, NewType): # pragma: no cover (`" # Some types require special handling if full_name == "typing.NewType": args_format = f"\\(:py:data:`~{annotation.__name__}`, {{}})" role = "class" if sys.version_info > (3, 10) else "func" elif full_name in {"typing.Union", "types.UnionType"} and len(args) == 2 and type(None) in args: full_name = "typing.Optional" elif full_name == "types.UnionType": full_name = "typing.Union" role = "data" elif full_name == "typing.Callable" and args and args[0] is not ...: formatted_args = "\\[\\[" + ", ".join(format_annotation(arg) for arg in args[:-1]) + ']' formatted_args += ", " + format_annotation(args[-1]) + ']' elif full_name == "typing.Literal": # TODO: Enums? formatted_arg_list: DelimitedList[str] = DelimitedList() for arg in args: if isinstance(arg, bool): formatted_arg_list.append(format_annotation(arg)) else: formatted_arg_list.append(code_repr(arg)) formatted_args = f"\\[{formatted_arg_list:, }]" if full_name == "typing.Optional": args = tuple(x for x in args if x is not type(None)) # noqa: E721 # TODO: unions with one or more forward refs if args and not formatted_args: formatted_args = args_format.format(", ".join(format_annotation(arg, fully_qualified) for arg in args)) return f":py:{role}:`{prefix}{full_name}`{formatted_args}" #: Type hint for default preprocessor functions. Preprocessor = Callable[[Type], Any] default_preprocessors: List[Tuple[Callable[[Type], bool], Preprocessor]] = [ (lambda d: isinstance(d, ModuleType), lambda d: Module(d.__name__)), (lambda d: isinstance(d, FunctionType), lambda d: Function(d.__name__)), (inspect.isclass, lambda d: Class(d.__name__)), (lambda d: d is Ellipsis, lambda d: etc), (lambda d: d == "...", lambda d: etc), ] """ A list of 2-element tuples, comprising a function to check the default value against and a preprocessor to pass the function to if True. """ def preprocess_function_defaults(obj: Callable) -> Tuple[Optional[inspect.Signature], List[inspect.Parameter]]: """ Pre-processes the default values for the arguments of a function. .. versionadded:: 0.8.0 :param obj: The function. :return: The function signature and a list of arguments/parameters. """ try: signature = Signature(inspect.unwrap(obj)) except ValueError: # pragma: no cover return None, [] parameters = [] preprocessor_list = default_preprocessors for param in signature.parameters.values(): default = param.default # pylint: disable=dotted-import-in-loop if default is not inspect.Parameter.empty: for check, preprocessor in preprocessor_list: if check(default): default = preprocessor(default) break parameters.append(param.replace(annotation=inspect.Parameter.empty, default=default)) # pylint: enable=dotted-import-in-loop return signature, parameters def preprocess_class_defaults( obj: Callable ) -> Tuple[Optional[Callable], Optional[inspect.Signature], List[inspect.Parameter]]: """ Pre-processes the default values for the arguments of a class. .. versionadded:: 0.8.0 :param obj: The class. :return: The class signature and a list of arguments/parameters. """ init: Optional[Callable[..., Any]] = getattr(obj, "__init__", getattr(obj, "__new__", None)) if is_namedtuple(obj): init = getattr(obj, "__new__") try: signature = Signature(inspect.unwrap(init)) # type: ignore[arg-type] except ValueError: # pragma: no cover return init, None, [] parameters = [] preprocessor_list = default_preprocessors for argname, param in signature.parameters.items(): default = param.default if default is not inspect.Parameter.empty: for check, preprocessor in preprocessor_list: if check(default): default = preprocessor(default) break else: if hasattr(obj, "__attrs_attrs__") and default is attr.NOTHING: # Special casing for attrs classes for value in obj.__attrs_attrs__: # type: ignore[attr-defined] if value.name == argname: if isinstance(value.default, attr.Factory): # type: ignore[arg-type] default = value.default.factory() parameters.append(param.replace(annotation=inspect.Parameter.empty, default=default)) return init, signature, parameters def process_signature( # noqa: MAN001 app: Sphinx, what: str, name: str, obj, options, signature, return_annotation: Any, ) -> Optional[Tuple[str, None]]: """ Process the signature for a function/method. :param app: The Sphinx application. :param what: :param name: The name of the object being documented. :param obj: The object being documented. :param options: Mapping of autodoc options to values. :param signature: :param return_annotation: :rtype: .. versionchanged:: 0.8.0 Added support for factory function default values in attrs classes. """ if not callable(obj): return None original_obj = obj if inspect.isclass(obj): obj, signature, parameters = preprocess_class_defaults(obj) else: signature, parameters = preprocess_function_defaults(obj) obj = inspect.unwrap(obj) if not getattr(obj, "__annotations__", None): return None # The generated dataclass __init__() and class are weird and need extra checks # This helper function operates on the generated class and methods # of a dataclass, not an instantiated dataclass object. As such, # it cannot be replaced by a call to `dataclasses.is_dataclass()`. def _is_dataclass(name: str, what: str, qualname: str) -> bool: if what == "method" and name.endswith(".__init__"): # generated __init__() return True if what == "class" and qualname.endswith(".__init__"): # generated class return True return False # The generated dataclass __init__() is weird and needs the second condition if ( hasattr(obj, "__qualname__") and "" in obj.__qualname__ and not _is_dataclass(name, what, obj.__qualname__) ): sat_logger.warning( "Cannot treat a function defined as a local function: '%s' (use @functools.wraps)", name ) return None if parameters: if inspect.isclass(original_obj) or (what == "method" and name.endswith(".__init__")): del parameters[0] elif what == "method": try: outer = inspect.getmodule(obj) if outer is not None: for clsname in obj.__qualname__.split('.')[:-1]: outer = getattr(outer, clsname) except AttributeError: outer = None method_name = obj.__name__ if method_name.startswith("__") and not method_name.endswith("__"): # If the method starts with double underscore (dunder) # Python applies mangling so we need to prepend the class name. # This doesn't happen if it always ends with double underscore. class_name = obj.__qualname__.split('.')[-2] method_name = f"_{class_name}{method_name}" if outer is not None: method_object = outer.__dict__[method_name] if outer else obj if not isinstance(method_object, (classmethod, staticmethod)): del parameters[0] else: if not inspect.ismethod(obj) and parameters[0].name in {"self", "cls", "_cls"}: del parameters[0] signature = signature.replace(parameters=parameters, return_annotation=inspect.Signature.empty) return stringify_signature(signature), None # .replace('\\', '\\\\') def _docstring_property_hook(obj: Any) -> Callable: if isinstance(obj, property): obj = obj.fget return obj def _docstring_class_hook(obj: Any) -> Callable: if callable(obj): if inspect.isclass(obj): obj = getattr(obj, "__init__") return obj def _docstring_namedtuple_hook(obj: Any) -> Callable: if is_namedtuple(obj): obj = getattr(obj, "__new__") return obj docstring_hooks: List[Tuple[Callable[[Any], Callable], int]] = [ (_docstring_property_hook, 20), (_docstring_namedtuple_hook, 90), (_docstring_class_hook, 100), ] """ List of additional hooks to run in :func:`~sphinx_toolbox.more_autodoc.typehints.process_docstring`. Each entry in the list consists of: * a function that takes the object being documented as its only argument and returns that object after modification. * a number giving the priority of the hook, in ascending order. * ``< 20`` runs before ``fget`` functions are extracted from properties * ``< 90`` runs before ``__new__`` functions are extracted from :class:`NamedTuples `. * ``< 100`` runs before ``__init__`` functions are extracted from classes. """ def process_docstring( app: Sphinx, what: str, name: str, obj: Any, options: Dict[str, Any], lines: List[str], ) -> None: """ Process the docstring of a class, function, method etc. :param app: The Sphinx application. :param what: :param name: The name of the object being documented. :param obj: The object being documented. :param options: Mapping of autodoc options to values. :param lines: List of strings representing the current contents of the docstring. .. versionchanged:: 1.1.0 An empty ``:rtype:`` flag can be used to control the position of the return type annotation in the docstring. """ if what in {"variable", "regex"}: # Doesn't have parameters or return type return original_obj = obj for hook, priority in sorted(docstring_hooks, key=itemgetter(1)): obj = hook(obj) if callable(obj): obj = inspect.unwrap(obj) type_hints = get_all_type_hints(obj, name, original_obj) signature_params: Mapping[str, Any] try: signature_params = inspect.signature(obj).parameters except Exception: # Ignore errors. Can be a multitude of things including builtin functions or extension modules. signature_params = {} for argname, annotation in type_hints.items(): if argname == "return": continue # this is handled separately later if argname in signature_params: # Ensure *args and **kwargs have *s if signature_params[argname].kind == 2: # *args argname = f"\\*{argname}" elif signature_params[argname].kind == 4: # **kwargs argname = f"\\*\\*{argname}" argname = escape_trailing__(argname) formatted_annotation = format_annotation( annotation, fully_qualified=app.config.typehints_fully_qualified, ) searchfor = [f":{field} {argname}:" for field in ("param", "parameter", "arg", "argument")] insert_index = None for i, line in enumerate(lines): if any(line.startswith(search_string) for search_string in searchfor): insert_index = i break if insert_index is None and app.config.always_document_param_types: lines.append(f":param {argname}:") insert_index = len(lines) if insert_index is not None: lines.insert(insert_index, f":type {argname}: {formatted_annotation}") if "return" in type_hints and not inspect.isclass(original_obj): # This avoids adding a return type for data class __init__ methods if what == "method" and name.endswith(".__init__"): return formatted_annotation = format_annotation( type_hints["return"], fully_qualified=app.config.typehints_fully_qualified, ) insert_index = len(lines) for i, line in enumerate(lines): if line.startswith(":rtype:"): if line[7:].strip(): insert_index = None else: insert_index = i lines.pop(i) break elif line.startswith(":return:") or line.startswith(":returns:"): insert_index = i if insert_index is not None and app.config.typehints_document_rtype: if insert_index == len(lines): # Ensure that :rtype: doesn't get joined with a paragraph of text, which # prevents it being interpreted. lines.append('') insert_index += 1 if not (formatted_annotation == ":py:obj:`None`" and app.config.hide_none_rtype): lines.insert(insert_index, f":rtype: {formatted_annotation}") def _class_get_type_hints(obj, globalns=None, localns=None): # noqa: MAN001,MAN002 """ Return type hints for an object. For classes, unlike :func:`typing.get_type_hints` this will attempt to use the global namespace of the modules where the class and its parents were defined until it can resolve all forward references. """ if not inspect.isclass(obj): return get_type_hints(obj, localns=localns, globalns=globalns) mro_stack = list(obj.__mro__) if localns is None: localns = {} while True: try: # pylint: disable=R8203 return get_type_hints(obj.__init__, localns=localns, globalns=globalns) except NameError: if not mro_stack: raise klasse = mro_stack.pop(0) if klasse is object or klasse.__module__ == "builtins": raise localns = {**sys.modules[klasse.__module__].__dict__, **localns} def get_all_type_hints(obj: Any, name: str, original_obj: Any) -> Dict[str, Any]: """ Returns the resolved type hints for the given objects. :param obj: :param name: :param original_obj: The original object, before the class if ``obj`` is its ``__init__`` method. """ def log(exc: Exception) -> None: sat_logger.warning( 'Cannot resolve forward reference in type annotations of "%s": %s', name, exc, ) rv = {} try: if inspect.isclass(original_obj): rv = _class_get_type_hints(original_obj) else: rv = get_type_hints(obj) except (AttributeError, TypeError, RecursionError) as exc: # Introspecting a slot wrapper will raise TypeError, and some recursive type # definitions will cause a RecursionError (https://github.com/python/typing/issues/574). # If one is using PEP563 annotations, Python will raise a (e.g.,) # TypeError("TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'") # on 'str | None', therefore we accept TypeErrors with that error message # if 'annotations' is imported from '__future__'. if isinstance(exc, TypeError): if _future_annotations_imported(obj) and "unsupported operand type" in str(exc): rv = obj.__annotations__ except NameError as exc: log(exc) rv = obj.__annotations__ if rv: return rv rv = backfill_type_hints(obj, name) try: if rv != {}: obj.__annotations__ = rv except (AttributeError, TypeError): return rv try: if inspect.isclass(original_obj): rv = _class_get_type_hints(original_obj) else: rv = get_type_hints(obj) except (AttributeError, TypeError): pass except NameError as exc: log(exc) rv = obj.__annotations__ return rv @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autodoc.typehints`. :param app: The Sphinx application. """ if "sphinx_autodoc_typehints" in app.extensions: raise ExtensionError( "'sphinx_toolbox.more_autodoc.typehints' must be loaded before 'sphinx_autodoc_typehints'." ) sphinx_autodoc_typehints.format_annotation = format_annotation # type: ignore[assignment] sphinx_autodoc_typehints.process_signature = process_signature sphinx_autodoc_typehints.process_docstring = process_docstring # type: ignore[assignment] app.setup_extension("sphinx.ext.autodoc") app.setup_extension("sphinx_autodoc_typehints") app.add_config_value("hide_none_rtype", False, "env", [bool]) return {"parallel_read_safe": True} def _resolve_forwardref( fr: Union[ForwardRef, sphinx.util.typing.ForwardRef], module: str, ) -> object: """ Resolve a forward reference. This is not part of the public API. :param fr: :param module: The module the forward reference was defined in. :return: The evaluated object. """ module_dict = sys.modules[module].__dict__ if sys.version_info < (3, 9): return fr._evaluate(module_dict, module_dict) else: return fr._evaluate(module_dict, module_dict, set()) PK7V׀#x*x*'sphinx_toolbox/more_autodoc/typevars.py#!/usr/bin/env python3 # # typevars.py r""" Documenter for module level :class:`typing.TypeVar`\'s, similar to Sphinx's ``autotypevar`` but with a different appearance. .. versionadded:: 1.3.0 .. extensions:: sphinx_toolbox.more_autodoc.typevars .. latex:vspace:: -15px Configuration ------------- .. confval:: all_typevars :type: :class:`bool` :default: False Document all :class:`typing.TypeVar`\s, even if they have no docstring. .. confval:: no_unbound_typevars :type: :class:`bool` :default: True Only document :class:`typing.TypeVar`\s that have a constraint of are bound. This option has no effect if :confval:`all_typevars` is False. .. latex:clearpage:: Usage ---------- .. rst:directive:: autotypevar Directive to automatically document a :class:`typing.TypeVar`. The output is based on the :rst:dir:`autodata` directive, and takes all of its options plus these additional ones: .. rst:directive:option:: no-value Don't show the value of the variable. .. rst:directive:option:: value: value :type: string Show this instead of the value taken from the Python source code. .. rst:directive:option:: no-type Don't show the type of the variable. API Reference ---------------- """ # noqa: D400 # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts based on https://github.com/sphinx-doc/sphinx # | Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file). # | BSD Licensed # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib import sys from typing import Any, Dict, ForwardRef, List, Optional, Tuple, Type, TypeVar, Union # 3rd party from docutils.statemachine import StringList from domdf_python_tools.words import word_join from sphinx.application import Sphinx from typing_extensions import Protocol # this package from sphinx_toolbox._data_documenter import DataDocumenter from sphinx_toolbox.config import ToolboxConfig from sphinx_toolbox.more_autodoc import _documenter_add_content from sphinx_toolbox.more_autodoc.typehints import format_annotation from sphinx_toolbox.more_autodoc.variables import VariableDocumenter from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ( "TypeVarDocumenter", "unskip_typevars", "setup", ) class TypeVarDocumenter(VariableDocumenter): r""" Alternative version of :class:`sphinx.ext.autodoc.TypeVarDocumenter` with better type hint rendering. Specialized Documenter subclass for :class:`typing.TypeVar`\s. """ # noqa: D400 objtype = "typevar" directivetype = "data" priority = DataDocumenter.priority + 1 @classmethod def can_document_member( cls, member: Any, membername: str, isattr: bool, parent: Any, ) -> bool: """ Called to see if a member can be documented by this documenter. :param member: The member being checked. :param membername: The name of the member. :param isattr: :param parent: The parent of the member. """ return isinstance(member, TypeVar) def resolve_type(self, forward_ref: ForwardRef) -> Type: """ Resolve a :class:`typing.ForwardRef` using the module the :class:`~typing.TypeVar` belongs to. :param forward_ref: """ if forward_ref.__forward_evaluated__: return forward_ref.__forward_value__ # type: ignore[return-value] else: globanls = sys.modules[self.object.__module__].__dict__ eval_ = eval return eval_(forward_ref.__forward_code__, globanls, globanls) def add_content(self, more_content: Optional[StringList], no_docstring: bool = False) -> None: """ Add content from docstrings, attribute documentation and user. :param more_content: :param no_docstring: """ obj: _TypeVar = self.object sourcename = self.get_sourcename() constraints = [self.resolve_type(c) if isinstance(c, ForwardRef) else c for c in obj.__constraints__] description = [] bound_to: Optional[Type] if isinstance(obj.__bound__, ForwardRef): bound_to = self.resolve_type(obj.__bound__) else: bound_to = obj.__bound__ if obj.__covariant__: description.append("Covariant") elif obj.__contravariant__: description.append("Contravariant") else: description.append("Invariant") description.append(":class:`~typing.TypeVar`") if constraints: description.append("constrained to") description.append(word_join(format_annotation(c, fully_qualified=True) for c in constraints)) elif bound_to: description.append("bound to") description.append(format_annotation(bound_to, fully_qualified=True)) # if self.analyzer: # attr_docs = self.analyzer.find_attr_docs() # if self.objpath: # key = ('.'.join(self.objpath[:-1]), self.objpath[-1]) # if key in attr_docs: # return self.add_line('', sourcename) self.add_line(' '.join(description).rstrip() + '.', sourcename) # " " + self.add_line('', sourcename) _documenter_add_content(self, more_content, no_docstring) def add_directive_header(self, sig: str) -> None: """ Add the directive's header. :param sig: """ obj: _TypeVar = self.object sourcename = self.get_sourcename() constraints = [self.resolve_type(c) if isinstance(c, ForwardRef) else c for c in obj.__constraints__] sig_elements = [obj.__name__, *(c.__name__ for c in constraints)] bound_to: Optional[Type] if isinstance(obj.__bound__, ForwardRef): bound_to = self.resolve_type(obj.__bound__) else: bound_to = obj.__bound__ if bound_to is not None: try: sig_elements.append(f"bound={bound_to.__name__}") except AttributeError: sig_elements.append(f"bound={repr(bound_to)}") if obj.__covariant__: sig_elements.append(f"covariant=True") elif obj.__contravariant__: sig_elements.append(f"contravariant=True") self.options["value"] = f"TypeVar({', '.join(sig_elements)})" self.add_line('', sourcename) super().add_directive_header(sig) def get_doc( self, encoding: Optional[str] = None, ignore: Optional[int] = None, ) -> List[List[str]]: """ Decode and return lines of the docstring(s) for the object. :param encoding: :param ignore: :rtype: .. latex:clearpage:: """ if self.object.__doc__ != TypeVar.__doc__: return super().get_doc() or [] else: return [] def validate_config(app: Sphinx, config: ToolboxConfig) -> None: r""" Validate the provided configuration values. See :class:`~sphinx_toolbox.config.ToolboxConfig` for a list of the configuration values. :param app: The Sphinx application. :param config: :type config: :class:`~sphinx.config.Config` """ if config.all_typevars: app.connect("autodoc-skip-member", unskip_typevars) def unskip_typevars( app: Sphinx, what: str, name: str, obj: Any, skip: bool, options: Dict[str, Any], ) -> Optional[bool]: r""" Unskip undocumented :class:`typing.TypeVar`\s if :confval:`all_typevars` is :py:obj:`True`. :param app: The Sphinx application. :param what: The type of the object which the docstring belongs to (one of ``'module'``, ``'class'``, ``'exception'``, ``'function'``, ``'method'``, ``'attribute'``). :param name: The fully qualified name of the object. :param obj: The object itself. :param skip: A boolean indicating if autodoc will skip this member if the user handler does not override the decision. :param options: The options given to the directive: an object with attributes ``inherited_members``, ``undoc_members``, ``show_inheritance`` and ``noindex`` that are true if the flag option of same name was given to the auto directive. """ assert app.env is not None if isinstance(obj, TypeVar): if app.env.config.no_unbound_typevars: if obj.__bound__ or obj.__constraints__: return False else: return True else: return False return None @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autodoc.typevars`. :param app: The Sphinx application. """ app.setup_extension("sphinx.ext.autodoc") app.add_autodocumenter(TypeVarDocumenter, override=True) app.add_config_value("all_typevars", False, "env", types=[bool]) app.add_config_value("no_unbound_typevars", True, "env", types=[bool]) app.connect("config-inited", validate_config, priority=850) return {"parallel_read_safe": True} class _TypeVar(Protocol): __constraints__: Tuple[Union[Type, ForwardRef], ...] __bound__: Union[Type, ForwardRef, None] __covariant__: bool __contravariant__: bool __name__: str PK7VlIQQ(sphinx_toolbox/more_autodoc/variables.py#!/usr/bin/env python3 # # variables.py r""" Documenter for module level variables, similar to :rst:dir:`autodata` but with a different appearance and more customisation options. .. versionadded:: 0.6.0 .. extensions:: sphinx_toolbox.more_autodoc.variables .. versionchanged:: 0.7.0 Added ``*AttributeDocumenter``\s .. versionchanged:: 1.1.0 Added :class:`~.SlotsAttributeDocumenter` Usage ---------- .. rst:directive:: autovariable Directive to automatically document a variable. The output is based on the :rst:dir:`autodata` directive, and takes all of its options, plus these additional ones: .. rst:directive:option:: no-value Don't show the value of the variable. .. rst:directive:option:: value: value :type: string Show this instead of the value taken from the Python source code. .. rst:directive:option:: no-type Don't show the type of the variable. .. rst:directive:option:: type: type :type: string Show this instead of the type taken from the Python source code. An example of the output cen be seen below for :py:obj:`~.type_template`. API Reference ---------------- """ # noqa: D400 # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts based on https://github.com/sphinx-doc/sphinx # | Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file). # | BSD Licensed # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib import importlib import sys import warnings from contextlib import suppress from typing import Any, List, Optional, cast, get_type_hints # 3rd party import sphinx from docutils.statemachine import StringList from sphinx.application import Sphinx from sphinx.errors import PycodeError from sphinx.ext.autodoc import ( INSTANCEATTR, SLOTSATTR, UNINITIALIZED_ATTR, ClassLevelDocumenter, DocstringStripSignatureMixin, Documenter, ModuleDocumenter, ModuleLevelDocumenter, Options, annotation_option, import_object, logger, mock ) from sphinx.ext.autodoc.directive import DocumenterBridge from sphinx.pycode import ModuleAnalyzer from sphinx.util import inspect from sphinx.util.inspect import ForwardRef, getdoc, object_description, safe_getattr # this package from sphinx_toolbox._data_documenter import DataDocumenter from sphinx_toolbox.more_autodoc import _documenter_add_content from sphinx_toolbox.more_autodoc.typehints import _resolve_forwardref, format_annotation from sphinx_toolbox.utils import ( RemovedInSphinx50Warning, SphinxExtMetadata, add_nbsp_substitution, flag, metadata_add_version, prepare_docstring ) __all__ = ( "VariableDocumenter", "TypedAttributeDocumenter", "InstanceAttributeDocumenter", "SlotsAttributeDocumenter", "type_template", "old_type_template", "get_variable_type", "setup", ) def get_variable_type(documenter: Documenter) -> str: """ Returns the formatted type annotation for a variable. :param documenter: """ try: annotations = get_type_hints(documenter.parent) except NameError: # Failed to evaluate ForwardRef (maybe TYPE_CHECKING) annotations = safe_getattr(documenter.parent, "__annotations__", {}) except (TypeError, KeyError, AttributeError): # KeyError: a broken class found (refs: https://github.com/sphinx-doc/sphinx/issues/8084) # AttributeError is raised on 3.5.2 (fixed by 3.5.3) annotations = {} if documenter.objpath[-1] in annotations: ann = annotations.get(documenter.objpath[-1]) if isinstance(ann, str): return format_annotation(ann.strip("'\"")) return format_annotation(ann) else: # Instance attribute key = ('.'.join(documenter.objpath[:-1]), documenter.objpath[-1]) if documenter.analyzer and key in documenter.analyzer.annotations: # Forward references will have quotes annotation: str = documenter.analyzer.annotations[key].strip("'\"") try: module_dict = sys.modules[documenter.parent.__module__].__dict__ if annotation.isidentifier() and annotation in module_dict: return format_annotation(module_dict[annotation]) else: ref = ForwardRef(annotation) evaled = _resolve_forwardref(ref, documenter.parent.__module__) return format_annotation(evaled) except (NameError, TypeError, ValueError, AttributeError): return annotation else: return '' old_type_template = " **Type:** |nbsp| |nbsp| |nbsp| |nbsp| %s" """ Old template for rendering type annotations in :class:`~.VariableDocumenter`, :class:`~.TypedAttributeDocumenter` and :class:`~.InstanceAttributeDocumenter`. Renders like: **Type:** |nbsp| |nbsp| |nbsp| |nbsp| :class:`str` .. note:: Be sure to call :func:~.add_nbsp_substitution` in the ``setup`` function of any extensions using this template. """ type_template = " **Type:**    %s" """ Template for rendering type annotations in :class:`~.VariableDocumenter`, :class:`~.TypedAttributeDocumenter` and :class:`~.InstanceAttributeDocumenter`. Renders like: **Type:** \u00A0\u00A0\u00A0\u00A0 :class:`str` .. versionchanged:: 3.4.0 This template now uses the unicode codepoint for a non-breaking space, rather than the ReStructuredText substitution used in 3.3.0 and earlier. The old template is available as ``old_type_template``. """ class VariableDocumenter(DataDocumenter): """ Specialized Documenter subclass for data items. """ directivetype = "data" objtype = "variable" # keeps it below TypeVarDocumenter priority: float = DataDocumenter.priority + 0.5 # type: ignore[assignment] option_spec = { "no-value": flag, "no-type": flag, "type": str, "value": str, **DataDocumenter.option_spec, } def __init__(self, directive: DocumenterBridge, name: str, indent: str = '') -> None: super().__init__(directive=directive, name=name, indent=indent) self.options = Options(self.options.copy()) add_nbsp_substitution(self.env.app.config) def add_directive_header(self, sig: str) -> None: """ Add the directive's header. :param sig: """ sourcename = self.get_sourcename() no_value = self.options.get("no-value", False) no_type = self.options.get("no-type", False) if not self.options.get("annotation", ''): ModuleLevelDocumenter.add_directive_header(self, sig) if not no_value: if "value" in self.options: self.add_line(f" :value: {self.options['value']}", sourcename) else: with suppress(ValueError): if self.object is not UNINITIALIZED_ATTR: objrepr = object_description(self.object) self.add_line(f" :value: {objrepr}", sourcename) self.add_line('', sourcename) if not no_type: if "type" in self.options: the_type = self.options["type"] else: # obtain type annotation for this data the_type = get_variable_type(self) if not the_type.strip(): obj_type = type(self.object) if obj_type is object: return try: the_type = format_annotation(obj_type) except Exception: return line = type_template % the_type self.add_line(line, sourcename) else: super().add_directive_header(sig) class TypedAttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): """ Alternative version of :class:`autodoc.AttributeDocumenter ` with better type hint rendering. Specialized Documenter subclass for attributes. .. versionadded:: 0.7.0 .. versionchanged:: 1.0.0 Now uses the type of the variable if it is not explicitly annotated. """ # noqa: D400 objtype = "attribute" member_order = 60 option_spec = dict(ModuleLevelDocumenter.option_spec) option_spec["annotation"] = annotation_option # must be higher than the MethodDocumenter, else it will recognize # some non-data descriptors as methods priority = 10 def __init__(self, directive: DocumenterBridge, name: str, indent: str = '') -> None: super().__init__(directive=directive, name=name, indent=indent) self.options = Options(self.options.copy()) self._datadescriptor = True @staticmethod def is_function_or_method(obj: Any) -> bool: # noqa: D102 return inspect.isfunction(obj) or inspect.isbuiltin(obj) or inspect.ismethod(obj) @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any) -> bool: """ Called to see if a member can be documented by this documenter. """ if inspect.isattributedescriptor(member): return True elif ( not isinstance(parent, ModuleDocumenter) and not inspect.isroutine(member) and not isinstance(member, type) ): return True else: return False def document_members(self, all_members: bool = False) -> None: # noqa: D102 pass def isinstanceattribute(self) -> bool: """ Check the subject is an instance attribute. """ try: analyzer = ModuleAnalyzer.for_module(self.modname) attr_docs = analyzer.find_attr_docs() if self.objpath: key = ('.'.join(self.objpath[:-1]), self.objpath[-1]) if key in attr_docs: return True return False except PycodeError: return False def import_object(self, raiseerror: bool = False) -> bool: """ Import the object given by *self.modname* and *self.objpath* and set it as ``self.object``. :returns: :py:obj:`True` if successful, :py:obj:`False` if an error occurred. """ try: ret = super().import_object(raiseerror=True) if inspect.isenumattribute(self.object): self.object = self.object.value if inspect.isattributedescriptor(self.object): self._datadescriptor = True else: # if it's not a data descriptor self._datadescriptor = False except ImportError as exc: if self.isinstanceattribute(): self.object = INSTANCEATTR self._datadescriptor = False ret = True elif raiseerror: raise else: logger.warning(exc.args[0], type="autodoc", subtype="import_object") self.env.note_reread() ret = False return ret def get_real_modname(self) -> str: """ Get the real module name of an object to document. It can differ from the name of the module through which the object was imported. """ return self.get_attr(self.parent or self.object, "__module__", None) or self.modname def add_directive_header(self, sig: str) -> None: """ Add the directive's header. :param sig: """ sourcename = self.get_sourcename() no_value = self.options.get("no-value", False) no_type = self.options.get("no-type", False) if not self.options.get("annotation", ''): ClassLevelDocumenter.add_directive_header(self, sig) # data descriptors do not have useful values if not no_value and not self._datadescriptor: if "value" in self.options: self.add_line(" :value: " + self.options["value"], sourcename) else: with suppress(ValueError): if self.object is not INSTANCEATTR: objrepr = object_description(self.object) self.add_line(" :value: " + objrepr, sourcename) self.add_line('', sourcename) if not no_type: if "type" in self.options: self.add_line(type_template % self.options["type"], sourcename) else: # obtain type annotation for this attribute the_type = get_variable_type(self) if not the_type.strip(): obj_type = type(self.object) if obj_type is object: return try: the_type = format_annotation(obj_type) except Exception: return line = type_template % the_type self.add_line(line, sourcename) else: super().add_directive_header(sig) def get_doc( self, encoding: Optional[str] = None, ignore: Optional[int] = None, ) -> List[List[str]]: """ Decode and return lines of the docstring(s) for the object. :param encoding: :param ignore: """ # Disable `autodoc_inherit_docstring` temporarily to avoid to obtain # a docstring from the value which descriptor returns unexpectedly. # ref: https://github.com/sphinx-doc/sphinx/issues/7805 orig = self.env.config.autodoc_inherit_docstrings try: self.env.config.autodoc_inherit_docstrings = False # type: ignore[attr-defined] # Sphinx's signature is wrong wrt Optional if sphinx.version_info >= (4, 0): if encoding is not None: raise TypeError("The 'encoding' argument to get_doc was removed in Sphinx 4") if self._new_docstrings is not None: return self._new_docstrings or [] docstring = getdoc( self.object, self.get_attr, self.config.autodoc_inherit_docstrings, self.parent, self.object_name, ) if docstring: tab_width = self.directive.state.document.settings.tab_width return [prepare_docstring(docstring, ignore, tab_width)] return [] else: enc = cast(str, encoding) ign = cast(int, ignore) super_get_doc = super().get_doc(enc, ign) # type: ignore[call-arg] return super_get_doc or [] finally: self.env.config.autodoc_inherit_docstrings = orig # type: ignore[attr-defined] def add_content(self, more_content: Optional[StringList], no_docstring: bool = False) -> None: """ Add content from docstrings, attribute documentation and user. """ if not self._datadescriptor: # if it's not a data descriptor, its docstring is very probably the wrong thing to display no_docstring = True _documenter_add_content(self, more_content, no_docstring) class InstanceAttributeDocumenter(TypedAttributeDocumenter): """ Alternative version of :class:`autodoc.InstanceAttributeDocumenter ` with better type hint rendering. Specialized Documenter subclass for attributes that cannot be imported because they are instance attributes (e.g. assigned in ``__init__``). .. versionadded:: 0.7.0 .. versionchanged:: 1.0.0 Now uses the type of the variable if it is not explicitly annotated. """ # noqa: D400 objtype = "instanceattribute" directivetype = "attribute" member_order = 60 # must be higher than TypedAttributeDocumenter priority = 11 @classmethod def can_document_member( cls, member: Any, membername: str, isattr: bool, parent: Any, ) -> bool: """ Called to see if a member can be documented by this documenter. This documenter only documents INSTANCEATTR members. :param member: The member being checked. :param membername: The name of the member. :param isattr: :param parent: The parent of the member. """ return not isinstance(parent, ModuleDocumenter) and isattr and member is INSTANCEATTR def import_parent(self) -> Any: """ Import and return the attribute's parent. """ try: parent = importlib.import_module(self.modname) for name in self.objpath[:-1]: parent = self.get_attr(parent, name) return parent except (ImportError, AttributeError): return None def import_object(self, raiseerror: bool = False) -> bool: """ Never import anything. :param raiseerror: """ # disguise as an attribute self.objtype = "attribute" self.object = INSTANCEATTR self.parent = self.import_parent() self._datadescriptor = False return True def add_content(self, more_content: Any, no_docstring: bool = False) -> None: """ Never try to get a docstring from the object. """ super().add_content(more_content, no_docstring=True) class SlotsAttributeDocumenter(TypedAttributeDocumenter): r""" Alternative version of :class:`autodoc.InstanceAttributeDocumenter ` with better type hint rendering. Specialized Documenter subclass for attributes that cannot be imported because they are attributes in __slots__. .. versionadded:: 1.1.0 .. latex:vspace:: 10px """ # noqa: D400 objtype = "slotsattribute" directivetype = "attribute" member_order = 60 # must be higher than AttributeDocumenter priority = 11 @classmethod def can_document_member( cls, member: Any, membername: str, isattr: bool, parent: Any, ) -> bool: """ Called to see if a member can be documented by this documenter. This documenter only documents SLOTSATTR members. :param member: The member being checked. :param membername: The name of the member. :param isattr: :param parent: The parent of the member. """ return member is SLOTSATTR def import_object(self, raiseerror: bool = False) -> bool: """ Never import anything. :param raiseerror: """ # disguise as an attribute self.objtype = "attribute" self._datadescriptor = True with mock(self.env.config.autodoc_mock_imports): try: ret = import_object( self.modname, self.objpath[:-1], "class", attrgetter=self.get_attr, warningiserror=self.env.config.autodoc_warningiserror ) self.module, _, _, self.parent = ret return True except ImportError as exc: if raiseerror: raise else: logger.warning(exc.args[0], type="autodoc", subtype="import_object") self.env.note_reread() return False def get_doc( self, encoding: Optional[str] = None, ignore: Optional[int] = None, ) -> List[List[str]]: """ Decode and return lines of the docstring(s) for the object. :param encoding: :param ignore: """ if sphinx.version_info >= (4, 0): if encoding is not None: raise TypeError("The 'encoding' argument to get_doc was removed in Sphinx 4") if ignore is not None: if sphinx.version_info >= (5, 0): raise TypeError("The 'ignore' argument to get_doc was removed in Sphinx 5") else: warnings.warn( "The 'ignore' argument to get_doc() is deprecated.", RemovedInSphinx50Warning, # type: ignore[arg-type] stacklevel=2 ) name = self.objpath[-1] __slots__ = safe_getattr(self.parent, "__slots__", []) if isinstance(__slots__, dict) and isinstance(__slots__.get(name), str): return [prepare_docstring(__slots__[name])] else: return [] @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autodoc.variables`. :param app: The Sphinx application. """ app.setup_extension("sphinx.ext.autodoc") app.setup_extension("sphinx_toolbox.more_autosummary") app.add_autodocumenter(VariableDocumenter) app.add_autodocumenter(TypedAttributeDocumenter, override=True) app.add_autodocumenter(InstanceAttributeDocumenter, override=True) app.add_autodocumenter(SlotsAttributeDocumenter, override=True) app.connect("config-inited", lambda _, config: add_nbsp_substitution(config)) return {"parallel_read_safe": True} PK7V;VHXHX+sphinx_toolbox/more_autosummary/__init__.py#!/usr/bin/env python3 # # __init__.py r""" Extensions to :mod:`sphinx.ext.autosummary`. Provides an enhanced version of https://autodocsumm.readthedocs.io/ which respects the autodoc ``member-order`` option. This can be given for an individual directive, in the `autodoc_member_order `_ configuration value, or via :confval:`autodocsumm_member_order`. Also patches :class:`sphinx.ext.autosummary.Autosummary` to fix an issue where the module name is sometimes duplicated. I.e. ``foo.bar.baz()`` became ``foo.bar.foo.bar.baz()``, which of course doesn't exist and created a broken link. .. versionadded:: 0.7.0 .. versionchanged:: 1.3.0 Autosummary now selects the appropriate documenter for attributes rather than falling back to :class:`~sphinx.ext.autodoc.DataDocumenter`. .. versionchanged:: 2.13.0 Also patches :class:`sphinx.ext.autodoc.ModuleDocumenter` to fix an issue where ``__all__`` is not respected for autosummary tables. Configuration -------------- .. latex:vspace:: -20px .. confval:: autodocsumm_member_order :type: :py:obj:`str` :default: ``'alphabetical'`` Determines the sort order of members in ``autodocsumm`` summary tables. Valid values are ``'alphabetical'`` and ``'bysource'``. Note that for ``'bysource'`` the module must be a Python module with the source code available. The member order can also be set on a per-directive basis using the ``:member-order: [order]`` option. This applies not only to :rst:dir:`automodule` etc. directives, but also to :rst:dir:`automodulesumm` etc. directives. .. confval:: autosummary_col_type :type: :py:obj:`str` :default: ``'\X'`` The LaTeX column type to use for autosummary tables. Custom columns can be defined in the LaTeX preamble for use with this option. For example: .. code-block:: python latex_elements["preamble"] = r''' \makeatletter \newcolumntype{\Xx}[2]{>{\raggedright\arraybackslash}p{\dimexpr (\linewidth-\arrayrulewidth)*#1/#2-\tw@\tabcolsep-\arrayrulewidth\relax}} \makeatother ''' autosummary_col_type = "\\Xx" .. versionadded:: 2.13.0 API Reference ---------------- """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts based on https://github.com/sphinx-doc/sphinx # | Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file). # | BSD Licensed # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # Builds on top of, and PatchedAutoDocSummDirective based on, https://github.com/Chilipp/autodocsumm # | Copyright 2016-2019, Philipp S. Sommer # | Copyright 2020-2021, Helmholtz-Zentrum Hereon # | # | Licensed under the Apache License, Version 2.0 (the "License"); # | you may not use this file except in compliance with the License. # | You may obtain a copy of the License at # | # | http://www.apache.org/licenses/LICENSE-2.0 # | # | Unless required by applicable law or agreed to in writing, # | software distributed under the License is distributed on an "AS IS" BASIS, # | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # | See the License for the specific language governing permissions and limitations under the License. # # stdlib import inspect import operator import re from typing import Any, Dict, List, Optional, Tuple, Type # 3rd party import autodocsumm # type: ignore[import] import docutils import sphinx from docutils import nodes from domdf_python_tools.stringlist import StringList from sphinx import addnodes from sphinx.application import Sphinx from sphinx.config import ENUM from sphinx.ext.autodoc import ( ALL, INSTANCEATTR, ClassDocumenter, Documenter, ModuleDocumenter, logger, special_member_re ) from sphinx.ext.autodoc.directive import DocumenterBridge, process_documenter_options from sphinx.ext.autosummary import Autosummary, FakeDirective, autosummary_table from sphinx.locale import __ from sphinx.util.inspect import getdoc, safe_getattr # this package from sphinx_toolbox._data_documenter import DataDocumenter from sphinx_toolbox.more_autodoc import ObjectMembers from sphinx_toolbox.utils import SphinxExtMetadata, allow_subclass_add, get_first_matching, metadata_add_version if sphinx.version_info > (4, 1): # 3rd party from sphinx.util.docstrings import separate_metadata else: # 3rd party from sphinx.util.docstrings import extract_metadata __all__ = ( "PatchedAutosummary", "PatchedAutoSummModuleDocumenter", "PatchedAutoSummClassDocumenter", "get_documenter", "setup", ) try: # 3rd party from sphinx.ext.autodoc.importer import get_module_members as _get_module_members # type: ignore[attr-defined] except ImportError: # 3rd party from sphinx.util.inspect import getannotations def _get_module_members(module: Any) -> List[Tuple[str, Any]]: """Get members of target module.""" # 3rd party from sphinx.ext.autodoc import INSTANCEATTR members: Dict[str, Tuple[str, Any]] = {} for name in dir(module): try: value = safe_getattr(module, name, None) members[name] = (name, value) except AttributeError: continue # annotation only member (ex. attr: int) for name in getannotations(module): if name not in members: members[name] = (name, INSTANCEATTR) return sorted(list(members.values())) def add_autosummary(self, relative_ref_paths: bool = False) -> None: """ Add the :rst:dir:`autosummary` table of this documenter. :param relative_ref_paths: Use paths relative to the current module instead of absolute import paths for each object. """ if not self.options.get("autosummary", False): return content = StringList() content.indent_type = ' ' * 4 sourcename = self.get_sourcename() grouped_documenters = self.get_grouped_documenters() if not self.options.get("autosummary-no-titles", False) and grouped_documenters: content.blankline() content.append(".. latex:vspace:: 10px") content.blankline() member_order = get_first_matching( lambda x: x != "groupwise", ( self.options.get("member-order", ''), self.env.config.autodocsumm_member_order, self.env.config.autodoc_member_order, ), default="alphabetical", ) for section, documenters in grouped_documenters.items(): if not self.options.get("autosummary-no-titles", False): content.append(f"**{section}:**") content.blankline() content.append(".. latex:vspace:: -5px") content.blankline(ensure_single=True) # TODO transform to make caption associated with table in LaTeX content.append(".. autosummary::") if self.options.autosummary_nosignatures: content.append(" :nosignatures:") content.blankline(ensure_single=True) with content.with_indent_size(content.indent_size + 1): for documenter, _ in self.sort_members(documenters, member_order): obj_ref_path = documenter.fullname # if relative_ref_paths: # modname = self.modname + '.' # if documenter.fullname.startswith(modname): # obj_ref_path = documenter.fullname[len(modname):] content.append(f"~{obj_ref_path}") content.blankline() for line in content: self.add_line(line, sourcename) class PatchedAutosummary(Autosummary): """ Pretty table containing short signatures and summaries of functions etc. Patched version of :class:`sphinx.ext.autosummary.Autosummary` to fix an issue where the module name is sometimes duplicated. I.e. ``foo.bar.baz()`` became ``foo.bar.foo.bar.baz()``, which of course doesn't exist and created a broken link. .. versionadded:: 0.5.1 .. versionchanged:: 0.7.0 Moved from :mod:`sphinx_toolbox.patched_autosummary`. .. versionchanged:: 2.13.0 Added support for customising the column type with the :confval:`autosummary_col_type` option. """ def import_by_name(self, name: str, prefixes: List[Optional[str]]) -> Tuple[str, Any, Any, str]: """ Import the object with the give name. :param name: :param prefixes: :return: The real name of the object, the object, the parent of the object, and the name of the module. """ real_name, obj, parent, modname = super().import_by_name(name=name, prefixes=prefixes) real_name = re.sub(rf"((?:{modname}\.)+)", f"{modname}.", real_name) return real_name, obj, parent, modname def create_documenter( self, app: Sphinx, obj: Any, parent: Any, full_name: str, ) -> Documenter: """ Get an :class:`autodoc.Documenter` class suitable for documenting the given object. :param app: The Sphinx application. :param obj: The object being documented. :param parent: The parent of the object (e.g. a module or a class). :param full_name: The full name of the object. .. versionchanged:: 1.3.0 Now selects the appropriate documenter for attributes rather than falling back to :class:`~sphinx.ext.autodoc.DataDocumenter`. """ doccls = get_documenter(app, obj, parent) return doccls(self.bridge, full_name) def get_table(self, items: List[Tuple[str, str, str, str]]) -> List[nodes.Node]: """ Generate a list of table nodes for the :rst:dir:`autosummary` directive. :param items: A list produced by ``self.get_items``. :rtype: .. latex:clearpage:: """ table_spec, table, *other_nodes = super().get_table(items) assert isinstance(table_spec, addnodes.tabular_col_spec) assert isinstance(table, autosummary_table) if docutils.__version_info__ >= (0, 18): table.children[0]["classes"] += ["colwidths-given"] # type: ignore[index] column_type = getattr(self.env.config, "autosummary_col_type", r"\X") table_spec["spec"] = f'{column_type}{{1}}{{2}}{column_type}{{1}}{{2}}' return [table_spec, table, *other_nodes] def get_documenter(app: Sphinx, obj: Any, parent: Any) -> Type[Documenter]: """ Returns an :class:`autodoc.Documenter` class suitable for documenting the given object. .. versionadded:: 1.3.0 :param app: The Sphinx application. :param obj: The object being documented. :param parent: The parent of the object (e.g. a module or a class). """ if inspect.ismodule(obj): # ModuleDocumenter.can_document_member always returns False return ModuleDocumenter # Construct a fake documenter for *parent* if parent is not None: parent_doc_cls = get_documenter(app, parent, None) else: parent_doc_cls = ModuleDocumenter if hasattr(parent, "__name__"): parent_doc = parent_doc_cls(FakeDirective(), parent.__name__) else: parent_doc = parent_doc_cls(FakeDirective(), '') # Get the correct documenter class for *obj* classes = [ cls for cls in app.registry.documenters.values() if cls.can_document_member(obj, '', False, parent_doc) ] data_doc_classes = [ cls for cls in app.registry.documenters.values() if cls.can_document_member(obj, '', True, parent_doc) ] if classes: classes.sort(key=lambda cls: cls.priority) return classes[-1] elif data_doc_classes: data_doc_classes.sort(key=lambda cls: cls.priority) return data_doc_classes[-1] else: return DataDocumenter class PatchedAutoSummModuleDocumenter(autodocsumm.AutoSummModuleDocumenter): """ Patched version of :class:`autodocsumm.AutoSummClassDocumenter` which works around a bug in Sphinx 3.4 and above where ``__all__`` is not respected. .. versionadded:: 2.13.0 """ # noqa: D400 def filter_members(self, members: ObjectMembers, want_all: bool) -> List[Tuple[str, Any, bool]]: """ Filter the given member list. Members are skipped if: * they are private (except if given explicitly or the ``private-members`` option is set) * they are special methods (except if given explicitly or the ``special-members`` option is set) * they are undocumented (except if the ``undoc-members`` option is set) The user can override the skipping decision by connecting to the :event:`autodoc-skip-member` event. """ def is_filtered_inherited_member(name: str) -> bool: if inspect.isclass(self.object): for cls in self.object.__mro__: if cls.__name__ == self.options.inherited_members and cls != self.object: # given member is a member of specified *super class* return True elif name in cls.__dict__: return False elif name in self.get_attr(cls, "__annotations__", {}): return False return False ret = [] # search for members in source code too namespace = '.'.join(self.objpath) # will be empty for modules if self.analyzer: attr_docs = self.analyzer.find_attr_docs() else: attr_docs = {} doc: Optional[str] sphinx_gt_41 = sphinx.version_info > (4, 1) # process members and determine which to skip for (membername, member) in members: # if isattr is True, the member is documented as an attribute isattr = (member is INSTANCEATTR or (namespace, membername) in attr_docs) doc = getdoc( member, self.get_attr, self.env.config.autodoc_inherit_docstrings, self.parent, self.object_name ) if not isinstance(doc, str): # Ignore non-string __doc__ doc = None # if the member __doc__ is the same as self's __doc__, it's just # inherited and therefore not the member's doc cls = self.get_attr(member, "__class__", None) if cls: cls_doc = self.get_attr(cls, "__doc__", None) if cls_doc == doc: doc = None if sphinx_gt_41: doc, metadata = separate_metadata(doc) # type: ignore[arg-type] else: metadata = extract_metadata(doc) # type: ignore[arg-type] has_doc = bool(doc) if "private" in metadata: # consider a member private if docstring has "private" metadata isprivate = True elif "public" in metadata: # consider a member public if docstring has "public" metadata isprivate = False else: isprivate = membername.startswith('_') keep = False if safe_getattr(member, "__sphinx_mock__", False): # mocked module or object pass elif self.options.exclude_members and membername in self.options.exclude_members: # remove members given by exclude-members keep = False elif want_all and special_member_re.match(membername): # special __methods__ if self.options.special_members and membername in self.options.special_members: if membername == "__doc__": keep = False elif is_filtered_inherited_member(membername): keep = False else: keep = has_doc or self.options.undoc_members else: keep = False elif (namespace, membername) in attr_docs: if want_all and isprivate: if self.options.private_members is None: keep = False else: keep = membername in self.options.private_members else: # keep documented attributes keep = True isattr = True elif want_all and isprivate: if has_doc or self.options.undoc_members: if self.options.private_members is None: keep = False elif is_filtered_inherited_member(membername): keep = False else: keep = membername in self.options.private_members else: keep = False else: if self.options.members is ALL and is_filtered_inherited_member(membername): keep = False else: # ignore undocumented members if :undoc-members: is not given keep = has_doc or self.options.undoc_members # give the user a chance to decide whether this member # should be skipped if self.env.app: # let extensions preprocess docstrings try: # pylint: disable=R8203 skip_user = self.env.app.emit_firstresult( "autodoc-skip-member", self.objtype, membername, member, not keep, self.options, ) if skip_user is not None: keep = not skip_user except Exception as exc: msg = 'autodoc: failed to determine %r to be documented, the following exception was raised:\n%s' logger.warning(__(msg), member, exc, type="autodoc") keep = False if keep: ret.append((membername, member, isattr)) return ret def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]: """ Return a tuple of ``(members_check_module, members)``, where ``members`` is a list of ``(membername, member)`` pairs of the members of ``self.object``. If ``want_all`` is :py:obj:`True`, return all members. Otherwise, only return those members given by ``self.options.members`` (which may also be none). """ # noqa: D400 if want_all: if self.__all__: memberlist = self.__all__ else: # for implicit module members, check __module__ to avoid # documenting imported objects return True, _get_module_members(self.object) else: memberlist = self.options.members or [] ret = [] for mname in memberlist: try: # pylint: disable=R8203 ret.append((mname, safe_getattr(self.object, mname))) except AttributeError: # pylint: disable=dotted-import-in-loop) logger.warning( operator.mod( __("missing attribute mentioned in :members: or __all__: module %s, attribute %s"), (safe_getattr(self.object, "__name__", "???"), mname), ), type="autodoc" ) # pylint: enable=dotted-import-in-loop) return False, ret class PatchedAutoSummClassDocumenter(autodocsumm.AutoSummClassDocumenter): """ Patched version of :class:`autodocsumm.AutoSummClassDocumenter` which doesn't show summary tables for aliased objects. .. versionadded:: 0.9.0 """ # noqa: D400 def add_content(self, *args, **kwargs) -> None: """ Add content from docstrings, attribute documentation and user. """ ClassDocumenter.add_content(self, *args, **kwargs) if not self.doc_as_attr: self.add_autosummary() class PatchedAutoDocSummDirective(autodocsumm.AutoDocSummDirective): """ Patched ``AutoDocSummDirective`` which uses :py:obj:`None` for the members option rather than an empty string. .. attention:: This class is not part of the public API. """ def run(self) -> List[nodes.Node]: reporter = self.state.document.reporter if hasattr(reporter, "get_source_and_line"): _, lineno = reporter.get_source_and_line(self.lineno) else: _, lineno = (None, None) # look up target Documenter objtype = self.name[4:-4] # strip prefix (auto-) and suffix (-summ). doccls = self.env.app.registry.documenters[objtype] self.options["autosummary-force-inline"] = "True" self.options["autosummary"] = "True" if "no-members" not in self.options: self.options["members"] = None # process the options with the selected documenter's option_spec try: documenter_options = process_documenter_options(doccls, self.config, self.options) except (KeyError, ValueError, TypeError) as exc: # an option is either unknown or has a wrong type logger.error( "An option to %s is either unknown or has an invalid value: %s", self.name, exc, location=(self.env.docname, lineno), ) return [] # generate the output params = DocumenterBridge(self.env, reporter, documenter_options, lineno, self.state) documenter = doccls(params, self.arguments[0]) documenter.add_autosummary() node = nodes.paragraph() node.document = self.state.document self.state.nested_parse(params.result, 0, node) return node.children @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autosummary`. :param app: The Sphinx application. """ app.setup_extension("sphinx.ext.autosummary") app.setup_extension("autodocsumm") app.setup_extension("sphinx_toolbox.latex") app.add_directive("autosummary", PatchedAutosummary, override=True) app.add_directive("autoclasssumm", PatchedAutoDocSummDirective, override=True) app.add_directive("automodulesumm", PatchedAutoDocSummDirective, override=True) autodocsumm.AutosummaryDocumenter.add_autosummary = add_autosummary allow_subclass_add(app, PatchedAutoSummModuleDocumenter) allow_subclass_add(app, PatchedAutoSummClassDocumenter) app.add_config_value( "autodocsumm_member_order", default="alphabetical", rebuild=True, types=ENUM("alphabetic", "alphabetical", "bysource"), ) app.add_config_value( "autosummary_col_type", default=r"\X", rebuild="latex", types=[str], ) return {"parallel_read_safe": True} PK7V3#3#0sphinx_toolbox/more_autosummary/column_widths.py#!/usr/bin/env python3 # # column_widths.py """ Sphinx extension to allow customisation of column widths in autosummary tables with the LaTeX builder. .. versionadded:: 3.0.0 Usage -------------- This extension provides the :rst:dir:`autosummary-widths` directive. This sets the autosummary table's column widths with the LaTeX builder until the end of the current reStructuredText document, or until the next :rst:dir:`autosummary-widths` directive. .. rst:directive:: autosummary-widths Set the width of the autosummary table's columns with the LaTeX builder. The directive takes up to two arguments -- the column widths as vulgar fractions (e.g. ``5/10``). If only one argument is provided, this sets the width of the first column, and the width of the second column is calculated from it. If both arguments are provided, they set the width of the first and second columns respectively. .. latex:clearpage:: :bold-title:`Examples:` .. code-block:: rst .. autosummary-widths:: 5/10 .. autosummary-widths:: 3/10, 7/10 .. autosummary-widths:: 35/100 .. attention:: This directive ignores the :confval:`autosummary_col_type` configuration option. API Reference ---------------- """ # Copyright © 2022 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts based on https://github.com/sphinx-doc/sphinx/blob/3.x/sphinx/ext/autosummary/__init__.py # | Copyright (c) 2007-2022 by the Sphinx team (see AUTHORS file). # | BSD Licensed # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib from contextlib import suppress from fractions import Fraction from itertools import chain from typing import Iterable, List, Tuple, cast # 3rd party import docutils from docutils import nodes from docutils.statemachine import StringList from domdf_python_tools import stringlist from sphinx import addnodes from sphinx.application import Sphinx from sphinx.config import Config from sphinx.ext.autosummary import autosummary_table from sphinx.util import rst from sphinx.util.docutils import SphinxDirective, switch_source_input # this package from sphinx_toolbox import latex from sphinx_toolbox.more_autosummary import PatchedAutosummary from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ("AutosummaryWidths", "WidthsDirective", "configure", "setup") class AutosummaryWidths(PatchedAutosummary): """ Customised :rst:dir:`autosummary` directive with customisable width with the LaTeX builder. .. attention:: This directive ignores the :confval:`autosummary_col_type` configuration option. """ def get_table(self, items: List[Tuple[str, str, str, str]]) -> List[nodes.Node]: """ Generate a proper list of table nodes for autosummary:: directive. :param items: A list produced by ``self.get_items``. """ table_spec = addnodes.tabular_col_spec() widths = tuple(chain.from_iterable(getattr(self.state.document, "autosummary_widths", ((1, 2), (1, 2))))) assert len(widths) == 4 table_spec["spec"] = r'\Xx{%d}{%d}\Xx{%d}{%d}' % widths table = autosummary_table('') classes = ["autosummary", "longtable"] if docutils.__version_info__ >= (0, 18): classes.append("colwidths-given") real_table = nodes.table('', classes=classes) table.append(real_table) group = nodes.tgroup('', cols=2) real_table.append(group) group.append(nodes.colspec('', colwidth=10)) group.append(nodes.colspec('', colwidth=90)) body = nodes.tbody('') group.append(body) def append_row(*column_texts: str) -> None: row = nodes.row('') source, line = self.state_machine.get_source_and_line() for text in column_texts: node = nodes.paragraph('') vl = StringList() vl.append(text, f"{source}:{line:d}:") # pylint: disable=loop-invariant-statement with switch_source_input(self.state, vl): self.state.nested_parse(vl, 0, node) with suppress(IndexError): if isinstance(node[0], nodes.paragraph): node = node[0] row.append(nodes.entry('', node)) body.append(row) add_signature = "nosignatures" not in self.options for name, sig, summary, real_name in items: col1 = f":obj:`{name} <{real_name}>`" if add_signature: col1 += f"\\ {rst.escape(sig)}" append_row(col1, summary) return [table_spec, table] class WidthsDirective(SphinxDirective): """ Sphinx directive which configures the column widths of an :rst:dir:`autosummary` table for the remainder of the document, or until the next `autosummary-widths` directive. """ # noqa: D400 required_arguments = 1 optional_arguments = 1 @staticmethod def parse_widths(raw_widths: Iterable[str]) -> List[Tuple[int, int]]: """ Parse a width string (as a vulgar fraction) into a list of 2-element ``(numerator, denominator)`` tuples. For example, ``'5/10'`` becomes ``(5, 10)``. :param raw_widths: """ widths = [cast(Tuple[int, int], tuple(map(int, arg.split('/')))) for arg in raw_widths] if len(widths) == 1: left_width = Fraction(*widths[0]) right_width = 1 - left_width widths.append((right_width.numerator, right_width.denominator)) return widths def run(self) -> List: """ Process the directive's arguments. """ self.state.document.autosummary_widths = self.parse_widths(self.arguments) # type: ignore[attr-defined] return [] def configure(app: Sphinx, config: Config) -> None: """ Configure :mod:`sphinx_toolbox.more_autosummary.column_widths`. :param app: The Sphinx application. :param config: """ latex_elements = getattr(config, "latex_elements", {}) latex_preamble = stringlist.StringList(latex_elements.get("preamble", '')) latex_preamble.blankline() latex_preamble.append(r"\makeatletter") latex_preamble.append(r"\newcolumntype{\Xx}[2]{>{\raggedright\arraybackslash}p{\dimexpr") latex_preamble.append(r" (\linewidth-\arrayrulewidth)*#1/#2-\tw@\tabcolsep-\arrayrulewidth\relax}}") latex_preamble.append(r"\makeatother") latex_preamble.blankline() latex_elements["preamble"] = str(latex_preamble) config.latex_elements = latex_elements # type: ignore[attr-defined] @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.more_autosummary.column_widths`. :param app: The Sphinx application. """ app.setup_extension("sphinx_toolbox.more_autosummary") app.add_directive("autosummary", AutosummaryWidths, override=True) app.add_directive("autosummary-widths", WidthsDirective) app.connect("config-inited", configure) app.connect("build-finished", latex.replace_unknown_unicode) return {"parallel_read_safe": True} PK7V !sphinx_toolbox/tweaks/__init__.py#!/usr/bin/env python3 # # tweaks.py """ Small tweaks for Sphinx you may or may not want. .. versionadded:: 0.9.0 """ # # Copyright © 2020 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # PK7V'{ { )sphinx_toolbox/tweaks/footnote_symbols.py#!/usr/bin/env python3 # # footnote_symbols.py r""" Tweak which monkeypatches docutils to use the following symbols for footnotes: .. rst-class:: bullet-hidden * † -- dagger * ‡ -- double dagger * § -- section mark * ¶ -- paragraph mark (pilcrow) * # -- number sign * ♠ -- spade suit * ♥ -- heart suit * ♦ -- diamond suit * ♣ -- club suit With some themes the superscript asterisk becomes very hard to see. .. versionadded:: 2.7.0 .. extensions:: sphinx_toolbox.tweaks.footnote_symbols ----- """ # noqa: D400 # # Copyright © 2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib from typing import List # 3rd party from docutils.transforms.references import Footnotes from sphinx.application import Sphinx # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ("setup", ) #: The list of symbols symbols: List[str] = [ # Entries 1-3 and 5 below are from section 12.51 of # The Chicago Manual of Style, 14th edition. '†', # dagger † '‡', # double dagger ‡ '§', # section mark § '¶', # paragraph mark (pilcrow) ¶ # (parallels ['||'] in CMoS) '#', # number sign # The entries below were chosen arbitrarily. '♠', # spade suit ♠ '♥', # heart suit ♥ '♦', # diamond suit ♦ '♣', # club suit ♣ ] @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.tweaks.footnote_symbols`. :param app: The Sphinx application. """ Footnotes.symbols = symbols return {"parallel_read_safe": True} PK7VulH= = %sphinx_toolbox/tweaks/latex_layout.py#!/usr/bin/env python3 # # latex_layout.py r""" Makes minor adjustments to the LaTeX layout. * Increases the whitespace above function signatures by 5px, to prevent the function visually merging with the previous one. * Remove unnecessary indentation and allow "raggedright" for the fields in the body of functions, which prevents ugly whitespace and line breaks. * Disables justification for function signatures. This is a backport of changes from Sphinx 4 added in :github:pull:`8997 `. .. versionadded:: 2.12.0 * With Sphinx 3.5, doesn't add ``\sphinxAtStartPar`` before every paragraph. The change in :github:issue:`8781 ` was to solve an issue with *tables*, but it isn't clear why it then gets added for *every* paragraph so this extension removes it. .. versionadded:: 2.13.0 * Configures hyperref to apply correct page numbering to the frontmatter. .. versionadded:: 2.14.0 * Optionally, configures the ``needspace`` package. The :confval:`needspace_amount` option can be set in ``conf.py`` to add the ``\needspace{}`` command before each ``addnodes.desc`` node (i.e. a function or class description). The amount of space is set by the ``needspace_amount`` option, e.g.: .. code-block:: python needspace_amount = r"4\baselineskip" .. versionadded:: 3.0.0 .. versionadded:: 2.10.0 .. extensions:: sphinx_toolbox.tweaks.latex_layout .. versionchanged:: 3.0.0 The functionality has moved to :mod:`sphinx_toolbox.latex.toc`. Please use that extension instead. ----- """ # # Copyright © 2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # 3rd party from sphinx.application import Sphinx # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ("setup", ) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.tweaks.latex_layout`. :param app: The Sphinx application. """ app.setup_extension("sphinx_toolbox.latex.layout") return {"parallel_read_safe": True} PK7VpE  "sphinx_toolbox/tweaks/latex_toc.py#!/usr/bin/env python3 # # latex_toc.py """ Adjusts the default LaTeX output as follows: * The captions from ``toctree`` directives are converted into document parts. * The PDF outline has the correct hierarchy, including having the indices as top-level elements. .. versionadded:: 2.1.0 .. extensions:: sphinx_toolbox.tweaks.latex_toc .. versionchanged:: 3.0.0 The functionality has moved to :mod:`sphinx_toolbox.latex.toc`. Please use that extension instead. ----- """ # noqa: D400 # # Copyright © 2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib # 3rd party from sphinx.application import Sphinx # this package from sphinx_toolbox.latex import use_package from sphinx_toolbox.utils import Config, SphinxExtMetadata, metadata_add_version __all__ = ("setup", "configure") def configure(app: Sphinx, config: Config) -> None: """ Configure :mod:`sphinx_toolbox.tweaks.latex_toc`. :param app: The Sphinx application. :param config: """ use_package("bookmark", config) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.tweaks.latex_toc`. :param app: The Sphinx application. """ app.setup_extension("sphinx_toolbox.latex.toc") return {"parallel_read_safe": True} PK7VX]]#sphinx_toolbox/tweaks/param_dash.py#!/usr/bin/env python3 # # param_dash.py """ Monkeypatches :class:`sphinx.util.docfields.TypedField` to only output the endash (--) separating the parameter name from its description if a description was given. .. versionadded:: 0.9.0 :bold-title:`Example` .. rest-example:: .. class:: MyClass(foo, bar) This is my class. :param foo: An argument :param bar: .. extensions:: sphinx_toolbox.tweaks.param_dash ----- """ # noqa: D400 # # Copyright © 2020 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts based on https://github.com/sphinx-doc/sphinx # | Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file). # | BSD Licensed # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib from typing import Dict, List, Optional, Tuple # 3rd party import sphinx.util.docfields from docutils import nodes from sphinx import addnodes from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ("setup", ) def make_field( self, types: Dict[str, List[nodes.Node]], domain: str, items: Tuple, env: Optional[BuildEnvironment] = None, **kwargs, # To support Sphinx 4.1 and later ) -> nodes.field: def handle_item(fieldarg: str, content: List[nodes.Element]) -> nodes.paragraph: par = nodes.paragraph() par.extend( self.make_xrefs( self.rolename, domain, fieldarg, addnodes.literal_strong, env=env, **kwargs, # To support Sphinx 4.1 and later ) ) if fieldarg in types: par += nodes.Text(" (") # NOTE: using .pop() here to prevent a single type node to be # inserted twice into the doctree, which leads to # inconsistencies later when references are resolved fieldtype = types.pop(fieldarg) if len(fieldtype) == 1 and isinstance(fieldtype[0], nodes.Text): typename = fieldtype[0].astext() par.extend( self.make_xrefs(self.typerolename, domain, typename, addnodes.literal_emphasis, env=env) ) else: par += fieldtype par += nodes.Text(')') if (content and len(content) == 1 and isinstance(content[0], nodes.inline) and not content[0].children): return par par += nodes.Text(" -- ") par += content return par fieldname = nodes.field_name('', self.label) bodynode: nodes.Node if len(items) == 1 and self.can_collapse: fieldarg, content = items[0] bodynode = handle_item(fieldarg, content) else: bodynode = self.list_type() for fieldarg, content in items: bodynode += nodes.list_item('', handle_item(fieldarg, content)) # type: ignore[assignment,operator] fieldbody = nodes.field_body('', bodynode) return nodes.field('', fieldname, fieldbody) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.tweaks.param_dash`. :param app: The Sphinx application. """ sphinx.util.docfields.TypedField.make_field = make_field # type: ignore[assignment] return {"parallel_read_safe": True} PK7Vm.sphinx_toolbox/tweaks/revert_footnote_style.py#!/usr/bin/env python3 # # revert_footnote_style.py """ Reverts the docutils footnote behaviour from ``>=0.18`` to that of ``<=0.17``. .. versionadded:: 3.1.2 .. extensions:: sphinx_toolbox.tweaks.revert_footnote_style ----- """ # # Copyright © 2022 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Based on docutils # | Copyright © 2016 David Goodger, Günter Milde # | BSD Licensed # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # 3rd party import docutils from sphinx.application import Sphinx from sphinx.writers.html import HTMLTranslator from sphinx.writers.html5 import HTML5Translator # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ["setup"] def visit_footnote(self: HTMLTranslator, node: docutils.nodes.footnote) -> None: # pragma: no cover if not self.in_footnote_list: listnode = node.copy() listnode["ids"] = [] classes = [node.tagname, self.settings.footnote_references] self.body.append(self.starttag(listnode, "dl", CLASS=' '.join(classes))) # role="note" self.in_footnote_list = True def depart_footnote(self, node: docutils.nodes.footnote) -> None: # pragma: no cover self.body.append('\n') if not isinstance(node.next_node(descend=False, siblings=True), docutils.nodes.footnote): self.body.append('\n') self.in_footnote_list = False def visit_footnote_reference(self, node: docutils.nodes.footnote_reference) -> None: # pragma: no cover href = '#' + node["refid"] classes = ["footnote-reference", self.settings.footnote_references] self.body.append(self.starttag(node, 'a', suffix='', CLASS=' '.join(classes), href=href)) # role='doc-noteref' def depart_footnote_reference(self, node: docutils.nodes.footnote_reference) -> None: # pragma: no cover self.body.append("") # footnote and citation labels: def visit_label(self, node: docutils.nodes.label) -> None: # pragma: no cover if (isinstance(node.parent, docutils.nodes.footnote)): classes = self.settings.footnote_references else: classes = "brackets" # pass parent node to get id into starttag: self.body.append(self.starttag(node.parent, "dt", '', CLASS="label")) self.body.append(self.starttag(node, "span", '', CLASS=classes)) # footnote/citation backrefs: if self.settings.footnote_backlinks: backrefs = node.parent.get("backrefs", []) if len(backrefs) == 1: self.body.append('' % backrefs[0]) def depart_label(self, node: docutils.nodes.label) -> None: # pragma: no cover if self.settings.footnote_backlinks: backrefs = node.parent["backrefs"] if len(backrefs) == 1: self.body.append("") self.body.append("") if self.settings.footnote_backlinks and len(backrefs) > 1: backlinks = [f'{i}' for (i, ref) in enumerate(backrefs, 1)] self.body.append('(%s)' % ','.join(backlinks)) self.body.append('\n
') @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.tweaks.revert_footnote_style`. :param app: The Sphinx application. """ if docutils.__version_info__ >= (0, 18): # pragma: no cover app.add_node( docutils.nodes.footnote, html=(visit_footnote, depart_footnote), override=True, ) app.add_node( docutils.nodes.footnote_reference, html=(visit_footnote_reference, depart_footnote_reference), override=True, ) app.add_node( docutils.nodes.label, html=(visit_label, depart_label), override=True, ) HTMLTranslator.in_footnote_list = False HTML5Translator.in_footnote_list = False return {"parallel_read_safe": True} PK7V|%%+sphinx_toolbox/tweaks/sphinx_panels_tabs.py#!/usr/bin/env python3 # # sphinx_panels_tabs.py """ Tweak to :github:repo:`executablebooks/sphinx-tabs` to fix a CSS conflict with :github:repo:`executablebooks/sphinx-panels`. Fix for :github:issue:`51 `. .. versionadded:: 1.9.0 .. extensions:: sphinx_toolbox.tweaks.sphinx_panels_tabs ----- """ # noqa: D400 # # Copyright © 2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts based on https://github.com/executablebooks/sphinx-panels # Copyright (c) 2020 Executable Books # MIT Licensed # # stdlib from typing import Optional # 3rd party import dict2css from docutils import nodes from domdf_python_tools.paths import PathPlus from sphinx.application import Sphinx from sphinx.writers.html import HTMLTranslator # this package from sphinx_toolbox import _css from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ("copy_asset_files", "setup") def visit_container(self: HTMLTranslator, node: nodes.container) -> None: classes = "docutils container" if node.get("is_div", False): # we don't want the CSS for container for these nodes classes = "docutils" if any(c.startswith("sphinx-data-tab-") for c in node["classes"]): classes = "docutils" self.body.append(self.starttag(node, "div", CLASS=classes)) def depart_container(self: HTMLTranslator, node: nodes.Node) -> None: self.body.append("\n") def copy_asset_files(app: Sphinx, exception: Optional[Exception] = None) -> None: """ Copy asset files to the output. :param app: The Sphinx application. :param exception: Any exception which occurred and caused Sphinx to abort. .. versionchanged:: 2.7.0 Renamed from ``copy_assets``. The former name was kept as an alias until version 3.0.0 """ if exception: # pragma: no cover return if app.builder is None or app.builder.format.lower() != "html": # pragma: no cover return # style = StringList([ # ".docutils.container {", # " padding-left: 0 !important;", # " padding-right: 0 !important;", # '}', # '', # # "div.sphinx-tabs.docutils.container {", # # " padding-left: 0 !important;", # # " padding-right: 0 !important;", # # "}", # # '', # "div.ui.top.attached.tabular.menu.sphinx-menu.docutils.container {", # # " padding-left: 0 !important;", # # " padding-right: 0 !important;", # " margin-left: 0 !important;", # " margin-right: 0 !important;", # '}', # ]) css_static_dir = PathPlus(app.builder.outdir) / "_static" / "css" css_static_dir.maybe_make(parents=True) dict2css.dump(_css.tweaks_sphinx_panels_tabs_styles, css_static_dir / "tabs_customise.css") @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.tweaks.sphinx_panels_tabs`. :param app: The Sphinx application. """ app.setup_extension("sphinx_tabs.tabs") app.setup_extension("sphinx_toolbox._css") app.add_node(nodes.container, override=True, html=(visit_container, depart_container)) return {"parallel_read_safe": True} PK7V+; ; sphinx_toolbox/tweaks/tabsize.py#!/usr/bin/env python3 # # tabsize.py r""" Hack to get the docutils tab size, as there doesn't appear to be any other way. .. versionadded:: 1.0.0 You probably don't need to use this extension directly, but if you're developing an extension of your own you can enable it like so: .. code-block:: def setup(app: Sphinx) -> Dict[str, Any]: app.setup_extension('sphinx_toolbox.github') return {} This will guarantee that the following value will be available via :attr:`app.config `: * **docutils_tab_width** (:class:`int`\) -- The number of spaces that correspond to a tab when Docutils parses source files. """ # # Copyright © 2020 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib from typing import Union # 3rd party from docutils.nodes import document from docutils.statemachine import StringList from sphinx.application import Sphinx from sphinx.parsers import RSTParser # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ("setup", ) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.tweaks.tabsize`. :param app: The Sphinx application. """ class CustomRSTParser(RSTParser): def parse(self, inputstring: Union[str, StringList], document: document) -> None: app.config.docutils_tab_width = document.settings.tab_width # type: ignore[attr-defined] super().parse(inputstring, document) app.add_source_parser(CustomRSTParser, override=True) return {"parallel_read_safe": True} PK7V5]D D sphinx_toolbox/__init__.py#!/usr/bin/env python3 # # __init__.py """ Box of handy tools for Sphinx. """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib import sys # This all has to be up here so it's triggered first. if sys.version_info >= (3, 10): # stdlib import types types.Union = types.UnionType # 3rd party from sphinx.application import Sphinx # this package from sphinx_toolbox import ( # noqa: F401 assets, code, config, confval, installation, issues, rest_example, shields, source, utils, wikipedia ) from sphinx_toolbox.cache import cache # noqa: F401 __author__: str = "Dominic Davis-Foster" __copyright__: str = "2020 Dominic Davis-Foster" __license__: str = "MIT License" __version__: str = "3.4.0" __email__: str = "dominic@davis-foster.co.uk" __all__ = ("setup", ) def setup(app: Sphinx) -> "utils.SphinxExtMetadata": """ Setup :mod:`sphinx_toolbox`. :param app: The Sphinx application. """ # Ensure dependencies are set up app.setup_extension("sphinx.ext.viewcode") app.setup_extension("sphinx_toolbox.github") app.connect("config-inited", config.validate_config, priority=850) # Setup standalone extensions app.setup_extension("sphinx_toolbox.assets") app.setup_extension("sphinx_toolbox.changeset") app.setup_extension("sphinx_toolbox.code") app.setup_extension("sphinx_toolbox.collapse") app.setup_extension("sphinx_toolbox.confval") app.setup_extension("sphinx_toolbox.decorators") app.setup_extension("sphinx_toolbox.formatting") app.setup_extension("sphinx_toolbox.installation") app.setup_extension("sphinx_toolbox.issues") app.setup_extension("sphinx_toolbox.latex") app.setup_extension("sphinx_toolbox.rest_example") app.setup_extension("sphinx_toolbox.shields") app.setup_extension("sphinx_toolbox.sidebar_links") app.setup_extension("sphinx_toolbox.source") app.setup_extension("sphinx_toolbox.wikipedia") app.setup_extension("sphinx_toolbox.more_autodoc.autoprotocol") app.setup_extension("sphinx_toolbox.more_autodoc.autotypeddict") app.setup_extension("sphinx_toolbox.more_autodoc.autonamedtuple") # Hack to get the docutils tab size, as there doesn't appear to be any other way app.setup_extension("sphinx_toolbox.tweaks.tabsize") return { "version": __version__, "parallel_read_safe": True, } PK7Vhsphinx_toolbox/__main__.py#!/usr/bin/env python3 # # __main__.py """ CLI entry point for sphinx-toolbox. """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib import sys # this package from sphinx_toolbox import cache __all__ = ("clear_cache", ) def clear_cache() -> int: """ Clear any cached URLs. """ if cache.clear(): print("Cache cleared successfully.") return 0 else: return 1 if __name__ == "__main__": sys.exit(clear_cache()) PK7VO ,,sphinx_toolbox/_css.py#!/usr/bin/env python3 # # _css.py """ Internal Sphinx extension to provide custom CSS. .. versionadded:: 2.7.0 """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib from typing import MutableMapping, Optional # 3rd party import dict2css from domdf_python_tools.paths import PathPlus from sphinx.application import Sphinx # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ("copy_asset_files", "setup") installation_styles: MutableMapping[str, dict2css.Style] = { 'div[id*="installation"] .sphinx-tabs-tab': {"color": "#2980b9"}, "button.sphinx-tabs-tab,div.sphinx-tabs-panel": {"outline": (None, dict2css.IMPORTANT)}, } shields_styles: MutableMapping[str, dict2css.Style] = { ".table-wrapper td p img.sphinx_toolbox_shield": {"vertical-align": "middle"}, } regex_styles: MutableMapping[str, dict2css.Style] = { "span.regex_literal": {"color": "dimgrey"}, "span.regex_at": {"color": "orangered"}, "span.regex_repeat_brace": {"color": "orangered"}, "span.regex_branch": {"color": "orangered"}, "span.regex_subpattern": {"color": "dodgerblue"}, "span.regex_in": {"color": "darkorange"}, "span.regex_category": {"color": "darkseagreen"}, "span.regex_repeat": {"color": "orangered"}, "span.regex_any": {"color": "orangered"}, "code.regex": {"font-size": "80%"}, "span.regex": {"font-weight": "bold"}, } tweaks_sphinx_panels_tabs_styles: MutableMapping[str, dict2css.Style] = { ".docutils.container": { "padding-left": (0, dict2css.IMPORTANT), "padding-right": (0, dict2css.IMPORTANT), }, "div.ui.top.attached.tabular.menu.sphinx-menu.docutils.container": { "margin-left": (0, dict2css.IMPORTANT), "margin-right": (0, dict2css.IMPORTANT), }, } def copy_asset_files(app: Sphinx, exception: Optional[Exception] = None) -> None: """ Copy additional stylesheets into the HTML build directory. :param app: The Sphinx application. :param exception: Any exception which occurred and caused Sphinx to abort. """ if exception: # pragma: no cover return if app.builder is None or app.builder.format.lower() != "html": # pragma: no cover return extensions_selector = ", ".join([ "p.sphinx-toolbox-extensions", "div.sphinx-toolbox-extensions.highlight-python", "div.sphinx-toolbox-extensions.highlight-python div.highlight", ]) rest_example_style = { "padding-left": "5px", "border-style": "dotted", "border-width": "1px", "border-color": "darkgray", } style: MutableMapping[str, dict2css.Style] = { "p.source-link": {"margin-bottom": 0}, "p.source-link + hr.docutils": {"margin-top": "10px"}, extensions_selector: {"margin-bottom": "10px"}, "div.rest-example.docutils.container": rest_example_style, **installation_styles, **shields_styles, **regex_styles, } css_static_dir = PathPlus(app.outdir) / "_static" / "css" css_static_dir.maybe_make(parents=True) dict2css.dump(style, css_static_dir / "sphinx-toolbox.css") @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox._css`. :param app: The Sphinx application. """ app.add_css_file("css/sphinx-toolbox.css") app.connect("build-finished", copy_asset_files) return {"parallel_read_safe": True} PK7Vq"sphinx_toolbox/_data_documenter.py#!/usr/bin/env python3 # # _data_documenter.py """ Relevant parts of :mod:`sphinx.ext.autodoc` from Sphinx 3.3.1. """ # # Based on Sphinx # Copyright (c) 2007-2020 by the Sphinx team. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib from typing import Any, Optional, get_type_hints # 3rd party from docutils.statemachine import StringList from sphinx.ext.autodoc import ( SUPPRESS, UNINITIALIZED_ATTR, ModuleDocumenter, ModuleLevelDocumenter, annotation_option ) from sphinx.util import logging from sphinx.util.inspect import object_description, safe_getattr try: # 3rd party from sphinx.util.typing import stringify_annotation as stringify_typehint # type: ignore[attr-defined] except ImportError: from sphinx.util.typing import stringify as stringify_typehint # this package from sphinx_toolbox.more_autodoc import _documenter_add_content __all__ = ("DataDocumenter", ) logger = logging.getLogger(__name__) class DataDocumenter(ModuleLevelDocumenter): """ Specialized Documenter subclass for data items. """ objtype = "data" member_order = 40 priority = -10 option_spec = dict(ModuleLevelDocumenter.option_spec) option_spec["annotation"] = annotation_option @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any) -> bool: """ Called to see if a member can be documented by this documenter. """ return isinstance(parent, ModuleDocumenter) and isattr def add_directive_header(self, sig: str) -> None: """ Add the directive header and options to the generated content. """ super().add_directive_header(sig) sourcename = self.get_sourcename() if not self.options.annotation: # obtain annotation for this data try: annotations = get_type_hints(self.parent) except NameError: # Failed to evaluate ForwardRef (maybe TYPE_CHECKING) annotations = safe_getattr(self.parent, "__annotations__", {}) except TypeError: annotations = {} except KeyError: # a broken class found (refs: https://github.com/sphinx-doc/sphinx/issues/8084) annotations = {} except AttributeError: # AttributeError is raised on 3.5.2 (fixed by 3.5.3) annotations = {} if self.objpath[-1] in annotations: objrepr = stringify_typehint(annotations.get(self.objpath[-1])) self.add_line(" :type: " + objrepr, sourcename) else: key = ('.'.join(self.objpath[:-1]), self.objpath[-1]) if self.analyzer and key in self.analyzer.annotations: self.add_line(" :type: " + self.analyzer.annotations[key], sourcename) try: if self.object is UNINITIALIZED_ATTR: pass else: objrepr = object_description(self.object) self.add_line(" :value: " + objrepr, sourcename) except ValueError: pass elif self.options.annotation is SUPPRESS: pass else: self.add_line(" :annotation: %s" % self.options.annotation, sourcename) def document_members(self, all_members: bool = False) -> None: # noqa: D102 pass def get_real_modname(self) -> str: """ Get the real module name of an object to document. It can differ from the name of the module through which the object was imported. """ return self.get_attr(self.parent or self.object, "__module__", None) or self.modname def add_content(self, more_content: Optional[StringList], no_docstring: bool = False) -> None: """ Add content from docstrings, attribute documentation and user. """ _documenter_add_content(self, more_content, no_docstring) PK7V;sphinx_toolbox/assets.py#!/usr/bin/env python3 # # assets.py """ Role to provide a link to open a file within the web browser, rather than downloading it. .. versionadded:: 0.5.0 .. extensions:: sphinx_toolbox.assets .. TODO:: Handle non-HTML builders Perhaps just put a message to see the online documentation? Usage ------ .. latex:vspace:: -10px .. rst:role:: asset Adds a link to a local file that can be viewed within the web browser. The file will be copied from the directory set in :confval:`assets_dir` to ``/_assets`` in the HTML output. This is similar to the :rst:role:`download` role, but that role will download the file to the user's computer instead. This role may be useful for PDFs, which most web browsers can display. If the file can't be found an error will be shown in the build output: .. code-block:: text : Asset file '' not found. **Asset** .. rest-example:: :asset:`hello_world.txt` :asset:`hello_world ` **Download** .. rest-example:: :download:`hello_world.txt <../assets/hello_world.txt>` Configuration --------------- .. confval:: assets_dir :type: :class:`str` :required: False :default: ``'./assets'`` The directory in which to find assets for the :rst:role:`asset` role. API Reference --------------- """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib import pathlib import posixpath import shutil from typing import Dict, List, Sequence, Tuple # 3rd party from docutils import nodes from docutils.nodes import system_message from docutils.parsers.rst.states import Inliner from domdf_python_tools.paths import PathPlus from domdf_python_tools.utils import stderr_writer from sphinx.application import Sphinx from sphinx.util import split_explicit_title from sphinx.writers.html import HTMLTranslator __all__ = ( "asset_role", "AssetNode", "visit_asset_node", "depart_asset_node", "setup", ) # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version class AssetNode(nodes.reference): """ Node representing a link to an asset. """ def asset_role( name: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: Dict = {}, content: List[str] = [] ) -> Tuple[Sequence[AssetNode], List[system_message]]: """ Adds a link to an asset. :param name: The local name of the interpreted role, the role name actually used in the document. :param rawtext: A string containing the entire interpreted text input, including the role and markup. :param text: The interpreted text content. :param lineno: The line number where the interpreted text begins. :param inliner: The :class:`docutils.parsers.rst.states.Inliner` object that called :func:`~.source_role`. It contains the several attributes useful for error reporting and document tree access. :param options: A dictionary of directive options for customization (from the ``role`` directive), to be interpreted by the function. Used for additional attributes for the generated elements and other functionality. :param content: A list of strings, the directive content for customization (from the ``role`` directive). To be interpreted by the function. :return: A list containing the created node, and a list containing any messages generated during the function. """ has_t, title, target = split_explicit_title(text) title = nodes.unescape(title) target = nodes.unescape(target) if not has_t: if target.startswith('~'): target = target[1:] title = pathlib.PurePosixPath(text[1:]).name app = inliner.document.settings.env.app base = app.config.assets_dir node = AssetNode(rawtext, title, refuri=target, source_file=PathPlus(base) / target, **options) return [node], [] def visit_asset_node(translator: HTMLTranslator, node: AssetNode) -> None: """ Visit an :class:`~.AssetNode`. :param translator: :param node: The node being visited. """ if not hasattr(translator, "_asset_node_seen_files"): # Files that have already been seen translator._asset_node_seen_files = [] # type: ignore[attr-defined] assets_out_dir = PathPlus(translator.builder.outdir) / "_assets" assets_out_dir.maybe_make(parents=True) source_file = PathPlus(translator.builder.confdir) / node["source_file"] source_file_exists = source_file.is_file() source_file_not_seen = source_file not in translator._asset_node_seen_files # type: ignore[attr-defined] if source_file_not_seen and source_file_exists: # Avoid unnecessary copies of potentially large files. translator._asset_node_seen_files.append(source_file) # type: ignore[attr-defined] shutil.copy2(source_file, assets_out_dir) elif not source_file_exists: stderr_writer( f"\x1b[31m{translator.builder.current_docname}: " f"Asset file '{source_file}' not found.\x1b[39m" ) translator.context.append('') return # Create the HTML current_uri = (pathlib.PurePosixPath('/') / translator.builder.current_docname).parent refuri = posixpath.relpath(f"/_assets/{node['refuri']}", str(current_uri)) translator.body.append(f'') translator.context.append("") def depart_asset_node(translator: HTMLTranslator, node: AssetNode) -> None: """ Depart an :class:`~.AssetNode`. :param translator: :param node: The node being visited. """ translator.body.append(translator.context.pop()) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.assets`. .. versionadded:: 1.0.0 :param app: The Sphinx application. """ app.add_role("asset", asset_role) app.add_config_value("assets_dir", "./assets", "env", [str]) app.add_node(AssetNode, html=(visit_asset_node, depart_asset_node)) return {"parallel_read_safe": True} PK7V7sphinx_toolbox/cache.py#!/usr/bin/env python3 # # cache.py """ Caching functions. """ # # Copyright (c) 2020 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib from datetime import timedelta # 3rd party from apeye.rate_limiter import HTTPCache __all__ = ("cache", ) #: HTTP Cache that caches requests for up to 4 hours. cache = HTTPCache("sphinx-toolbox", expires_after=timedelta(hours=4)) PK7VIAAsphinx_toolbox/changeset.py#!/usr/bin/env python3 # # changeset.py """ Customised versions of Sphinx's :rst:dir:`versionadded`, :rst:dir:`versionchanged` and :rst:dir:`deprecated` directives to correctly handle bullet lists. .. versionadded:: 2.11.0 .. extensions:: sphinx_toolbox.changeset Usage ------ .. rst:directive:: .. versionadded:: version Documents the version of the project which added the described feature. The first argument must be given and is the version in question; you can add a second argument consisting of a *brief* explanation of the change. Alternatively, a longer description my be given in the body of the directive. .. rst:directive:: .. versionchanged:: version Similar to :rst:dir:`versionadded`, but describes when and what changed in the feature in some way (new parameters, changed side effects, etc.). .. rst:directive:: .. deprecated:: version Similar to :rst:dir:`versionchanged`, but describes when the feature was deprecated. An explanation can also be given, for example to inform the reader what should be used instead. .. latex:vspace:: 10px This extension also adds the following directive: .. rst:directive:: .. versionremoved:: version [details] Similar to :rst:dir:`versionchanged`, but describes when the feature was or will be removed. An explanation can also be given, for example to inform the reader what should be used instead. .. latex:clearpage:: API Reference ---------------- """ # noqa: D400 # # Copyright © 2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Based on Sphinx # Copyright (c) 2007-2021 by the Sphinx team. # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib from typing import List, Optional, cast # 3rd party import sphinx.domains.changeset from docutils import nodes from docutils.nodes import Node from sphinx import addnodes from sphinx.application import Sphinx from sphinx.locale import _ # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ("VersionChange", "setup") versionlabels = { "versionremoved": _("Removed in version %s"), **sphinx.domains.changeset.versionlabels, } versionlabel_classes = { "versionremoved": "removed", **sphinx.domains.changeset.versionlabel_classes, } class VersionChange(sphinx.domains.changeset.VersionChange): """ Directive to describe a addition/change/deprecation/removal in a specific version. """ def run(self) -> List[Node]: """ Process the content of the directive. """ node = addnodes.versionmodified() node.document = self.state.document self.set_source_info(node) node["type"] = self.name node["version"] = self.arguments[0] text = versionlabels[self.name] % self.arguments[0] if len(self.arguments) == 2: inodes, messages = self.state.inline_text(self.arguments[1], self.lineno + 1) para = nodes.paragraph(self.arguments[1], '', *inodes, translatable=False) self.set_source_info(para) node.append(para) else: messages = [] # pylint: disable=W8301 if self.content: self.state.nested_parse(self.content, self.content_offset, node) classes = ["versionmodified", versionlabel_classes[self.name]] # pylint: disable=W8301 if len(node): to_add: Optional[nodes.Node] = None if isinstance(node[0], nodes.paragraph) and node[0].rawsource: content = nodes.inline(node[0].rawsource, translatable=True) content.source = node[0].source content.line = node[0].line content += node[0].children node[0].replace_self(nodes.paragraph('', '', content, translatable=False)) elif isinstance(node[0], (nodes.bullet_list, nodes.enumerated_list)): # Fix for incorrect ordering with bullet lists node.insert(0, nodes.compound('')) to_add = nodes.paragraph('', '') para = cast(nodes.paragraph, node[0]) para.insert(0, nodes.inline('', f'{text}: ', classes=classes)) if to_add is not None: node.insert(0, to_add) else: para = nodes.paragraph( '', '', nodes.inline('', f'{text}.', classes=classes), translatable=False, ) node.append(para) domain = cast( sphinx.domains.changeset.ChangeSetDomain, self.env.get_domain("changeset"), ) domain.note_changeset(node) ret: List[Node] = [node] # pylint: disable=W8301 ret += messages return ret @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.changeset`. :param app: The Sphinx application. """ app.add_directive("deprecated", VersionChange, override=True) app.add_directive("versionadded", VersionChange, override=True) app.add_directive("versionchanged", VersionChange, override=True) app.add_directive("versionremoved", VersionChange, override=True) return { "parallel_read_safe": True, "parallel_write_safe": True, } PK7V//sphinx_toolbox/code.py#!/usr/bin/env python3 # # code.py """ Customised ``.. code-block::`` directive with an adjustable indent size. .. extensions:: sphinx_toolbox.code Usage ------ .. rst:directive:: .. code-block:: [language] .. sourcecode:: [language] Customised ``.. code-block::`` directive with an adjustable indent size. .. rst:directive:option:: tab-width: width :type: integer Sets the size of the indentation in spaces. All other options from :rst:dir:`sphinx:code-block` are available, see the `Sphinx documentation`_ for details. .. _Sphinx documentation: https://www.sphinx-doc.org/en/3.x/usage/restructuredtext/directives.html#directive-code-block **Examples** .. rest-example:: .. code-block:: python def print(text): sys.stdout.write(text) .. rest-example:: .. code-block:: python :tab-width: 8 def print(text): sys.stdout.write(text) .. clearpage:: .. rst:directive:: .. code-cell:: [language] .. output-cell:: [language] Customised ``.. code-block::`` directives which display an execution count to the left of the code block, similar to a Jupyter Notebook cell. .. versionadded:: 2.6.0 .. rst:directive:option:: execution-count: count :type: positive integer The execution count of the cell. All other options from the :rst:dir:`code-block` directive above are available. **Examples** .. rest-example:: .. code-cell:: python :execution-count: 1 def print(text): sys.stdout.write(text) print("hello world") .. output-cell:: :execution-count: 1 hello world .. rest-example:: .. code-cell:: python :execution-count: 2 :tab-width: 8 def print(text): sys.stdout.write(text) .. seealso:: `nbsphinx `_, which inspired these directives and provides additional functionality for integrating Jupyter Notebooks with Sphinx. API Reference ---------------- """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Based on Sphinx # Copyright (c) 2007-2020 by the Sphinx team. # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib from typing import List, MutableMapping, Optional # 3rd party import dict2css import docutils.nodes import docutils.statemachine import sphinx.directives.code from docutils.nodes import Node from docutils.parsers.rst import directives from domdf_python_tools.paths import PathPlus from domdf_python_tools.stringlist import StringList from domdf_python_tools.utils import convert_indents from sphinx.application import Sphinx from sphinx.writers.html import HTMLTranslator from sphinx.writers.latex import LaTeXTranslator # this package from sphinx_toolbox.utils import Config, OptionSpec, SphinxExtMetadata, metadata_add_version __all__ = ( "CodeBlock", "CodeCell", "OutputCell", "Prompt", "visit_prompt_html", "visit_prompt_latex", "copy_asset_files", "configure", "setup", ) class CodeBlock(sphinx.directives.code.CodeBlock): """ Directive for a code block with special highlighting or line numbering settings. The indent_size can be adjusted with the ``:tab-width: `` option. .. autoclasssumm:: CodeBlock :autosummary-sections: ;; """ option_spec: OptionSpec = { # type: ignore[assignment] "force": directives.flag, "linenos": directives.flag, "tab-width": int, "dedent": int, "lineno-start": int, "emphasize-lines": directives.unchanged_required, "caption": directives.unchanged_required, "class": directives.class_option, "name": directives.unchanged, } def run(self) -> List[Node]: """ Process the content of the code block. """ code = '\n'.join(self.content) if "tab-width" in self.options: tab_width = self.options["tab-width"] else: tab_width = 4 code = convert_indents(code, tab_width=tab_width, from_=' ' * self.config.docutils_tab_width) self.content = docutils.statemachine.StringList(code.split('\n')) return super().run() class Prompt(docutils.nodes.General, docutils.nodes.FixedTextElement): """ Represents a cell prompt for a :class:`CodeCell` and :class:`OutputCell`. .. versionadded:: 2.6.0 """ class CodeCell(CodeBlock): """ Customised code block which displays an execution count to the left of the code block, similar to a Jupyter Notebook cell. The indent_size can be adjusted with the ``:tab-width: `` option. The execution count can be set using the ``:execution-count: `` option. .. autoclasssumm:: CodeCell :autosummary-sections: ;; .. versionadded:: 2.6.0 """ # noqa: D400 option_spec: OptionSpec = { **CodeBlock.option_spec, "execution-count": directives.positive_int, } _prompt: str = "In [%s]:" _class: str = "code-cell" def run(self) -> List[Node]: """ Process the content of the code block. """ self.options.setdefault("class", []) self.options["class"].append(f"{self._class}-code") prompt = self._prompt % self.options.get("execution-count", ' ') outer_node = docutils.nodes.container(classes=[self._class]) outer_node += Prompt( prompt, prompt, language="none", classes=["prompt", f"{self._class}-prompt"], ) outer_node += super().run()[0] return [outer_node] class OutputCell(CodeCell): """ Variant of :class:`~.CodeCell` for displaying the output of a cell in a Jupyter Notebook. The indent_size can be adjusted with the ``:tab-width: `` option. The execution count can be set using the ``:execution-count: `` option. .. versionadded:: 2.6.0 .. autoclasssumm:: OutputCell :autosummary-sections: ;; """ _prompt: str = "[%s]:" _class: str = "output-cell" def visit_prompt_html(translator: HTMLTranslator, node: Prompt) -> None: """ Visit a :class:`~.Prompt` node with the HTML translator. .. versionadded:: 2.6.0 :param translator: :param node: """ starttag = translator.starttag(node, "div", suffix='', CLASS="notranslate") translator.body.append(starttag + node.rawsource + '\n') raise docutils.nodes.SkipNode def visit_prompt_latex(translator: LaTeXTranslator, node: Prompt) -> None: """ Visit a :class:`~.Prompt` node with the LaTeX translator. .. versionadded:: 2.6.0 :param translator: :param node: """ translator.body.append("\n\n") translator.body.append(r"\vspace{4mm}") if f"code-cell-prompt" in node["classes"]: colour = "nbsphinxin" elif f"output-cell-prompt" in node["classes"]: colour = "nbsphinxout" else: # pragma: no cover colour = "black" translator.body.append( rf"\llap{{\color{{{colour}}}\texttt{{{node.rawsource}}}" r"\,\hspace{\fboxrule}\hspace{\fboxrule}\hspace{\fboxsep}}" ) translator.body.append(r"\vspace{-7mm}") raise docutils.nodes.SkipNode def copy_asset_files(app: Sphinx, exception: Optional[Exception] = None) -> None: """ Copy additional stylesheets into the HTML build directory. .. versionadded:: 2.6.0 :param app: The Sphinx application. :param exception: Any exception which occurred and caused Sphinx to abort. """ if exception: # pragma: no cover return if app.builder is None or app.builder.format.lower() != "html": # pragma: no cover return prompt_style: dict2css.Style = { "user-select": None, "font-size": "13px", "font-family": '"SFMono-Regular", Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace', "border": None, "padding": "11px 0 0", "margin": "0 5px 0 0", "box-shadow": None, "wrap-option": None, "white-space": "nowrap", } container_style: dict2css.Style = { "padding-top": "5px", "display": "flex", "align-items": "stretch", "margin": 0, } code_style_string = "div.code-cell.container div.code-cell-code, div.output-cell.container div.output-cell-code" code_style: dict2css.Style = { "width": "100%", "padding-top": 0, "margin-top": 0, } style: MutableMapping[str, dict2css.Style] = { "div.code-cell.container div.prompt": {"color": "#307FC1"}, "div.output-cell.container div.prompt": {"color": "#BF5B3D"}, "div.code-cell.container div.prompt, div.output-cell.container div.prompt": prompt_style, "div.code-cell.container, div.output-cell.container": container_style, code_style_string: code_style, } static_dir = PathPlus(app.outdir) / "_static" static_dir.maybe_make(parents=True) dict2css.dump(style, static_dir / "sphinx-toolbox-code.css") def configure(app: Sphinx, config: Config) -> None: """ Configure :mod:`sphinx_toolbox.code`. .. versionadded:: 2.9.0 :param app: The Sphinx application. :param config: """ latex_elements = getattr(config, "latex_elements", {}) latex_preamble = StringList(latex_elements.get("preamble", '')) latex_preamble.blankline() latex_preamble.append(r"\definecolor{nbsphinxin}{HTML}{307FC1}") latex_preamble.append(r"\definecolor{nbsphinxout}{HTML}{BF5B3D}") latex_elements["preamble"] = str(latex_preamble) config.latex_elements = latex_elements @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.code`. .. versionadded:: 1.0.0 :param app: The Sphinx application. """ # Code block with customisable indent size. app.add_directive("code-block", CodeBlock, override=True) app.add_directive("sourcecode", CodeBlock, override=True) app.add_directive("code-cell", CodeCell) app.add_directive("output-cell", OutputCell) # Hack to get the docutils tab size, as there doesn't appear to be any other way app.setup_extension("sphinx_toolbox.tweaks.tabsize") app.add_node( Prompt, html=(visit_prompt_html, lambda *args, **kwargs: None), latex=(visit_prompt_latex, lambda *args, **kwargs: None) ) app.connect("config-inited", configure) app.add_css_file("sphinx-toolbox-code.css") app.connect("build-finished", copy_asset_files) return {"parallel_read_safe": True} PK7VpDsphinx_toolbox/collapse.py#!/usr/bin/env python3 # # collapse.py r""" Adds a collapsible section to an HTML page using a details_ element. .. _details: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details> .. versionadded:: 2.5.0 .. extensions:: sphinx_toolbox.collapse Usage ------ .. rst:directive:: .. collapse:: [label] Adds a collapsible section to an HTML page using a details_ element. .. _details: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details> With non-HTML builders, the content will be added as-is. .. rest-example:: .. collapse:: Details Something small enough to escape casual notice. .. collapse:: A Different Label :class: custom-summary :name: summary0 Something else that might escape notice. .. collapse:: A long code block .. code-block:: python print("Not really") .. rst:directive:option:: open :type: flag The ``:open:`` option can be used to have the section open by default. .. versionadded:: 3.0.0 .. rest-example:: .. collapse:: Open :open: This section is open by default. API Reference ---------------- """ # # Copyright © 2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib from typing import Optional, Sequence # 3rd party from docutils import nodes from docutils.parsers.rst import directives from docutils.parsers.rst.roles import set_classes from domdf_python_tools.stringlist import DelimitedList from sphinx.application import Sphinx from sphinx.util.docutils import SphinxDirective from sphinx.writers.html import HTMLTranslator # this package from sphinx_toolbox.utils import SphinxExtMetadata, flag, metadata_add_version __all__ = ("CollapseDirective", "CollapseNode", "visit_collapse_node", "depart_collapse_node", "setup") class CollapseDirective(SphinxDirective): r""" A Sphinx directive to add a collapsible section to an HTML page using a details_ element. .. _details: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details> """ final_argument_whitespace: bool = True has_content: bool = True # The label required_arguments: int = 1 option_spec = { "class": directives.class_option, "name": directives.unchanged, "open": flag, } def run(self) -> Sequence[nodes.Node]: # type: ignore[override] """ Process the content of the directive. """ set_classes(self.options) self.assert_has_content() text = '\n'.join(self.content) label = self.arguments[0] collapse_node = CollapseNode(text, label, **self.options) self.add_name(collapse_node) collapse_node["classes"].append(f"summary-{nodes.make_id(label)}") self.state.nested_parse(self.content, self.content_offset, collapse_node) return [collapse_node] class CollapseNode(nodes.Body, nodes.Element): """ Node that represents a collapsible section. :param rawsource: :param label: """ def __init__(self, rawsource: str = '', label: Optional[str] = None, *children, **attributes): super().__init__(rawsource, *children, **attributes) self.label = label def visit_collapse_node(translator: HTMLTranslator, node: CollapseNode) -> None: """ Visit a :class:`~.CollapseNode`. :param translator: :param node: The node being visited. """ tag_parts = DelimitedList(["details"]) if node.get("names", None): names = DelimitedList(node["names"]) tag_parts.append(f'name="{names: }"') if node.get("classes", None): classes = DelimitedList(node["classes"]) tag_parts.append(f'class="{classes: }"') if node.attributes.get("open", False): tag_parts.append("open") translator.body.append(f"<{tag_parts: }>\n{node.label}") translator.context.append("") def depart_collapse_node(translator: HTMLTranslator, node: CollapseNode) -> None: """ Depart a :class:`~.CollapseNode`. :param translator: :param node: The node being visited. """ translator.body.append(translator.context.pop()) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.collapse`. :param app: The Sphinx application. """ app.add_directive("collapse", CollapseDirective) app.add_node( CollapseNode, html=(visit_collapse_node, depart_collapse_node), latex=(lambda *args, **kwargs: None, lambda *args, **kwargs: None) ) return { "parallel_read_safe": True, } PK7VKQsphinx_toolbox/config.py#!/usr/bin/env python3 # # config.py """ Internal configuration for ``sphinx-toolbox``. """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib from typing import List # 3rd party from apeye.requests_url import RequestsURL from sphinx.application import Sphinx # this package from sphinx_toolbox.utils import Config, add_nbsp_substitution __all__ = ("MissingOptionError", "InvalidOptionError", "validate_config", "ToolboxConfig") class MissingOptionError(ValueError): """ Subclass of :exc:`ValueError` to indicate a missing configuration option. """ class InvalidOptionError(ValueError): """ Subclass of :exc:`ValueError` to indicate an invalid value for a configuration value. """ class ToolboxConfig(Config): """ Subclass of :class:`sphinx.config.Config` with type annotations for the configuration values added by ``sphinx-toolbox``. Depending on the extensions enabled not all of these configuration values will be present. Functionally compatible with :class:`sphinx.config.Config`. """ # noqa: D400 source_link_target: str """ The target of the source link, either ``'github'`` or ``'sphinx'``. Will be lowercase after :func:`~.validate_config` has run. """ #: The username of the GitHub account that owns the repository this documentation corresponds to. github_username: str github_repository: str """ The GitHub repository this documentation corresponds to. .. clearpage:: """ #: The complete URL of the repository on GitHub. github_url: RequestsURL #: The base URL for the source code on GitHub. github_source_url: RequestsURL #: The base URL for the issues on GitHub. github_issues_url: RequestsURL #: The base URL for the pull requests on GitHub. github_pull_url: RequestsURL #: List of required Conda channels. conda_channels: List[str] #: The directory in which to find assets for the :rst:role:`asset` role. assets_dir: str docutils_tab_width: int """ The tab size used by docutils. This is usually 8 spaces, but can be configured in the ``docutils.conf`` file. """ #: The Wikipedia language to use for :rst:role:`wikipedia` roles. wikipedia_lang: str #: A string of reStructuredText that will be included at the beginning of every source file that is read. rst_prolog: str #: Document all :class:`typing.TypeVar`\s, even if they have no docstring. all_typevars: bool no_unbound_typevars: bool r""" Only document :class:`typing.TypeVar`\s that have a constraint of are bound. This option has no effect if :confval:`all_typevars` is False. """ def validate_config(app: Sphinx, config: ToolboxConfig) -> None: """ Validate the provided configuration values. See :class:`~sphinx_toolbox.config.ToolboxConfig` for a list of the configuration values. :param app: The Sphinx application. :param config: :type config: :class:`~sphinx.config.Config` """ # this package from sphinx_toolbox import github, source source._configure(app, config) github.validate_config(app, config) add_nbsp_substitution(config) PK7VxBsphinx_toolbox/confval.py#!/usr/bin/env python3 # # confval.py r""" The confval directive and role for configuration values. .. extensions:: sphinx_toolbox.confval Usage ------- .. rst:directive:: .. confval:: name Used to document a configuration value. .. raw:: latex \begin{multicols}{2} .. rst:directive:option:: type :type: string Indicates the configuration value's type. .. rst:directive:option:: required :type: flag Indicates the whether the configuration value is required. .. rst:directive:option:: default :type: string Indicates the default value. .. rst:directive:option:: noindex :type: flag Disables the index entry and cross-referencing for this configuration value. .. versionadded:: 2.11.0 .. raw:: latex \end{multicols} .. latex:vspace:: -0px .. rst:role:: confval Role which provides a cross-reference to a :rst:dir:`confval` directive. .. latex:vspace:: 10px **Examples:** .. rest-example:: .. confval:: demo :type: string :default: ``"Hello World"`` :required: False .. latex:vspace:: -20px .. rest-example:: To enable this feature set the :confval:`demo` configuration value to "True". .. latex:clearpage:: API Reference -------------- """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Based on https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/conf.py # Copyright (c) 2013-2018 Dave Snider, Read the Docs, Inc. & contributors # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib from typing import List # 3rd party from docutils.nodes import Node from docutils.parsers.rst import directives from docutils.statemachine import StringList from domdf_python_tools.utils import strtobool from sphinx.application import Sphinx from sphinx.domains import ObjType from sphinx.domains.std import GenericObject, StandardDomain from sphinx.errors import ExtensionError from sphinx.roles import XRefRole # this package from sphinx_toolbox.utils import OptionSpec, SphinxExtMetadata, flag, metadata_add_version __all__ = ("ConfigurationValue", "register_confval", "setup") class ConfigurationValue(GenericObject): """ The confval directive. .. versionchanged:: 1.1.0 The formatting of the type, required and default options can be customised using the ``self.format_*`` methods. .. versionchanged:: 2.11.0 Added the ``:noindex:`` option, which disables the index entry and cross-referencing for this configuration value. """ indextemplate: str = "%s (configuration value)" option_spec: OptionSpec = { # type: ignore[assignment] "type": directives.unchanged_required, "required": directives.unchanged_required, "default": directives.unchanged_required, "noindex": flag, } def run(self) -> List[Node]: """ Process the content of the directive. """ content: List[str] = [] if self.options and set(self.options.keys()) != {"noindex"}: content.extend(('', ".. raw:: latex", '', r" \vspace{-45px}", '')) if "type" in self.options: content.append(f"| **Type:** {self.format_type(self.options['type'])}") if "required" in self.options: content.append(f"| **Required:** ``{self.format_required(self.options['required'])}``") if "default" in self.options: content.append(f"| **Default:** {self.format_default(self.options['default'])}") if self.content: content.extend(( '', ".. raw:: latex", '', r" \vspace{-25px}", '', )) content.extend(self.content) self.content = StringList(content) return super().run() @staticmethod def format_type(the_type: str) -> str: """ Formats the ``:type:`` option. .. versionadded:: 1.1.0 :param the_type: """ return the_type @staticmethod def format_required(required: str) -> bool: """ Formats the ``:required:`` option. .. versionadded:: 1.1.0 :param required: """ return strtobool(required) @staticmethod def format_default(default: str) -> str: """ Formats the ``:default:`` option. .. versionadded:: 1.1.0 :param default: """ return default def register_confval(app: Sphinx, override: bool = False) -> None: """ Create and register the ``confval`` role and directive. :param app: The Sphinx application. :param override: """ if "std" not in app.registry.domains: app.add_domain(StandardDomain) # pragma: no cover name = "confval" app.registry.add_directive_to_domain("std", name, ConfigurationValue) app.registry.add_role_to_domain("std", name, XRefRole()) object_types = app.registry.domain_object_types.setdefault("std", {}) if name in object_types and not override: # pragma: no cover raise ExtensionError(f"The {name!r} object_type is already registered") object_types[name] = ObjType(name, name) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.confval`. .. versionadded:: 0.7.0 :param app: The Sphinx application. """ register_confval(app) return {"parallel_read_safe": True} PK7V%%sphinx_toolbox/decorators.py#!/usr/bin/env python3 # # decorators.py """ reStructuredText XRef role for decorators. .. versionadded:: 0.9.0 .. extensions:: sphinx_toolbox.decorators Usage ------ .. rst:role:: deco Adds a cross reference to a decorator, prefixed with an ``@``. .. rest-example:: .. decorator:: my_decorator A decorator. :deco:`my_decorator` :deco:`@my_decorator` :deco:`Title ` .. latex:clearpage:: API Reference ---------------- """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Based on Sphinx # Copyright (c) 2007-2020 by the Sphinx team. # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib from typing import Tuple, Type # noqa: F401 # 3rd party from docutils.nodes import Element from sphinx.application import Sphinx from sphinx.domains.python import PyXRefRole from sphinx.environment import BuildEnvironment # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ("PyDecoXRefRole", "setup") class PyDecoXRefRole(PyXRefRole): """ XRef Role for decorators members. """ def process_link( self, env: BuildEnvironment, refnode: Element, has_explicit_title: bool, title: str, target: str, ) -> Tuple[str, str]: """ Called after parsing title and target text, and creating the reference node (given in ``refnode``). This method can alter the reference node and must return a new (or the same) ``(title, target)`` tuple. :param env: :param refnode: :param has_explicit_title: :param title: :param target: """ target = target.lstrip('@') title, target = super().process_link( env=env, refnode=refnode, has_explicit_title=has_explicit_title, title=title, target=target, ) if not has_explicit_title and not title.startswith('@'): title = f"@{title}" # Ensure the reference is correctly found in the broader scope. refnode["reftype"] = "obj" return title, target @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.decorators`. :param app: The Sphinx application. """ app.add_role_to_domain("py", "deco", PyDecoXRefRole()) return {"parallel_read_safe": True} PK7VI'sphinx_toolbox/documentation_summary.py#!/usr/bin/env python3 # # documentation_summary.py """ Allows insertion of a summary line on the title page generated with the LaTeX builder, and at a custom location throughout the document. .. versionadded:: 2.2.0 .. extensions:: sphinx_toolbox.documentation_summary Configuration -------------- .. latex:vspace:: -10px .. confval:: documentation_summary :type: :class:`str` The documentation summary to display on the title page with the LaTeX builder, and at the location of :rst:dir:`documentation-summary` directives for other builders. If undefined no summary is shown. Usage ------ .. latex:vspace:: -10px .. rst:directive:: documentation-summary Adds the documentation summary as configured above. .. only:: html :bold-title:`Example` .. rest-example:: .. documentation-summary:: .. rst:directive:option:: meta Include the summary as a meta_ "description" tag in the HTML output. The structure of the description is ``{project} -- {summary}``, where ``project`` is configured in ``conf.py``. See `the sphinx documentation`_ for more information on the ``project`` option. .. versionadded:: 2.10.0 .. _meta: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta .. _the sphinx documentation: https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-project .. latex:clearpage:: API Reference ---------------- """ # noqa: D400 # # Copyright © 2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib from typing import List # 3rd party from docutils import nodes from docutils.statemachine import StringList from docutils.utils.smartquotes import educateQuotes from sphinx import addnodes from sphinx.application import Sphinx from sphinx.util.docutils import SphinxDirective # this package from sphinx_toolbox.utils import Config, Purger, SphinxExtMetadata, flag, metadata_add_version __all__ = ("DocumentationSummaryDirective", "configure", "setup") summary_node_purger = Purger("all_summary_nodes") RENEW = r""" \makeatletter \renewcommand{\py@release}{ \releasename\space\version \par \vspace{25pt} \textup{\thesummary} } \makeatother """ RESET = r"\makeatletter\renewcommand{\py@release}{\releasename\space\version}\makeatother" class DocumentationSummaryDirective(SphinxDirective): """ A Sphinx directive for creating a summary line. """ option_spec = {"meta": flag} def run(self) -> List[nodes.Node]: """ Process the content of the directive. """ summary = getattr(self.config, "documentation_summary", '').strip() if not summary: return [] # pragma: no cover # if self.env.app.builder.format.lower() == "latex" or not summary: # return [] targetid = f'documentation-summary-{self.env.new_serialno("documentation-summary"):d}' onlynode = addnodes.only(expr="html") content = f'**{summary}**' content_node = nodes.paragraph(rawsource=content, ids=[targetid]) onlynode += content_node self.state.nested_parse(StringList([content]), self.content_offset, content_node) summary_node_purger.add_node(self.env, content_node, content_node, self.lineno) if "meta" in self.options: meta_content = f'.. meta::\n :description: {self.config.project} -- {summary}\n' meta_node = nodes.paragraph(rawsource=meta_content, ids=[targetid]) onlynode += meta_node self.state.nested_parse( StringList(meta_content.split('\n')), self.content_offset, meta_node, ) summary_node_purger.add_node(self.env, meta_node, meta_node, self.lineno) return [onlynode] def configure(app: Sphinx, config: Config) -> None: """ Configure :mod:`sphinx_toolbox.documentation_summary`. :param app: The Sphinx application. :param config: """ if not hasattr(config, "latex_elements"): # pragma: no cover config.latex_elements = {} latex_elements = (config.latex_elements or {}) latex_preamble = latex_elements.get("preamble", '') summary = getattr(config, "documentation_summary", '').strip() if not summary: return # pragma: no cover # Escape latex special characters summary = summary.replace("~ ", r"\textasciitilde\space ") summary = summary.replace("^ ", r"\textasciicircum\space ") summary = summary.replace("\\ ", r"\textbackslash\space ") summary = summary.translate({ 35: r"\#", 36: r"\$", 37: r"\%", 38: r"\&", 94: r"\textasciicircum", 95: r"\_", 123: r"\{", 125: r"\}", 126: r"\textasciitilde", }) # TODO: escape backslashes without breaking the LaTeX commands summary_command = rf"\newcommand{{\thesummary}}{{{educateQuotes(summary)}}}" if summary_command not in latex_preamble: config.latex_elements["preamble"] = '\n'.join([ latex_preamble, summary_command, RENEW, ]) config.latex_elements["maketitle"] = '\n'.join([ config.latex_elements.get("maketitle", r"\sphinxmaketitle"), RESET, ]) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.documentation_summary`. :param app: The Sphinx application. """ app.connect("config-inited", configure, priority=550) app.add_directive("documentation-summary", DocumentationSummaryDirective) app.add_config_value("documentation_summary", None, "env", types=[str, None]) app.connect("env-purge-doc", summary_node_purger.purge_nodes) return {"parallel_read_safe": True} PK7VckGsphinx_toolbox/flake8.py#!/usr/bin/env python3 # # flake8.py r""" A Sphinx directive for documenting flake8 codes. .. versionadded:: 1.6.0 .. extensions:: sphinx_toolbox.flake8 Usage ------ .. rst:directive:: .. flake8-codes:: plugin Adds a table documenting a flake8 plugin's codes. The directive takes a single argument -- the fully qualified name of the flake8 plugin module. Codes to document are given in the body of the directive. :bold-title:`Example` .. rest-example:: .. flake8-codes:: flake8_dunder_all DALL000 .. latex:clearpage:: API Reference ---------------- """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib import warnings from typing import List, Sequence, Tuple # 3rd party import docutils import tabulate from docutils import nodes from docutils.statemachine import StringList from sphinx.application import Sphinx from sphinx.ext.autodoc.importer import import_module from sphinx.util.docutils import SphinxDirective # this package from sphinx_toolbox.utils import Purger, SphinxExtMetadata, metadata_add_version __all__ = ("Flake8CodesDirective", "setup") table_node_purger = Purger("all_flake8_code_table_nodes") class Flake8CodesDirective(SphinxDirective): """ A Sphinx directive for documenting flake8 codes. """ has_content: bool = True # the fully qualified name of the flake8 plugin module required_arguments: int = 1 def run(self) -> Sequence[nodes.Node]: # type: ignore[override] """ Process the content of the directive. """ plugin: str = self.arguments[0] if not self.content: warnings.warn("No codes specified") return [] module = import_module(plugin) codes: List[Tuple[str, str]] = [] for code in self.content: if code.strip(): if hasattr(module, code): description = getattr(module, code) if description.startswith(code): description = description[len(code):] codes.append((code, description.strip())) else: warnings.warn(f"No such code {code!r}") if not codes: warnings.warn("No codes specified") return [] targetid = f'flake8codes-{self.env.new_serialno("flake8codes"):d}' targetnode = nodes.section(ids=[targetid]) table = tabulate.tabulate(codes, headers=["Code", "Description"], tablefmt="rst") content = '\n' + table.replace('\t', " ") + '\n' view = StringList(content.split('\n')) table_node = nodes.paragraph(rawsource=content) self.state.nested_parse(view, self.content_offset, table_node) if docutils.__version_info__ >= (0, 18): table_node.children[0]["classes"] += ["colwidths-given"] # type: ignore[index] table_node_purger.add_node(self.env, table_node, targetnode, self.lineno) return [table_node] @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.flake8`. :param app: The Sphinx application. """ app.add_directive("flake8-codes", Flake8CodesDirective) app.connect("env-purge-doc", table_node_purger.purge_nodes) return {"parallel_read_safe": True} PK7Vsphinx_toolbox/formatting.py#!/usr/bin/env python3 # # formatting.py """ Directives, roles and nodes for text formatting. .. versionadded:: 0.2.0 .. extensions:: sphinx_toolbox.formatting .. latex:vspace:: -10px Usage ------- .. rst:role:: iabbr An abbreviation. If the role content contains a parenthesized explanation, it will be treated specially: it will be shown in a tool-tip in HTML, and output only once in LaTeX. Unlike Sphinx's :rst:role:`abbr` role, this one shows the abbreviation in italics. .. versionadded:: 0.2.0 :bold-title:`Example` .. rest-example:: :iabbr:`LIFO (last-in, first-out)` .. rst:role:: bold-title Role for displaying a pseudo title in bold. This is useful for breaking up Python docstrings. .. versionadded:: 2.12.0 :bold-title:`Example` .. rest-example:: :bold-title:`Examples:` :bold-title:`Other Extensions` API Reference --------------- """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Parts of the docstrings based on https://docutils.sourceforge.io/docs/howto/rst-roles.html # # stdlib from typing import List, Tuple # 3rd party from docutils import nodes from docutils.nodes import Node, system_message from docutils.parsers.rst import roles from sphinx.application import Sphinx from sphinx.roles import Abbreviation from sphinx.util.docutils import SphinxRole from sphinx.writers.html import HTMLTranslator from sphinx.writers.latex import LaTeXTranslator # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ( "ItalicAbbreviationNode", "ItalicAbbreviation", "visit_iabbr_node", "depart_iabbr_node", "latex_visit_iabbr_node", "latex_depart_iabbr_node", "setup" ) class ItalicAbbreviationNode(nodes.abbreviation): """ Docutils Node to show an abbreviation in italics. """ class ItalicAbbreviation(Abbreviation): """ Docutils role to show an abbreviation in italics. """ def run(self) -> Tuple[List[Node], List[system_message]]: """ Process the content of the italic abbreviation role. """ options = self.options.copy() matched = self.abbr_re.search(self.text) if matched: text = self.text[:matched.start()].strip() options["explanation"] = matched.group(1) else: text = self.text return [ItalicAbbreviationNode(self.rawtext, text, **options)], [] class BoldTitle(SphinxRole): """ Role for displaying a pseudo title in bold. This is useful for breaking up Python docstrings. """ def run(self) -> Tuple[List[nodes.Node], List[nodes.system_message]]: assert self.text is not None node_list: List[nodes.Node] = [ nodes.raw('', r"\vspace{10px}", format="latex"), nodes.strong(f"**{self.text}**", self.text), ] return node_list, [] def visit_iabbr_node(translator: HTMLTranslator, node: ItalicAbbreviationNode) -> None: """ Visit an :class:`~.ItalicAbbreviationNode`. :param translator: :param node: The node being visited. """ translator.body.append('') attrs = {} if node.hasattr("explanation"): attrs["title"] = node["explanation"] translator.body.append(translator.starttag(node, "abbr", '', **attrs)) def depart_iabbr_node(translator: HTMLTranslator, node: ItalicAbbreviationNode) -> None: """ Depart an :class:`~.ItalicAbbreviationNode`. :param translator: :param node: The node being visited. """ translator.body.append("") def latex_visit_iabbr_node(translator: LaTeXTranslator, node: ItalicAbbreviationNode) -> None: """ Visit an :class:`~.ItalicAbbreviationNode`. :param translator: :param node: The node being visited. """ abbr = node.astext() translator.body.append(r"\textit{\sphinxstyleabbreviation{") # spell out the explanation once if node.hasattr("explanation") and abbr not in translator.handled_abbrs: translator.context.append(f'}} ({translator.encode(node["explanation"])})') translator.handled_abbrs.add(abbr) else: translator.context.append('}') def latex_depart_iabbr_node(translator: LaTeXTranslator, node: ItalicAbbreviationNode) -> None: """ Depart an :class:`~.ItalicAbbreviationNode`. :param translator: :param node: The node being visited. """ translator.body.append(translator.context.pop()) translator.body.append('}') @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.formatting`. :param app: The Sphinx application. """ roles.register_local_role("iabbr", ItalicAbbreviation()) app.add_role("bold-title", BoldTitle()) app.add_node( ItalicAbbreviationNode, html=(visit_iabbr_node, depart_iabbr_node), latex=(latex_visit_iabbr_node, latex_depart_iabbr_node), ) return {"parallel_read_safe": True} PK7V=gFWFWsphinx_toolbox/installation.py#!/usr/bin/env python3 # # installation.py r""" .. extensions:: sphinx_toolbox.installation Configuration -------------- .. confval:: conda_channels :type: :class:`~typing.List`\[:class:`str`\] :required: False :default: ``[]`` The conda channels required to install the library from Anaconda. An alternative to setting it within the :rst:dir:`installation` directive. Usage ------- .. rst:directive:: .. installation:: name Adds a series of tabs providing installation instructions for the project from a number of sources. The directive takes a single required argument -- the name of the project. If the project uses a different name on PyPI and/or Anaconda, the ``:pypi-name:`` and ``:conda-name:`` options can be used to set the name for those repositories. .. rst:directive:option:: pypi :type: flag Flag to indicate the project can be installed from PyPI. .. rst:directive:option:: pypi-name: name :type: string The name of the project on PyPI. .. rst:directive:option:: conda :type: flag Flag to indicate the project can be installed with Conda. .. rst:directive:option:: conda-name: name :type: string The name of the project on Conda. .. rst:directive:option:: conda-channels: channels :type: comma separated strings Comma-separated list of required Conda channels. This can also be set via the :confval:`conda_channels` option. .. rst:directive:option:: github :type: flag Flag to indicate the project can be installed from GitHub. To use this option add the following to your ``conf.py``: .. code-block:: python extensions = [ ... 'sphinx_toolbox.github', ] github_username = '' github_repository = '' See :mod:`sphinx_toolbox.github` for more information. :bold-title:`Example` .. rest-example:: .. installation:: sphinx-toolbox :pypi: :anaconda: :conda-channels: domdfcoding,conda-forge :github: .. rst:directive:: extensions Shows instructions on how to enable a Sphinx extension. The directive takes a single argument -- the name of the extension. .. rst:directive:option:: import-name :type: string The name used to import the extension, if different from the name of the extension. .. rst:directive:option:: no-preamble :type: flag Disables the preamble text. .. rst:directive:option:: no-postamble :type: flag Disables the postamble text. .. rst:directive:option:: first :type: flag Puts the entry for extension before its dependencies. By default is is placed at the end. .. versionadded:: 0.4.0 :bold-title:`Example` .. rest-example:: .. extensions:: sphinx-toolbox :import-name: sphinx_toolbox sphinx.ext.viewcode sphinx_tabs.tabs sphinx-prompt API Reference -------------- """ # noqa: D400 # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib import inspect import re import textwrap import warnings from typing import Any, Callable, Dict, List, Optional, Tuple # 3rd party import dict2css import sphinx.environment from docutils import nodes from docutils.parsers.rst import directives from docutils.statemachine import ViewList from domdf_python_tools.paths import PathPlus from domdf_python_tools.stringlist import StringList from domdf_python_tools.words import word_join from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment from sphinx.util.docutils import SphinxDirective # this package from sphinx_toolbox import _css from sphinx_toolbox.utils import Config, OptionSpec, Purger, SphinxExtMetadata, flag, metadata_add_version __all__ = ( "InstallationDirective", "ExtensionsDirective", "make_installation_instructions", "Sources", "sources", "pypi_installation", "conda_installation", "github_installation", "installation_node_purger", "extensions_node_purger", "copy_asset_files", "setup", ) class _Purger(Purger): def purge_nodes(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None: # pragma: no cover """ Remove all redundant nodes. :param app: The Sphinx application. :param env: The Sphinx build environment. :param docname: The name of the document to remove nodes for. """ if not hasattr(env, self.attr_name): return setattr(env, self.attr_name, []) installation_node_purger = _Purger("all_installation_node_nodes") extensions_node_purger = Purger("all_extensions_node_nodes") class Sources(List[Tuple[str, str, Callable, Callable, Optional[Dict[str, Callable]]]]): """ Class to store functions that provide installation instructions for different sources. The syntax of each entry is: .. code-block:: python (option_name, source_name, getter_function, validator_function, extra_options) * ``option_name`` -- a string to use in the directive to specify the source to use, * ``source_name`` -- a string to use in the tabs to indicate the installation source, * ``getter_function`` -- the function that returns the installation instructions, * ``validator_function`` -- a function to validate the option value provided by the user, * ``extra_options`` -- a mapping of additional options for the directive that are used by the getter_function. .. autoclasssumm:: Sources :autosummary-sections: ;; """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) _args = ["options", "env"] # pylint: disable=W8301 _directive_name = "installation" def register( self, option_name: str, source_name: str, validator: Callable = directives.unchanged, extra_options: Optional[Dict[str, Callable]] = None, ) -> Callable: """ Decorator to register a function. The function must have the following signature: .. code-block:: python def function( options: Dict[str, Any], # Mapping of option names to values. env: sphinx.environment.BuildEnvironment, # The Sphinx build environment. ) -> List[str]: ... :param option_name: A string to use in the directive to specify the source to use. :param source_name: A string to use in tabbed installation instructions to represent this source. :param validator: A function to validate the option value provided by the user. :default validator: :func:`docutils.parsers.rst.directives.unchanged` :param extra_options: An optional mapping of extra option names to validator functions. :default extra_options: ``{}`` :return: The registered function. :raises: :exc:`SyntaxError` if the decorated function does not take the correct arguments. """ def _decorator(function: Callable) -> Callable: signature = inspect.signature(function) if list(signature.parameters.keys()) != self._args: raise SyntaxError( # pragma: no cover "The decorated function must take only the following arguments: " f"{word_join(self._args, use_repr=True, oxford=True)}" ) self.append((option_name, source_name, function, validator, extra_options or {})) setattr(function, f"_{self._directive_name}_registered", True) return function return _decorator #: Instance of :class:`~.Sources`. sources: Sources = Sources() # pypi_name: The name of the project on PyPI. @sources.register("pypi", "PyPI", flag, {"pypi-name": directives.unchanged}) def pypi_installation( options: Dict[str, Any], env: sphinx.environment.BuildEnvironment, ) -> List[str]: """ Source to provide instructions for installing from PyPI. :param options: Mapping of option names to values. :param env: The Sphinx build environment. """ if "pypi-name" in options: pypi_name = options["pypi-name"] elif "project_name" in options: pypi_name = options["project_name"] else: raise ValueError("No PyPI project name supplied for the PyPI installation instructions.") return [".. prompt:: bash", '', f" python3 -m pip install {pypi_name} --user"] # conda_name: The name of the project on PyPI. @sources.register( "anaconda", "Anaconda", flag, {"conda-name": directives.unchanged, "conda-channels": directives.unchanged}, ) def conda_installation( options: Dict[str, Any], env: sphinx.environment.BuildEnvironment, ) -> List[str]: """ Source to provide instructions for installing from Anaconda. :param options: Mapping of option names to values. :param env: The Sphinx build environment. """ if "conda-name" in options: conda_name = options["conda-name"] elif "pypi-name" in options: conda_name = options["pypi-name"] elif "project_name" in options: conda_name = options["project_name"] else: raise ValueError("No username supplied for the Anaconda installation instructions.") lines: StringList = StringList() lines.indent_type = " " if "conda-channels" in options: channels = str(options["conda-channels"]).split(',') else: channels = env.config.conda_channels if channels: lines.append("First add the required channels\n\n.. prompt:: bash\n") with lines.with_indent_size(lines.indent_size + 1): for channel in channels: # pylint: disable=use-list-copy lines.append(f"conda config --add channels https://conda.anaconda.org/{channel.strip()}") lines.append("\nThen install") if lines: lines.blankline(ensure_single=True) lines.append(f".. prompt:: bash") lines.blankline(ensure_single=True) with lines.with_indent_size(lines.indent_size + 1): lines.append(f"conda install {conda_name}") lines.blankline(ensure_single=True) return list(lines) @sources.register("github", "GitHub", flag) def github_installation( options: Dict[str, Any], env: sphinx.environment.BuildEnvironment, ) -> List[str]: """ Source to provide instructions for installing from GitHub. :param options: Mapping of option names to values. :param env: The Sphinx build environment. """ if "sphinx_toolbox.github" not in env.app.extensions: raise ValueError( "The 'sphinx_toolbox.github' extension is required for the " ":github: option but it is not enabled!" ) username = getattr(env.config, "github_username", None) if username is None: raise ValueError("'github_username' has not been set in 'conf.py'!") repository = getattr(env.config, "github_repository", None) if repository is None: raise ValueError("'github_repository' has not been set in 'conf.py'!") return [ ".. prompt:: bash", '', f" python3 -m pip install git+https://github.com/{username}/{repository}@master --user" ] class InstallationDirective(SphinxDirective): """ Directive to show installation instructions. """ has_content: bool = True optional_arguments: int = 1 # The name of the project; can be overridden for each source # Registered sources option_spec: OptionSpec = { # type: ignore[assignment] source[0].lower(): source[3] for source in sources # pylint: disable=not-an-iterable } # Extra options for registered sources for source in sources: # pylint: disable=not-an-iterable,loop-global-usage if source[4] is not None: option_spec.update(source[4]) # type: ignore[attr-defined] options: Dict[str, Any] """ Mapping of option names to values. The options are as follows: * **pypi**: Flag to indicate the project can be installed from PyPI. * **pypi-name**: The name of the project on PyPI. * **conda**: Flag to indicate the project can be installed with Conda. * **conda-name**: The name of the project on Conda. * **conda-channels**: Comma-separated list of required Conda channels. * **github**: Flag to indicate the project can be installed from GitHub. The GitHub username and repository are configured in ``conf.py`` and are available in ``env.config``. """ def run_html(self) -> List[nodes.Node]: """ Generate output for ``HTML`` builders. :rtype: .. latex:clearpage:: """ targetid = f'installation-{self.env.new_serialno("sphinx-toolbox installation"):d}' targetnode = nodes.target('', '', ids=[targetid]) content = make_installation_instructions(self.options, self.env) view = ViewList(content) installation_node = nodes.paragraph(rawsource=content) # type: ignore[arg-type] self.state.nested_parse(view, self.content_offset, installation_node) # type: ignore[arg-type] installation_node_purger.add_node(self.env, installation_node, targetnode, self.lineno) return [targetnode, installation_node] def run_generic(self) -> List[nodes.Node]: """ Generate generic reStructuredText output. """ targetid = f'installation-{self.env.new_serialno("sphinx-toolbox installation"):d}' targetnode = nodes.target('', '', ids=[targetid]) tabs: Dict[str, List[str]] = _get_installation_instructions(self.options, self.env) if not tabs: warnings.warn("No installation source specified. No installation instructions will be shown.") return [] nodes_to_return: List[nodes.Node] = [targetnode] non_word_sub = re.compile(r'\W+') for tab_name, tab_content in tabs.items(): # pylint: disable=loop-global-usage section_id = non_word_sub.sub('_', tab_name) section = nodes.section(ids=[f"{targetid}-{section_id}"]) section += nodes.title(tab_name, tab_name) nodes_to_return.append(section) installation_node_purger.add_node(self.env, section, targetnode, self.lineno) view = ViewList(tab_content) paragraph_node = nodes.paragraph(rawsource=tab_content) # type: ignore[arg-type] self.state.nested_parse(view, self.content_offset, paragraph_node) # type: ignore[arg-type] nodes_to_return.append(paragraph_node) installation_node_purger.add_node(self.env, paragraph_node, targetnode, self.lineno) # pylint: enable=loop-global-usage return nodes_to_return def run(self) -> List[nodes.Node]: """ Create the installation node. """ assert self.env.app.builder is not None if self.arguments: self.options["project_name"] = self.arguments[0] if self.env.app.builder.format.lower() == "html": return self.run_html() else: return self.run_generic() def make_installation_instructions(options: Dict[str, Any], env: BuildEnvironment) -> List[str]: """ Make the content of an installation node. :param options: :param env: The Sphinx build environment. """ tabs: Dict[str, List[str]] = _get_installation_instructions(options, env) if not tabs: warnings.warn("No installation source specified. No installation instructions will be shown.") return [] content = StringList([".. tabs::", '']) content.set_indent_type(" ") for tab_name, tab_content in tabs.items(): with content.with_indent_size(1): content.append(f".. tab:: {tab_name}") content.blankline(ensure_single=True) with content.with_indent_size(2): content.extend([f"{line}" if line else '' for line in tab_content]) # pylint: disable=loop-invariant-statement return list(content) def _get_installation_instructions(options: Dict[str, Any], env: BuildEnvironment) -> Dict[str, List[str]]: """ Returns a mapping of tab/section names to their content. :param options: :param env: The Sphinx build environment. """ tabs: Dict[str, List[str]] = {} # pylint: disable=loop-global-usage,use-dict-comprehension for option_name, source_name, getter_function, validator_function, extra_options in sources: # pylint: enable=loop-global-usage,use-dict-comprehension if option_name in options: tabs[f"from {source_name}"] = getter_function(options, env) return tabs class ExtensionsDirective(SphinxDirective): """ Directive to show instructions for enabling the extension. """ has_content: bool = True # Other required extensions, one per line optional_arguments: int = 1 # The name of the project option_spec: OptionSpec = { # type: ignore[assignment] "import-name": directives.unchanged_required, # If different to project name "no-preamble": flag, "no-postamble": flag, "first": flag, } def run(self) -> List[nodes.Node]: """ Create the extensions node. :rtype: .. latex:clearpage:: """ extensions = list(self.content) first = self.options.get("first", False) if "import-name" in self.options and first: extensions.insert(0, self.options["import-name"]) elif "import-name" in self.options: extensions.append(self.options["import-name"]) elif first: extensions.insert(0, self.arguments[0]) else: extensions.append(self.arguments[0]) targetid = f'extensions-{self.env.new_serialno("sphinx-toolbox extensions"):d}' targetnode = nodes.target('', '', ids=[targetid]) top_text = ( ".. latex:vspace:: 10px", ".. rst-class:: sphinx-toolbox-extensions", '', f" Enable ``{self.arguments[0]}`` by adding the following", f" to the ``extensions`` variable in your ``conf.py``:", ) bottom_text = textwrap.dedent( r""" .. raw:: latex \begin{flushleft} For more information see https://www.sphinx-doc.org/en/master/usage/extensions#third-party-extensions . .. raw:: latex \end{flushleft} """ ).expandtabs(4).splitlines() if "no-preamble" in self.options: content = [] else: content = [*top_text, ''] content.append(".. code-block:: python", ) if "no-postamble" not in self.options: content.append(" :class: sphinx-toolbox-extensions") content.extend([ '', " extensions = [", " ...", ]) for extension in extensions: # pylint: disable=use-list-copy content.append(f" {extension!r},") content.extend([" ]", '']) if "no-postamble" not in self.options: content.extend(bottom_text) extensions_node = nodes.paragraph(rawsource=content) # type: ignore[arg-type] self.state.nested_parse(ViewList(content), self.content_offset, extensions_node) # type: ignore[arg-type] extensions_node_purger.add_node(self.env, extensions_node, targetnode, self.lineno) return [targetnode, extensions_node] def copy_asset_files(app: Sphinx, exception: Optional[Exception] = None) -> None: """ Copy additional stylesheets into the HTML build directory. .. versionadded:: 1.2.0 :param app: The Sphinx application. :param exception: Any exception which occurred and caused Sphinx to abort. """ if exception: # pragma: no cover return if app.builder is None or app.builder.format.lower() != "html": # pragma: no cover return static_dir = PathPlus(app.outdir) / "_static" static_dir.maybe_make(parents=True) dict2css.dump(_css.installation_styles, static_dir / "sphinx_toolbox_installation.css", minify=True) (static_dir / "sphinx_toolbox_installation.js").write_lines([ "// Based on https://github.com/executablebooks/sphinx-tabs/blob/master/sphinx_tabs/static/tabs.js", "// Copyright (c) 2017 djungelorm", "// MIT Licensed", '', "function deselectTabset(target) {", " const parent = target.parentNode;", " const grandparent = parent.parentNode;", '', ' if (parent.parentNode.parentNode.getAttribute("id").startsWith("installation")) {', '', " // Hide all tabs in current tablist, but not nested", " Array.from(parent.children).forEach(t => {", ' if (t.getAttribute("name") !== target.getAttribute("name")) {', ' t.setAttribute("aria-selected", "false");', " }", " });", '', " // Hide all associated panels", " Array.from(grandparent.children).slice(1).forEach(p => { // Skip tablist", ' if (p.getAttribute("name") !== target.getAttribute("name")) {', ' p.setAttribute("hidden", "false")', " }", " });", " }", '', " else {", " // Hide all tabs in current tablist, but not nested", " Array.from(parent.children).forEach(t => {", ' t.setAttribute("aria-selected", "false");', " });", '', " // Hide all associated panels", " Array.from(grandparent.children).slice(1).forEach(p => { // Skip tablist", ' p.setAttribute("hidden", "true")', " });", " }", '', '}', '', "// Compatibility with sphinx-tabs 2.1.0 and later", "function deselectTabList(tab) {deselectTabset(tab)}", '', ]) def _on_config_inited(app: Sphinx, config: Config) -> None: app.add_css_file("sphinx_toolbox_installation.css") app.add_js_file("sphinx_toolbox_installation.js") @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.installation`. .. versionadded:: 0.7.0 :param app: The Sphinx application. """ if "sphinx_inline_tabs" not in getattr(app, "extensions", ()): app.setup_extension("sphinx_tabs.tabs") app.setup_extension("sphinx-prompt") app.setup_extension("sphinx_toolbox._css") app.setup_extension("sphinx_toolbox.latex") app.add_config_value("conda_channels", [], "env", types=[list]) # Instructions for installing a python package app.add_directive("installation", InstallationDirective) app.connect("env-get-outdated", installation_node_purger.get_outdated_docnames) # app.connect("env-purge-doc", installation_node_purger.purge_nodes) # Instructions for enabling a sphinx extension app.add_directive("extensions", ExtensionsDirective) app.connect("env-purge-doc", extensions_node_purger.purge_nodes) # Ensure this happens after tabs.js has been added app.connect("config-inited", _on_config_inited, priority=510) app.connect("build-finished", copy_asset_files) return {"parallel_read_safe": True} PK7V`sphinx_toolbox/issues.py#!/usr/bin/env python3 # # issues.py r""" Add links to GitHub issues and Pull Requests. .. extensions:: sphinx_toolbox.issues Usage ------ .. latex:vspace:: -10px .. rst:role:: issue Role which shows a link to the given issue on GitHub. If the issue exists, the link has a tooltip that shows the title of the issue. **Example** .. rest-example:: :issue:`1` You can also reference an issue in a different repository by adding the repository name inside ``<>``. .. rest-example:: :issue:`7680 ` .. rst:role:: pull Role which shows a link to the given pull request on GitHub. If the pull requests exists, the link has a tooltip that shows the title of the pull requests. **Example** .. rest-example:: :pull:`2` You can also reference a pull request in a different repository by adding the repository name inside ``<>``. .. rest-example:: :pull:`7671 ` .. versionchanged:: 2.4.0 :rst:role:`issue` and :rst:role:`pull` now show the repository name when the name differs from that configured in ``conf.py``. .. versionchanged:: 2.4.0 These directives are also available in the :mod:`~.sphinx_toolbox.github` domain. The only difference between the :rst:role:`issue` and :rst:role:`pull` roles is in the URL. GitHub uses the same numbering scheme for issues and pull requests, and automatically redirects to the pull request if the user tries to navigate to an issue with that same number. Caching ----------- HTTP requests to obtain issue/pull request titles are cached for four hours. To clear the cache manually, run: .. prompt:: bash python3 -m sphinx_toolbox API Reference --------------- .. versionchanged:: 2.4.0 The following moved to :mod:`sphinx_toolbox.github.issues`: * :class:`~.IssueNode` * :class:`~.IssueNodeWithName` * :func:`~.issue_role` * :func:`~.pull_role` * :func:`~.visit_issue_node` * :func:`~.depart_issue_node` * :func:`~.get_issue_title` .. latex:vspace:: 10px """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Based on pyspecific.py from the Python documentation. # Copyright 2008-2014 by Georg Brandl. # Licensed under the PSF License 2.0 # # Parts of the docstrings based on https://docutils.sourceforge.io/docs/howto/rst-roles.html # # 3rd party from sphinx.application import Sphinx # this package from sphinx_toolbox.github.issues import issue_role, pull_role from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ( "issue_role", "pull_role", "setup", ) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.issues`. .. versionadded:: 1.0.0 :param app: The Sphinx application. """ app.setup_extension("sphinx_toolbox.github") # Link to GH issue app.add_role("issue", issue_role) # Link to GH pull request app.add_role("pr", pull_role) app.add_role("pull", pull_role) return {"parallel_read_safe": True} PK7VF)F)sphinx_toolbox/pre_commit.py#!/usr/bin/env python3 # # pre_commit.py """ Sphinx extension to show examples of ``.pre-commit-config.yaml`` configuration. .. versionadded:: 1.6.0 .. extensions:: sphinx_toolbox.pre_commit Usage ------ .. rst:directive:: pre-commit Directive which shows an example snippet of ``.pre-commit-config.yaml``. .. rst:directive:option:: rev :type: string The revision or tag to clone at. .. rst:directive:option:: hooks :type: comma separated list A list of hooks IDs to document. If not given the hooks will be parsed from ``.pre-commit-hooks.yaml``. .. rst:directive:option:: args :type: comma separated list A list arguments that should or can be provided to the first hook ID. .. versionadded:: 1.7.2 :bold-title:`Example` .. rest-example:: .. pre-commit:: :rev: v0.0.4 :hooks: some-hook,some-other-hook .. clearpage:: .. rst:directive:: .. pre-commit:flake8:: version Directive which shows an example snippet of ``.pre-commit-config.yaml`` for a flake8 plugin. The directive takes a single argument -- the version of the flake8 plugin to install from PyPI. .. rst:directive:option:: flake8-version :type: string The version of flake8 to use. Default ``3.8.4``. .. rst:directive:option:: plugin-name :type: string The name of the plugin to install from PyPI. Defaults to the repository name. :bold-title:`Example` .. rest-example:: .. pre-commit:flake8:: 0.0.4 .. versionchanged:: 2.8.0 The repository URL now points to GitHub. API Reference ---------------- """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib import re import warnings from io import StringIO from textwrap import indent from typing import Any, List, Sequence # 3rd party import sphinx.util.docutils from docutils import nodes from docutils.statemachine import StringList from domdf_python_tools.paths import PathPlus from ruamel.yaml import YAML from sphinx.application import Sphinx from sphinx.util.docutils import SphinxDirective from typing_extensions import TypedDict # this package from sphinx_toolbox.utils import Purger, SphinxExtMetadata, make_github_url, metadata_add_version __all__ = ( "pre_commit_node_purger", "pre_commit_f8_node_purger", "parse_hooks", "PreCommitDirective", "Flake8PreCommitDirective", "setup", ) pre_commit_node_purger = Purger("all_pre_commit_nodes") pre_commit_f8_node_purger = Purger("all_pre_commit_f8_nodes") def parse_hooks(hooks: str) -> List[str]: """ Parses the comma, semicolon and/or space delimited list of hook IDs. :param hooks: """ return list(filter(None, re.split("[,; ]", hooks))) class _BaseHook(TypedDict): id: str # noqa: A003 # pylint: disable=redefined-builtin class _Hook(_BaseHook, total=False): args: List[str] class _BaseConfig(TypedDict): repo: str class _Config(_BaseConfig, total=False): rev: str hooks: List[_Hook] class PreCommitDirective(SphinxDirective): """ A Sphinx directive for documenting pre-commit hooks. .. clearpage:: """ has_content: bool = False option_spec = { "rev": str, # the revision or tag to clone at. "hooks": parse_hooks, "args": parse_hooks, } def run(self) -> Sequence[nodes.Node]: # type: ignore[override] """ Process the content of the directive. """ if "hooks" in self.options: hooks = self.options["hooks"] else: cwd = PathPlus.cwd() for directory in (cwd, *cwd.parents): hook_file = directory / ".pre-commit-hooks.yaml" if hook_file.is_file(): hooks_dict = YAML(typ="safe", pure=True).load(hook_file.read_text()) hooks = [h["id"] for h in hooks_dict] # pylint: disable=loop-invariant-statement break else: warnings.warn("No hooks specified and no .pre-commit-hooks.yaml file found.") return [] repo = make_github_url(self.env.config.github_username, self.env.config.github_repository) config: _Config = {"repo": str(repo)} if "rev" in self.options: config["rev"] = self.options["rev"] config["hooks"] = [{"id": hook_name} for hook_name in hooks] if "args" in self.options: config["hooks"][0]["args"] = self.options["args"] targetid = f'pre-commit-{self.env.new_serialno("pre-commit"):d}' targetnode = nodes.section(ids=[targetid]) yaml_dumper = YAML() yaml_dumper.default_flow_style = False yaml_output_stream = StringIO() yaml_dumper.dump([config], stream=yaml_output_stream) yaml_output = yaml_output_stream.getvalue() if not yaml_output: return [] content = f".. code-block:: yaml\n\n{indent(yaml_output, ' ')}\n\n" view = StringList(content.split('\n')) pre_commit_node = nodes.paragraph(rawsource=content) self.state.nested_parse(view, self.content_offset, pre_commit_node) pre_commit_node_purger.add_node(self.env, pre_commit_node, targetnode, self.lineno) return [pre_commit_node] class Flake8PreCommitDirective(SphinxDirective): """ A Sphinx directive for documenting flake8 plugins' pre-commit hooks. """ has_content: bool = False option_spec = { "flake8-version": str, "plugin-name": str, # defaults to repository name } required_arguments = 1 # the plugin version def run(self) -> Sequence[nodes.Node]: # type: ignore[override] """ Process the content of the directive. """ plugin_name = self.options.get("plugin-name", self.env.config.github_repository) flake8_version = self.options.get("flake8-version", "3.8.4") config = { "repo": "https://github.com/pycqa/flake8", "rev": flake8_version, "hooks": [{"id": "flake8", "additional_dependencies": [f"{plugin_name}=={self.arguments[0]}"]}] } targetid = f'pre-commit-{self.env.new_serialno("pre-commit"):d}' targetnode = nodes.section(ids=[targetid]) yaml_dumper = YAML() yaml_dumper.default_flow_style = False yaml_output_stream = StringIO() yaml_dumper.dump([config], stream=yaml_output_stream) yaml_output = yaml_output_stream.getvalue() if not yaml_output: return [] content = f".. code-block:: yaml\n\n{indent(yaml_output, ' ')}\n\n" view = StringList(content.split('\n')) pre_commit_node = nodes.paragraph(rawsource=content) self.state.nested_parse(view, self.content_offset, pre_commit_node) pre_commit_f8_node_purger.add_node(self.env, pre_commit_node, targetnode, self.lineno) return [pre_commit_node] def revert_8345() -> None: # pragma: no cover # Only for Sphinx 4+ """ Remove the incorrect warning emitted since https://github.com/sphinx-doc/sphinx/pull/8345. """ # Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file). # BSD Licensed # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. def lookup_domain_element(self, type: str, name: str) -> Any: # noqa: A002 # pylint: disable=redefined-builtin """ Lookup a markup element (directive or role), given its name which can be a full name (with domain). """ name = name.lower() # explicit domain given? if ':' in name: domain_name, name = name.split(':', 1) if domain_name in self.env.domains: domain = self.env.get_domain(domain_name) element = getattr(domain, type)(name) if element is not None: return element, [] # else look in the default domain else: def_domain = self.env.temp_data.get("default_domain") if def_domain is not None: element = getattr(def_domain, type)(name) if element is not None: return element, [] # always look in the std domain element = getattr(self.env.get_domain("std"), type)(name) if element is not None: return element, [] raise sphinx.util.docutils.ElementLookupError sphinx.util.docutils.sphinx_domains.lookup_domain_element = lookup_domain_element # type: ignore[assignment] @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.pre_commit`. :param app: The Sphinx application. """ app.add_directive("pre-commit", PreCommitDirective) app.add_directive("pre-commit:flake8", Flake8PreCommitDirective) app.connect("env-purge-doc", pre_commit_node_purger.purge_nodes) app.connect("env-purge-doc", pre_commit_f8_node_purger.purge_nodes) if sphinx.version_info >= (4, 0): revert_8345() return {"parallel_read_safe": True} PK7Vsphinx_toolbox/py.typedPK7V(g==sphinx_toolbox/rest_example.py#!/usr/bin/env python3 # # rest_example.py """ Directive to show example reStructuredText and the rendered output. .. extensions:: sphinx_toolbox.rest_example Usage --------- .. rst:directive:: rest-example Directive to show example reStructuredText and the rendered output. .. rst:directive:option:: force :type: flag If given, minor errors on highlighting are ignored. .. rst:directive:option:: emphasize-lines: line numbers :type: comma separated numbers Emphasize particular lines of the code block: .. rst:directive:option:: tab-width: number :type: number Sets the size of the indentation in spaces. .. rst:directive:option:: dedent: number :type: number Strip indentation characters from the code block, .. latex:clearpage:: :bold-title:`Example` .. rest-example:: .. rest-example:: :source:`sphinx_toolbox/config.py` Here is the :source:`source code ` API Reference --------------- """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib from typing import Any, Dict, List, Sequence # 3rd party import sphinx.environment from docutils import nodes from docutils.parsers.rst import directives from docutils.statemachine import ViewList from domdf_python_tools.stringlist import StringList from sphinx.application import Sphinx from sphinx.util.docutils import SphinxDirective # this package from sphinx_toolbox.utils import OptionSpec, Purger, SphinxExtMetadata, metadata_add_version __all__ = ("reSTExampleDirective", "make_rest_example", "rest_example_purger", "setup") class reSTExampleDirective(SphinxDirective): """ Directive to show some reStructuredText source, and the rendered output. """ has_content: bool = True # Options to pass through to .. code-block:: option_spec: OptionSpec = { # type: ignore[assignment] "force": directives.flag, "emphasize-lines": directives.unchanged, "tab-width": int, "dedent": int, } def run(self) -> List[nodes.Node]: """ Create the rest_example node. """ targetid = f'example-{self.env.new_serialno("sphinx-toolbox rest_example"):d}' targetnode = nodes.target('', '', ids=[targetid]) content = make_rest_example( self.options, self.env, self.content, # type: ignore[arg-type] ) view = ViewList(content) example_node = nodes.paragraph(rawsource=content) # type: ignore[arg-type] self.state.nested_parse(view, self.content_offset, example_node) # type: ignore[arg-type] rest_example_purger.add_node(self.env, example_node, targetnode, self.lineno) return [targetnode, example_node] def make_rest_example( options: Dict[str, Any], env: sphinx.environment.BuildEnvironment, content: Sequence[str], ) -> List[str]: """ Make the content of a reST Example node. :param options: :param content: The user-provided content of the directive. """ output = StringList(".. container:: rest-example") output.indent_type = ' ' * env.config.docutils_tab_width output.blankline() with output.with_indent_size(1): output.append(".. code-block:: rest") with output.with_indent_size(2): for option, value in options.items(): if value is None: output.append(f":{option}:") else: output.append(f":{option}: {value}") output.blankline() output.extend(content) output.blankline(ensure_single=True) output.extend(content) output.blankline(ensure_single=True) return list(output) #: Purger to track rest-example nodes, and remove redundant ones. rest_example_purger = Purger("all_rest_example_nodes") @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.rest_example`. .. versionadded:: 0.7.0 :param app: The Sphinx application. """ # Hack to get the docutils tab size, as there doesn't appear to be any other way app.setup_extension("sphinx_toolbox.tweaks.tabsize") app.setup_extension("sphinx_toolbox._css") app.add_directive("rest-example", reSTExampleDirective) app.connect("env-purge-doc", rest_example_purger.purge_nodes) return {"parallel_read_safe": True} PK7V_u?g\g\sphinx_toolbox/shields.py#!/usr/bin/env python3 # # shields.py r""" Directives for shield/badge images. .. extensions:: sphinx_toolbox.shields Usage --------- Several shield/badge directives are available. They function similarly to the ``.. image::`` directives, although not all options are available. As with the image directive, shields can be used as part of substitutions, e.g. .. code-block:: rest This repository uses pre-commit |pre-commit| .. |pre-commit| pre-commit:: All shields have the following options: .. rst:directive:option:: alt Alternative text for the shield, used when the image cannot be displayed or the user uses a screen reader. .. rst:directive:option:: height width scale The height/width/scale of the shield. .. rst:directive:option:: name .. rst:directive:option:: class :type: string Additional CSS class for the shield. All shields have the ``sphinx_toolbox_shield`` class by default. .. latex:clearpage:: Shields ^^^^^^^^^^^^ .. rst:directive:: rtfd-shield Shield to show the `ReadTheDocs `_ documentation build status. .. rst:directive:option:: project The name of the project on *ReadTheDocs*. .. rst:directive:option:: version The documentation version. Default ``latest``. .. rst:directive:option:: target The hyperlink target of the shield. Useful if the documentation uses a custom domain. .. versionadded:: 1.8.0 .. only:: html **Examples** .. rest-example:: .. rtfd-shield:: :project: sphinx-toolbox .. rest-example:: .. rtfd-shield:: :project: attrs :target: https://www.attrs.org/ .. rst:directive:: pypi-shield Shield to show information about the project on `PyPI `_. .. rst:directive:option:: project The name of the project on *PyPI*. Only one of the following options is permitted: .. rst:directive:option:: version Show the package version. .. rst:directive:option:: py-versions Show the supported python versions. .. rst:directive:option:: implementations Show the supported python implementations. .. rst:directive:option:: wheel Show whether the package has a wheel. .. rst:directive:option:: license Show the license listed on PyPI. .. rst:directive:option:: downloads Show the downloads for the given period (day / week / month) .. versionchanged:: 2.5.0 Shields created with this option now link to pypistats_. .. _pypistats: https://pypistats.org .. only:: html **Examples** .. rest-example:: .. pypi-shield:: :version: \ .. pypi-shield:: :project: sphinx :downloads: month .. rst:directive:: maintained-shield Shield to indicate whether the project is maintained. Takes a single argument: the current year. .. only:: html **Example** .. rest-example:: .. maintained-shield:: 2020 .. rst:directive:: github-shield Shield to show information about a GitHub repository. .. rst:directive:option:: username The GitHub username. Defaults to :confval:`github_username`. .. rst:directive:option:: repository The GitHub repository. Defaults to :confval:`github_repository`. .. rst:directive:option:: branch The branch to show information about. Default ``master``. Required for ``commits-since`` and ``last-commit``. Only one of the following options is permitted: .. rst:directive:option:: contributors :type: flag Show the number of contributors. .. rst:directive:option:: commits-since: tag :type: string Show the number of commits since the given tag. .. rst:directive:option:: last-commit :type: flag Show the date of the last commit. .. rst:directive:option:: top-language :type: flag Show the top language and percentage. .. rst:directive:option:: license :type: flag Show the license detected by GitHub. .. only:: html **Examples** .. rest-example:: .. github-shield:: :last-commit: \ .. github-shield:: :commits-since: v0.1.0 .. rst:directive:: actions-shield Shield to show the *GitHub Actions* build status. .. rst:directive:option:: username The GitHub username. Defaults to :confval:`github_username`. .. rst:directive:option:: repository The GitHub repository. Defaults to :confval:`github_repository`. .. rst:directive:option:: workflow The workflow to show the status for. .. only:: html **Example** .. rest-example:: .. actions-shield:: :workflow: Windows Tests .. rst:directive:: requires-io-shield Shield to show the *Requires.io* status. .. rst:directive:option:: username The GitHub username. Defaults to :confval:`github_username`. .. rst:directive:option:: repository The GitHub repository. Defaults to :confval:`github_repository`. .. rst:directive:option:: branch The branch to show the build status for. Default ``master``. .. only:: html **Example** .. rest-example:: .. requires-io-shield:: .. rst:directive:: coveralls-shield Shield to show the code coverage from `Coveralls.io `_. .. rst:directive:option:: username The GitHub username. Defaults to :confval:`github_username`. .. rst:directive:option:: repository The GitHub repository. Defaults to :confval:`github_repository`. .. rst:directive:option:: branch The branch to show the build status for. Default ``master``. .. only:: html **Example** .. rest-example:: .. coveralls-shield:: .. rst:directive:: codefactor-shield Shield to show the code quality score from `Codefactor `_. .. rst:directive:option:: username The GitHub username. Defaults to :confval:`github_username`. .. rst:directive:option:: repository The GitHub repository. Defaults to :confval:`github_repository`. .. only:: html **Example** .. rest-example:: .. codefactor-shield:: .. rst:directive:: pre-commit-shield Shield to indicate that the project uses `pre-commit `_. .. only:: html **Example** .. rest-example:: .. pre-commit-shield:: .. rst:directive:: pre-commit-ci-shield .. versionadded:: 1.7.0 Shield to show the `pre-commit.ci `_ status. .. rst:directive:option:: username The GitHub username. Defaults to :confval:`github_username`. .. rst:directive:option:: repository The GitHub repository. Defaults to :confval:`github_repository`. .. rst:directive:option:: branch The branch to show the status for. Default ``master``. .. only:: html **Example** .. rest-example:: .. pre-commit-ci-shield:: .. latex:vspace:: 5mm """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Based on public domain code from Docutils # # stdlib from typing import List, Optional, Tuple from urllib.parse import quote # 3rd party import dict2css import docutils from apeye.url import URL from docutils import nodes from docutils.nodes import fully_normalize_name, whitespace_normalize_name from docutils.parsers.rst import directives from docutils.parsers.rst.roles import set_classes from domdf_python_tools.paths import PathPlus from sphinx.application import Sphinx from sphinx.util.docutils import SphinxDirective # this package from sphinx_toolbox import _css from sphinx_toolbox.utils import OptionSpec, SphinxExtMetadata, flag, make_github_url, metadata_add_version __all__ = ( "SHIELDS_IO", "shield_default_option_spec", "Shield", "RTFDShield", "PyPIShield", "MaintainedShield", "GitHubBackedShield", "GitHubShield", "GitHubActionsShield", "RequiresIOShield", "CoverallsShield", "CodefactorShield", "PreCommitShield", "PreCommitCIShield", "copy_asset_files", "setup", ) #: Base URL for shields.io SHIELDS_IO = URL("https://img.shields.io") #: Options common to all shields. shield_default_option_spec: OptionSpec = { "alt": directives.unchanged, "height": directives.length_or_unitless, "width": directives.length_or_percentage_or_unitless, "scale": directives.percentage, "name": directives.unchanged, "class": directives.class_option, } class Shield(SphinxDirective): """ Directive for `shields.io `_ shields/badges. """ required_arguments = 1 optional_arguments = 0 final_argument_whitespace = True option_spec: OptionSpec = { # type: ignore[assignment] "target": directives.unchanged_required, **shield_default_option_spec, } def run(self) -> List[nodes.Node]: """ Process the content of the shield directive. """ if "class" in self.options: self.options["class"].append("sphinx_toolbox_shield") else: self.options["class"] = ["sphinx_toolbox_shield"] self.arguments = [str(x) for x in self.arguments] messages = [] reference = directives.uri(self.arguments[0]) self.options["uri"] = reference reference_node = None if "target" in self.options: block = docutils.utils.escape2null(self.options["target"]).splitlines() block = [line for line in block] target_type, data = self.state.parse_target(block, self.block_text, self.lineno) # type: ignore[attr-defined] if target_type == "refuri": reference_node = nodes.reference(refuri=data) elif target_type == "refname": # pragma: no cover reference_node = nodes.reference( refname=fully_normalize_name(data), name=whitespace_normalize_name(data), ) reference_node.indirect_reference_name = data # type: ignore[attr-defined] self.state.document.note_refname(reference_node) else: # pragma: no cover # malformed target # data is a system message messages.append(data) del self.options["target"] set_classes(self.options) image_node = nodes.image(self.block_text, **self.options) self.add_name(image_node) if reference_node: reference_node += image_node return messages + [reference_node] else: return messages + [image_node] class GitHubBackedShield(Shield): """ Base class for badges that are based around GitHub. """ required_arguments = 0 option_spec: OptionSpec = { "username": str, # Defaults to "github_username" if undefined "repository": str, # Defaults to "github_repository" if undefined **shield_default_option_spec, } def get_repo_details(self) -> Tuple[str, str]: """ Returns the username and repository name, either parsed from the directive's options or from ``conf.py``. """ username = self.options.pop("username", self.env.config.github_username) if username is None: raise ValueError("'github_username' has not been set in 'conf.py'!") repository = self.options.pop("repository", self.env.config.github_repository) if repository is None: raise ValueError("'github_repository' has not been set in 'conf.py'!") return username, repository class RTFDShield(Shield): """ Shield to show the `ReadTheDocs `_ documentation build status. .. versionchanged:: 1.8.0 Added the ``:target:`` option, to allow a custom target to be specified. Useful if the documentation uses a custom domain. """ required_arguments = 0 option_spec: OptionSpec = { "project": directives.unchanged_required, "version": str, "target": str, **shield_default_option_spec, } def run(self) -> List[nodes.Node]: """ Process the content of the shield directive. """ project = self.options.pop("project") version = self.options.pop("version", "latest") image = SHIELDS_IO / "readthedocs" / project / f"{version}?logo=read-the-docs" self.arguments = [image] if "target" not in self.options: self.options["target"] = f"https://{project}.readthedocs.io/en/{version}/" return super().run() class GitHubActionsShield(GitHubBackedShield): """ Shield to show the *GitHub Actions* build status. """ option_spec: OptionSpec = { "username": str, # Defaults to "github_username" if undefined "repository": str, # Defaults to "github_repository" if undefined "workflow": directives.unchanged_required, # The name of the workflow **shield_default_option_spec, } def run(self) -> List[nodes.Node]: """ Process the content of the shield directive. """ username, repository = self.get_repo_details() workflow = quote(self.options["workflow"]) self.arguments = [str(make_github_url(username, repository) / "workflows" / workflow / "badge.svg")] self.options["target"] = str( make_github_url(username, repository) / f"actions?query=workflow%3A%22{workflow}%22" ) return super().run() class RequiresIOShield(GitHubBackedShield): """ Shield to show the `Requires.io `_ status. """ option_spec: OptionSpec = { "username": str, # Defaults to "github_username" if undefined "repository": str, # Defaults to "github_repository" if undefined "branch": str, **shield_default_option_spec, } def run(self) -> List[nodes.Node]: """ Process the content of the shield directive. """ username, repository = self.get_repo_details() branch = self.options.pop("branch", "master") base_url = URL("https://requires.io/github/") / username / repository self.arguments = [str(base_url / f"requirements.svg?branch={branch}")] self.options["target"] = str(base_url / f"requirements/?branch={branch}") return super().run() class CoverallsShield(GitHubBackedShield): """ Shield to show the code coverage from `Coveralls.io `_. """ option_spec: OptionSpec = { "username": str, # Defaults to "github_username" if undefined "repository": str, # Defaults to "github_repository" if undefined "branch": str, **shield_default_option_spec, } def run(self) -> List[nodes.Node]: """ Process the content of the shield directive. """ username, repository = self.get_repo_details() branch = self.options.pop("branch", "master") url = SHIELDS_IO / "coveralls" / "github" / username / repository / f"{branch}?logo=coveralls" self.arguments = [str(url)] self.options["target"] = f"https://coveralls.io/github/{username}/{repository}?branch={branch}" return super().run() class CodefactorShield(GitHubBackedShield): """ Shield to show the code quality score from `Codefactor `_. """ def run(self) -> List[nodes.Node]: """ Process the content of the shield directive. """ username, repository = self.get_repo_details() url = SHIELDS_IO / "codefactor" / "grade" / "github" / username / f"{repository}?logo=codefactor" self.arguments = [str(url)] self.options["target"] = f"https://codefactor.io/repository/github/{username}/{repository}" return super().run() class PyPIShield(Shield): """ Shield to show information about the project on `PyPI `_. """ required_arguments = 0 option_spec: OptionSpec = { "project": directives.unchanged_required, "version": flag, # Show the package version. "py-versions": flag, # Show the supported python versions. "implementations": flag, # Show the supported python implementations. "wheel": flag, # Show whether the package has a wheel on PyPI. "license": flag, # Show the license listed on PyPI. "downloads": str, # Show the downloads for the given period (day / week / month). **shield_default_option_spec, } def run(self) -> List[nodes.Node]: """ Process the content of the shield directive. """ base_url = SHIELDS_IO / "pypi" project = self.options.pop("project", self.env.config.github_repository) self.options["target"] = f"https://pypi.org/project/{project}" info = { 'v': self.options.pop("version", False), "py-versions": self.options.pop("py-versions", False), "implementation": self.options.pop("implementations", False), "wheel": self.options.pop("wheel", False), 'l': self.options.pop("license", False), "downloads": self.options.pop("downloads", False), } n_info_options: int = len([k for k, v in info.items() if v]) if n_info_options > 1: raise ValueError("Only one information option is allowed for the 'pypi-badge' directive.") elif n_info_options == 0: raise ValueError("An information option is required for the 'pypi-badge' directive.") for option in {'v', "implementation", "wheel", 'l'}: if info[option]: self.arguments = [base_url / option / project] break if info["py-versions"]: self.arguments = [str(base_url / "pyversions" / f"{project}?logo=python&logoColor=white")] elif info["downloads"]: if info["downloads"] in {"week", "dw"}: self.arguments = [base_url / "dw" / project] elif info["downloads"] in {"month", "dm"}: self.arguments = [base_url / "dm" / project] elif info["downloads"] in {"day", "dd"}: self.arguments = [base_url / "dd" / project] else: raise ValueError("Unknown time period for the PyPI download statistics.") self.options["target"] = f"https://pypistats.org/packages/{project.lower()}" return super().run() class GitHubShield(GitHubBackedShield): """ Shield to show information about a GitHub repository. """ option_spec: OptionSpec = { "username": str, # Defaults to "github_username" if undefined "repository": str, # Defaults to "github_repository" if undefined "branch": str, "contributors": flag, # Show the number of contributors. "commits-since": str, # Show the number of commits since the given tag. "last-commit": flag, # Show the date of the last commit. "top-language": flag, # Show the top language and % "license": flag, **shield_default_option_spec, } def run(self) -> List[nodes.Node]: """ Process the content of the shield directive. """ base_url = "https://img.shields.io/github" username, repository = self.get_repo_details() branch = self.options.pop("branch", "master") info = { "contributors": self.options.pop("contributors", False), "commits-since": self.options.pop("commits-since", False), "last-commit": self.options.pop("last-commit", False), "top-language": self.options.pop("top-language", False), "license": self.options.pop("license", False), } n_info_options: int = len([k for k, v in info.items() if v]) if n_info_options > 1: raise ValueError("Only one information option is allowed for the 'github-badge' directive.") elif n_info_options == 0: raise ValueError("An information option is required for the 'github-badge' directive.") if info["contributors"]: self.arguments = [f"{base_url}/contributors/{username}/{repository}"] self.options["target"] = f"https://github.com/{username}/{repository}/graphs/contributors" elif info["commits-since"]: self.arguments = [f"{base_url}/commits-since/{username}/{repository}/{info['commits-since']}/{branch}"] self.options["target"] = f"https://github.com/{username}/{repository}/pulse" elif info["last-commit"]: self.arguments = [f"{base_url}/last-commit/{username}/{repository}/{branch}"] self.options["target"] = f"https://github.com/{username}/{repository}/commit/{branch}" elif info["top-language"]: self.arguments = [f"{base_url}/languages/top/{username}/{repository}"] elif info["license"]: self.arguments = [f"{base_url}/license/{username}/{repository}"] self.options["target"] = f"https://github.com/{username}/{repository}/blob/master/LICENSE" return super().run() class MaintainedShield(Shield): """ Shield to indicate whether the project is maintained. """ required_arguments = 1 # The year option_spec = dict(shield_default_option_spec) def run(self) -> List[nodes.Node]: """ Process the content of the shield directive. """ self.arguments = [f"https://img.shields.io/maintenance/yes/{self.arguments[0]}"] return super().run() class PreCommitShield(Shield): """ Shield to indicate that the project uses `pre-commit `_. """ required_arguments = 0 option_spec = dict(shield_default_option_spec) def run(self) -> List[nodes.Node]: """ Process the content of the shield directive. """ self.arguments = [ "https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white" ] self.options["target"] = "https://github.com/pre-commit/pre-commit" return super().run() RESULTS_PRE_COMMIT_CI = URL("https://results.pre-commit.ci") class PreCommitCIShield(GitHubBackedShield): """ Shield to show the `pre-commit.ci `_ status. .. versionadded:: 1.7.0 """ option_spec: OptionSpec = { "username": str, # Defaults to "github_username" if undefined "repository": str, # Defaults to "github_repository" if undefined "branch": str, **shield_default_option_spec, } def run(self) -> List[nodes.Node]: """ Process the content of the shield directive. """ username, repository = self.get_repo_details() branch = self.options.pop("branch", "master") url = RESULTS_PRE_COMMIT_CI / "badge" / "github" / username / repository / f"{branch}.svg" self.arguments = [str(url)] self.options["target"] = str(RESULTS_PRE_COMMIT_CI / "latest" / "github" / username / repository / branch) return super().run() def copy_asset_files(app: Sphinx, exception: Optional[Exception] = None) -> None: """ Copy additional stylesheets into the HTML build directory. .. versionadded:: 2.3.1 :param app: The Sphinx application. :param exception: Any exception which occurred and caused Sphinx to abort. """ if exception: # pragma: no cover return if app.builder is None or app.builder.format.lower() != "html": # pragma: no cover return static_dir = PathPlus(app.outdir) / "_static" static_dir.maybe_make(parents=True) dict2css.dump(_css.shields_styles, static_dir / "toolbox-shields.css", minify=True) @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.shields`. :param app: The Sphinx application. """ app.setup_extension("sphinx_toolbox.github") app.setup_extension("sphinx_toolbox._css") # Shields/badges app.add_directive("rtfd-shield", RTFDShield) app.add_directive("actions-shield", GitHubActionsShield) app.add_directive("requires-io-shield", RequiresIOShield) app.add_directive("coveralls-shield", CoverallsShield) app.add_directive("codefactor-shield", CodefactorShield) app.add_directive("pypi-shield", PyPIShield) app.add_directive("github-shield", GitHubShield) app.add_directive("maintained-shield", MaintainedShield) app.add_directive("pre-commit-shield", PreCommitShield) app.add_directive("pre-commit-ci-shield", PreCommitCIShield) return {"parallel_read_safe": True} PK7V;`sphinx_toolbox/sidebar_links.py#!/usr/bin/env python3 # # sidebar_links.py r""" Directive which adds a toctree to the sidebar containing links to the GitHub repository, PyPI project page etc. .. versionadded:: 2.9.0 .. extensions:: sphinx_toolbox.sidebar_links .. latex:vspace:: -5px Usage ------- .. latex:vspace:: -10px .. rst:directive:: sidebar-links Adds a toctree to the sidebar containing links to the GitHub repository, PyPI project page etc. The toctree is only shown in the sidebar and is hidden with non-HTML builders. .. only:: html You can see an example of this in the sidebar of this documentation. .. note:: This directive can only be used on the root document (i.e. index.rst). .. rst:directive:option:: github :type: flag Flag to add a link to the project's GitHub repository. To use this option add the following to your ``conf.py``: .. code-block:: python extensions = [ ... 'sphinx_toolbox.github', ] github_username = '' github_repository = '' See :mod:`sphinx_toolbox.github` for more information. .. rst:directive:option:: pypi :type: string Flag to add a link to the project page on PyPI. The name of the project on PyPI must be passed as the option's value. .. rst:directive:option:: caption :type: string The caption of the toctree. Defaults to ``Links`` Additional toctree entries may be added as the content of the directive, in the same manner as normal toctrees. API Reference -------------- """ # # Copyright © 2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # stdlib import warnings from typing import List # 3rd party import docutils from docutils import nodes from docutils.parsers.rst import directives from domdf_python_tools.stringlist import StringList from sphinx import addnodes from sphinx.application import Sphinx from sphinx.util.docutils import SphinxDirective # this package from sphinx_toolbox.utils import OptionSpec, SphinxExtMetadata, flag, metadata_add_version __all__ = ("SidebarLinksDirective", "setup") class SidebarLinksDirective(SphinxDirective): """ Directive which adds a toctree to the sidebar containing links to the GitHub repository, PyPI project page etc. """ has_content: bool = True option_spec: OptionSpec = { # type: ignore[assignment] "pypi": directives.unchanged_required, "github": flag, "caption": directives.unchanged_required, } def process_github_option(self) -> str: """ Process the ``:github:`` flag. """ if "sphinx_toolbox.github" not in self.env.app.extensions: raise ValueError( "The 'sphinx_toolbox.github' extension is required for the " ":github: option but it is not enabled!" ) username = getattr(self.env.config, "github_username", None) if username is None: raise ValueError("'github_username' has not been set in 'conf.py'!") repository = getattr(self.env.config, "github_repository", None) if repository is None: raise ValueError("'github_repository' has not been set in 'conf.py'!") return f"GitHub " def run(self) -> List[nodes.Node]: """ Create the installation node. """ if self.env.docname != self.env.config.master_doc: # pragma: no cover warnings.warn( "The 'sidebar-links' directive can only be used on the Sphinx master doc. " "No links will be shown.", UserWarning, ) return [] body = StringList([ ".. toctree::", " :hidden:", ]) with body.with_indent(" ", 1): if "caption" in self.options: body.append(f":caption: {self.options['caption']}") else: # pragma: no cover body.append(":caption: Links") body.blankline() if "github" in self.options: body.append(self.process_github_option()) if "pypi" in self.options: body.append(f"PyPI ") body.extend(self.content) body.blankline() body.blankline() only_node = addnodes.only(expr="html") content_node = nodes.paragraph(rawsource=str(body)) only_node += content_node self.state.nested_parse(docutils.statemachine.StringList(body), self.content_offset, content_node) return [only_node] @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.sidebar_links`. :param app: The Sphinx application. """ app.add_directive("sidebar-links", SidebarLinksDirective) return {"parallel_read_safe": True} PK7V qqsphinx_toolbox/source.py#!/usr/bin/env python3 # # source.py """ Add hyperlinks to source files, either on GitHub or in the documentation itself. .. extensions:: sphinx_toolbox.source If you're looking for a ``[source]`` button to go at the end of your class and function signatures, checkout :mod:`sphinx.ext.linkcode` and :mod:`sphinx.ext.viewcode`. Usage ------- .. confval:: source_link_target :type: :class:`str` :required: False :default: ``'Sphinx'`` The target of the source link, either ``'GitHub'`` or ``'Sphinx'``. Case insensitive. .. rst:role:: source Role which shows a link to the given source file, either on GitHub or within the Sphinx documentation. By default, the link points to the code within the documentation, but can be configured to point to GitHub by setting :confval:`source_link_target` to ``'GitHub'``. :bold-title:`Example` .. rest-example:: :source:`sphinx_toolbox/config.py` Here is the :source:`source code ` API Reference -------------- """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Based on pyspecific.py from the Python documentation. # Copyright 2008-2014 by Georg Brandl. # Licensed under the PSF License 2.0 # # Parts of the docstrings based on https://docutils.sourceforge.io/docs/howto/rst-roles.html # # stdlib from typing import TYPE_CHECKING, Dict, List, Sequence, Tuple # 3rd party import sphinx from docutils import nodes from docutils.nodes import system_message from docutils.parsers.rst.states import Inliner from sphinx import addnodes from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment from sphinx.errors import NoUri from sphinx.util import split_explicit_title # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version if TYPE_CHECKING: # this package from sphinx_toolbox.utils import Config __all__ = ("source_role", "setup") # TODO: rawstring: Return it as a problematic node linked to a system message if a problem is encountered. _sphinx_version = sphinx.version_info[:3] def _make_viewcode_node( title: str, pagename: str, env: BuildEnvironment, ) -> nodes.Node: """ Construct a node for the :mod:`sphinx.ext.viewcode` link. Handles Sphinx 3.5+ compatibility. """ if _sphinx_version < (3, 5, 0): return addnodes.pending_xref( title, nodes.inline(title, title), reftype="viewcode", refdomain="std", refexplicit=False, reftarget=pagename, refid=title, refdoc=env.docname, ) else: # 3rd party from sphinx.util.nodes import make_refnode assert env.app.builder is not None try: return make_refnode( env.app.builder, fromdocname=env.docname, todocname=pagename, targetid=title, child=nodes.inline(title, title), ) except NoUri: return nodes.inline(title, title) def source_role( name: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: Dict = {}, content: List[str] = [] ) -> Tuple[Sequence[nodes.Node], List[system_message]]: """ Adds a link to the given Python source file in the documentation or on GitHub. :param name: The local name of the interpreted role, the role name actually used in the document. :param rawtext: A string containing the entire interpreted text input, including the role and markup. :param text: The interpreted text content. :param lineno: The line number where the interpreted text begins. :param inliner: The :class:`docutils.parsers.rst.states.Inliner` object that called :func:`~.source_role`. It contains the several attributes useful for error reporting and document tree access. :param options: A dictionary of directive options for customization (from the ``role`` directive), to be interpreted by the function. Used for additional attributes for the generated elements and other functionality. :param content: A list of strings, the directive content for customization (from the ``role`` directive). To be interpreted by the function. :return: A list containing the created node, and a list containing any messages generated during the function. .. versionchanged:: 2.8.0 Now returns a sequence of :class:`nodes.reference ` and :class:`addnodes.pending_xref ` as the first tuple element, rather than :class:`nodes.reference ` and :class:`addnodes.pending_xref ` as in previous versions. """ has_t, title, target = split_explicit_title(text) title = nodes.unescape(title) target = nodes.unescape(target) env = inliner.document.settings.env config = env.app.config nodes_: List[nodes.Node] = [] messages: List[system_message] = [] refnode: nodes.Node if config.source_link_target == "sphinx": if target.endswith("/__init__.py"): pagename = "_modules/" + target.rsplit('/', 1)[0] else: pagename = "_modules/" + target.replace(".py", '') # refnode = addnodes.only(expr="html") # refnode += addnodes.pending_xref( refnode = _make_viewcode_node( title, pagename, env, ) # refnode = addnodes.pending_xref( # title, # nodes.inline(title, title), # reftype="viewcode", # refdomain="std", # refexplicit=False, # reftarget=pagename, # refid=title, # refdoc=env.docname, # ) nodes_.append(refnode) elif config.source_link_target == "github": refnode = nodes.reference( title, title, refuri=str(config.github_source_url / target), ) nodes_.append(refnode) else: message = inliner.document.reporter.error(f"Unsupported source link target '{config.source_link_target}'.") messages.append(message) return nodes_, messages def _configure(app: Sphinx, config: "Config") -> None: """ Validate the provided configuration values. :param app: The Sphinx application. :param config: """ config.source_link_target = str(config.source_link_target).lower().strip() # type: ignore[attr-defined] if config.source_link_target not in {"sphinx", "github"}: # this package from sphinx_toolbox.config import InvalidOptionError raise InvalidOptionError("Invalid value for 'source_link_target'.") @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.source`. :param app: The Sphinx application. """ # Link to source code app.add_role("source", source_role) # The target for the source link. One of GitHub or Sphinx (GitLab coming soon™) app.add_config_value("source_link_target", "Sphinx", "env", types=[str]) app.connect("config-inited", _configure) app.setup_extension("sphinx_toolbox.github") return {"parallel_read_safe": True} PK7V6$llsphinx_toolbox/testing.py#!/usr/bin/env python3 # # testing.py r""" Functions for testing Sphinx extensions. .. extras-require:: testing :pyproject: .. seealso:: Sphinx's own ``testing`` library: https://github.com/sphinx-doc/sphinx/tree/3.x/sphinx/testing .. latex:vspace:: 10px .. _pytest-regressions: https://pypi.org/project/pytest-regressions/ """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Based on Sphinx # Copyright (c) 2007-2020 by the Sphinx team. # | All rights reserved. # | # | Redistribution and use in source and binary forms, with or without # | modification, are permitted provided that the following conditions are # | met: # | # | * Redistributions of source code must retain the above copyright # | notice, this list of conditions and the following disclaimer. # | # | * Redistributions in binary form must reproduce the above copyright # | notice, this list of conditions and the following disclaimer in the # | documentation and/or other materials provided with the distribution. # | # | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # stdlib import copy import re import sys import tempfile from functools import partial from types import SimpleNamespace from typing import Any, Callable, Dict, List, NamedTuple, Optional, Set, Tuple, Type, Union, cast # 3rd party import pytest # nodep import sphinx.application from bs4 import BeautifulSoup # type: ignore[import] from coincidence.regressions import ( # nodep AdvancedFileRegressionFixture, check_file_output, check_file_regression ) from docutils import __version_info__ as docutils_version from docutils import nodes from docutils.parsers.rst import Directive, roles from docutils.transforms import Transform from domdf_python_tools.doctools import prettify_docstrings from domdf_python_tools.paths import PathPlus from domdf_python_tools.stringlist import StringList from domdf_python_tools.typing import PathLike from jinja2 import Template # nodep from pygments.lexer import Lexer # type: ignore[import] # nodep from pytest_regressions.common import check_text_files # nodep from pytest_regressions.file_regression import FileRegressionFixture # nodep from sphinx.builders import Builder from sphinx.domains import Domain, Index from sphinx.domains.python import PythonDomain from sphinx.environment.collectors import EnvironmentCollector from sphinx.events import EventListener from sphinx.events import EventManager as BaseEventManager from sphinx.ext.autodoc.directive import AutodocDirective from sphinx.highlighting import lexer_classes from sphinx.registry import SphinxComponentRegistry from sphinx.roles import XRefRole from sphinx.util import docutils from sphinx.util.typing import RoleFunction, TitleGetter # this package from sphinx_toolbox.utils import Config, SphinxExtMetadata # _ModuleWrapper unwrapping BS due to # https://github.com/sphinx-doc/sphinx/commit/8866adeacfb045c97302cc9c7e3b60dec5ca38fd try: # 3rd party from sphinx.deprecation import _ModuleWrapper if isinstance(docutils, _ModuleWrapper): docutils = docutils._module except ImportError: # Unnecessary if unimportable pass __all__ = ( "Sphinx", "run_setup", "RunSetupOutput", "remove_html_footer", "check_html_regression", "remove_html_link_tags", "check_asset_copy", "HTMLRegressionFixture", "html_regression", "LaTeXRegressionFixture", "latex_regression", ) class FakeBuilder(Builder): pass class EventManager(BaseEventManager): def connect(self, name: str, callback: Callable, priority: int) -> int: """ Connect a handler to specific event. """ listener_id = self.next_listener_id self.next_listener_id += 1 self.listeners[name].append(EventListener(listener_id, callback, priority)) return listener_id class Sphinx: """ A class that pretends to be :class:`sphinx.application.Sphinx` but that is stripped back to allow the internals to be inspected. This can be used in tests to ensure the nodes, roles etc. being registered in an extension's ``setup()`` function are actually being registered. """ # noqa: D400 registry: SphinxComponentRegistry #: Instance of :class:`sphinx.registry.SphinxComponentRegistry` config: Config #: Instance of :class:`sphinx.config.Config` events: EventManager #: Instance of :class:`sphinx.events.EventManager` html_themes: Dict[str, str] #: Mapping of HTML theme names to filesystem paths. # builder: Builder #: Instance of :class:`sphinx.builder.Builder` def __init__(self): # , buildername: str = "html" self.registry = SphinxComponentRegistry() self.config = Config({}, {}) self.events = EventManager(self) # type: ignore[arg-type] self.html_themes: Dict[str, str] = {} # self.builder = self.registry.create_builder(self, buildername) def add_builder(self, builder: Type[Builder], override: bool = False) -> None: r""" Register a new builder. The registered values are stored in the ``app.registry.builders`` dictionary (:class:`typing.Dict`\[:class:`str`\, :class:`typing.Type`\[:class:`sphinx.builders.Builder`\]]). """ self.registry.add_builder(builder, override=override) def add_config_value( self, name: str, default: Any, rebuild: Union[bool, str], types: Any = (), ) -> None: r""" Register a configuration value. The registered values are stored in the ``app.config.values`` dictionary (:class:`typing.Dict`\[:class:`str`\, :class:`typing.Tuple`]). """ if rebuild in {False, True}: rebuild = "env" if rebuild else '' self.config.add(name, default, rebuild, types) def add_event(self, name: str) -> None: r""" Register an event called ``name``. The registered values are stored in the ``app.events.events`` dictionary (:class:`typing.Dict`\[:class:`str`\, :class:`str`\]). """ self.events.add(name) def set_translator( self, name: str, translator_class: Type[nodes.NodeVisitor], override: bool = False, ) -> None: r""" Register or override a Docutils translator class. The registered values are stored in the ``app.registry.translators`` dictionary. (:class:`typing.Dict`\[:class:`str`\, :class:`typing.Type`\[:class:`docutils.nodes.NodeVisitor`\]]). .. clearpage:: """ self.registry.add_translator(name, translator_class, override=override) def add_node( self, node: Type[nodes.Element], override: bool = False, **kwargs: Tuple[Callable, Callable], ) -> None: r""" Register a Docutils node class. The registered values are stored in the ``additional_nodes`` set returned by :func:`~sphinx_toolbox.testing.run_setup` (:class:`typing.Set`\[:class:`typing.Type`\[:class:`docutils.nodes.Node`\]]). """ if not override and docutils.is_node_registered(node): raise ValueError( f"node class {node.__name__!r} is already registered, its visitors will be overridden" ) docutils.register_node(node) self.registry.add_translation_handlers(node, **kwargs) def add_enumerable_node( self, node: Type[nodes.Element], figtype: str, title_getter: Optional[TitleGetter] = None, override: bool = False, **kwargs: Tuple[Callable, Callable], ) -> None: """ Register a Docutils node class as a numfig target. """ # Sphinx's signature is wrong WRT Optional self.registry.add_enumerable_node( node, figtype, title_getter, override=override, ) self.add_node(node, override=override, **kwargs) def add_directive(self, name: str, cls: Type[Directive], override: bool = False) -> None: """ Register a Docutils directive. """ if not override and docutils.is_directive_registered(name): raise ValueError(f"directive {name!r} is already registered, it will be overridden") docutils.register_directive(name, cls) def add_role(self, name: str, role: Any, override: bool = False) -> None: r""" Register a Docutils role. The registered values are stored in the ``roles`` dictionary returned by :func:`~sphinx_toolbox.testing.run_setup`. (:class:`typing.Dict`\[:class:`str`\, :class:`typing.Callable`\]). """ if not override and docutils.is_role_registered(name): raise ValueError(f"role {name!r} is already registered, it will be overridden") docutils.register_role(name, role) def add_generic_role(self, name: str, nodeclass: Any, override: bool = False) -> None: """ Register a generic Docutils role. """ if not override and docutils.is_role_registered(name): raise ValueError(f"role {name!r} is already registered, it will be overridden") role = roles.GenericRole(name, nodeclass) docutils.register_role(name, role) def add_domain( self, domain: Type[Domain], override: bool = False, ) -> None: """ Register a domain. """ self.registry.add_domain(domain, override=override) def add_directive_to_domain( self, domain: str, name: str, cls: Type[Directive], override: bool = False, ) -> None: """ Register a Docutils directive in a domain. """ self.registry.add_directive_to_domain(domain, name, cls, override=override) def add_role_to_domain( self, domain: str, name: str, role: Union[RoleFunction, XRefRole], override: bool = False, ) -> None: """ Register a Docutils role in a domain. """ self.registry.add_role_to_domain(domain, name, role, override=override) def add_index_to_domain( self, domain: str, index: Type[Index], override: bool = False, ) -> None: """ Register a custom index for a domain. """ self.registry.add_index_to_domain(domain, index) def add_object_type( self, directivename: str, rolename: str, indextemplate: str = '', parse_node: Optional[Callable] = None, ref_nodeclass: Optional[Type[nodes.TextElement]] = None, objname: str = '', doc_field_types: List = [], override: bool = False, ) -> None: """ Register a new object type. """ # Sphinx's signature is wrong WRT Optional self.registry.add_object_type( directivename, rolename, indextemplate, parse_node, ref_nodeclass, objname, doc_field_types, override=override, ) def add_crossref_type( self, directivename: str, rolename: str, indextemplate: str = '', ref_nodeclass: Optional[Type[nodes.TextElement]] = None, objname: str = '', override: bool = False, ) -> None: """ Register a new crossref object type. """ # Sphinx's signature is wrong WRT Optional self.registry.add_crossref_type( directivename, rolename, indextemplate, ref_nodeclass, objname, override=override, ) def add_transform(self, transform: Type[Transform]) -> None: """ Register a Docutils transform to be applied after parsing. """ self.registry.add_transform(transform) def add_post_transform(self, transform: Type[Transform]) -> None: """ Register a Docutils transform to be applied before writing. """ self.registry.add_post_transform(transform) def add_js_file(self, filename: str, **kwargs: str) -> None: """ Register a JavaScript file to include in the HTML output. .. versionadded:: 2.8.0 """ self.registry.add_js_file(filename, **kwargs) # if hasattr(self.builder, 'add_js_file'): # self.builder.add_js_file(filename, **kwargs) # def add_css_file(self, filename: str, **kwargs: str) -> None: """ Register a stylesheet to include in the HTML output. .. versionadded:: 2.7.0 """ self.registry.add_css_files(filename, **kwargs) # if hasattr(self.builder, 'add_css_file'): # self.builder.add_css_file(filename, **kwargs) def add_latex_package( self, packagename: str, options: Optional[str] = None, after_hyperref: bool = False, ) -> None: """ Register a package to include in the LaTeX source code. """ # Sphinx's signature is wrong WRT Optional self.registry.add_latex_package(packagename, cast(str, options), after_hyperref) def add_lexer(self, alias: str, lexer: Type[Lexer]) -> None: """ Register a new lexer for source code. """ if isinstance(lexer, Lexer): raise TypeError("app.add_lexer() API changed; Please give lexer class instead instance") else: lexer_classes[alias] = lexer def add_autodocumenter(self, cls: Any, override: bool = False) -> None: """ Register a new documenter class for the autodoc extension. """ self.registry.add_documenter(cls.objtype, cls) self.add_directive("auto" + cls.objtype, AutodocDirective, override=override) def add_autodoc_attrgetter( self, typ: Type, getter: Callable[[Any, str, Any], Any], ) -> None: """ Register a new ``getattr``-like function for the autodoc extension. """ self.registry.add_autodoc_attrgetter(typ, getter) def add_source_suffix(self, suffix: str, filetype: str, override: bool = False) -> None: """ Register a suffix of source files. """ self.registry.add_source_suffix(suffix, filetype, override=override) def add_source_parser(self, *args: Any, **kwargs: Any) -> None: """ Register a parser class. """ self.registry.add_source_parser(*args, **kwargs) def add_env_collector(self, collector: Type[EnvironmentCollector]) -> None: """ No-op for now. .. TODO:: Make this do something """ # def add_env_collector(self, collector: Type[EnvironmentCollector]) -> None: # """ # Register an environment collector class. # """ # # collector().enable(self) def add_html_theme(self, name: str, theme_path: str) -> None: """ Register an HTML Theme. """ self.html_themes[name] = theme_path def add_html_math_renderer( self, name: str, inline_renderers: Optional[Tuple[Callable, Callable]] = None, block_renderers: Optional[Tuple[Callable, Callable]] = None, ) -> None: """ Register a math renderer for HTML. """ self.registry.add_html_math_renderer(name, inline_renderers, block_renderers) # type: ignore[arg-type] def setup_extension(self, extname: str) -> None: """ Import and setup a Sphinx extension module. .. TODO:: implement this """ # self.registry.load_extension(self, extname) def require_sphinx(self, version: str) -> None: """ Check the Sphinx version if requested. No-op when testing """ # event interface def connect(self, event: str, callback: Callable, priority: int = 500) -> int: """ Register *callback* to be called when *event* is emitted. """ listener_id = self.events.connect(event, callback, priority) return listener_id @prettify_docstrings class RunSetupOutput(NamedTuple): """ :class:`~typing.NamedTuple` representing the output from :func:`~sphinx_toolbox.testing.run_setup`. """ setup_ret: Union[None, Dict[str, Any], "SphinxExtMetadata"] #: The output from the ``setup()`` function. directives: Dict[str, Callable] #: Mapping of directive names to directive functions. roles: Dict[str, Callable] #: Mapping of role names to role functions. additional_nodes: Set[Type[Any]] #: Set of custom docutils nodes registered in ``setup()``. app: Sphinx #: Instance of :class:`sphinx_toolbox.testing.Sphinx`. _sphinx_dict_setup = Callable[[sphinx.application.Sphinx], Optional[Dict[str, Any]]] _sphinx_metadata_setup = Callable[[sphinx.application.Sphinx], Optional["SphinxExtMetadata"]] _fake_dict_setup = Callable[[Sphinx], Optional[Dict[str, Any]]] _fake_metadata_setup = Callable[[Sphinx], Optional["SphinxExtMetadata"]] _setup_func_type = Union[_sphinx_dict_setup, _sphinx_metadata_setup, _fake_dict_setup, _fake_metadata_setup] def run_setup(setup_func: _setup_func_type) -> RunSetupOutput: # , buildername: str = "html" """ Function for running an extension's ``setup()`` function for testing. :param setup_func: The ``setup()`` function under test. :returns: 5-element namedtuple """ app = Sphinx() # buildername app.add_domain(PythonDomain) _additional_nodes = copy.copy(docutils.additional_nodes) try: docutils.additional_nodes = set() with docutils.docutils_namespace(): setup_ret = setup_func(app) # type: ignore[arg-type] directives = copy.copy(docutils.directives._directives) # type: ignore[attr-defined] roles = copy.copy(docutils.roles._roles) # type: ignore[attr-defined] additional_nodes = copy.copy(docutils.additional_nodes) finally: docutils.additional_nodes = _additional_nodes return RunSetupOutput(setup_ret, directives, roles, additional_nodes, app) def remove_html_footer(page: BeautifulSoup) -> BeautifulSoup: """ Remove the Sphinx footer from HTML pages. The footer contains the Sphinx and theme versions and therefore changes between versions. This can cause unwanted, false positive test failures. :param page: The page to remove the footer from. :return: The page without the footer. """ for div in page.select("div.footer"): div.extract() return page def remove_html_link_tags(page: BeautifulSoup) -> BeautifulSoup: """ Remove link tags from HTML pages. These may vary between different versions of Sphinx and its extensions. This can cause unwanted, false positive test failures. :param page: The page to remove the link tags from. :return: The page without the link tags. """ for div in page.select("head link"): div.extract() return page def check_html_regression(page: BeautifulSoup, file_regression: FileRegressionFixture) -> None: """ Check an HTML page generated by Sphinx for regressions, using `pytest-regressions`_. :param page: The page to test. :param file_regression: The file regression fixture. **Example usage** .. code-block:: python @pytest.mark.parametrize("page", ["index.html"], indirect=True) def test_page(page: BeautifulSoup, file_regression: FileRegressionFixture): check_html_regression(page, file_regression) """ # noqa: RST306 __tracebackhide__ = True page = remove_html_footer(page) page = remove_html_link_tags(page) for div in page.select("script"): if "_static/language_data.js" in str(div): div.extract() for div in page.select("div.sphinxsidebar"): div.extract() check_file_regression( StringList(page.prettify()), file_regression, extension=".html", ) class HTMLRegressionFixture(FileRegressionFixture): """ Subclass of :class:`pytest_regressions.file_regression.FileRegressionFixture` for checking HTML files. .. versionadded:: 2.0.0 """ def check( # type: ignore[override] self, page: BeautifulSoup, *, extension: str = ".html", jinja2: bool = False, jinja2_namespace: Optional[Dict[str, Any]] = None, **kwargs ) -> None: r""" Check an HTML page generated by Sphinx for regressions, using `pytest-regressions`_. :param page: The page to test. :param jinja2: Whether to render the reference file as a jinja2 template. :param jinja2_namespace: If ``jinja2`` is :py:obj:`True`, a mapping of variable names to values to make available in the jinja2 template. :param \*\*kwargs: Additional keyword arguments passed to :meth:`pytest_regressions.file_regression.FileRegressionFixture.check`. .. versionchanged:: 2.14.0 Added the ``jinja2`` keyword argument. .. versionchanged:: 2.17.0 Added the ``jinja2_namespace`` keyword argument. .. latex:clearpage:: When ``jinja2`` is :py:obj:`True`, the reference file will be rendered as a jinja2 template. The template is passed the following variables: * ``sphinx_version`` -- the Sphinx version number, as a tuple of integers. * ``python_version`` -- the Python version number, in the form returned by :data:`sys.version_info`. * ``docutils_version`` -- the docutils version number, as a tuple of integers (*New in version 2.16.0*). **Example usage** .. code-block:: python @pytest.mark.parametrize("page", ["index.html"], indirect=True) def test_page(page: BeautifulSoup, html_regression: HTMLRegressionFixture): html_regression.check(page, file_regression) """ # noqa: RST306 __tracebackhide__ = True page = remove_html_footer(page) page = remove_html_link_tags(page) for div in page.select("script"): if "_static/language_data.js" in str(div): div.extract() for div in page.select("div.sphinxsidebar"): div.extract() if sphinx.version_info >= (4, 3): # pragma: no cover for div in page.select("span.w"): div.extract() for div in page.select("span.p"): if div.string == '=': sibling = div.next_sibling div.replace_with('') sibling.replace_with(f"= {sibling.text}") kwargs.pop("encoding", None) kwargs.pop("extension", None) if jinja2: def check_fn(obtained_filename: PathPlus, expected_filename: PathPlus): # noqa: MAN002 __tracebackhide__ = True expected_filename = PathPlus(expected_filename) template = Template(expected_filename.read_text()) expected_filename.write_text( template.render( sphinx_version=sphinx.version_info, python_version=sys.version_info, docutils_version=docutils_version, **jinja2_namespace or {}, ) ) return check_text_files(obtained_filename, expected_filename, encoding="UTF-8") else: check_fn = partial(check_text_files, encoding="UTF-8") super().check( str(StringList(page.prettify())), encoding="UTF-8", extension=extension, check_fn=check_fn, ) @pytest.fixture() def html_regression(datadir, original_datadir, request) -> HTMLRegressionFixture: # noqa: MAN001 """ Returns an :class:`~.HTMLRegressionFixture` scoped to the test function. .. versionadded:: 2.0.0 """ return HTMLRegressionFixture(datadir, original_datadir, request) def check_asset_copy( func: Callable[[sphinx.application.Sphinx, Exception], Any], *asset_files: PathLike, file_regression: FileRegressionFixture, ) -> None: r""" Helper to test functions which respond to Sphinx ``build-finished`` events and copy asset files. .. versionadded:: 2.0.0 :param func: The function to test. :param \*asset_files: The paths of asset files copied by the function, relative to the Sphinx output directory. :param file_regression: """ __tracebackhide__ = True with tempfile.TemporaryDirectory() as tmpdir: tmp_pathplus = PathPlus(tmpdir) fake_app = SimpleNamespace() fake_app.builder = SimpleNamespace() fake_app.builder.format = "html" fake_app.outdir = fake_app.builder.outdir = tmp_pathplus func(fake_app, None) # type: ignore[arg-type] for filename in asset_files: filename = tmp_pathplus / filename check_file_output(filename, file_regression, extension=f"_{filename.stem}{filename.suffix}") _latex_date_re = re.compile(r"\\date{.*}") class LaTeXRegressionFixture(AdvancedFileRegressionFixture): """ Subclass of :class:`coincidence.regressions.AdvancedFileRegressionFixture` for checking LaTeX files. .. versionadded:: 2.17.0 """ def check( # type: ignore[override] # noqa: MAN002 self, contents: Union[str, StringList], *, extension: str = ".html", jinja2: bool = False, jinja2_namespace: Optional[Dict[str, Any]] = None, **kwargs ): r""" Check a LaTeX file generated by Sphinx for regressions, using `pytest-regressions `__ :param contents: :param jinja2: Whether to render the reference file as a jinja2 template. :param jinja2_namespace: If ``jinja2`` is :py:obj:`True`, a mapping of variable names to values to make available in the jinja2 template. :param \*\*kwargs: Additional keyword arguments passed to :meth:`pytest_regressions.file_regression.FileRegressionFixture.check`. When ``jinja2`` is :py:obj:`True`, the reference file will be rendered as a jinja2 template. The template is passed the following variables: * ``sphinx_version`` -- the Sphinx version number, as a tuple of integers. * ``python_version`` -- the Python version number, in the form returned by :data:`sys.version_info`. * ``docutils_version`` -- the docutils version number, as a tuple of integers (*New in version 2.16.0*). .. note:: Unlike standard HTML jinja2 templates, this class expects the use of ``<`` and ``>`` rather than ``{`` and ``}``. For example:: <% if foo %> <# This should only happen on Tuesdays #> << foo.upper() >> <% endif %> **Example usage** .. code-block:: python @pytest.mark.sphinx("latex") def test_latex_output(app: Sphinx, latex_regression: LaTeXRegressionFixture): app.build() output_file = app.outdir / "python.tex" latex_regression.check(output_file.read_text()) """ # noqa: D400 __tracebackhide__ = True if jinja2: def check_fn(obtained_filename: PathPlus, expected_filename: PathLike): # noqa: MAN002 __tracebackhide__ = True expected_filename = PathPlus(expected_filename) template = Template( expected_filename.read_text(), block_start_string="<%", block_end_string="%>", variable_start_string="<<", variable_end_string=">>", comment_start_string="<#", comment_end_string="#>", ) expected_filename.write_text( template.render( sphinx_version=sphinx.version_info, python_version=sys.version_info, docutils_version=docutils_version, **jinja2_namespace or {}, ) ) return check_text_files(obtained_filename, expected_filename, encoding="UTF-8") else: check_fn = partial(check_text_files, encoding="UTF-8") new_contents = _latex_date_re.sub( r"\\date{Mar 11, 2021}", str(contents).replace("\\sphinxAtStartPar\n", ''), ) new_contents = new_contents.replace("%% let collapsible ", "%% let collapsable ") # changed in Sphinx 4.2 return super().check( new_contents, extension=".tex", check_fn=check_fn, ) @pytest.fixture() def latex_regression(datadir, original_datadir, request) -> LaTeXRegressionFixture: # noqa: MAN001 """ Returns a :class:`~.LaTeXRegressionFixture` scoped to the test function. .. versionadded:: 2.17.0 """ return LaTeXRegressionFixture(datadir, original_datadir, request) PK7VJOQQsphinx_toolbox/utils.py#!/usr/bin/env python3 # # utils.py """ General utility functions. """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # singleton function based on attrs # https://github.com/python-attrs/attrs # Copyright (c) 2015 Hynek Schlawack # MIT Licensed # # stdlib import atexit import functools import re import sys from typing import ( TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Mapping, Optional, Pattern, Set, Tuple, Type, TypeVar, cast ) # 3rd party import sphinx.config from apeye.requests_url import RequestsURL from docutils.nodes import Node from domdf_python_tools.doctools import prettify_docstrings from sphinx.addnodes import desc_content from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment from sphinx.ext.autodoc import Documenter, logger from sphinx.locale import __ from typing_extensions import TypedDict __all__ = ( "add_nbsp_substitution", "allow_subclass_add", "baseclass_is_private", "code_repr", "escape_trailing__", "filter_members_warning", "flag", "get_first_matching", "GITHUB_COM", "is_namedtuple", "make_github_url", "metadata_add_version", "NoMatchError", "OptionSpec", "Param", "parse_parameters", "Purger", "SetupFunc", "SphinxExtMetadata", "typed_flag_regex", "typed_param_regex", "unknown_module_warning", "untyped_param_regex", "add_fallback_css_class", ) #: Instance of :class:`apeye.requests_url.RequestsURL` that points to the GitHub website. GITHUB_COM: RequestsURL = RequestsURL("https://github.com") #: Type hint for the ``option_spec`` variable of Docutils directives. OptionSpec = Mapping[str, Callable[[str], Any]] _T = TypeVar("_T") atexit.register(GITHUB_COM.session.close) @functools.lru_cache() def make_github_url(username: str, repository: str) -> RequestsURL: """ Construct a URL to a GitHub repository from a username and repository name. :param username: The username of the GitHub account that owns the repository. :param repository: The name of the repository. """ return GITHUB_COM / username / repository def flag(argument: Any) -> bool: """ Check for a valid flag option (no argument) and return :py:obj:`True`. Used in the ``option_spec`` of directives. .. seealso:: :class:`docutils.parsers.rst.directives.flag`, which returns :py:obj:`None` instead of :py:obj:`True`. :raises: :exc:`ValueError` if an argument is given. """ if argument and argument.strip(): raise ValueError(f"No argument is allowed; {argument!r} supplied") else: return True @prettify_docstrings class Purger: """ Class to purge redundant nodes. :param attr_name: The name of the build environment's attribute that stores the list of nodes, e.g. ``all_installation_nodes``. .. autosummary-widths:: 55/100 """ def __init__(self, attr_name: str): self.attr_name = str(attr_name) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.attr_name!r})" def purge_nodes( # pragma: no cover self, app: Sphinx, env: BuildEnvironment, docname: str, ) -> None: """ Remove all redundant nodes. This function can be configured for the :event:`env-purge-doc` event: .. code-block:: my_node_purger = Purger("all_my_node_nodes") def setup(app: Sphinx): app.connect("env-purge-doc", my_node_purger.purge_nodes) :param app: The Sphinx application. :param env: The Sphinx build environment. :param docname: The name of the document to remove nodes for. """ if not hasattr(env, self.attr_name): return all_nodes = [todo for todo in getattr(env, self.attr_name) if todo["docname"] != docname] setattr(env, self.attr_name, all_nodes) def get_outdated_docnames( self, app: Sphinx, env: BuildEnvironment, added: Set[str], changed: Set[str], removed: Set[str], ) -> List[str]: """ Returns a list of all docnames containing one or more nodes this :class:`~.Purger` is aware of. This function can be configured for the :event:`env-get-outdated` event: .. code-block:: my_node_purger = Purger("all_my_node_nodes") def setup(app: Sphinx): app.connect("env-get-outdated", my_node_purger.get_outdated_docnames) .. versionadded:: 2.7.0 :param app: The Sphinx application. :param env: The Sphinx build environment. :param added: A set of newly added documents. :param changed: A set of document names whose content has changed. :param removed: A set of document names which have been removed. """ if not hasattr(env, self.attr_name): return [] return list({todo["docname"] for todo in getattr(env, self.attr_name)}) def add_node(self, env: BuildEnvironment, node: Node, targetnode: Node, lineno: int) -> None: """ Add a node. :param env: The Sphinx build environment. :param node: :param targetnode: :param lineno: """ if not hasattr(env, self.attr_name): setattr(env, self.attr_name, []) all_nodes = getattr(env, self.attr_name) all_nodes.append({ "docname": env.docname, "lineno": lineno, "installation_node": node.deepcopy(), "target": targetnode, }) def singleton(name: str) -> object: """ Factory function to return a string singleton. :param name: The name of the singleton. """ name = str(name) class Singleton: _singleton = None def __new__(cls): if Singleton._singleton is None: Singleton._singleton = super().__new__(cls) return Singleton._singleton def __repr__(self) -> str: return name def __str__(self) -> str: return name Singleton.__name__ = name Singleton.__doc__ = f"Singleton {name}" return Singleton() no_default = singleton("no_default") class NoMatchError(ValueError): """ Raised when no matching values were found in :func:`~.get_first_matching`. .. versionadded:: 0.7.0 """ def get_first_matching( condition: Callable[[Any], bool], iterable: Iterable[_T], default: _T = no_default # type: ignore[assignment] ) -> _T: """ Returns the first value in ``iterable`` that meets ``condition``, or ``default`` if none match. .. versionadded:: 0.7.0 :param condition: The condition to evaluate. :param iterable: :param default: The default value to return if no values in ``iterable`` match. """ if default is not no_default: if not condition(default): raise ValueError("The condition must evaluate to True for the default value.") iterable = (*iterable, default) for match in iterable: if condition(match): return match raise NoMatchError(f"No matching values for '{condition}' in {iterable}") def escape_trailing__(string: str) -> str: """ Returns the given string with trailing underscores escaped to prevent Sphinx treating them as references. .. versionadded:: 0.8.0 :param string: """ if string.endswith('_'): return f"{string[:-1]}\\_" return string def code_repr(obj: Any) -> str: """ Returns the repr of the given object as reStructuredText inline code. .. versionadded:: 0.9.0 :param obj: """ return f"``{obj!r}``" class SphinxExtMetadata(TypedDict, total=False): """ :class:`typing.TypedDict` representing the metadata dictionary returned by Sphinx extensions' ``setup`` functions. This is treated by Sphinx as metadata of the extension. """ # noqa: D400 version: str """ A string that identifies the extension version. It is used for extension version requirement checking and informational purposes. If not given, ``'unknown version'`` is substituted. """ env_version: int """ An integer that identifies the version of env data structure if the extension stores any data to environment. It is used to detect the data structure has been changed from last build. Extensions have to increment the version when data structure has changed. If not given, Sphinx considers the extension does not stores any data to environment. """ parallel_read_safe: bool """ A boolean that specifies if parallel reading of source files can be used when the extension is loaded. It defaults to :py:obj:`False`, i.e. you have to explicitly specify your extension to be parallel-read-safe after checking that it is. """ parallel_write_safe: bool """ A boolean that specifies if parallel writing of output files can be used when the extension is loaded. Since extensions usually don’t negatively influence the process, this defaults to :py:obj:`True`. """ SetupFunc = Callable[[Sphinx], Optional[SphinxExtMetadata]] """ Type annotation for Sphinx extensions' ``setup`` functions. .. versionadded:: 1.9.0 """ def unknown_module_warning(documenter: Documenter) -> None: """ Log a warning that the module to import the object from is unknown. .. versionadded:: 0.2.0 :param documenter: """ msg = __( "don't know which module to import for autodocumenting %r " '(try placing a "module" or "currentmodule" directive in the document, ' "or giving an explicit module name)" ) logger.warning(msg % documenter.name, type="autodoc") def filter_members_warning(member: Any, exception: Exception) -> None: """ Log a warning when filtering members. .. versionadded:: 0.2.0 :param member: :param exception: """ logger.warning( __("autodoc: failed to determine %r to be documented, the following exception was raised:\n%s"), member, exception, type="autodoc" ) class Param(TypedDict): """ :class:`~typing.TypedDict` to represent a parameter parsed from a class or function's docstring. .. versionadded:: 0.8.0 """ #: The docstring of the parameter. doc: List[str] #: The type of the parameter. type: str # noqa: A003 # pylint: disable=redefined-builtin _identifier_pattern = r"[A-Za-z_]\w*" typed_param_regex: Pattern[str] = re.compile( fr"^:(param|parameter|arg|argument)\s*({_identifier_pattern}\s+)({_identifier_pattern}\s*):\s*(.*)", flags=re.ASCII, ) """ Regex to match ``:param : `` flags. .. versionadded:: 0.8.0 """ untyped_param_regex: Pattern[str] = re.compile( fr"^:(param|parameter|arg|argument)\s*({_identifier_pattern}\s*):\s*(.*)", flags=re.ASCII, ) """ Regex to match ``:param : `` flags. .. versionadded:: 0.8.0 """ typed_flag_regex: Pattern[str] = re.compile( fr"^:(paramtype|type)\s*({_identifier_pattern}\s*):\s*(.*)", flags=re.ASCII, ) """ Regex to match ``:type : `` flags. .. versionadded:: 0.8.0 """ def parse_parameters(lines: List[str], tab_size: int = 8) -> Tuple[Dict[str, Param], List[str], List[str]]: """ Parse parameters from the docstring of a class/function. .. versionadded:: 0.8.0 :param lines: The lines of the docstring :param tab_size: :return: A mapping of parameter names to their docstrings and types, a list of docstring lines that appeared before the parameters, and the list of docstring lines that appear after the parameters. """ a_tab = ' ' * tab_size params: Dict[str, Param] = {} last_arg: Optional[str] = None pre_output: List[str] = [] post_output: List[str] = [] def add_empty(param_name: str) -> None: if param_name not in params: params[param_name] = {"doc": [], "type": ''} for line in lines: if post_output: post_output.append(line) continue # pylint: disable=loop-global-usage typed_m = typed_param_regex.match(line) untyped_m = untyped_param_regex.match(line) type_only_m = typed_flag_regex.match(line) # pylint: enable=loop-global-usage if typed_m: last_arg = typed_m.group(3).strip() add_empty(cast(str, last_arg)) params[last_arg]["doc"] = [typed_m.group(4)] params[last_arg]["type"] = typed_m.group(2).strip() elif untyped_m: last_arg = untyped_m.group(2).strip() add_empty(cast(str, last_arg)) params[last_arg]["doc"] = [untyped_m.group(3)] elif type_only_m: add_empty(type_only_m.group(2)) params[type_only_m.group(2)]["type"] = type_only_m.group(3) elif line.startswith(a_tab) and last_arg is not None: params[last_arg]["doc"].append(line) elif last_arg is None: pre_output.append(line) else: post_output.append(line) return params, pre_output, post_output def is_namedtuple(obj: Any) -> bool: """ Returns whether the given object is a :func:`collections.namedtuple` class. .. versionadded:: 0.8.0 :param obj: """ return isinstance(obj, type) and issubclass(obj, tuple) and hasattr(obj, "_fields") def allow_subclass_add(app: Sphinx, *documenters: Type[Documenter]) -> None: """ Add the given autodocumenters, but only if a subclass of it is not already registered. This allows other libraries to extend the autodocumenters. .. versionadded:: 0.8.0 :param app: The Sphinx application. :param documenters: """ for cls in documenters: existing_documenter = app.registry.documenters.get(cls.objtype) if existing_documenter is None or not issubclass(existing_documenter, cls): app.add_autodocumenter(cls, override=True) def baseclass_is_private(obj: Type) -> bool: """ Returns :py:obj:`True` if the first and only base class starts with a double underscore. :param obj: """ if hasattr(obj, "__bases__") and len(obj.__bases__) == 1: return obj.__bases__[0].__name__.startswith("__") return False def metadata_add_version(func: SetupFunc) -> SetupFunc: """ Internal decorator for Sphinx ``setup`` functions to add the ``sphinx-toolbox`` version number to the returned metadata dict. .. versionadded:: 1.9.0 :param func: """ # noqa: D400 @functools.wraps(func) def wrapper(app: Sphinx) -> SphinxExtMetadata: # this package from sphinx_toolbox import __version__ ret = func(app) or {} ret["version"] = __version__ return ret return wrapper def add_nbsp_substitution(config: sphinx.config.Config) -> None: """ Adds the ``|nbsp|`` substitution directive to the reStructuredText prolog. .. versionadded:: 2.1.0 :param config: """ nbsp_sub = ".. |nbsp| unicode:: 0xA0\n :trim:" if not config.rst_prolog: config.rst_prolog = '' # type: ignore[attr-defined] if nbsp_sub not in config.rst_prolog: config.rst_prolog = '\n'.join([config.rst_prolog, '', nbsp_sub]) # type: ignore[attr-defined] _OBJTYPES_CSS_FALLBACKS = { "namedtuple": "class", "protocol": "class", "typeddict": "class", } # From https://github.com/mansenfranzen/autodoc_pydantic/pull/86/files # MIT Licensed # Copyright (c) 2021 Franz Wöllert def add_fallback_css_class( objtypes_css_fallbacks: Dict[str, str] ) -> Callable[[Sphinx, str, str, desc_content], None]: """ Registers a transform which will edit the CSS classes of documented objects based on their ``objtype``. :param objtypes_css_fallbacks: A mapping of Sphinx objtypes to the CSS class which should be added to them. The class is usually the ``objtype`` attribute from the documenter's parent class. .. versionadded:: 2.16.0 Used as follows: .. code-block:: python app.connect("object-description-transform", add_fallback_css_class({"typeddict": "class"})) This will apply the transformation to documented objects with the ``typeddict`` CSS class by adding the ``class`` CSS class. :param objtypes_css_fallbacks: """ def func( app: Sphinx, domain: str, objtype: str, contentnode: desc_content, ) -> None: if objtype not in objtypes_css_fallbacks: return classes = contentnode.parent.attributes["classes"] # for older sphinx versions, add objtype explicitly if sphinx.version_info < (3, 6): classes.append(objtype) idx = classes.index(objtype) fallback = objtypes_css_fallbacks[objtype] classes.insert(idx, fallback) return func if TYPE_CHECKING: class Config(sphinx.config.Config): project: Any author: Any project_copyright: str copyright: str # noqa: A003 # pylint: disable=redefined-builtin version: Any release: Any today: Any today_fmt: str language: str locale_dirs: Any figure_language_filename: str gettext_allow_fuzzy_translations: Any master_doc: Any root_doc: Any source_suffix: Any source_encoding: Any exclude_patterns: Any default_role: str add_function_parentheses: Any add_module_names: Any trim_footnote_reference_space: Any show_authors: Any pygments_style: Any highlight_language: Any highlight_options: Any templates_path: Any template_bridge: Any keep_warnings: Any suppress_warnings: Any modindex_common_prefix: Any rst_epilog: str rst_prolog: str trim_doctest_flags: Any primary_domain: Any needs_sphinx: str needs_extensions: Any manpages_url: Any nitpicky: Any nitpick_ignore: Any nitpick_ignore_regex: Any numfig: Any numfig_secnum_depth: Any numfig_format: Any math_number_all: Any math_eqref_format: str math_numfig: Any tls_verify: Any tls_cacerts: Any user_agent: str smartquotes: Any smartquotes_action: Any smartquotes_excludes: Any config_values: Dict[str, Tuple] overrides: Dict values: Dict[str, Tuple] setup: Optional[Callable] extensions: List[str] latex_engine: str latex_documents: Any latex_logo: str latex_appendices: Any latex_use_latex_multicolumn: Any latex_use_xindy: bool latex_toplevel_sectioning: str latex_domain_indices: List latex_show_urls: Any latex_show_pagerefs: Any latex_elements: Any latex_additional_files: Any latex_theme: str latex_theme_options: Any latex_theme_path: Any latex_docclass: Any html_theme: Any html_theme_path: Any html_theme_options: Any html_title: str html_short_title: Any html_style: str html_logo: str html_favicon: str html_css_files: Any html_js_files: Any html_static_path: Any html_extra_path: Any html_last_updated_fmt: str html_sidebars: Any html_additional_pages: Any html_domain_indices: List html_add_permalinks: Any html_permalinks: Any html_permalinks_icon: Any html_use_index: Any html_split_index: Any html_copy_source: Any html_show_sourcelink: Any html_sourcelink_suffix: Any html_use_opensearch: Any html_file_suffix: str html_link_suffix: str html_show_copyright: Any html_show_search_summary: Any html_show_sphinx: Any html_context: Any html_output_encoding: Any html_compact_lists: Any html_secnumber_suffix: Any html_search_language: str html_search_options: Any html_search_scorer: Any html_scaled_image_link: Any html_baseurl: Any html_codeblock_linenos_style: str html_math_renderer: Any html4_writer: Any else: Config = sphinx.config.Config if sphinx.version_info[0] >= 5 or TYPE_CHECKING: class RemovedInSphinx50Warning(RuntimeError): pass def prepare_docstring(s: str, ignore: Optional[int] = None, tabsize: int = 8) -> List[str]: if ignore is None: ignore = 1 else: raise TypeError("The 'ignore' argument to prepare_docstring() was removed in Sphinx 5.0") lines = s.expandtabs(tabsize).splitlines() # Find minimum indentation of any non-blank lines after ignored lines. margin = sys.maxsize for line in lines[ignore:]: content = len(line.lstrip()) if content: indent = len(line) - content margin = min(margin, indent) # Remove indentation from ignored lines. for i in range(ignore): if i < len(lines): lines[i] = lines[i].lstrip() if margin < sys.maxsize: for i in range(ignore, len(lines)): lines[i] = lines[i][margin:] # Remove any leading blank lines. while lines and not lines[0]: lines.pop(0) # make sure there is an empty line at the end if lines and lines[-1]: lines.append('') return lines else: # 3rd party from sphinx.deprecation import RemovedInSphinx50Warning # type: ignore[attr-defined,no-redef] # noqa: F401 from sphinx.util.docstrings import prepare_docstring # noqa: F401 PK7V5c4//sphinx_toolbox/wikipedia.py#!/usr/bin/env python3 # # wikipedia.py """ Sphinx extension to create links to Wikipedia articles. .. versionadded:: 0.2.0 .. extensions:: sphinx_toolbox.wikipedia Configuration -------------- .. latex:vspace:: -5px .. confval:: wikipedia_lang :type: :class:`str` :required: False :default: ``'en'`` The Wikipedia language to use for :rst:role:`wikipedia` roles. .. versionadded:: 0.2.0 Usage ------ .. latex:vspace:: -5px .. rst:role:: wikipedia Role which shows a link to the given article on Wikipedia. The title and language can be customised. :bold-title:`Example` .. rest-example:: :wikipedia:`Sphinx` :wikipedia:`mythical creature ` :wikipedia:`Answer to the Ultimate Question of Life, the Universe, and Everything <:de:42 (Antwort)>` .. only:: html .. rest-example:: :wikipedia:`:zh:斯芬克斯` API Reference ---------------- """ # # Copyright © 2020-2021 Dominic Davis-Foster # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. # # Based on https://github.com/quiver/sphinx-ext-wikipedia # BSD Licensed # # Parts of the docstrings based on https://docutils.sourceforge.io/docs/howto/rst-roles.html # # stdlib import re from typing import Dict, List, Tuple from urllib.parse import quote # 3rd party from apeye.url import URL from docutils import nodes from docutils.nodes import system_message from docutils.parsers.rst.states import Inliner from sphinx.application import Sphinx from sphinx.util.nodes import split_explicit_title # this package from sphinx_toolbox.utils import SphinxExtMetadata, metadata_add_version __all__ = ("make_wikipedia_link", "setup") base_url = "https://%s.wikipedia.org/wiki" _wiki_lang_re = re.compile(":(.*?):(.*)") def _get_wikipedia_lang(inliner: Inliner) -> str: # pragma: no cover return inliner.document.settings.env.config.wikipedia_lang def make_wikipedia_link( name: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: Dict = {}, content: List[str] = [] ) -> Tuple[List[nodes.reference], List[system_message]]: """ Adds a link to the given article on :wikipedia:`Wikipedia`. :param name: The local name of the interpreted role, the role name actually used in the document. :param rawtext: A string containing the entire interpreted text input, including the role and markup. :param text: The interpreted text content. :param lineno: The line number where the interpreted text begins. :param inliner: The :class:`docutils.parsers.rst.states.Inliner` object that called :func:`~.source_role`. It contains the several attributes useful for error reporting and document tree access. :param options: A dictionary of directive options for customization (from the ``role`` directive), to be interpreted by the function. Used for additional attributes for the generated elements and other functionality. :param content: A list of strings, the directive content for customization (from the ``role`` directive). To be interpreted by the function. :return: A list containing the created node, and a list containing any messages generated during the function. """ text = nodes.unescape(text) has_explicit, title, target = split_explicit_title(text) m = _wiki_lang_re.match(target) if m: lang, target = m.groups() if not has_explicit: title = target else: lang = _get_wikipedia_lang(inliner) ref = URL(base_url % lang) / quote(target.replace(' ', '_'), safe='') node = nodes.reference(rawtext, title, refuri=str(ref), **options) return [node], [] @metadata_add_version def setup(app: Sphinx) -> SphinxExtMetadata: """ Setup :mod:`sphinx_toolbox.wikipedia`. .. versionadded:: 1.0.0 :param app: The Sphinx application. """ app.add_role("wikipedia", make_wikipedia_link) app.add_config_value("wikipedia_lang", "en", "env", [str]) return {"parallel_read_safe": True} PK"7V9 --&sphinx_toolbox-3.4.0.dist-info/LICENSECopyright (c) 2020-2022 Dominic Davis-Foster Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK"7VY%%'sphinx_toolbox-3.4.0.dist-info/METADATAMetadata-Version: 2.1 Name: sphinx-toolbox Version: 3.4.0 Summary: Box of handy tools for Sphinx 🧰 📔 Author-email: Dominic Davis-Foster License: MIT Keywords: documentation,sphinx,sphinx-extension Home-page: https://github.com/sphinx-toolbox/sphinx-toolbox Project-URL: Issue Tracker, https://github.com/sphinx-toolbox/sphinx-toolbox/issues Project-URL: Source Code, https://github.com/sphinx-toolbox/sphinx-toolbox Project-URL: Documentation, https://sphinx-toolbox.readthedocs.io/en/latest Platform: Windows Platform: macOS Platform: Linux Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Sphinx :: Extension Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Documentation Classifier: Topic :: Documentation :: Sphinx Classifier: Topic :: Software Development :: Documentation Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Classifier: Typing :: Typed Requires-Python: >=3.7 Requires-Dist: apeye>=0.4.0 Requires-Dist: autodocsumm>=0.2.0 Requires-Dist: beautifulsoup4>=4.9.1 Requires-Dist: cachecontrol[filecache]>=0.12.6 Requires-Dist: dict2css>=0.2.3 Requires-Dist: docutils<0.19,>=0.16 Requires-Dist: domdf-python-tools>=2.9.0 Requires-Dist: html5lib>=1.1 Requires-Dist: lockfile>=0.12.2 Requires-Dist: ruamel.yaml>=0.16.12 Requires-Dist: sphinx>=3.2.0 Requires-Dist: sphinx-autodoc-typehints>=1.11.1 Requires-Dist: sphinx-jinja2-compat>=0.1.0 Requires-Dist: sphinx-prompt>=1.1.0 Requires-Dist: sphinx-tabs<3.5.0,>=1.2.1 Requires-Dist: tabulate>=0.8.7 Requires-Dist: typing-extensions!=3.10.0.1,>=3.7.4.3 Requires-Dist: typing-inspect>=0.6.0; python_version < "3.8" Requires-Dist: coincidence>=0.4.3; extra == 'testing' Requires-Dist: pygments<=2.13.0,>=2.7.4; extra == 'testing' Requires-Dist: coincidence>=0.4.3; extra == 'all' Requires-Dist: pygments<=2.13.0,>=2.7.4; extra == 'all' Provides-Extra: testing Provides-Extra: all Description-Content-Type: text/x-rst ############### sphinx-toolbox ############### .. start short_desc **Box of handy tools for Sphinx 🧰 📔** .. end short_desc .. start shields .. list-table:: :stub-columns: 1 :widths: 10 90 * - Docs - |docs| |docs_check| * - Tests - |actions_linux| |actions_windows| |actions_macos| |coveralls| * - PyPI - |pypi-version| |supported-versions| |supported-implementations| |wheel| * - Anaconda - |conda-version| |conda-platform| * - Activity - |commits-latest| |commits-since| |maintained| |pypi-downloads| * - QA - |codefactor| |actions_flake8| |actions_mypy| * - Other - |license| |language| |requires| .. |docs| image:: https://img.shields.io/readthedocs/sphinx-toolbox/latest?logo=read-the-docs :target: https://sphinx-toolbox.readthedocs.io/en/latest :alt: Documentation Build Status .. |docs_check| image:: https://github.com/sphinx-toolbox/sphinx-toolbox/workflows/Docs%20Check/badge.svg :target: https://github.com/sphinx-toolbox/sphinx-toolbox/actions?query=workflow%3A%22Docs+Check%22 :alt: Docs Check Status .. |actions_linux| image:: https://github.com/sphinx-toolbox/sphinx-toolbox/workflows/Linux/badge.svg :target: https://github.com/sphinx-toolbox/sphinx-toolbox/actions?query=workflow%3A%22Linux%22 :alt: Linux Test Status .. |actions_windows| image:: https://github.com/sphinx-toolbox/sphinx-toolbox/workflows/Windows/badge.svg :target: https://github.com/sphinx-toolbox/sphinx-toolbox/actions?query=workflow%3A%22Windows%22 :alt: Windows Test Status .. |actions_macos| image:: https://github.com/sphinx-toolbox/sphinx-toolbox/workflows/macOS/badge.svg :target: https://github.com/sphinx-toolbox/sphinx-toolbox/actions?query=workflow%3A%22macOS%22 :alt: macOS Test Status .. |actions_flake8| image:: https://github.com/sphinx-toolbox/sphinx-toolbox/workflows/Flake8/badge.svg :target: https://github.com/sphinx-toolbox/sphinx-toolbox/actions?query=workflow%3A%22Flake8%22 :alt: Flake8 Status .. |actions_mypy| image:: https://github.com/sphinx-toolbox/sphinx-toolbox/workflows/mypy/badge.svg :target: https://github.com/sphinx-toolbox/sphinx-toolbox/actions?query=workflow%3A%22mypy%22 :alt: mypy status .. |requires| image:: https://dependency-dash.repo-helper.uk/github/sphinx-toolbox/sphinx-toolbox/badge.svg :target: https://dependency-dash.repo-helper.uk/github/sphinx-toolbox/sphinx-toolbox/ :alt: Requirements Status .. |coveralls| image:: https://img.shields.io/coveralls/github/sphinx-toolbox/sphinx-toolbox/master?logo=coveralls :target: https://coveralls.io/github/sphinx-toolbox/sphinx-toolbox?branch=master :alt: Coverage .. |codefactor| image:: https://img.shields.io/codefactor/grade/github/sphinx-toolbox/sphinx-toolbox?logo=codefactor :target: https://www.codefactor.io/repository/github/sphinx-toolbox/sphinx-toolbox :alt: CodeFactor Grade .. |pypi-version| image:: https://img.shields.io/pypi/v/sphinx-toolbox :target: https://pypi.org/project/sphinx-toolbox/ :alt: PyPI - Package Version .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/sphinx-toolbox?logo=python&logoColor=white :target: https://pypi.org/project/sphinx-toolbox/ :alt: PyPI - Supported Python Versions .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/sphinx-toolbox :target: https://pypi.org/project/sphinx-toolbox/ :alt: PyPI - Supported Implementations .. |wheel| image:: https://img.shields.io/pypi/wheel/sphinx-toolbox :target: https://pypi.org/project/sphinx-toolbox/ :alt: PyPI - Wheel .. |conda-version| image:: https://img.shields.io/conda/v/domdfcoding/sphinx-toolbox?logo=anaconda :target: https://anaconda.org/domdfcoding/sphinx-toolbox :alt: Conda - Package Version .. |conda-platform| image:: https://img.shields.io/conda/pn/domdfcoding/sphinx-toolbox?label=conda%7Cplatform :target: https://anaconda.org/domdfcoding/sphinx-toolbox :alt: Conda - Platform .. |license| image:: https://img.shields.io/github/license/sphinx-toolbox/sphinx-toolbox :target: https://github.com/sphinx-toolbox/sphinx-toolbox/blob/master/LICENSE :alt: License .. |language| image:: https://img.shields.io/github/languages/top/sphinx-toolbox/sphinx-toolbox :alt: GitHub top language .. |commits-since| image:: https://img.shields.io/github/commits-since/sphinx-toolbox/sphinx-toolbox/v3.4.0 :target: https://github.com/sphinx-toolbox/sphinx-toolbox/pulse :alt: GitHub commits since tagged version .. |commits-latest| image:: https://img.shields.io/github/last-commit/sphinx-toolbox/sphinx-toolbox :target: https://github.com/sphinx-toolbox/sphinx-toolbox/commit/master :alt: GitHub last commit .. |maintained| image:: https://img.shields.io/maintenance/yes/2023 :alt: Maintenance .. |pypi-downloads| image:: https://img.shields.io/pypi/dm/sphinx-toolbox :target: https://pypi.org/project/sphinx-toolbox/ :alt: PyPI - Downloads .. end shields | Installation -------------- .. start installation ``sphinx-toolbox`` can be installed from PyPI or Anaconda. To install with ``pip``: .. code-block:: bash $ python -m pip install sphinx-toolbox To install with ``conda``: * First add the required channels .. code-block:: bash $ conda config --add channels https://conda.anaconda.org/conda-forge $ conda config --add channels https://conda.anaconda.org/domdfcoding * Then install .. code-block:: bash $ conda install sphinx-toolbox .. end installation PK"7V=TT$sphinx_toolbox-3.4.0.dist-info/WHEELWheel-Version: 1.0 Generator: whey (0.0.23) Root-Is-Purelib: true Tag: py3-none-any PK"7V/sphinx_toolbox-3.4.0.dist-info/entry_points.txtPK"7Ve %sphinx_toolbox-3.4.0.dist-info/RECORDsphinx_toolbox/github/__init__.py,sha256=qScMENfGrSVNfqWw1ONYAqEp8v7Gfi-wl8f48jnyvt4,7758 sphinx_toolbox/github/issues.py,sha256=wPskZiPLgL9fDy_TmZutPgu9T6Fr0L4r_6CNJQGyFJQ,10784 sphinx_toolbox/github/repos_and_users.py,sha256=H-AIJdoeyOZYWIhOHgcOSopV281SLNv8yd9mKrSp0Nk,7456 sphinx_toolbox/latex/__init__.py,sha256=k0j1yVab0bMEyQdQhPxeb-tJ0t1l8U37Qz1ZewldH78,18221 sphinx_toolbox/latex/layout.py,sha256=DWhf-KkdcJjQNIJ6Kp1uM2ryRHqVqcTaDN8LYLEbufI,7205 sphinx_toolbox/latex/succinct_seealso.py,sha256=4Ea1-r6vIMSQ2SwCfWAtk8KjVmGC07UAVT7o24fKtHA,2827 sphinx_toolbox/latex/toc.py,sha256=x2l8A_G01zBPxmj-61VlrdFRKoY3cnScde8nnSPanNk,4077 sphinx_toolbox/more_autodoc/__init__.py,sha256=6SaS9DCKcx3kpGhdlYd2pSUx7E-1t5QZqmy-wWBJaSM,5754 sphinx_toolbox/more_autodoc/augment_defaults.py,sha256=SiOyA7PlLggttL329U-gF8Kg8pAbYTSzVyuqBCQwG4k,5464 sphinx_toolbox/more_autodoc/autonamedtuple.py,sha256=I0tAz2asAor9j40WwY0OPlZisVqPcCe2sf8yv-nGqUs,16560 sphinx_toolbox/more_autodoc/autoprotocol.py,sha256=64nDi_qmZRPoZ2-_5IAdkDThvy4hXiIdqd_WUmflaEo,11620 sphinx_toolbox/more_autodoc/autotypeddict.py,sha256=yvUBHPpC-gh1h25rqjjIWHbVZRy3iFxCE0noMJkkBqY,13121 sphinx_toolbox/more_autodoc/generic_bases.py,sha256=KCCZtBHAzyjecx7MgZaDmDouoZOObgX-BoJnhbXHrEY,5743 sphinx_toolbox/more_autodoc/genericalias.py,sha256=a_-2QW6iTpaJAu-24BrG5GykI-OXCqbAAZyvr72r7ZM,5019 sphinx_toolbox/more_autodoc/no_docstring.py,sha256=O2bYkv_2GvyrKusJ778SfNiaayf4gSItabOrfJQYFNw,3354 sphinx_toolbox/more_autodoc/overloads.py,sha256=GAObOV0kIRAs6MCeAFFRf3uuN45bWZUCXf1iEFqpffc,14660 sphinx_toolbox/more_autodoc/regex.py,sha256=ob6U0WXd6tS05G8o7c0hoTAvp02kX4bwwuNvrUTN_AU,27056 sphinx_toolbox/more_autodoc/sourcelink.py,sha256=J9FtwMAcBYpzZE9rH0CkEoS4XHirg0heqn2FRDu_lqY,4753 sphinx_toolbox/more_autodoc/typehints.py,sha256=q2RvnsAA7winQ5gQe3U98OloWRFQETyC_sEX1TDOoeM,27915 sphinx_toolbox/more_autodoc/typevars.py,sha256=v24bd0i1FKjmDz7hCNuArsxS7QCdgQ0VXukAnJ-n0qI,10872 sphinx_toolbox/more_autodoc/variables.py,sha256=gM8CxFDiPUYMstvGw_s6Mos1jgflhbWIp4mI_G3PP2g,20878 sphinx_toolbox/more_autosummary/__init__.py,sha256=fJWqjmCiySGY6qgdojaBotMbSEoswgiJfVUKb6pmqzM,22600 sphinx_toolbox/more_autosummary/column_widths.py,sha256=Si71zwDguqUAbjAoiHkkyY4YiswTnZPbjsnfnTZOO6c,9011 sphinx_toolbox/tweaks/__init__.py,sha256=25IMO1-6WZKorYZ84NbNI94z5dlTLqRPfqiRhk8RqtE,1267 sphinx_toolbox/tweaks/footnote_symbols.py,sha256=pcKyNOOVeFvs1Rk9sSsXHGqlnT11xMSj3mXTZ4T_XfE,2683 sphinx_toolbox/tweaks/latex_layout.py,sha256=JsVYEp73TE9AITpOxUlhN7pdAr_HxKzWBAmTB5ycfkQ,3133 sphinx_toolbox/tweaks/latex_toc.py,sha256=tqU013868ISvwuUaBY4Z7aM94kLDgkpiVaoVHC7qnMM,2333 sphinx_toolbox/tweaks/param_dash.py,sha256=RVyImIP6OvZUnMI44Dc6DdNvjtjB_kAYHOJHPPfIQzo,5469 sphinx_toolbox/tweaks/revert_footnote_style.py,sha256=vWXm7zTcXY_zF_Boks-XMuzh-CuEG3f5pDiPM5030So,6089 sphinx_toolbox/tweaks/sphinx_panels_tabs.py,sha256=5vRy4qpbh21_ItPERNo9mgVw-bfU1-YYJuXi03Bw_fU,4133 sphinx_toolbox/tweaks/tabsize.py,sha256=WEvZKxLwXN3Zy7bEmUFWtJldV7BpLm8FcmClwsTHr5M,2619 sphinx_toolbox/__init__.py,sha256=C984UpXx6KMapqgi6J-eiROq-8AfDsyplLyiJJXUxU4,3396 sphinx_toolbox/__main__.py,sha256=VLPsTocytL6xERqSyCtvsR8RioTpNSrJXUY1ImEM4GY,1539 sphinx_toolbox/_css.py,sha256=9VYCt1R88eRYA5UDO6hXXwWzL18Tez4xivFiF2tyX-8,4396 sphinx_toolbox/_data_documenter.py,sha256=9-Zrn3_uRxRjM3kOrGYCgHYH6LLhC4p6u41vIIzDWEc,4780 sphinx_toolbox/assets.py,sha256=mYKhla3vGAsNkExL1rRaR2sXzLQedN7Ysd0m9H41hgU,6904 sphinx_toolbox/cache.py,sha256=Av9051iSKsx0EVrEn2wOFJdRhCAph23ZsHgjlxiUpto,1455 sphinx_toolbox/changeset.py,sha256=H2ynL-RyVA6VmxcdKZmKJYW1d6DuiGwN2ijqYllWrLA,7233 sphinx_toolbox/code.py,sha256=fkOS-6rO3j4PjJGDJXkvjyvbqqv4RpPNhiNSGEcKK80,12261 sphinx_toolbox/collapse.py,sha256=kod1T_LagyLhvfabMK6IvIetglsoryFn8zQibR2Be5g,5397 sphinx_toolbox/config.py,sha256=ZcLmM2zgRl_FcgiQOMB9wZwZ1kT-GscTMeTt1-1z_ig,4124 sphinx_toolbox/confval.py,sha256=kDo7OXm0fU2whq7URuOuEvje70PuCeexaq276TrYfYA,6019 sphinx_toolbox/decorators.py,sha256=M_sBqyUdlrQNpeb-bk8nQQLBDYal2u7HPsJ6_nJSt7o,4645 sphinx_toolbox/documentation_summary.py,sha256=VYzVz8z1Zy89Y-DZReFZbmCuXcfhYFH74dXsAEmQ-Is,6415 sphinx_toolbox/flake8.py,sha256=UQDPMOos9OR45oGLny9bWW4AWVl-s3B-gtIcON7oWwU,4100 sphinx_toolbox/formatting.py,sha256=stvXkb8WH9UY2QM-BImC67yV1LgNplI8Y0Bi64puQA0,5789 sphinx_toolbox/installation.py,sha256=--6BKuI6q3swql4qGSisEFqBFlDp8cEAFfpQ7dDkM4I,22342 sphinx_toolbox/issues.py,sha256=jT4DrsUX5yJcJTDBvk2q7mLCfUEXix7kg0sYPgKdLcE,4059 sphinx_toolbox/pre_commit.py,sha256=BhkrO4VLcoKha1wS7yAoJA5xOl-yZHlBE9p_uPStowk,10566 sphinx_toolbox/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 sphinx_toolbox/rest_example.py,sha256=jzV4dnUaeKLvkYK8A6gFHF147nrymaZL9WXbPSU7KXw,5181 sphinx_toolbox/shields.py,sha256=J18zHkJdxTxQCh5YmgyOzi39Yvm2qT__9Pk8Ctjy1kI,23655 sphinx_toolbox/sidebar_links.py,sha256=hQNMmSx_oOzVSNvmlvUF7RCl2qAQI-3WTYnnKN1ieMU,5535 sphinx_toolbox/source.py,sha256=j2bP_WzKhFjAmZ-WKvrZ_p8xmtH8i-6ZT_KHJnC-VlY,7793 sphinx_toolbox/testing.py,sha256=QhTFnzGoRVqNyEZI_P-Mcmq06oRHBn-dijUUl47eVoo,27824 sphinx_toolbox/utils.py,sha256=3g1BiBXFTZjhP2bXAf4gKEjA1I2G8_tteOqF8Rfcf10,20756 sphinx_toolbox/wikipedia.py,sha256=hA5B6_QTWZAoG3OePCs9SKb1THLgDjax_XBloSnmlxA,4911 sphinx_toolbox-3.4.0.dist-info/LICENSE,sha256=02agPL-7g6AY5rNwD4d7A2RyP7gHrWUDaljAgERQm_c,1069 sphinx_toolbox-3.4.0.dist-info/METADATA,sha256=ZzHd6LsMWqaJxnaf_TrnNJV110gdFTb6OsH6LgbKxco,7973 sphinx_toolbox-3.4.0.dist-info/WHEEL,sha256=eWG-VKlkr28uNKLeibEoqxkr0uD-5B1bTkj-6eVDwmk,84 sphinx_toolbox-3.4.0.dist-info/entry_points.txt,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 sphinx_toolbox-3.4.0.dist-info/RECORD,, PK7V9gNN!sphinx_toolbox/github/__init__.pyPK7V?7 * *sphinx_toolbox/github/issues.pyPK7Vo  (Hsphinx_toolbox/github/repos_and_users.pyPK7Vݝ-G-G Pfsphinx_toolbox/latex/__init__.pyPK7V@%%sphinx_toolbox/latex/layout.pyPK7VK (sphinx_toolbox/latex/succinct_seealso.pyPK7Vcmsphinx_toolbox/latex/toc.pyPK7Vj5Fzz'sphinx_toolbox/more_autodoc/__init__.pyPK7Vv7'XX/Rsphinx_toolbox/more_autodoc/augment_defaults.pyPK7V+Qm@@-sphinx_toolbox/more_autodoc/autonamedtuple.pyPK7V6gid-d-+Rsphinx_toolbox/more_autodoc/autoprotocol.pyPK7V,LTSA3A3,sphinx_toolbox/more_autodoc/autotypeddict.pyPK7V_Foo,*sphinx_toolbox/more_autodoc/generic_bases.pyPK7VrD9+sphinx_toolbox/more_autodoc/genericalias.pyPK7V-w  +sphinx_toolbox/more_autodoc/no_docstring.pyPK7VΗD9D9(*sphinx_toolbox/more_autodoc/overloads.pyPK7V&ii$%sphinx_toolbox/more_autodoc/regex.pyPK7Vd!j)sphinx_toolbox/more_autodoc/sourcelink.pyPK7VC m m(~sphinx_toolbox/more_autodoc/typehints.pyPK7V׀#x*x*'sphinx_toolbox/more_autodoc/typevars.pyPK7VlIQQ(:sphinx_toolbox/more_autodoc/variables.pyPK7V;VHXHX+`sphinx_toolbox/more_autosummary/__init__.pyPK7V3#3#0sphinx_toolbox/more_autosummary/column_widths.pyPK7V !rsphinx_toolbox/tweaks/__init__.pyPK7V'{ { ) sphinx_toolbox/tweaks/footnote_symbols.pyPK7VulH= = %fsphinx_toolbox/tweaks/latex_layout.pyPK7VpE  "$sphinx_toolbox/tweaks/latex_toc.pyPK7VX]]#C.sphinx_toolbox/tweaks/param_dash.pyPK7Vm.Csphinx_toolbox/tweaks/revert_footnote_style.pyPK7V|%%+[sphinx_toolbox/tweaks/sphinx_panels_tabs.pyPK7V+; ; dlsphinx_toolbox/tweaks/tabsize.pyPK7V5]D D vsphinx_toolbox/__init__.pyPK7VhYsphinx_toolbox/__main__.pyPK7VO ,,sphinx_toolbox/_css.pyPK7Vq"sphinx_toolbox/_data_documenter.pyPK7V;sphinx_toolbox/assets.pyPK7V7sphinx_toolbox/cache.pyPK7VIAAsphinx_toolbox/changeset.pyPK7V//lsphinx_toolbox/code.pyPK7VpDsphinx_toolbox/collapse.pyPK7VKQ1sphinx_toolbox/config.pyPK7VxB$Bsphinx_toolbox/confval.pyPK7V%%Ysphinx_toolbox/decorators.pyPK7VI'=lsphinx_toolbox/documentation_summary.pyPK7VckGsphinx_toolbox/flake8.pyPK7V˕sphinx_toolbox/formatting.pyPK7V=gFWFWsphinx_toolbox/installation.pyPK7V`$sphinx_toolbox/issues.pyPK7VF)F)5sphinx_toolbox/pre_commit.pyPK7V=sphinx_toolbox/py.typedPK7V(g===sphinx_toolbox/rest_example.pyPK7V_u?g\g\cRsphinx_toolbox/shields.pyPK7V;`sphinx_toolbox/sidebar_links.pyPK7V qqsphinx_toolbox/source.pyPK7V6$llsphinx_toolbox/testing.pyPK7VJOQQkPsphinx_toolbox/utils.pyPK7V5c4//sphinx_toolbox/wikipedia.pyPK"7V9 --&sphinx_toolbox-3.4.0.dist-info/LICENSEPK"7VY%%'sphinx_toolbox-3.4.0.dist-info/METADATAPK"7V=TT$sphinx_toolbox-3.4.0.dist-info/WHEELPK"7V/sphinx_toolbox-3.4.0.dist-info/entry_points.txtPK"7Ve %sphinx_toolbox-3.4.0.dist-info/RECORDPK>>R