1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
|
import re
import sys
from collections import Counter
from os.path import abspath, dirname, splitext
from unittest import mock
from sphinxlint.checkers import (
_ROLE_BODY,
_is_long_interpreted_text,
_is_very_long_string_literal,
_starts_with_anonymous_hyperlink,
_starts_with_directive_or_hyperlink,
)
from sphinxlint.checkers import checker as sphinxlint_checker
from sphinxlint.rst import SIMPLENAME
from sphinxlint.sphinxlint import check_text
from sphinxlint.utils import PER_FILE_CACHES, hide_non_rst_blocks, paragraphs
def django_check_file(filename, checkers, options=None):
try:
for checker in checkers:
# Django docs use ".txt" for docs file extension.
if ".rst" in checker.suffixes:
checker.suffixes = (".txt",)
ext = splitext(filename)[1]
if not any(ext in checker.suffixes for checker in checkers):
return Counter()
try:
with open(filename, encoding="utf-8") as f:
text = f.read()
except OSError as err:
return [f"{filename}: cannot open: {err}"]
except UnicodeDecodeError as err:
return [f"{filename}: cannot decode as UTF-8: {err}"]
return check_text(filename, text, checkers, options)
finally:
for memoized_function in PER_FILE_CACHES:
memoized_function.cache_clear()
_TOCTREE_DIRECTIVE_RE = re.compile(r"^ *.. toctree::")
_PARSED_LITERAL_DIRECTIVE_RE = re.compile(r"^ *.. parsed-literal::")
_IS_METHOD_RE = re.compile(r"^ *([\w.]+)\([\w ,*]*\)\s*$")
# https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html
# Use two trailing underscores when embedding the URL. Technically, a single
# underscore works as well, but that would create a named reference instead of
# an anonymous one. Named references typically do not have a benefit when the
# URL is embedded. Moreover, they have the disadvantage that you must make sure
# that you do not use the same “Link text” for another link in your document.
_HYPERLINK_DANGLING_RE = re.compile(r"^\s*<https?://[^>]+>`__?[\.,;]?$")
@sphinxlint_checker(".rst", enabled=False, rst_only=True)
def check_line_too_long_django(file, lines, options=None):
"""A modified version of Sphinx-lint's line-too-long check.
Original:
https://github.com/sphinx-contrib/sphinx-lint/blob/main/sphinxlint/checkers.py
"""
def is_multiline_block_to_exclude(line):
return _TOCTREE_DIRECTIVE_RE.match(line) or _PARSED_LITERAL_DIRECTIVE_RE.match(
line
)
# Ignore additional blocks from line length checks.
with mock.patch(
"sphinxlint.utils.is_multiline_non_rst_block", is_multiline_block_to_exclude
):
lines = hide_non_rst_blocks(lines)
table_rows = []
for lno, line in enumerate(lines):
# Beware, in `line` we have the trailing newline.
if len(line) - 1 > options.max_line_length:
# Sphinxlint default exceptions.
if line.lstrip()[0] in "+|":
continue # ignore wide tables
if _is_long_interpreted_text(line):
continue # ignore long interpreted text
if _starts_with_directive_or_hyperlink(line):
continue # ignore directives and hyperlink targets
if _starts_with_anonymous_hyperlink(line):
continue # ignore anonymous hyperlink targets
if _is_very_long_string_literal(line):
continue # ignore a very long literal string
# Additional exceptions
try:
# Ignore headings
if len(set(lines[lno + 1].strip())) == 1 and len(line) == len(
lines[lno + 1]
):
continue
except IndexError:
# End of file
pass
if len(set(line.strip())) == 1 and len(line) == len(lines[lno - 1]):
continue # Ignore heading underline
if lno in table_rows:
continue # Ignore lines in tables
if len(set(line.strip())) == 2 and " " in line:
# Ignore simple tables
borders = [lno_ for lno_, line_ in enumerate(lines) if line == line_]
table_rows.extend([n for n in range(min(borders), max(borders))])
continue
if _HYPERLINK_DANGLING_RE.match(line):
continue # Ignore dangling long links inside a ``_ ref.
if match := _IS_METHOD_RE.match(line):
# Ignore second definition of function signature.
previous_line = lines[lno - 1]
if previous_line.startswith(".. method:: ") and (
previous_line.find(match[1]) != -1
):
continue
yield lno + 1, f"Line too long ({len(line) - 1}/{options.max_line_length})"
_PYTHON_DOMAIN = re.compile(f":py:{SIMPLENAME}:`{_ROLE_BODY}`")
@sphinxlint_checker(".rst", enabled=False, rst_only=True)
def check_python_domain_in_roles(file, lines, options=None):
"""
:py: indicates the Python language domain. This means code writen in
Python, not Python built-ins in particular.
Bad: :py:class:`email.message.EmailMessage`
Good: :class:`email.message.EmailMessage`
"""
for lno, line in enumerate(lines, start=1):
role = _PYTHON_DOMAIN.search(line)
if role:
yield lno, f":py domain is the default and can be omitted {role.group(0)!r}"
_DOC_CAPTURE_TARGET_RE = re.compile(r":doc:`(?:[^<`]+<)?([^>`]+)>?`")
@sphinxlint_checker(".rst", rst_only=True)
def check_absolute_targets_doc_role(file, lines, options=None):
for paragraph_lno, paragraph in paragraphs(lines):
for error in _DOC_CAPTURE_TARGET_RE.finditer(paragraph):
target = error.group(1)
# Skip absolute or intersphinx refs like "python:using/windows".
if target.startswith("/") or ":" in target.split("/", 1)[0]:
continue
# Relative target, report as a violation.
error_offset = paragraph[: error.start()].count("\n")
yield (paragraph_lno + error_offset, target)
import sphinxlint # noqa: E402
sphinxlint.check_file = django_check_file
from sphinxlint.cli import main # noqa: E402
if __name__ == "__main__":
directory = dirname(abspath(__file__))
params = sys.argv[1:] if len(sys.argv) > 1 else []
print(f"Running sphinxlint for: {directory} {params=}")
sys.exit(
main(
[
directory,
"--jobs",
"0",
"--ignore",
"_build",
"--ignore",
"_theme",
"--ignore",
"_ext",
"--enable",
"all",
"--disable",
"line-too-long", # Disable sphinx-lint version
"--max-line-length",
"79",
*params,
]
)
)
|