Skip to content

Commit ac2cf72

Browse files
committed
Support file level prefaces (#180)
1 parent c3b81c2 commit ac2cf72

File tree

7 files changed

+109
-19
lines changed

7 files changed

+109
-19
lines changed

docs/src/reference.rst

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ Configuration
1414
-------------
1515
.. confval:: codeautolink_autodoc_inject
1616

17-
Type: ``bool``. Inject a :rst:dir:`autolink-examples` table
17+
Type: ``bool``. Inject an :rst:dir:`autolink-examples` table
1818
to the end of all autodoc definitions. Defaults to :code:`False`.
1919

2020
.. confval:: codeautolink_global_preface
2121

22-
Type: ``str``. Include a :rst:dir:`autolink-preface` before all blocks.
22+
Type: ``str``. Include an :rst:dir:`autolink-preface` before all blocks.
2323
When other prefaces or concatenated sources are used in a block,
2424
the global preface is included first and only once.
2525

@@ -117,11 +117,25 @@ Directives
117117

118118
.. rst:directive:: .. autolink-preface:: [code]
119119
120-
Include a hidden preface in the next code block.
121-
The next block consumes this directive even if it is not
122-
processed (e.g. non-Python blocks) to avoid placement confusion.
120+
Include a hidden preface before a code block.
123121
A multiline preface can be written in the content portion of the directive.
124-
Prefaces are included in block concatenation.
122+
Prefaces are preserved in block concatenation, and are added to the source
123+
in the following order: :confval:`codeautolink_global_preface` > file preface
124+
> :rst:dir:`autolink-concat` sources (with their block prefaces)
125+
> block prefaces > block source.
126+
127+
.. rubric:: Options
128+
129+
.. rst:directive:option:: level
130+
:type: preface level
131+
132+
- "next" - add preface only to the next block (default).
133+
Multiple prefaces are combined, and the next block consumes this
134+
directive even if it's not processed (e.g. non-Python blocks)
135+
to avoid placement confusion.
136+
- "file" - set a preface for all blocks in the current file, placed
137+
after but before block-level
138+
prefaces.
125139
126140
.. rst:directive:: .. autolink-skip:: [level]
127141

docs/src/release_notes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Unreleased
1414
- Fix attribute and call after walrus leading parser error (:issue:`174`)
1515
- Fix parsing error in doctest blocks with empty lines (:issue:`176`)
1616
- Improve error message on uncaught parsing errors (:issue:`177`)
17+
- Add ``level`` argument to :rst:dir:`autolink-preface` to support
18+
file-level prefaces (:issue:`180`)
1719

1820
0.17.0 (2025-02-18)
1921
-------------------

src/sphinx_codeautolink/extension/block.py

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,13 @@ def __init__(
123123
relative_path = Path(self.document["source"]).relative_to(source_dir)
124124
self.current_document = str(relative_path.with_suffix(""))
125125
self.global_preface = global_preface
126+
self.file_preface = []
127+
self.prefaces = []
126128
self.transformers = BUILTIN_BLOCKS.copy()
127129
self.transformers.update(custom_blocks)
128130
self.valid_blocks = self.transformers.keys()
129131
self.title_stack = []
130132
self.current_refid = None
131-
self.prefaces = []
132133
self.concat_global = concat_default
133134
self.concat_section = False
134135
self.concat_sources = []
@@ -153,7 +154,16 @@ def unknown_visit(self, node) -> None:
153154
self.concat_global = node.mode == "on"
154155
node.parent.remove(node)
155156
elif isinstance(node, PrefaceMarker):
156-
self.prefaces.extend(node.content.split("\n"))
157+
lines = node.content.split("\n") or []
158+
if node.level == "next":
159+
self.prefaces.extend(lines)
160+
elif node.level == "file":
161+
self.file_preface = lines
162+
else:
163+
msg = f"Invalid preface argument: `{node.level}`"
164+
logger.error(
165+
msg, type=warn_type, subtype="invalid_argument", location=node
166+
)
157167
node.parent.remove(node)
158168
elif isinstance(node, SkipMarker):
159169
if node.level not in ("next", "section", "file", "off"):
@@ -243,31 +253,45 @@ def parse_source( # noqa: C901,PLR0912
243253
clean_source = source
244254
transform.source = source
245255

246-
modified_source = "\n".join(
247-
self.global_preface + self.concat_sources + prefaces + [clean_source]
256+
full_source = "\n".join(
257+
self.global_preface
258+
+ self.file_preface
259+
+ self.concat_sources
260+
+ prefaces
261+
+ [clean_source]
248262
)
249263
try:
250-
names = parse_names(modified_source, node)
264+
names = parse_names(full_source, node)
251265
except SyntaxError as e:
252266
if language == "default" and not self.warn_default_parse_fail:
253267
return
254268

255269
show_source = self._format_source_for_error(
256-
self.global_preface, self.concat_sources, prefaces, transform.source
270+
self.global_preface,
271+
self.file_preface,
272+
self.concat_sources,
273+
prefaces,
274+
transform.source,
257275
)
258276
msg = self._parsing_error_msg(e, language, show_source)
259277
logger.warning(msg, type=warn_type, subtype="parse_block", location=node)
260278
return
261279
except Exception as e:
262280
show_source = self._format_source_for_error(
263-
self.global_preface, self.concat_sources, prefaces, transform.source
281+
self.global_preface,
282+
self.file_preface,
283+
self.concat_sources,
284+
prefaces,
285+
transform.source,
264286
)
265287
msg = self._parsing_error_msg(e, language, show_source)
266288
raise type(e)(msg) from e
267289

268-
if prefaces or self.concat_sources or self.global_preface:
290+
if prefaces or self.concat_sources or self.global_preface or self.file_preface:
269291
concat_lens = [s.count("\n") + 1 for s in self.concat_sources]
270-
hidden_len = len(prefaces) + sum(concat_lens) + len(self.global_preface)
292+
hidden_len = len(prefaces + self.global_preface + self.file_preface) + sum(
293+
concat_lens
294+
)
271295
for name in names:
272296
name.lineno -= hidden_len
273297
name.end_lineno -= hidden_len
@@ -281,16 +305,26 @@ def parse_source( # noqa: C901,PLR0912
281305
@staticmethod
282306
def _format_source_for_error(
283307
global_preface: list[str],
308+
file_preface: list[str],
284309
concat_sources: list[str],
285310
prefaces: list[str],
286311
source: str,
287312
) -> str:
288-
lines = global_preface + concat_sources + prefaces + source.split("\n")
313+
lines = (
314+
global_preface
315+
+ file_preface
316+
+ concat_sources
317+
+ prefaces
318+
+ source.split("\n")
319+
)
289320
guides = [""] * len(lines)
290321
ix = 0
291322
if global_preface:
292-
guides[0] = "global preface:"
323+
guides[ix] = "global preface:"
293324
ix += len(global_preface)
325+
if file_preface:
326+
guides[ix] = "file preface:"
327+
ix += len(concat_sources)
294328
if concat_sources:
295329
guides[ix] = "concatenations:"
296330
ix += len(concat_sources)

src/sphinx_codeautolink/extension/directive.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ def run(self):
7777
class PrefaceMarker(nodes.Element):
7878
"""Marker for :class:`Preface`."""
7979

80-
def __init__(self, content: str) -> None:
80+
def __init__(self, content: str, level: str) -> None:
8181
super().__init__()
8282
self.content = content
83+
self.level = level
8384

8485
def copy(self):
8586
"""Copy element."""
@@ -93,11 +94,13 @@ class Preface(Directive):
9394
required_arguments = 0
9495
optional_arguments = 1
9596
final_argument_whitespace = True
97+
option_spec: ClassVar = {"level": directives.unchanged}
9698

9799
def run(self):
98100
"""Insert :class:`PrefaceMarker`."""
99101
lines = list(self.arguments) + list(self.content)
100-
return [PrefaceMarker("\n".join(lines))]
102+
level = self.options.get("level", "next")
103+
return [PrefaceMarker("\n".join(lines), level)]
101104

102105

103106
class SkipMarker(nodes.Element):
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# split
2+
Test project
3+
============
4+
5+
.. autolink-preface:: import lib
6+
:level: whatevs
7+
8+
.. automodule:: test_project

tests/extension/fail/ref_invalid_syntax_complex.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ codeautolink_global_preface = "import test_project"
33
Test project
44
============
55

6+
.. autolink-preface:: import test_project
7+
:level: file
8+
69
.. autolink-concat::
710
.. code:: python
811

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
test_project.Foo
2+
test_project.bar
3+
# split
4+
# split
5+
Test project
6+
============
7+
8+
.. autolink-preface:: import test_project
9+
:level: file
10+
11+
.. code:: python
12+
13+
test_project.Foo()
14+
15+
.. code:: python
16+
17+
test_project.bar()
18+
19+
.. autolink-preface::
20+
:level: file
21+
22+
.. code:: python
23+
24+
test_project.baz()
25+
26+
.. automodule:: test_project

0 commit comments

Comments
 (0)