Skip to content

Commit eeff843

Browse files
committed
scripts: tighten ref directive check
1 parent c374bee commit eeff843

3 files changed

Lines changed: 142 additions & 4 deletions

File tree

scripts/check_ref_directives.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
Two rules are enforced:
55
1. No alphanumeric/underscore/colon character directly before ``:ref:``
66
(missing space before the role).
7-
2. No space between a closing :ref: backtick and a punctuation character
8-
``.``, ``,`` or ``:`` (trailing space before punctuation).
7+
2. No whitespace between ``:ref:`` and its opening backtick.
8+
3. No space between a closing ``:ref:`` backtick and a punctuation character
9+
``.``, ``,`` or ``:``. (trailing space before punctuation).
910
1011
"""
1112

@@ -15,7 +16,8 @@
1516
import sys
1617

1718
CHARACTER_BEFORE_RE = re.compile(r"[a-zA-Z0-9_:]:ref:")
18-
CHARACTER_AFTER_RE = re.compile(r"(:ref:`.*?`[_]{0,2}) ([\.,:])")
19+
CHARACTER_AFTER_RE = re.compile(r":ref:[^`]")
20+
SPACE_BEFORE_PUNCT_RE = re.compile(r"(:ref:`.*?`[_]{0,2}) ([\.,:])")
1921

2022

2123
def check_file(path: pathlib.Path) -> list[str]:
@@ -31,7 +33,11 @@ def check_file(path: pathlib.Path) -> list[str]:
3133
)
3234
if CHARACTER_AFTER_RE.search(line):
3335
errors.append(
34-
f'{path}:{i}: remove space between :ref: and following punctuation'
36+
f'{path}:{i}: remove whitespace between :ref: and its opening backtick'
37+
)
38+
if SPACE_BEFORE_PUNCT_RE.search(line):
39+
errors.append(
40+
f'{path}:{i}: remove space between :ref: closing backtick and punctuation'
3541
)
3642
return errors
3743

scripts/tests/__init__.py

Whitespace-only changes.

scripts/tests/test_checkers.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
1111

12+
import check_ref_directives # noqa: E402
13+
import check_rst_literal_blocks # noqa: E402
1214
import check_youtube_timestamps # noqa: E402
1315

1416

@@ -119,5 +121,135 @@ def test_unicode_error(self):
119121
self.assertIn("UnicodeDecodeError", errors[0])
120122

121123

124+
class TestCheckRefDirectives(unittest.TestCase):
125+
def setUp(self):
126+
self._tmp = tempfile.TemporaryDirectory()
127+
self.tmp = pathlib.Path(self._tmp.name)
128+
129+
def tearDown(self):
130+
self._tmp.cleanup()
131+
132+
def test_valid_ref(self):
133+
path = _rst(self.tmp, "valid.rst", "See :ref:`some-label` for details.\n")
134+
self.assertEqual(check_ref_directives.check_file(path), [])
135+
136+
def test_char_before_ref(self):
137+
path = _rst(self.tmp, "before.rst", "Seex:ref:`some-label` for details.\n")
138+
errors = check_ref_directives.check_file(path)
139+
self.assertEqual(len(errors), 1)
140+
self.assertIn("remove character directly before", errors[0])
141+
142+
def test_special_char_before_ref_not_caught(self):
143+
path = _rst(self.tmp, "ampersand.rst", "See (:ref:`some-label`) for details.\n")
144+
self.assertEqual(check_ref_directives.check_file(path), [])
145+
146+
def test_space_before_ref_is_valid(self):
147+
path = _rst(self.tmp, "space_before.rst", "See :ref:`some-label` for details.\n")
148+
self.assertEqual(check_ref_directives.check_file(path), [])
149+
150+
def test_space_after_ref(self):
151+
path = _rst(self.tmp, "after.rst", "See :ref: `some-label` for details.\n")
152+
errors = check_ref_directives.check_file(path)
153+
self.assertEqual(len(errors), 1)
154+
self.assertIn("remove whitespace", errors[0])
155+
156+
def test_char_after_ref(self):
157+
path = _rst(self.tmp, "char_after.rst", "See :ref:x`some-label` for details.\n")
158+
errors = check_ref_directives.check_file(path)
159+
self.assertEqual(len(errors), 1)
160+
self.assertIn("remove whitespace", errors[0])
161+
162+
def test_space_before_period(self):
163+
path = _rst(self.tmp, "punct_period.rst", "See :ref:`some-label` .\n")
164+
errors = check_ref_directives.check_file(path)
165+
self.assertEqual(len(errors), 1)
166+
self.assertIn("remove space between", errors[0])
167+
168+
def test_space_before_comma(self):
169+
path = _rst(self.tmp, "punct_comma.rst", "See :ref:`some-label` , and more.\n")
170+
errors = check_ref_directives.check_file(path)
171+
self.assertEqual(len(errors), 1)
172+
self.assertIn("remove space between", errors[0])
173+
174+
def test_space_before_colon(self):
175+
path = _rst(self.tmp, "punct_colon.rst", "See :ref:`some-label` : details.\n")
176+
errors = check_ref_directives.check_file(path)
177+
self.assertEqual(len(errors), 1)
178+
self.assertIn("remove space between", errors[0])
179+
180+
def test_no_space_before_period_valid(self):
181+
path = _rst(self.tmp, "valid_punct.rst", "See :ref:`some-label`.\n")
182+
self.assertEqual(check_ref_directives.check_file(path), [])
183+
184+
def test_unicode_error(self):
185+
path = self.tmp / "bad.rst"
186+
path.write_bytes(b"See :ref:`label` \xff.\n")
187+
errors = check_ref_directives.check_file(path)
188+
self.assertEqual(len(errors), 1)
189+
self.assertIn("UnicodeDecodeError", errors[0])
190+
191+
def test_main_valid(self):
192+
path = _rst(self.tmp, "valid.rst", "See :ref:`some-label` for details.\n")
193+
with patch("sys.argv", ["check_ref_directives", str(path)]):
194+
self.assertEqual(check_ref_directives.main(), 0)
195+
196+
def test_main_invalid(self):
197+
path = _rst(self.tmp, "invalid.rst", "Seex:ref:`some-label` for details.\n")
198+
with patch("sys.argv", ["check_ref_directives", str(path)]):
199+
self.assertEqual(check_ref_directives.main(), 1)
200+
201+
def test_non_rst_skipped(self):
202+
path = self.tmp / "errors.txt"
203+
path.write_text("See&:ref:`some-label` for details.\n", encoding="utf-8")
204+
with patch("sys.argv", ["check_ref_directives", str(path)]):
205+
self.assertEqual(check_ref_directives.main(), 0)
206+
207+
208+
class TestCheckRstLiteralBlocks(unittest.TestCase):
209+
def setUp(self):
210+
self._tmp = tempfile.TemporaryDirectory()
211+
self.tmp = pathlib.Path(self._tmp.name)
212+
213+
def tearDown(self):
214+
self._tmp.cleanup()
215+
216+
def test_valid_literal_block(self):
217+
path = _rst(self.tmp, "valid.rst", "Some text.\n\n::\n\n code here\n")
218+
self.assertEqual(check_rst_literal_blocks.check_file(path), [])
219+
220+
def test_missing_blank_before(self):
221+
path = _rst(self.tmp, "before.rst", "Some text.\n::\n\n code here\n")
222+
errors = check_rst_literal_blocks.check_file(path)
223+
self.assertEqual(len(errors), 1)
224+
self.assertIn("blank line before", errors[0])
225+
226+
def test_missing_blank_after(self):
227+
path = _rst(self.tmp, "after.rst", "Some text.\n\n::\n code here\n")
228+
errors = check_rst_literal_blocks.check_file(path)
229+
self.assertEqual(len(errors), 1)
230+
self.assertIn("blank line after", errors[0])
231+
232+
def test_missing_blank_both(self):
233+
path = _rst(self.tmp, "both.rst", "Some text.\n::\n code here\n")
234+
errors = check_rst_literal_blocks.check_file(path)
235+
self.assertEqual(len(errors), 2)
236+
237+
def test_main_valid(self):
238+
path = _rst(self.tmp, "valid.rst", "Some text.\n\n::\n\n code here\n")
239+
with patch("sys.argv", ["check_rst_literal_blocks", str(path)]):
240+
self.assertEqual(check_rst_literal_blocks.main(), 0)
241+
242+
def test_main_invalid(self):
243+
path = _rst(self.tmp, "invalid.rst", "Some text.\n::\n\n code here\n")
244+
with patch("sys.argv", ["check_rst_literal_blocks", str(path)]):
245+
self.assertEqual(check_rst_literal_blocks.main(), 1)
246+
247+
def test_non_rst_skipped(self):
248+
path = self.tmp / "errors.txt"
249+
path.write_text("Some text.\n::\n code here\n", encoding="utf-8")
250+
with patch("sys.argv", ["check_rst_literal_blocks", str(path)]):
251+
self.assertEqual(check_rst_literal_blocks.main(), 0)
252+
253+
122254
if __name__ == "__main__":
123255
unittest.main()

0 commit comments

Comments
 (0)