From ffb8f98869748ad2351597ce7325ff91b4b934e8 Mon Sep 17 00:00:00 2001 From: Julien Palard Date: Thu, 3 Feb 2022 11:14:19 +0100 Subject: [PATCH 1/5] bpo-42238: [doc] moving from rstlint.py to sphinx-lint. --- Doc/Makefile | 5 +- Doc/make.bat | 12 +- Doc/requirements.txt | 2 + Doc/tools/rstlint.py | 403 ------------------------------------------- 4 files changed, 16 insertions(+), 406 deletions(-) delete mode 100755 Doc/tools/rstlint.py diff --git a/Doc/Makefile b/Doc/Makefile index e60aa3427fea24..61a7ce0d0981f0 100644 --- a/Doc/Makefile +++ b/Doc/Makefile @@ -7,6 +7,7 @@ PYTHON = python3 VENVDIR = ./venv SPHINXBUILD = PATH=$(VENVDIR)/bin:$$PATH sphinx-build +SPHINXLINT = PATH=$(VENVDIR)/bin:$$PATH sphinx-lint BLURB = PATH=$(VENVDIR)/bin:$$PATH blurb PAPER = SOURCES = @@ -214,8 +215,8 @@ dist: rm dist/python-$(DISTVERSION)-docs-texinfo.tar check: - $(PYTHON) tools/rstlint.py -i tools -i $(VENVDIR) -i README.rst - $(PYTHON) tools/rstlint.py ../Misc/NEWS.d/next/ + $(SPHINXLINT) -i tools -i $(VENVDIR) -i README.rst + $(SPHINXLINT) ../Misc/NEWS.d/next/ serve: $(PYTHON) ../Tools/scripts/serve.py build/html $(SERVE_PORT) diff --git a/Doc/make.bat b/Doc/make.bat index 7fde0636427713..9eaaa46806829f 100644 --- a/Doc/make.bat +++ b/Doc/make.bat @@ -36,6 +36,16 @@ if not defined BLURB ( set BLURB=%PYTHON% -m blurb ) +if not defined SPHINXLINT ( + %PYTHON% -c "import sphinxlint" > nul 2> nul + if errorlevel 1 ( + echo Installing sphinx-lint with %PYTHON% + %PYTHON% -m pip install sphinx-lint + if errorlevel 1 exit /B + ) + set SPHINXLINT=%PYTHON% -m sphinxlint +) + if "%1" NEQ "htmlhelp" goto :skiphhcsearch if exist "%HTMLHELP%" goto :skiphhcsearch @@ -168,7 +178,7 @@ if EXIST "%BUILDDIR%\html\index.html" ( goto end :check -cmd /S /C "%PYTHON% tools\rstlint.py -i tools" +cmd /S /C "%SPHINXLINT% -i tools" goto end :serve diff --git a/Doc/requirements.txt b/Doc/requirements.txt index 0331a8dbebc46c..687f2ee1862e7a 100644 --- a/Doc/requirements.txt +++ b/Doc/requirements.txt @@ -7,6 +7,8 @@ sphinx==4.2.0 blurb +sphinx-lint + # The theme used by the documentation is stored separately, so we need # to install that as well. python-docs-theme>=2022.1 diff --git a/Doc/tools/rstlint.py b/Doc/tools/rstlint.py deleted file mode 100755 index 33cbaadfce944e..00000000000000 --- a/Doc/tools/rstlint.py +++ /dev/null @@ -1,403 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# Check for stylistic and formal issues in .rst and .py -# files included in the documentation. -# -# 01/2009, Georg Brandl - -# TODO: - wrong versions in versionadded/changed -# - wrong markup after versionchanged directive - -import os -import re -import sys -import getopt -from string import ascii_letters -from os.path import join, splitext, abspath, exists -from collections import defaultdict - -directives = [ - # standard docutils ones - 'admonition', 'attention', 'caution', 'class', 'compound', 'container', - 'contents', 'csv-table', 'danger', 'date', 'default-role', 'epigraph', - 'error', 'figure', 'footer', 'header', 'highlights', 'hint', 'image', - 'important', 'include', 'line-block', 'list-table', 'meta', 'note', - 'parsed-literal', 'pull-quote', 'raw', 'replace', - 'restructuredtext-test-directive', 'role', 'rubric', 'sectnum', 'sidebar', - 'table', 'target-notes', 'tip', 'title', 'topic', 'unicode', 'warning', - # Sphinx and Python docs custom ones - 'acks', 'attribute', 'autoattribute', 'autoclass', 'autodata', - 'autoexception', 'autofunction', 'automethod', 'automodule', - 'availability', 'centered', 'cfunction', 'class', 'classmethod', 'cmacro', - 'cmdoption', 'cmember', 'code-block', 'confval', 'cssclass', 'ctype', - 'currentmodule', 'cvar', 'data', 'decorator', 'decoratormethod', - 'deprecated-removed', 'deprecated(?!-removed)', 'describe', 'directive', - 'doctest', 'envvar', 'event', 'exception', 'function', 'glossary', - 'highlight', 'highlightlang', 'impl-detail', 'index', 'literalinclude', - 'method', 'miscnews', 'module', 'moduleauthor', 'opcode', 'pdbcommand', - 'productionlist', 'program', 'role', 'sectionauthor', 'seealso', - 'sourcecode', 'staticmethod', 'tabularcolumns', 'testcode', 'testoutput', - 'testsetup', 'toctree', 'todo', 'todolist', 'versionadded', - 'versionchanged' -] - -roles = [ - "(? 81: - # don't complain about tables, links and function signatures - if line.lstrip()[0] not in '+|' and \ - 'http://' not in line and \ - not line.lstrip().startswith(('.. function', - '.. method', - '.. cfunction')): - yield lno+1, "line too long" - - -@checker('.html', severity=2, falsepositives=True) -def check_leaked_markup(fn, lines): - """Check HTML files for leaked reST markup; this only works if - the HTML files have been built. - """ - for lno, line in enumerate(lines): - if leaked_markup_re.search(line): - yield lno+1, 'possibly leaked markup: %r' % line - - -def hide_literal_blocks(lines): - """Tool to remove literal blocks from given lines. - - It yields empty lines in place of blocks, so line numbers are - still meaningful. - """ - in_block = False - for line in lines: - if line.endswith("::\n"): - in_block = True - elif in_block: - if line == "\n" or line.startswith(" "): - line = "\n" - else: - in_block = False - yield line - - -def type_of_explicit_markup(line): - if re.match(fr'\.\. {all_directives}::', line): - return 'directive' - if re.match(r'\.\. \[[0-9]+\] ', line): - return 'footnote' - if re.match(r'\.\. \[[^\]]+\] ', line): - return 'citation' - if re.match(r'\.\. _.*[^_]: ', line): - return 'target' - if re.match(r'\.\. \|[^\|]*\| ', line): - return 'substitution_definition' - return 'comment' - - -def hide_comments(lines): - """Tool to remove comments from given lines. - - It yields empty lines in place of comments, so line numbers are - still meaningful. - """ - in_multiline_comment = False - for line in lines: - if line == "..\n": - in_multiline_comment = True - elif in_multiline_comment: - if line == "\n" or line.startswith(" "): - line = "\n" - else: - in_multiline_comment = False - if line.startswith(".. ") and type_of_explicit_markup(line) == 'comment': - line = "\n" - yield line - - - -@checker(".rst", severity=2) -def check_missing_surrogate_space_on_plural(fn, lines): - r"""Check for missing 'backslash-space' between a code sample a letter. - - Good: ``Point``\ s - Bad: ``Point``s - """ - in_code_sample = False - check_next_one = False - for lno, line in enumerate(hide_comments(hide_literal_blocks(lines))): - tokens = line.split("``") - for token_no, token in enumerate(tokens): - if check_next_one: - if token[0] in ascii_letters: - yield lno + 1, f"Missing backslash-space between code sample and {token!r}." - check_next_one = False - if token_no == len(tokens) - 1: - continue - if in_code_sample: - check_next_one = True - in_code_sample = not in_code_sample - -def main(argv): - usage = '''\ -Usage: %s [-v] [-f] [-s sev] [-i path]* [path] - -Options: -v verbose (print all checked file names) - -f enable checkers that yield many false positives - -s sev only show problems with severity >= sev - -i path ignore subdir or file path -''' % argv[0] - try: - gopts, args = getopt.getopt(argv[1:], 'vfs:i:') - except getopt.GetoptError: - print(usage) - return 2 - - verbose = False - severity = 1 - ignore = [] - falsepos = False - for opt, val in gopts: - if opt == '-v': - verbose = True - elif opt == '-f': - falsepos = True - elif opt == '-s': - severity = int(val) - elif opt == '-i': - ignore.append(abspath(val)) - - if len(args) == 0: - path = '.' - elif len(args) == 1: - path = args[0] - else: - print(usage) - return 2 - - if not exists(path): - print('Error: path %s does not exist' % path) - return 2 - - count = defaultdict(int) - - for root, dirs, files in os.walk(path): - # ignore subdirs in ignore list - if abspath(root) in ignore: - del dirs[:] - continue - - for fn in files: - fn = join(root, fn) - if fn[:2] == './': - fn = fn[2:] - - # ignore files in ignore list - if abspath(fn) in ignore: - continue - - ext = splitext(fn)[1] - checkerlist = checkers.get(ext, None) - if not checkerlist: - continue - - if verbose: - print('Checking %s...' % fn) - - try: - with open(fn, 'r', encoding='utf-8') as f: - lines = list(f) - except (IOError, OSError) as err: - print('%s: cannot open: %s' % (fn, err)) - count[4] += 1 - continue - - for checker in checkerlist: - if checker.falsepositives and not falsepos: - continue - csev = checker.severity - if csev >= severity: - for lno, msg in checker(fn, lines): - print('[%d] %s:%d: %s' % (csev, fn, lno, msg)) - count[csev] += 1 - if verbose: - print() - if not count: - if severity > 1: - print('No problems with severity >= %d found.' % severity) - else: - print('No problems found.') - else: - for severity in sorted(count): - number = count[severity] - print('%d problem%s with severity %d found.' % - (number, number > 1 and 's' or '', severity)) - return int(bool(count)) - - -if __name__ == '__main__': - sys.exit(main(sys.argv)) From 22da852f0cabe5a5ea4603a8b8ebfd1da58078c5 Mon Sep 17 00:00:00 2001 From: Julien Palard Date: Thu, 3 Feb 2022 11:25:29 +0100 Subject: [PATCH 2/5] Add news. --- .../next/Documentation/2022-02-03-11-24-59.bpo-42238.yJcMa8.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Documentation/2022-02-03-11-24-59.bpo-42238.yJcMa8.rst diff --git a/Misc/NEWS.d/next/Documentation/2022-02-03-11-24-59.bpo-42238.yJcMa8.rst b/Misc/NEWS.d/next/Documentation/2022-02-03-11-24-59.bpo-42238.yJcMa8.rst new file mode 100644 index 00000000000000..a8dffff3fcf289 --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2022-02-03-11-24-59.bpo-42238.yJcMa8.rst @@ -0,0 +1,2 @@ +``Doc/tools/rstlint.py`` has moved to its own repository and is now packaged +on PyPI as ``sphinx-lint``. From a9bc65fe19987f4928344f3d907348c9022d6588 Mon Sep 17 00:00:00 2001 From: Julien Palard Date: Thu, 3 Feb 2022 13:14:28 +0100 Subject: [PATCH 3/5] Pin a bit, just in case we want to release breaking changes. --- Doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/requirements.txt b/Doc/requirements.txt index 687f2ee1862e7a..3b28495d4b4d04 100644 --- a/Doc/requirements.txt +++ b/Doc/requirements.txt @@ -7,7 +7,7 @@ sphinx==4.2.0 blurb -sphinx-lint +sphinx-lint<1 # The theme used by the documentation is stored separately, so we need # to install that as well. From ee7f4d9fac03b697ab625420de903afdc8ecb50c Mon Sep 17 00:00:00 2001 From: Julien Palard Date: Mon, 7 Feb 2022 11:00:32 +0100 Subject: [PATCH 4/5] Don't drop the file that soon, notify about its move instead. --- Doc/tools/rstlint.py | 408 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 Doc/tools/rstlint.py diff --git a/Doc/tools/rstlint.py b/Doc/tools/rstlint.py new file mode 100644 index 00000000000000..d1c53dcb1a698e --- /dev/null +++ b/Doc/tools/rstlint.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Check for stylistic and formal issues in .rst and .py +# files included in the documentation. +# +# 01/2009, Georg Brandl + +# TODO: - wrong versions in versionadded/changed +# - wrong markup after versionchanged directive + +import os +import re +import sys +import getopt +from string import ascii_letters +from os.path import join, splitext, abspath, exists +from collections import defaultdict + +directives = [ + # standard docutils ones + 'admonition', 'attention', 'caution', 'class', 'compound', 'container', + 'contents', 'csv-table', 'danger', 'date', 'default-role', 'epigraph', + 'error', 'figure', 'footer', 'header', 'highlights', 'hint', 'image', + 'important', 'include', 'line-block', 'list-table', 'meta', 'note', + 'parsed-literal', 'pull-quote', 'raw', 'replace', + 'restructuredtext-test-directive', 'role', 'rubric', 'sectnum', 'sidebar', + 'table', 'target-notes', 'tip', 'title', 'topic', 'unicode', 'warning', + # Sphinx and Python docs custom ones + 'acks', 'attribute', 'autoattribute', 'autoclass', 'autodata', + 'autoexception', 'autofunction', 'automethod', 'automodule', + 'availability', 'centered', 'cfunction', 'class', 'classmethod', 'cmacro', + 'cmdoption', 'cmember', 'code-block', 'confval', 'cssclass', 'ctype', + 'currentmodule', 'cvar', 'data', 'decorator', 'decoratormethod', + 'deprecated-removed', 'deprecated(?!-removed)', 'describe', 'directive', + 'doctest', 'envvar', 'event', 'exception', 'function', 'glossary', + 'highlight', 'highlightlang', 'impl-detail', 'index', 'literalinclude', + 'method', 'miscnews', 'module', 'moduleauthor', 'opcode', 'pdbcommand', + 'productionlist', 'program', 'role', 'sectionauthor', 'seealso', + 'sourcecode', 'staticmethod', 'tabularcolumns', 'testcode', 'testoutput', + 'testsetup', 'toctree', 'todo', 'todolist', 'versionadded', + 'versionchanged' +] + +roles = [ + "(? 81: + # don't complain about tables, links and function signatures + if line.lstrip()[0] not in '+|' and \ + 'http://' not in line and \ + not line.lstrip().startswith(('.. function', + '.. method', + '.. cfunction')): + yield lno+1, "line too long" + + +@checker('.html', severity=2, falsepositives=True) +def check_leaked_markup(fn, lines): + """Check HTML files for leaked reST markup; this only works if + the HTML files have been built. + """ + for lno, line in enumerate(lines): + if leaked_markup_re.search(line): + yield lno+1, 'possibly leaked markup: %r' % line + + +def hide_literal_blocks(lines): + """Tool to remove literal blocks from given lines. + + It yields empty lines in place of blocks, so line numbers are + still meaningful. + """ + in_block = False + for line in lines: + if line.endswith("::\n"): + in_block = True + elif in_block: + if line == "\n" or line.startswith(" "): + line = "\n" + else: + in_block = False + yield line + + +def type_of_explicit_markup(line): + if re.match(fr'\.\. {all_directives}::', line): + return 'directive' + if re.match(r'\.\. \[[0-9]+\] ', line): + return 'footnote' + if re.match(r'\.\. \[[^\]]+\] ', line): + return 'citation' + if re.match(r'\.\. _.*[^_]: ', line): + return 'target' + if re.match(r'\.\. \|[^\|]*\| ', line): + return 'substitution_definition' + return 'comment' + + +def hide_comments(lines): + """Tool to remove comments from given lines. + + It yields empty lines in place of comments, so line numbers are + still meaningful. + """ + in_multiline_comment = False + for line in lines: + if line == "..\n": + in_multiline_comment = True + elif in_multiline_comment: + if line == "\n" or line.startswith(" "): + line = "\n" + else: + in_multiline_comment = False + if line.startswith(".. ") and type_of_explicit_markup(line) == 'comment': + line = "\n" + yield line + + + +@checker(".rst", severity=2) +def check_missing_surrogate_space_on_plural(fn, lines): + r"""Check for missing 'backslash-space' between a code sample a letter. + + Good: ``Point``\ s + Bad: ``Point``s + """ + in_code_sample = False + check_next_one = False + for lno, line in enumerate(hide_comments(hide_literal_blocks(lines))): + tokens = line.split("``") + for token_no, token in enumerate(tokens): + if check_next_one: + if token[0] in ascii_letters: + yield lno + 1, f"Missing backslash-space between code sample and {token!r}." + check_next_one = False + if token_no == len(tokens) - 1: + continue + if in_code_sample: + check_next_one = True + in_code_sample = not in_code_sample + +def main(argv): + usage = '''\ +Usage: %s [-v] [-f] [-s sev] [-i path]* [path] + +Options: -v verbose (print all checked file names) + -f enable checkers that yield many false positives + -s sev only show problems with severity >= sev + -i path ignore subdir or file path +''' % argv[0] + try: + gopts, args = getopt.getopt(argv[1:], 'vfs:i:') + except getopt.GetoptError: + print(usage) + return 2 + + verbose = False + severity = 1 + ignore = [] + falsepos = False + for opt, val in gopts: + if opt == '-v': + verbose = True + elif opt == '-f': + falsepos = True + elif opt == '-s': + severity = int(val) + elif opt == '-i': + ignore.append(abspath(val)) + + if len(args) == 0: + path = '.' + elif len(args) == 1: + path = args[0] + else: + print(usage) + return 2 + + if not exists(path): + print('Error: path %s does not exist' % path) + return 2 + + count = defaultdict(int) + + print("""⚠ rstlint.py is no longer maintained here and will be removed +⚠ in a future release. +⚠ Please use https://pypi.org/p/sphinx-lint instead. +""") + + for root, dirs, files in os.walk(path): + # ignore subdirs in ignore list + if abspath(root) in ignore: + del dirs[:] + continue + + for fn in files: + fn = join(root, fn) + if fn[:2] == './': + fn = fn[2:] + + # ignore files in ignore list + if abspath(fn) in ignore: + continue + + ext = splitext(fn)[1] + checkerlist = checkers.get(ext, None) + if not checkerlist: + continue + + if verbose: + print('Checking %s...' % fn) + + try: + with open(fn, 'r', encoding='utf-8') as f: + lines = list(f) + except (IOError, OSError) as err: + print('%s: cannot open: %s' % (fn, err)) + count[4] += 1 + continue + + for checker in checkerlist: + if checker.falsepositives and not falsepos: + continue + csev = checker.severity + if csev >= severity: + for lno, msg in checker(fn, lines): + print('[%d] %s:%d: %s' % (csev, fn, lno, msg)) + count[csev] += 1 + if verbose: + print() + if not count: + if severity > 1: + print('No problems with severity >= %d found.' % severity) + else: + print('No problems found.') + else: + for severity in sorted(count): + number = count[severity] + print('%d problem%s with severity %d found.' % + (number, number > 1 and 's' or '', severity)) + return int(bool(count)) + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) From 9b02019543a32745411b09d38f0529f7d699dae9 Mon Sep 17 00:00:00 2001 From: Julien Palard Date: Mon, 7 Feb 2022 11:06:39 +0100 Subject: [PATCH 5/5] FIX: Missing newline at end of file. --- .../next/Library/2022-02-06-08-54-03.bpo-46655.DiLzYv.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2022-02-06-08-54-03.bpo-46655.DiLzYv.rst b/Misc/NEWS.d/next/Library/2022-02-06-08-54-03.bpo-46655.DiLzYv.rst index 4f0de9519a00e4..183e064b8308ef 100644 --- a/Misc/NEWS.d/next/Library/2022-02-06-08-54-03.bpo-46655.DiLzYv.rst +++ b/Misc/NEWS.d/next/Library/2022-02-06-08-54-03.bpo-46655.DiLzYv.rst @@ -1 +1 @@ -In :func:`typing.get_type_hints`, support evaluating bare stringified ``TypeAlias`` annotations. Patch by Gregory Beauregard. \ No newline at end of file +In :func:`typing.get_type_hints`, support evaluating bare stringified ``TypeAlias`` annotations. Patch by Gregory Beauregard.