Skip to content

Commit 51ebd57

Browse files
committed
Add more tests. Add exactness result.
1 parent e0e4b59 commit 51ebd57

File tree

2 files changed

+172
-10
lines changed

2 files changed

+172
-10
lines changed

src/antsibull_docutils/rst_code_finder.py

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
from docutils.parsers.rst.directives import unchanged as directive_param_unchanged
2525
from docutils.utils import Reporter, SystemMessage
2626

27+
_SPECIAL_ATTRIBUTES = (
28+
"antsibull-code-language",
29+
"antsibull-code-block",
30+
"antsibull-code-lineno",
31+
)
32+
2733

2834
class IgnoreDirective(Directive):
2935
"""
@@ -37,14 +43,24 @@ def run(self) -> list:
3743

3844

3945
def mark_antsibull_code_block(
40-
node: nodes.literal_block, *, language: str | None, line: int
46+
node: nodes.literal_block,
47+
*,
48+
language: str | None,
49+
line: int,
50+
other: dict[str, t.Any] | None = None,
4151
) -> None:
4252
"""
4353
Mark a literal block as an Antsibull code block with given language and line number.
54+
55+
Everything in ``other`` will be available as ``antsibull-other-{key}`` for a key ``key``
56+
in ``other`` in the node's attributes.
4457
"""
4558
node["antsibull-code-language"] = language
4659
node["antsibull-code-block"] = True
4760
node["antsibull-code-lineno"] = line
61+
if other:
62+
for key, value in other.items():
63+
node[f"antsibull-other-{key}"] = value
4864

4965

5066
class CodeBlockDirective(Directive):
@@ -92,7 +108,9 @@ def __init__(
92108
self,
93109
document: nodes.document,
94110
content: str,
95-
callback: t.Callable[[str, int, int, bool, str], None],
111+
callback: t.Callable[
112+
[str, int, int, bool, bool, str, nodes.literal_block], None
113+
],
96114
warn_unknown_block: t.Callable[[int | str, int, nodes.literal_block], None],
97115
):
98116
super().__init__(document)
@@ -114,6 +132,12 @@ def visit_error(self, node: nodes.error) -> None:
114132

115133
@staticmethod
116134
def _find_indent(content: str) -> int | None:
135+
"""
136+
Given concatenated lines, find the minimum indent if possible.
137+
138+
If all lines consist only out of whitespace (or are empty),
139+
``None`` is returned.
140+
"""
117141
min_indent = None
118142
for line in content.split("\n"):
119143
stripped_line = line.lstrip()
@@ -123,7 +147,14 @@ def _find_indent(content: str) -> int | None:
123147
min_indent = indent
124148
return min_indent
125149

126-
def _find_offset(self, lineno: int, content: str) -> tuple[int, int]:
150+
def _find_offset(self, lineno: int, content: str) -> tuple[int, int, bool]:
151+
"""
152+
Try to identify the row/col offset of the code in ``content`` in the document.
153+
154+
``lineno`` is assumed to be the line where the code-block starts.
155+
This function looks for an empty line, followed by the right pattern of
156+
empty and non-empty lines.
157+
"""
127158
row_offset = lineno
128159
found_empty_line = False
129160
found_content_lines = False
@@ -150,9 +181,15 @@ def _find_offset(self, lineno: int, content: str) -> tuple[int, int]:
150181

151182
min_source_indent = self._find_indent(content)
152183
col_offset = max(0, (min_indent or 0) - (min_source_indent or 0))
153-
return row_offset, col_offset
184+
return row_offset, col_offset, content_lines == 0
154185

155186
def _find_in_code(self, row_offset: int, col_offset: int, content: str) -> bool:
187+
"""
188+
Check whether the code can be found at the given row/col offset in a way
189+
that makes it easy to replace.
190+
191+
That is, it is surrounded only by whitespace.
192+
"""
156193
for index, line in enumerate(content.split("\n")):
157194
if row_offset + index >= len(self.__content_lines):
158195
return False
@@ -178,13 +215,29 @@ def visit_literal_block(self, node: nodes.literal_block) -> None:
178215

179216
language = node.attributes["antsibull-code-language"]
180217
lineno = node.attributes["antsibull-code-lineno"]
181-
row_offset, col_offset = self._find_offset(lineno, node.rawsource)
218+
row_offset, col_offset, position_exact = self._find_offset(
219+
lineno, node.rawsource
220+
)
221+
found_in_code = False
222+
if position_exact:
223+
# If we think we have the exact position, try to identify the code.
224+
# ``found_in_code`` indicates that it is easy to replace the code,
225+
# and at the same time it's easy to identify it.
226+
found_in_code = self._find_in_code(row_offset, col_offset, node.rawsource)
227+
if not found_in_code:
228+
position_exact = False
229+
if not found_in_code:
230+
# We were not able to find hte code 'the easy way'. This could be because
231+
# it is inside a table.
232+
pass # TODO search for the content, f.ex. in tables
182233
self.__callback(
183234
language,
184235
row_offset,
185236
col_offset,
186-
self._find_in_code(row_offset, col_offset, node.rawsource),
237+
position_exact,
238+
found_in_code,
187239
node.rawsource.rstrip() + "\n",
240+
node,
188241
)
189242
raise nodes.SkipNode
190243

@@ -253,13 +306,22 @@ class CodeBlockInfo:
253306
row_offset: int
254307
col_offset: int
255308

309+
# Whether the position (row/col_offset) is exact.
310+
# If set to ``False``, the position is approximate and col_offset is often 0.
311+
position_exact: bool
312+
256313
# Whether the code block's contents can be found as-is in the RST file,
257314
# only indented by whitespace, and with potentially trailing whitespace
258-
directly_in_content: bool
315+
directly_replacable_in_content: bool
259316

260317
# The code block's contents
261318
content: str
262319

320+
# The code block's attributes that start with ``antsibull-``.
321+
# Special attributes used by ``find_code_blocks()`` to keep track of
322+
# certain properties are not present.
323+
attributes: dict[str, t.Any]
324+
263325

264326
def find_code_blocks(
265327
content: str,
@@ -284,20 +346,28 @@ def find_code_blocks(
284346
# using this ugly list
285347
results = []
286348

287-
def callback(
349+
def callback( # pylint: disable=too-many-arguments,too-many-positional-arguments
288350
language: str,
289351
row_offset: int,
290352
col_offset: int,
291-
directly_in_content: bool,
353+
position_exact: bool,
354+
directly_replacable_in_content: bool,
292355
content: str,
356+
node: nodes.literal_block,
293357
) -> None:
294358
results.append(
295359
CodeBlockInfo(
296360
language=language,
297361
row_offset=row_offset,
298362
col_offset=col_offset,
299-
directly_in_content=directly_in_content,
363+
position_exact=position_exact,
364+
directly_replacable_in_content=directly_replacable_in_content,
300365
content=content,
366+
attributes={
367+
key: value
368+
for key, value in node.attributes.items()
369+
if key not in _SPECIAL_ATTRIBUTES and key.startswith("antsibull-")
370+
},
301371
)
302372
)
303373

tests/test_rst_code_finder.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,28 @@
5454
language=None,
5555
row_offset=5,
5656
col_offset=2,
57+
position_exact=True,
5758
directly_in_content=True,
5859
content="Foo\nBar\n",
60+
attributes={},
5961
),
6062
CodeBlockInfo(
6163
language="python",
6264
row_offset=13,
6365
col_offset=4,
66+
position_exact=True,
6467
directly_in_content=True,
6568
content=" Foo\n\nBar\n",
69+
attributes={},
6670
),
6771
CodeBlockInfo(
6872
language="foo",
6973
row_offset=19,
7074
col_offset=0,
75+
position_exact=False,
7176
directly_in_content=False,
7277
content="\n",
78+
attributes={},
7379
),
7480
],
7581
),
@@ -102,42 +108,128 @@
102108
| !bar | |
103109
| | !bar |
104110
+--------------------+-------------------------------+
111+
112+
1. Test
113+
2. Here is another table:
114+
115+
+------------------------------+
116+
| .. code:: |
117+
| |
118+
| A test |
119+
+------------------------------+
120+
| .. code:: |
121+
| :caption: bla |
122+
| |
123+
| Another test |
124+
+------------------------------+
125+
126+
3. Here is a CSV table:
127+
128+
.. csv-table::
129+
:header: "Foo", "Bar"
130+
131+
"A ""cell""!", ".. code::
132+
133+
foo
134+
bar 1!"
135+
"Another cell", "A final one"
136+
137+
4. And here's a list table:
138+
139+
.. list-table::
140+
:header-rows: 1
141+
142+
* - Foo
143+
- Bar
144+
* - A "cell"!
145+
- .. code::
146+
147+
foo
148+
bar 2!
149+
* - Another cell
150+
- A final one
105151
""".lstrip(),
106152
[
107153
CodeBlockInfo(
108154
language=None,
109155
row_offset=3,
110156
col_offset=0,
157+
position_exact=False,
111158
directly_in_content=False,
112159
content="foo\n\n bar\n",
160+
attributes={},
113161
),
114162
CodeBlockInfo(
115163
language="python",
116164
row_offset=5,
117165
col_offset=0,
166+
position_exact=False,
118167
directly_in_content=False,
119168
content="def foo(bar):\n return bar + 1\n",
169+
attributes={},
120170
),
121171
CodeBlockInfo(
122172
language="c++",
123173
row_offset=15,
124174
col_offset=0,
175+
position_exact=False,
125176
directly_in_content=False,
126177
content="template<typename T>\nstd::vector<T> create()\n{ return {}; }\n",
178+
attributes={},
127179
),
128180
CodeBlockInfo(
129181
language="foo",
130182
row_offset=22,
131183
col_offset=0,
184+
position_exact=False,
132185
directly_in_content=False,
133186
content="foo\n\n !bar\n",
187+
attributes={},
134188
),
135189
CodeBlockInfo(
136190
language="bar",
137191
row_offset=22,
138192
col_offset=0,
193+
position_exact=False,
139194
directly_in_content=False,
140195
content="foo\n\n !bar\n",
196+
attributes={},
197+
),
198+
CodeBlockInfo(
199+
language=None,
200+
row_offset=34,
201+
col_offset=3,
202+
position_exact=False,
203+
directly_in_content=False,
204+
content="A test\n",
205+
attributes={},
206+
),
207+
CodeBlockInfo(
208+
language=None,
209+
row_offset=38,
210+
col_offset=3,
211+
position_exact=False,
212+
directly_in_content=False,
213+
content="Another test\n",
214+
attributes={},
215+
),
216+
CodeBlockInfo(
217+
language=None,
218+
row_offset=49,
219+
col_offset=7,
220+
position_exact=False,
221+
directly_in_content=False,
222+
content="foo\nbar 1!\n",
223+
attributes={},
224+
),
225+
CodeBlockInfo(
226+
language=None,
227+
row_offset=63,
228+
col_offset=11,
229+
position_exact=True,
230+
directly_in_content=True,
231+
content="foo\nbar 2!\n",
232+
attributes={},
141233
),
142234
],
143235
),

0 commit comments

Comments
 (0)