mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			189 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			189 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 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,
 | |
|             ]
 | |
|         )
 | |
|     )
 |