-
Notifications
You must be signed in to change notification settings - Fork 5.3k
[WIP] docs: Add rstcheck for generarated rst #15786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| [rstcheck] | ||
| ignore_directives=ifconfig,substitution-code-block,tabs,validated-code-block | ||
| ignore_roles=repo | ||
| # reenable this | ||
| ignore_messages=(Hyperlink) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,3 @@ | ||
| .. _api: | ||
|
|
||
| API | ||
| === | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,3 @@ | ||
| .. _building: | ||
|
|
||
|
|
||
| Building | ||
| ======== | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
|
|
||
| import argparse | ||
| import logging | ||
| import multiprocessing | ||
| import os | ||
| import subprocess | ||
| import sys | ||
|
|
@@ -73,6 +74,15 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: | |
| """Override this method to add custom arguments to the arg parser""" | ||
| pass | ||
|
|
||
| def parallel(self, cmd, args, handle): | ||
| errors = 0 | ||
| pool = multiprocessing.Pool(multiprocessing.cpu_count()) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a context manager |
||
| try: | ||
| handle(pool.map(cmd, args)) | ||
| finally: | ||
| pool.close() | ||
| return errors | ||
|
|
||
|
|
||
| class ForkingAdapter(object): | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| #!/usr/bin/python3 | ||
|
|
||
| import argparse | ||
| import configparser | ||
| import multiprocessing | ||
| import re | ||
| import os | ||
| import shutil | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
| from contextlib import closing, contextmanager | ||
| from functools import cached_property | ||
|
|
||
| import docutils | ||
| import rstcheck | ||
| import sphinx | ||
|
|
||
| from tools.base import checker, utils | ||
|
|
||
|
|
||
| # things we dont want to see in generated docs | ||
| # TODO(phlax): move to .rstcheck.cfg when available | ||
| RSTCHECK_GREP_FAIL = (" ref:", "\\[\\#") | ||
| RSTCHECK_CONFIG=".rstcheck.cfg" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: spacing (why doesn't our linter cover this?)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think it does - flake8 might not because we dont enable most pep8 checks yet, but yapf isnt happy with it |
||
| RSTCHECK_COMMAND = ("rstcheck", "-r") | ||
|
|
||
|
|
||
| @contextmanager | ||
| def sphinx_enabled(): | ||
| """Register Sphinx directives and roles.""" | ||
| srcdir = tempfile.mkdtemp() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not |
||
| outdir = os.path.join(srcdir, '_build') | ||
| try: | ||
| sphinx.application.Sphinx( | ||
| srcdir=srcdir, | ||
| confdir=None, | ||
| outdir=outdir, | ||
| doctreedir=outdir, | ||
| buildername='dummy', | ||
| status=None) | ||
| yield | ||
| finally: | ||
| shutil.rmtree(srcdir) | ||
|
|
||
|
|
||
| def _check_rst_file(params: tuple) -> list: | ||
| """Check an rst file and return list of errors. | ||
|
|
||
| This function *must* be module level in order to be pickled | ||
| for multiprocessing | ||
| """ | ||
| filename, args = params | ||
|
|
||
| with closing(docutils.io.FileInput(source_path=filename)) as input_file: | ||
| contents = input_file.read() | ||
|
|
||
| rstcheck.ignore_directives_and_roles( | ||
| args.ignore_directives, args.ignore_roles) | ||
|
|
||
| for substitution in args.ignore_substitutions: | ||
| contents = contents.replace(f"|{substitution}|", "None") | ||
|
|
||
| ignore = { | ||
| "languages": args.ignore_language, | ||
| "messages": args.ignore_messages} | ||
| return filename, list( | ||
| rstcheck.check( | ||
| contents, | ||
| filename=filename, | ||
| report_level=args.report, | ||
| ignore=ignore)) | ||
|
|
||
|
|
||
| class RstChecker(checker.ForkingChecker): | ||
| checks = ("greps", "rstcheck") | ||
|
|
||
| @property | ||
| def failing_regexes(self) -> tuple: | ||
| return RSTCHECK_GREP_FAIL | ||
|
|
||
| @cached_property | ||
| def rstcheck_args(self) -> argparse.Namespace: | ||
| """Return new ``args`` with configuration loaded from file.""" | ||
| args = argparse.Namespace() | ||
| args.report = self.rstcheck_report_level | ||
| for ignore in ["language", "directives", "substitutions", "roles"]: | ||
| setattr( | ||
| args, | ||
| f"ignore_{ignore}", | ||
| [o.strip() | ||
| for o in self.rstcheck_config.get(f"ignore_{ignore}", "").split(",") | ||
| if o.strip()]) | ||
| args.ignore_messages = self.rstcheck_config.get("ignore_messages", "") | ||
| return args | ||
|
|
||
| @cached_property | ||
| def rstcheck_config(self) -> dict: | ||
| parser = configparser.ConfigParser() | ||
| parser.read(rstcheck.find_config(self.path)) | ||
| try: | ||
| return dict(parser.items("rstcheck")) | ||
| except configparser.NoSectionError: | ||
| return {} | ||
|
|
||
| @cached_property | ||
| def rstcheck_report_level(self) -> int: | ||
| report = self.rstcheck_config.get("report", "info") | ||
| threshold_dictionary = docutils.frontend.OptionParser.thresholds | ||
| return int(threshold_dictionary.get(report, report)) | ||
|
|
||
| @cached_property | ||
| def rst_files(self) -> list: | ||
| return list( | ||
| rstcheck.find_files( | ||
| filenames=self.paths, | ||
| recursive=True)) | ||
|
|
||
| def check_greps(self) -> None: | ||
| for check in self.failing_regexes: | ||
| self.grep_unwanted(check) | ||
|
|
||
| def check_rstcheck(self) -> None: | ||
| with sphinx_enabled(): | ||
| try: | ||
| self.parallel( | ||
| _check_rst_file, | ||
| [(path, self.rstcheck_args) for path in self.rst_files], | ||
| self._rstcheck_handle) | ||
| except (IOError, UnicodeError) as e: | ||
| self.error("rstcheck", [f"Rstcheck failed: {e}"]) | ||
|
|
||
| def grep_unwanted(self, check: str) -> None: | ||
| response = self.fork(["grep", "-nr", "--include", "\\*.rst"] + [check]) | ||
| if not response.returncode: | ||
| self.error("grep", [f"grepping found bad '{check}': {path}" for path in resp.stdout.split("\n")]) | ||
| else: | ||
| self.succeed("grep", [f"grepping found no errors for '{check}'"]) | ||
|
|
||
| def _rstcheck_handle(self, results: list) -> None: | ||
| """Handle multiprocessed error results of rstchecks""" | ||
| for (filename, _errors) in results: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: parens not needed here and below |
||
| for (line_number, message) in _errors: | ||
| if not re.match(r'\([A-Z]+/[0-9]+\)', message): | ||
| message = '(ERROR/3) ' + message | ||
| self.error( | ||
| "rstcheck", | ||
| [f"{self._strip_line(filename)}:{line_number}: {message}"]) | ||
| else: | ||
| self.succeed("rstcheck", [f"No errors for: {filename}"]) | ||
|
|
||
| def _strip_line(self, line: str) -> str: | ||
| return line[len(self.path) + 1:] if line.startswith(f"{self.path}/") else line | ||
|
|
||
|
|
||
| def main(*args: list) -> None: | ||
| return RstChecker(*args).run_checks() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main(*sys.argv[1:])) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
|
|
||
| from unittest.mock import patch | ||
|
|
||
| from tools.code_format import rst_check | ||
|
|
||
|
|
||
| def test_python_checker_main(): | ||
| class_mock = patch("tools.code_format.rst_check.RstChecker") | ||
|
|
||
| with class_mock as m_class: | ||
| assert ( | ||
| rst_check.main("arg0", "arg1", "arg2") | ||
| == m_class.return_value.run_checks.return_value) | ||
|
|
||
| assert ( | ||
| list(m_class.call_args) | ||
| == [('arg0', 'arg1', 'arg2'), {}]) | ||
| assert ( | ||
| list(m_class.return_value.run_checks.call_args) | ||
| == [(), {}]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handle this better