#!/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 <dominic@davis-foster.co.uk>
#
# 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")
[docs]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]
[docs]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]
[docs]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[method-assign]
[docs]@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}