sphinx-section-include 2026.11.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sphinx_section_include/__init__.py +40 -0
- sphinx_section_include/directive.py +232 -0
- sphinx_section_include/py.typed +0 -0
- sphinx_section_include/reader.py +110 -0
- sphinx_section_include/sections.py +78 -0
- sphinx_section_include-2026.11.1.dist-info/METADATA +116 -0
- sphinx_section_include-2026.11.1.dist-info/RECORD +10 -0
- sphinx_section_include-2026.11.1.dist-info/WHEEL +5 -0
- sphinx_section_include-2026.11.1.dist-info/licenses/LICENSE +21 -0
- sphinx_section_include-2026.11.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Include parsed reStructuredText sections in Sphinx documentation."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
from sphinx.application import Sphinx
|
|
6
|
+
|
|
7
|
+
from sphinx_section_include.directive import (
|
|
8
|
+
IncludeSection,
|
|
9
|
+
resolve_included_references,
|
|
10
|
+
)
|
|
11
|
+
from sphinx_section_include.reader import read_document, resolve_source
|
|
12
|
+
from sphinx_section_include.sections import (
|
|
13
|
+
copy_section,
|
|
14
|
+
find_sections,
|
|
15
|
+
select_section,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"copy_section",
|
|
20
|
+
"find_sections",
|
|
21
|
+
"read_document",
|
|
22
|
+
"resolve_source",
|
|
23
|
+
"select_section",
|
|
24
|
+
]
|
|
25
|
+
__version__ = version(distribution_name="sphinx-section-include")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def setup(app: Sphinx) -> dict[str, object]:
|
|
29
|
+
"""Register the directive and builder-independent reference
|
|
30
|
+
handling.
|
|
31
|
+
"""
|
|
32
|
+
app.add_directive(name="include-section", cls=IncludeSection)
|
|
33
|
+
_ = app.connect(
|
|
34
|
+
event="doctree-resolved", callback=resolve_included_references
|
|
35
|
+
)
|
|
36
|
+
return {
|
|
37
|
+
"version": __version__,
|
|
38
|
+
"parallel_read_safe": True,
|
|
39
|
+
"parallel_write_safe": True,
|
|
40
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Expose parsed section inclusion as a Sphinx directive."""
|
|
2
|
+
|
|
3
|
+
import posixpath
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, ClassVar, override
|
|
7
|
+
from urllib.parse import urlsplit
|
|
8
|
+
|
|
9
|
+
from docutils import nodes
|
|
10
|
+
from docutils.parsers.rst import directives
|
|
11
|
+
from sphinx import addnodes
|
|
12
|
+
from sphinx.application import Sphinx
|
|
13
|
+
from sphinx.errors import NoUri
|
|
14
|
+
from sphinx.util import docname_join
|
|
15
|
+
from sphinx.util.docutils import SphinxDirective
|
|
16
|
+
|
|
17
|
+
from sphinx_section_include.reader import read_document, resolve_source
|
|
18
|
+
from sphinx_section_include.sections import copy_section, select_section
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _prepare_nodes(
|
|
22
|
+
*,
|
|
23
|
+
directive: SphinxDirective,
|
|
24
|
+
content: list[nodes.Node],
|
|
25
|
+
source_docname: str,
|
|
26
|
+
) -> list[nodes.Node]:
|
|
27
|
+
"""Prefix copied targets and retain external reference origins."""
|
|
28
|
+
container = nodes.container()
|
|
29
|
+
container.extend(item=content)
|
|
30
|
+
serial = directive.env.new_serialno(category="section-include")
|
|
31
|
+
prefix = f"section-include-{serial}-"
|
|
32
|
+
elements = list(container.findall(condition=nodes.Element))
|
|
33
|
+
identities = {
|
|
34
|
+
identity: prefix + identity
|
|
35
|
+
for element in elements
|
|
36
|
+
for identity in element["ids"]
|
|
37
|
+
}
|
|
38
|
+
names = {
|
|
39
|
+
name: prefix + name
|
|
40
|
+
for element in elements
|
|
41
|
+
for name in element["names"]
|
|
42
|
+
}
|
|
43
|
+
for element in elements:
|
|
44
|
+
element["ids"] = [identities[identity] for identity in element["ids"]]
|
|
45
|
+
element["names"] = [names[name] for name in element["names"]]
|
|
46
|
+
if bool(element["ids"]) or bool(element["names"]):
|
|
47
|
+
directive.state.document.note_explicit_target(target=element)
|
|
48
|
+
element.document = directive.state.document
|
|
49
|
+
if isinstance(element, addnodes.pending_xref):
|
|
50
|
+
_prepare_cross_reference(
|
|
51
|
+
reference=element,
|
|
52
|
+
names=names,
|
|
53
|
+
source_docname=source_docname,
|
|
54
|
+
docname=directive.env.docname,
|
|
55
|
+
)
|
|
56
|
+
if isinstance(element, nodes.reference):
|
|
57
|
+
_prepare_reference(
|
|
58
|
+
reference=element,
|
|
59
|
+
identities=identities,
|
|
60
|
+
source_docname=source_docname,
|
|
61
|
+
)
|
|
62
|
+
if isinstance(element, nodes.image):
|
|
63
|
+
_prepare_image(
|
|
64
|
+
image=element,
|
|
65
|
+
directive=directive,
|
|
66
|
+
source_docname=source_docname,
|
|
67
|
+
)
|
|
68
|
+
return container.children
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _prepare_cross_reference(
|
|
72
|
+
*,
|
|
73
|
+
reference: addnodes.pending_xref,
|
|
74
|
+
names: dict[str, str],
|
|
75
|
+
source_docname: str,
|
|
76
|
+
docname: str,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Keep Sphinx roles tied to their selected target or original
|
|
79
|
+
document.
|
|
80
|
+
"""
|
|
81
|
+
if reference.get(key="refdomain") == "std":
|
|
82
|
+
if (
|
|
83
|
+
reference.get(key="reftype") == "ref"
|
|
84
|
+
and reference["reftarget"] in names
|
|
85
|
+
):
|
|
86
|
+
reference["reftarget"] = names[reference["reftarget"]]
|
|
87
|
+
elif reference.get(key="reftype") == "doc":
|
|
88
|
+
reference["reftarget"] = "/" + docname_join(
|
|
89
|
+
basedocname=source_docname, docname=reference["reftarget"]
|
|
90
|
+
)
|
|
91
|
+
reference["refdoc"] = docname
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _is_relative_uri(*, uri: str) -> bool:
|
|
95
|
+
"""Recognize local links without schemes, hosts, or rooted paths."""
|
|
96
|
+
parsed = urlsplit(url=uri)
|
|
97
|
+
return (
|
|
98
|
+
not bool(parsed.scheme)
|
|
99
|
+
and not bool(parsed.netloc)
|
|
100
|
+
and not parsed.path.startswith("/")
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _prepare_reference(
|
|
105
|
+
*,
|
|
106
|
+
reference: nodes.reference,
|
|
107
|
+
identities: dict[str, str],
|
|
108
|
+
source_docname: str,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Distinguish anchors inside the excerpt from links to its source."""
|
|
111
|
+
if "refid" in reference:
|
|
112
|
+
target = reference["refid"]
|
|
113
|
+
if target in identities:
|
|
114
|
+
reference["refid"] = identities[target]
|
|
115
|
+
else:
|
|
116
|
+
del reference["refid"]
|
|
117
|
+
_ = reference.setdefault(
|
|
118
|
+
key="section_include_origin", failobj=source_docname
|
|
119
|
+
)
|
|
120
|
+
reference["section_include_anchor"] = target
|
|
121
|
+
elif "refuri" in reference and _is_relative_uri(uri=reference["refuri"]):
|
|
122
|
+
_ = reference.setdefault(
|
|
123
|
+
key="section_include_origin", failobj=source_docname
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _prepare_image(
|
|
128
|
+
*,
|
|
129
|
+
image: nodes.image,
|
|
130
|
+
directive: SphinxDirective,
|
|
131
|
+
source_docname: str,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Resolve local image paths before the caller collects image
|
|
134
|
+
assets.
|
|
135
|
+
"""
|
|
136
|
+
if not _is_relative_uri(uri=image["uri"]):
|
|
137
|
+
return
|
|
138
|
+
origin = Path(
|
|
139
|
+
image.source
|
|
140
|
+
if image.source is not None
|
|
141
|
+
else directive.env.doc2path(docname=source_docname)
|
|
142
|
+
)
|
|
143
|
+
relative = (
|
|
144
|
+
origin.parent.resolve()
|
|
145
|
+
.relative_to(Path(directive.env.srcdir).resolve())
|
|
146
|
+
.as_posix()
|
|
147
|
+
)
|
|
148
|
+
image["uri"] = "/" + posixpath.normpath(
|
|
149
|
+
path=posixpath.join(relative, str(object=image["uri"]))
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def resolve_included_references(
|
|
154
|
+
app: Sphinx, doctree: nodes.document, docname: str
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Resolve source links using the active builder's URI conventions."""
|
|
157
|
+
for reference in doctree.findall(condition=nodes.reference):
|
|
158
|
+
origin = reference.get(key="section_include_origin")
|
|
159
|
+
if origin is None:
|
|
160
|
+
continue
|
|
161
|
+
try:
|
|
162
|
+
base = app.builder.get_relative_uri(from_=docname, to=origin)
|
|
163
|
+
except NoUri:
|
|
164
|
+
reference.replace_self(new=reference.children)
|
|
165
|
+
continue
|
|
166
|
+
anchor = reference.get(key="section_include_anchor")
|
|
167
|
+
if anchor is not None:
|
|
168
|
+
reference["refuri"] = base + "#" + anchor
|
|
169
|
+
else:
|
|
170
|
+
# Keep paths relative to the original rendered document.
|
|
171
|
+
|
|
172
|
+
parsed = urlsplit(url=str(object=reference["refuri"]))
|
|
173
|
+
path = (
|
|
174
|
+
posixpath.normpath(
|
|
175
|
+
path=posixpath.join(posixpath.dirname(base), parsed.path)
|
|
176
|
+
)
|
|
177
|
+
if bool(parsed.path)
|
|
178
|
+
else base
|
|
179
|
+
)
|
|
180
|
+
if parsed.path.endswith("/") and not path.endswith("/"):
|
|
181
|
+
path += "/"
|
|
182
|
+
reference["refuri"] = parsed._replace(path=path).geturl()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class IncludeSection(SphinxDirective):
|
|
186
|
+
"""Include one exact section while preserving its document
|
|
187
|
+
structure.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
required_arguments = 1
|
|
191
|
+
option_spec: ClassVar[dict[str, Callable[[str], Any]] | None] = { # pyrefly: ignore[explicit-any]
|
|
192
|
+
"section": directives.unchanged_required,
|
|
193
|
+
"include-heading": directives.flag,
|
|
194
|
+
"exclude-sections": directives.unchanged_required,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@override
|
|
198
|
+
def run(self) -> list[nodes.Node]:
|
|
199
|
+
"""Read, select, and copy a section without rendering it to
|
|
200
|
+
HTML.
|
|
201
|
+
"""
|
|
202
|
+
if "section" not in self.options:
|
|
203
|
+
raise self.error(message="include-section requires :section:")
|
|
204
|
+
try:
|
|
205
|
+
path = resolve_source(directive=self, source=self.arguments[0])
|
|
206
|
+
document = read_document(directive=self, source=self.arguments[0])
|
|
207
|
+
selected = select_section(
|
|
208
|
+
document=document, selector=self.options["section"]
|
|
209
|
+
)
|
|
210
|
+
excluded = tuple(
|
|
211
|
+
select_section(document=selected, selector=selector.strip())
|
|
212
|
+
for selector in self.options.get(
|
|
213
|
+
"exclude-sections", ""
|
|
214
|
+
).splitlines()
|
|
215
|
+
if bool(selector.strip())
|
|
216
|
+
)
|
|
217
|
+
content = copy_section(
|
|
218
|
+
section=selected,
|
|
219
|
+
include_heading="include-heading" in self.options,
|
|
220
|
+
exclude_sections=excluded,
|
|
221
|
+
)
|
|
222
|
+
return _prepare_nodes(
|
|
223
|
+
directive=self,
|
|
224
|
+
content=content,
|
|
225
|
+
source_docname=path.relative_to(
|
|
226
|
+
Path(self.env.srcdir).resolve()
|
|
227
|
+
)
|
|
228
|
+
.with_suffix(suffix="")
|
|
229
|
+
.as_posix(),
|
|
230
|
+
)
|
|
231
|
+
except ValueError as error:
|
|
232
|
+
raise self.error(message=str(object=error)) from error
|
|
File without changes
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Read source documents through the active Sphinx directive context."""
|
|
2
|
+
|
|
3
|
+
from copy import copy
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from docutils import nodes
|
|
7
|
+
from docutils.parsers.rst import Parser
|
|
8
|
+
from docutils.transforms.references import (
|
|
9
|
+
AnonymousHyperlinks,
|
|
10
|
+
DanglingReferences,
|
|
11
|
+
ExternalTargets,
|
|
12
|
+
IndirectHyperlinks,
|
|
13
|
+
InternalTargets,
|
|
14
|
+
PropagateTargets,
|
|
15
|
+
Substitutions,
|
|
16
|
+
)
|
|
17
|
+
from docutils.utils import new_document
|
|
18
|
+
from sphinx.transforms import DefaultSubstitutions
|
|
19
|
+
from sphinx.util.docutils import LoggingReporter, SphinxDirective
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_source(*, directive: SphinxDirective, source: str) -> Path:
|
|
23
|
+
"""Resolve an RST source inside the Sphinx source directory."""
|
|
24
|
+
root = Path(directive.env.srcdir).resolve()
|
|
25
|
+
_relative, filename = directive.env.relfn2path(filename=source)
|
|
26
|
+
path = Path(filename).resolve()
|
|
27
|
+
if not path.is_relative_to(root) or path.suffix != ".rst":
|
|
28
|
+
message = (
|
|
29
|
+
"Section source must be an RST file within the source directory"
|
|
30
|
+
)
|
|
31
|
+
raise ValueError(message)
|
|
32
|
+
current = Path(
|
|
33
|
+
directive.env.doc2path(docname=directive.env.docname)
|
|
34
|
+
).resolve()
|
|
35
|
+
if path == current:
|
|
36
|
+
message = "A section include cannot include itself"
|
|
37
|
+
raise ValueError(message)
|
|
38
|
+
if not path.is_file():
|
|
39
|
+
message = f"Section source does not exist: {source!r}"
|
|
40
|
+
raise ValueError(message)
|
|
41
|
+
return path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def read_document(
|
|
45
|
+
*, directive: SphinxDirective, source: str, docname: str | None = None
|
|
46
|
+
) -> nodes.document:
|
|
47
|
+
"""Parse source content and record its transitive include dependencies.
|
|
48
|
+
|
|
49
|
+
Registered directives, substitutions, and the caller's Sphinx configuration
|
|
50
|
+
remain available. Source-relative includes use the source document's path.
|
|
51
|
+
Temporary parsing context is restored even if parsing fails.
|
|
52
|
+
"""
|
|
53
|
+
path = resolve_source(directive=directive, source=source)
|
|
54
|
+
root = Path(directive.env.srcdir).resolve()
|
|
55
|
+
parent = directive.state.document
|
|
56
|
+
ancestors = tuple[str, ...](
|
|
57
|
+
parent.get(key="_section_include_sources", failobj=())
|
|
58
|
+
)
|
|
59
|
+
if str(object=path) in ancestors:
|
|
60
|
+
message = f"Circular section include involving {path.name!r}"
|
|
61
|
+
raise ValueError(message)
|
|
62
|
+
settings = copy(x=parent.settings)
|
|
63
|
+
document = new_document(
|
|
64
|
+
source_path=str(
|
|
65
|
+
object=directive.env.doc2path(
|
|
66
|
+
docname=directive.env.docname if docname is None else docname
|
|
67
|
+
)
|
|
68
|
+
),
|
|
69
|
+
settings=settings,
|
|
70
|
+
)
|
|
71
|
+
document.reporter = LoggingReporter.from_reporter(
|
|
72
|
+
reporter=document.reporter
|
|
73
|
+
)
|
|
74
|
+
document["_section_include_sources"] = (*ancestors, str(object=path))
|
|
75
|
+
relative = path.relative_to(root).as_posix()
|
|
76
|
+
env = directive.env
|
|
77
|
+
current = env.current_document
|
|
78
|
+
source_docname = path.relative_to(root).with_suffix(suffix="").as_posix()
|
|
79
|
+
context = copy(x=current)
|
|
80
|
+
context.docname = source_docname if docname is None else docname
|
|
81
|
+
env.note_dependency(filename=path)
|
|
82
|
+
env.current_document = context
|
|
83
|
+
try:
|
|
84
|
+
text = "\n\n".join(
|
|
85
|
+
part
|
|
86
|
+
for part in (
|
|
87
|
+
directive.config.rst_prolog or "",
|
|
88
|
+
f".. include:: /{relative}",
|
|
89
|
+
directive.config.rst_epilog or "",
|
|
90
|
+
)
|
|
91
|
+
if bool(part)
|
|
92
|
+
)
|
|
93
|
+
Parser().parse(inputstring=text, document=document)
|
|
94
|
+
for transform in (
|
|
95
|
+
DefaultSubstitutions,
|
|
96
|
+
Substitutions,
|
|
97
|
+
PropagateTargets,
|
|
98
|
+
AnonymousHyperlinks,
|
|
99
|
+
IndirectHyperlinks,
|
|
100
|
+
ExternalTargets,
|
|
101
|
+
InternalTargets,
|
|
102
|
+
DanglingReferences,
|
|
103
|
+
):
|
|
104
|
+
transform(document=document).apply()
|
|
105
|
+
finally:
|
|
106
|
+
env.current_document = current
|
|
107
|
+
env.dependencies[current.docname].update(
|
|
108
|
+
env.dependencies[context.docname]
|
|
109
|
+
)
|
|
110
|
+
return document
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Select and copy parsed sections without imposing presentation
|
|
2
|
+
policy.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from docutils import nodes
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def find_sections(
|
|
9
|
+
*, document: nodes.document | nodes.section, selector: str
|
|
10
|
+
) -> list[nodes.section]:
|
|
11
|
+
"""Find literal headings, heading paths, or standard RST labels.
|
|
12
|
+
|
|
13
|
+
Literal headings take precedence over path or label syntax.
|
|
14
|
+
"""
|
|
15
|
+
sections = list(document.findall(condition=nodes.section))
|
|
16
|
+
literal = [
|
|
17
|
+
section for section in sections if section[0].astext() == selector
|
|
18
|
+
]
|
|
19
|
+
if bool(literal):
|
|
20
|
+
return literal
|
|
21
|
+
if selector.startswith("#"):
|
|
22
|
+
label = nodes.fully_normalize_name(name=selector[1:])
|
|
23
|
+
return [
|
|
24
|
+
section
|
|
25
|
+
for section in sections
|
|
26
|
+
if label in section["names"] or label in section["ids"]
|
|
27
|
+
]
|
|
28
|
+
parts = [part.strip() for part in selector.split(sep="/")]
|
|
29
|
+
matches: list[nodes.section] = [
|
|
30
|
+
section for section in sections if section[0].astext() == parts[0]
|
|
31
|
+
]
|
|
32
|
+
for part in parts[1:]:
|
|
33
|
+
matches = [
|
|
34
|
+
child
|
|
35
|
+
for section in matches
|
|
36
|
+
for child in section.children
|
|
37
|
+
if isinstance(child, nodes.section) and child[0].astext() == part
|
|
38
|
+
]
|
|
39
|
+
return matches
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def select_section(
|
|
43
|
+
*, document: nodes.document | nodes.section, selector: str
|
|
44
|
+
) -> nodes.section:
|
|
45
|
+
"""Require exactly one matching section."""
|
|
46
|
+
matches = find_sections(document=document, selector=selector)
|
|
47
|
+
if len(matches) != 1:
|
|
48
|
+
message = (
|
|
49
|
+
f"Expected one section for {selector!r}, found {len(matches)}"
|
|
50
|
+
)
|
|
51
|
+
raise ValueError(message)
|
|
52
|
+
return matches[0]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def copy_section(
|
|
56
|
+
*,
|
|
57
|
+
section: nodes.section,
|
|
58
|
+
include_heading: bool = True,
|
|
59
|
+
exclude_sections: tuple[nodes.section, ...] = (),
|
|
60
|
+
) -> list[nodes.Node]:
|
|
61
|
+
"""Copy a subtree, preserving headings, anchors, and source metadata.
|
|
62
|
+
|
|
63
|
+
Exclusions must be resolved descendants of the selected section.
|
|
64
|
+
The source tree is never modified.
|
|
65
|
+
"""
|
|
66
|
+
originals = list(section.findall(condition=nodes.section))
|
|
67
|
+
identities = {id(node) for node in originals if node is not section}
|
|
68
|
+
if any(id(node) not in identities for node in exclude_sections):
|
|
69
|
+
message = "Excluded sections must be descendants of the selection"
|
|
70
|
+
raise ValueError(message)
|
|
71
|
+
copied = section.deepcopy()
|
|
72
|
+
copies = list(copied.findall(condition=nodes.section))
|
|
73
|
+
excluded = {id(node) for node in exclude_sections}
|
|
74
|
+
for original, target in zip(originals, copies, strict=True):
|
|
75
|
+
if id(original) in excluded:
|
|
76
|
+
assert target.parent is not None
|
|
77
|
+
_ = target.parent.remove(item=target)
|
|
78
|
+
return [copied] if include_heading else copied.children[1:]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sphinx-section-include
|
|
3
|
+
Version: 2026.11.1
|
|
4
|
+
Summary: Include selected sections of reStructuredText documents in Sphinx projects.
|
|
5
|
+
Author-email: Adam Dangoor <adamdangoor@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Issues, https://github.com/adamtheturtle/sphinx-section-include/issues
|
|
8
|
+
Project-URL: Source, https://github.com/adamtheturtle/sphinx-section-include
|
|
9
|
+
Keywords: documentation,rst,sphinx
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Web Environment
|
|
12
|
+
Classifier: Framework :: Sphinx :: Extension
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Classifier: Operating System :: POSIX
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Description-Content-Type: text/x-rst
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: docutils>=0.21.2
|
|
24
|
+
Requires-Dist: sphinx<10,>=9.1.1
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
|Build Status| |PyPI|
|
|
28
|
+
|
|
29
|
+
sphinx-section-include
|
|
30
|
+
======================
|
|
31
|
+
|
|
32
|
+
Include selected sections of reStructuredText documents in Sphinx projects.
|
|
33
|
+
The extension copies parsed nodes, retaining nested headings, code blocks, source locations, and references.
|
|
34
|
+
It also provides a typed Python API for extensions that select and transform sections themselves.
|
|
35
|
+
|
|
36
|
+
Installation
|
|
37
|
+
------------
|
|
38
|
+
|
|
39
|
+
.. code-block:: console
|
|
40
|
+
|
|
41
|
+
$ pip install sphinx-section-include
|
|
42
|
+
|
|
43
|
+
Enable the extension in ``conf.py``:
|
|
44
|
+
|
|
45
|
+
.. code-block:: python
|
|
46
|
+
|
|
47
|
+
"""Enable section inclusion in this Sphinx project."""
|
|
48
|
+
|
|
49
|
+
extensions = ["sphinx_section_include"]
|
|
50
|
+
|
|
51
|
+
Including a section
|
|
52
|
+
-------------------
|
|
53
|
+
|
|
54
|
+
.. code-block:: rst
|
|
55
|
+
|
|
56
|
+
.. include-section:: manual.rst
|
|
57
|
+
:section: Getting started / Configuration
|
|
58
|
+
:include-heading:
|
|
59
|
+
:exclude-sections: Advanced configuration
|
|
60
|
+
|
|
61
|
+
``:section:`` is required.
|
|
62
|
+
It accepts an exact heading, a slash-separated heading path, or an RST label prefixed by ``#``.
|
|
63
|
+
Literal heading matches take precedence, so headings containing slashes remain selectable.
|
|
64
|
+
Missing or ambiguous selections fail with a build diagnostic.
|
|
65
|
+
|
|
66
|
+
``:include-heading:`` includes the selected section's heading.
|
|
67
|
+
Without it, only the section's contents and nested sections are included.
|
|
68
|
+
``:exclude-sections:`` accepts one descendant selector per line.
|
|
69
|
+
|
|
70
|
+
Paths are relative to the including document.
|
|
71
|
+
A leading ``/`` selects a path relative to the Sphinx source directory.
|
|
72
|
+
Sources must be existing ``.rst`` files within that directory.
|
|
73
|
+
Self-inclusion and circular section includes are rejected.
|
|
74
|
+
|
|
75
|
+
Source documents are parsed with registered directives, substitutions, ``rst_prolog``, and ``rst_epilog``.
|
|
76
|
+
Dependencies from nested includes are tracked for incremental builds.
|
|
77
|
+
Internal anchors are namespaced so the same section can appear more than once.
|
|
78
|
+
Links outside the excerpt retain their source document, and local image paths retain their source directory.
|
|
79
|
+
Standard Sphinx reference resolution and builder URI conventions apply.
|
|
80
|
+
|
|
81
|
+
Python API
|
|
82
|
+
----------
|
|
83
|
+
|
|
84
|
+
``find_sections(document=..., selector=...)`` returns all matching sections.
|
|
85
|
+
``select_section(document=..., selector=...)`` requires exactly one match and raises ``ValueError`` otherwise.
|
|
86
|
+
|
|
87
|
+
``copy_section(section=..., include_heading=True, exclude_sections=())`` returns independent copied nodes without modifying the source tree.
|
|
88
|
+
Exclusions are resolved section nodes and must be descendants of the selection.
|
|
89
|
+
This function preserves document structure and does not apply rendering policy.
|
|
90
|
+
|
|
91
|
+
For Sphinx directive authors, ``read_document(directive=..., source=..., docname=None)`` parses an RST source and records its dependencies.
|
|
92
|
+
The default parsing context is the source document.
|
|
93
|
+
A ``docname`` override supports extensions whose embedded content uses the caller's context.
|
|
94
|
+
Temporary context is restored after parsing.
|
|
95
|
+
``resolve_source(directive=..., source=...)`` validates and resolves the path.
|
|
96
|
+
Both helpers require an active Sphinx build, which supplies the source directory and directive context.
|
|
97
|
+
|
|
98
|
+
This library was extracted from `sphinx-hackerrank <https://github.com/adamtheturtle/sphinx-hackerrank>`_.
|
|
99
|
+
HackerRank layouts, language filtering, and HTML export remain in that extension.
|
|
100
|
+
|
|
101
|
+
Development
|
|
102
|
+
-----------
|
|
103
|
+
|
|
104
|
+
See `CONTRIBUTING.rst <CONTRIBUTING.rst>`_ for local checks and release steps.
|
|
105
|
+
The sample project exercises section inclusion without HackerRank dependencies.
|
|
106
|
+
|
|
107
|
+
License
|
|
108
|
+
-------
|
|
109
|
+
|
|
110
|
+
MIT.
|
|
111
|
+
See ``LICENSE``.
|
|
112
|
+
|
|
113
|
+
.. |Build Status| image:: https://github.com/adamtheturtle/sphinx-section-include/actions/workflows/ci.yml/badge.svg?branch=main
|
|
114
|
+
:target: https://github.com/adamtheturtle/sphinx-section-include/actions
|
|
115
|
+
.. |PyPI| image:: https://badge.fury.io/py/sphinx-section-include.svg
|
|
116
|
+
:target: https://badge.fury.io/py/sphinx-section-include
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
sphinx_section_include/__init__.py,sha256=0eEbZPjCobVcfti28Th061whDkLmKJsqUkMgzttl9sQ,1039
|
|
2
|
+
sphinx_section_include/directive.py,sha256=gYKXYioqo6ZrXr_kyFCBXP7r3FErucjk0qipquKnbXY,7917
|
|
3
|
+
sphinx_section_include/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
sphinx_section_include/reader.py,sha256=Td25MC7Svl0fz7ZDREcdeolV6us_TuKcDAKRL1o5D0o,3870
|
|
5
|
+
sphinx_section_include/sections.py,sha256=XfBIao0a_oLPgE8uoC6lkhDErwev1M3fHaSJb6w3JWU,2722
|
|
6
|
+
sphinx_section_include-2026.11.1.dist-info/licenses/LICENSE,sha256=hvkADUCIndXpdC1L7yhnIKS99N0Wdtaf9YF06bRgZCo,1065
|
|
7
|
+
sphinx_section_include-2026.11.1.dist-info/METADATA,sha256=A5JdBWkjwmSBJifir5wO4euvW42K_Uew5_0Lz6l6Ieo,4847
|
|
8
|
+
sphinx_section_include-2026.11.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
9
|
+
sphinx_section_include-2026.11.1.dist-info/top_level.txt,sha256=MoKTLpvbIddgffcyurFCklHI2C5p-2HCYu314x92xdM,23
|
|
10
|
+
sphinx_section_include-2026.11.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rippling
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sphinx_section_include
|