summaryrefslogtreecommitdiff
path: root/docs/lint.py
diff options
context:
space:
mode:
authorDavid Smith <smithdc@gmail.com>2025-06-01 22:38:04 +0100
committernessita <124304+nessita@users.noreply.github.com>2025-08-25 10:51:10 -0300
commitef2f16bc4824ca2b10b7f2845baf4d313c9c0da1 (patch)
tree01fc768fdf18155e41448422fff0d7683899879e /docs/lint.py
parent0246f478882c26bc1fe293224653074cd46a90d0 (diff)
Refs #36485 -- Added sphinx-lint support and make lint rule for docs.
This adds a `lint.py` script to run sphinx-lint on Django's docs files, a mathing `lint` target in the `docs/Makefile` and `docs/make.bat`, and updates `docs/requirements.txt` accordingly.
Diffstat (limited to 'docs/lint.py')
-rw-r--r--docs/lint.py152
1 files changed, 152 insertions, 0 deletions
diff --git a/docs/lint.py b/docs/lint.py
new file mode 100644
index 0000000000..ccd0c7138b
--- /dev/null
+++ b/docs/lint.py
@@ -0,0 +1,152 @@
+import re
+import sys
+from collections import Counter
+from os.path import abspath, dirname, splitext
+from unittest import mock
+
+from sphinxlint.checkers import (
+ _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.sphinxlint import check_text
+from sphinxlint.utils import PER_FILE_CACHES, hide_non_rst_blocks
+
+
+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
+ continue
+ 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})"
+
+
+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",
+ "80",
+ *params,
+ ]
+ )
+ )