|
21 | 21 | from os.path import join, splitext, exists, isfile
|
22 | 22 | from collections import Counter
|
23 | 23 | from itertools import chain
|
| 24 | +from typing import Any |
24 | 25 | import multiprocessing
|
25 | 26 |
|
| 27 | +if sys.version_info[:2] >= (3, 11): |
| 28 | + import tomllib |
| 29 | +else: |
| 30 | + try: |
| 31 | + import tomli as tomllib |
| 32 | + except ImportError: |
| 33 | + tomllib = None |
26 | 34 |
|
27 | 35 | # The following chars groups are from docutils:
|
28 | 36 | closing_delimiters = "\\\\.,;!?"
|
|
103 | 111 | ]
|
104 | 112 | # fmt: on
|
105 | 113 |
|
106 |
| - |
107 |
| -all_directives = "(" + "|".join(directives) + ")" |
108 | 114 | before_role = r"(^|(?<=[\s(/'{\[*-]))"
|
109 | 115 | simplename = r"(?:(?!_)\w)+(?:[-._+:](?:(?!_)\w)+)*"
|
110 | 116 | role_head = rf"({before_role}:{simplename}:)" # A role, with a clean start
|
111 | 117 |
|
112 |
| -# Find comments that look like a directive, like: |
113 |
| -# .. versionchanged 3.6 |
114 |
| -# or |
115 |
| -# .. versionchanged: 3.6 |
116 |
| -# as it should be: |
117 |
| -# .. versionchanged:: 3.6 |
118 |
| -seems_directive_re = re.compile(rf"^\s*(?<!\.)\.\. {all_directives}([^a-z:]|:(?!:))") |
119 |
| - |
120 |
| -# Find directive prefixed with three dots instead of two, like: |
121 |
| -# ... versionchanged:: 3.6 |
122 |
| -# instead of: |
123 |
| -# .. versionchanged:: 3.6 |
124 |
| -three_dot_directive_re = re.compile(rf"\.\.\. {all_directives}::") |
125 |
| - |
126 | 118 | # Find role used with double backticks instead of simple backticks like:
|
127 | 119 | # :const:``None``
|
128 | 120 | # instead of:
|
|
159 | 151 | leaked_markup_re = re.compile(r"[a-z]::\s|`|\.\.\s*\w+:")
|
160 | 152 |
|
161 | 153 |
|
| 154 | +def get_all_directives() -> str: |
| 155 | + return "(" + "|".join(directives) + ")" |
| 156 | + |
| 157 | + |
162 | 158 | checkers = {}
|
163 | 159 |
|
164 | 160 | checker_props = {"severity": 1, "falsepositives": False, "rst_only": True}
|
@@ -244,6 +240,22 @@ def check_default_role(file, lines):
|
244 | 240 | @checker(".rst", severity=2)
|
245 | 241 | def check_directives(file, lines):
|
246 | 242 | """Check for mis-constructed directives."""
|
| 243 | + all_directives = get_all_directives() |
| 244 | + |
| 245 | + # Find comments that look like a directive, like: |
| 246 | + # .. versionchanged 3.6 |
| 247 | + # or |
| 248 | + # .. versionchanged: 3.6 |
| 249 | + # as it should be: |
| 250 | + # .. versionchanged:: 3.6 |
| 251 | + seems_directive_re = re.compile(rf"^\s*(?<!\.)\.\. {all_directives}([^a-z:]|:(?!:))") |
| 252 | + |
| 253 | + # Find directive prefixed with three dots instead of two, like: |
| 254 | + # ... versionchanged:: 3.6 |
| 255 | + # instead of: |
| 256 | + # .. versionchanged:: 3.6 |
| 257 | + three_dot_directive_re = re.compile(rf"\.\.\. {all_directives}::") |
| 258 | + |
247 | 259 | for lno, line in enumerate(lines, start=1):
|
248 | 260 | if seems_directive_re.search(line):
|
249 | 261 | yield lno, "comment seems to be intended as a directive"
|
@@ -386,7 +398,7 @@ def hide_non_rst_blocks(lines, hidden_block_cb=None):
|
386 | 398 |
|
387 | 399 |
|
388 | 400 | def type_of_explicit_markup(line):
|
389 |
| - if re.match(rf"\.\. {all_directives}::", line): |
| 401 | + if re.match(rf"\.\. {get_all_directives()}::", line): |
390 | 402 | return "directive"
|
391 | 403 | if re.match(r"\.\. \[[0-9]+\] ", line):
|
392 | 404 | return "footnote"
|
@@ -479,6 +491,22 @@ def parse_args(argv=None):
|
479 | 491 | return args
|
480 | 492 |
|
481 | 493 |
|
| 494 | +def _read_toml(filename: str) -> dict[str, Any]: |
| 495 | + if tomllib is None: |
| 496 | + return {} |
| 497 | + with open(filename, "rb") as f: |
| 498 | + return tomllib.load(f) |
| 499 | + |
| 500 | + |
| 501 | +def get_config() -> dict[str, Any]: |
| 502 | + if isfile("sphinx.toml"): |
| 503 | + return _read_toml("sphinx.toml").get("lint", {}) |
| 504 | + if isfile("pyproject.toml"): |
| 505 | + table = _read_toml("pyproject.toml") |
| 506 | + return table.get("tool", {}).get("sphinx", {}).get("lint", {}) |
| 507 | + return {} |
| 508 | + |
| 509 | + |
482 | 510 | def is_disabled(msg, disabled_messages):
|
483 | 511 | return any(disabled in msg for disabled in disabled_messages)
|
484 | 512 |
|
@@ -542,6 +570,10 @@ def check_file(filename, allow_false_positives=False, severity=1, disabled=()):
|
542 | 570 |
|
543 | 571 | def main(argv=None):
|
544 | 572 | args = parse_args(argv)
|
| 573 | + config = get_config() |
| 574 | + |
| 575 | + # Append extra directives |
| 576 | + directives.extend(config.get("known_directives", [])) |
545 | 577 |
|
546 | 578 | for path in args.paths:
|
547 | 579 | if not exists(path):
|
|
0 commit comments