sphinxcontrib-rust 0.1.0__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.
@@ -0,0 +1,97 @@
1
+ """Init module for the extension with the ``setup`` function used by Sphinx"""
2
+
3
+ from importlib.metadata import version
4
+ import subprocess
5
+ from shutil import which
6
+ from typing import Any, Mapping
7
+
8
+ import sphinx.config
9
+ from sphinx.application import Sphinx
10
+ from sphinx.util import logging
11
+
12
+ from sphinxcontrib_rust.domain import RustDomain
13
+
14
+ __PKG_NAME = "sphinxcontrib_rust"
15
+ __VERSION__ = version(__PKG_NAME)
16
+ __REQUIRED_EXTENSIONS = ["sphinx.ext.autodoc"]
17
+ __LOGGER = logging.getLogger(__PKG_NAME)
18
+
19
+ """The configuration options added by the extension.
20
+
21
+ Each entry is tuple consisting of
22
+ * The option name.
23
+ * The default value.
24
+ * The rebuild condition .
25
+ * "html": Change needs a full rebuild of HTML.
26
+ * "env": Change needs a full rebuild of the build environment.
27
+ * "": No rebuild required.
28
+ * A list of types for the value.
29
+ """
30
+ __CONFIG_OPTIONS = (
31
+ ("rust_crates", None, "env", [dict]),
32
+ ("rust_doc_dir", None, "env", [str]),
33
+ ("rust_rustdoc_fmt", "rst", "env", [str]),
34
+ ("rust_visibility", "pub", "html", [str]), # TODO: Define a type for this
35
+ )
36
+
37
+ """
38
+ See https://www.sphinx-doc.org/en/master/extdev/index.html#extension-metadata
39
+ """
40
+ __METADATA = {
41
+ "version": __VERSION__,
42
+ "parallel_read_safe": True,
43
+ "parallel_write_safe": True,
44
+ }
45
+ __RUSTDOCGEN = which("sphinx-rustdocgen")
46
+
47
+
48
+ def generate_docs(app: Sphinx, config: sphinx.config.Config):
49
+ """Generate the Rust docs once the configuration has been read
50
+
51
+ Args:
52
+ :app: The Sphinx application.
53
+ :config: The parsed configuration.
54
+ """
55
+ for crate, src_dir in config.rust_crates.items():
56
+ __LOGGER.info(
57
+ "[sphinxcontrib_rust] Processing contents of crate %s from directory %s",
58
+ crate,
59
+ src_dir,
60
+ )
61
+ __LOGGER.info(
62
+ "[sphinxcontrib_rust] Generated files will be saved in %s/%s",
63
+ config.rust_doc_dir,
64
+ crate,
65
+ )
66
+ subprocess.run(
67
+ [
68
+ __RUSTDOCGEN,
69
+ crate,
70
+ src_dir,
71
+ config.rust_doc_dir,
72
+ config.rust_rustdoc_fmt,
73
+ ],
74
+ check=True,
75
+ )
76
+
77
+
78
+ def setup(app: Sphinx) -> Mapping[str, Any]:
79
+ """Set up the extension"""
80
+ if __RUSTDOCGEN is None:
81
+ raise ValueError(
82
+ "Could not find the sphinx-rustdocgen executable. "
83
+ "Make sure it is installed and available on the default PATH."
84
+ )
85
+
86
+ app.require_sphinx("7.0")
87
+
88
+ for extension in __REQUIRED_EXTENSIONS:
89
+ app.setup_extension(extension)
90
+
91
+ for option in __CONFIG_OPTIONS:
92
+ app.add_config_value(*option)
93
+
94
+ app.add_domain(RustDomain)
95
+ app.connect("config-inited", generate_docs)
96
+
97
+ return __METADATA
@@ -0,0 +1,250 @@
1
+ """
2
+ The module provides all the directive classes for the Rust object types.
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Sequence, Type
7
+
8
+ from docutils.parsers.rst import directives
9
+ from sphinx import addnodes
10
+ from sphinx.addnodes import desc_signature
11
+ from sphinx.directives import (
12
+ ObjDescT,
13
+ ObjectDescription,
14
+ nl_escape_re,
15
+ strip_backslash_re,
16
+ )
17
+ from sphinx.util.nodes import make_id
18
+
19
+ from sphinxcontrib_rust.items import RustItem, RustItemType
20
+
21
+
22
+ class RustDirective(ABC, ObjectDescription[Sequence[str]]):
23
+ """Base class for Rust directives"""
24
+
25
+ option_spec = {
26
+ "no-index": directives.flag,
27
+ "vis": directives.unchanged_required,
28
+ "sig": directives.unchanged,
29
+ "toc": directives.unchanged,
30
+ "type": directives.unchanged,
31
+ }
32
+
33
+ @property
34
+ @abstractmethod
35
+ def rust_item_type(self) -> RustItemType:
36
+ """The Rust object type for the directive"""
37
+ raise NotImplementedError
38
+
39
+ @classmethod
40
+ def get_directives(cls) -> dict[RustItemType, Type["RustDirective"]]:
41
+ """Get all the directives for the object types"""
42
+ return {
43
+ RustItemType.CRATE: RustCrateDirective,
44
+ RustItemType.ENUM: RustEnumDirective,
45
+ RustItemType.FUNCTION: RustFunctionDirective,
46
+ RustItemType.IMPL: RustImplDirective,
47
+ RustItemType.MACRO: RustMacroDirective,
48
+ RustItemType.MODULE: RustModuleDirective,
49
+ RustItemType.STRUCT: RustStructDirective,
50
+ RustItemType.TRAIT: RustTraitDirective,
51
+ RustItemType.TYPE: RustTypeDirective,
52
+ RustItemType.VARIABLE: RustVariableDirective,
53
+ }
54
+
55
+ def add_target_and_index(
56
+ self, name: Sequence[str], sig: str, signode: desc_signature
57
+ ) -> None:
58
+ """Adds the item to the domain and generates the index for it.
59
+
60
+ This is called after :py:func:`handle_signature` has executed.
61
+
62
+ Args:
63
+ :name: The name of the item, which is the return value from
64
+ :py:func:`handle_signature`.
65
+ :sig: The argument to the directive, which is the Rust path
66
+ of the item set by the Rust doc generator.
67
+ :signode: The docutils node of the for item.
68
+ """
69
+ fullname = "::".join(name) # XXX: Can we just use sig here?
70
+ node_id = make_id(self.env, self.state.document, "", fullname)
71
+ signode["ids"].append(node_id)
72
+
73
+ rust_obj = RustItem(
74
+ name=fullname,
75
+ dispname=self.options.get("sig", name[-1]),
76
+ type_=self.rust_item_type,
77
+ docname=self.env.docname,
78
+ anchor="-".join(name), # Need to join with - for HTML anchors
79
+ )
80
+
81
+ # Add to the domain
82
+ self.env.domains["rust"].data[self.rust_item_type].append(rust_obj)
83
+
84
+ if "no-index" not in self.options:
85
+ self.indexnode["entries"].append(("single", fullname, node_id, "", None))
86
+
87
+ def get_signatures(self) -> list[str]:
88
+ """Override the default get_signatures method, which splits the signatures by line.
89
+
90
+ Currently, only one signature is allowed for each Rust object, and due to the use
91
+ of where on new lines, it is all part of one signature. Only the final newline is
92
+ removed.
93
+
94
+ See Also:
95
+ * `Docs for the method`_
96
+ * `Source code for super`_
97
+
98
+ .. _`Docs for the method`:
99
+ https://www.sphinx-doc.org/en/master/_modules/sphinx/directives.html#ObjectDescription.get_signatures
100
+ .. _`Source code for super`:
101
+ https://www.sphinx-doc.org/en/master/_modules/sphinx/directives.html#ObjectDescription.get_signatures
102
+ """
103
+ sig = nl_escape_re.sub("", self.arguments[0]).strip()
104
+ return (
105
+ [strip_backslash_re.sub(r"\1", sig)]
106
+ if self.config.strip_signature_backslash
107
+ else [sig]
108
+ )
109
+
110
+ def handle_signature(self, sig: str, signode: addnodes.desc_signature) -> ObjDescT:
111
+ """Handle the directive and generate its display signature.
112
+
113
+ The display signature is what is displayed instead of the directive name and
114
+ options in the generated output. The ``:sig:`` option of the directive is used
115
+ to override the provided ``sig`` value. If the option is not set, the item type
116
+ and the final component of the path are used.
117
+
118
+ Raising ``ValueError`` will put the ``sig`` value into a single node, which
119
+ is what the super implementation does.
120
+
121
+ Args:
122
+ :sig: The argument of the directive as set during doc generation, not the
123
+ ``:sig:`` option. The Rust side of the code will put the full Rust path
124
+ of the item as the argument.
125
+ :signode: The docutils node for the item, to which the display signature nodes
126
+ should be appended.
127
+
128
+ Returns:
129
+ The path for the object, which is a tuple of the Rust path components and
130
+ defines the hierarchy of the object for indexing.
131
+ """
132
+ path = tuple(sig.split("::"))
133
+ # TODO: Parse the signature as RST content and handle multi-line
134
+ signode += addnodes.desc_name(
135
+ sig,
136
+ self.options.get("sig", f"{self.rust_item_type.display_name} {path[-1]}"),
137
+ )
138
+ signode.path = path
139
+ return path
140
+
141
+ def _object_hierarchy_parts(
142
+ self, sig_node: addnodes.desc_signature
143
+ ) -> tuple[str, ...]:
144
+ """Returns the hierarchy of the object for indexing and de-duplication.
145
+
146
+ Args:
147
+ :sig_node: The docutils node of the for item.
148
+
149
+ Returns:
150
+ A tuple of the Rust path for the item, as set during the
151
+ doc generation.
152
+ """
153
+ return sig_node.path
154
+
155
+ def _toc_entry_name(self, sig_node: addnodes.desc_signature) -> str:
156
+ """Generate the TOC entry for the item.
157
+
158
+ For most directives, this is just the item type and identifier of the
159
+ item. The ``:toc:`` option is set during doc generation where that is
160
+ not sufficient (``impl`` blocks) or not desired (enum variants).
161
+
162
+ Args:
163
+ sig_node: The docutils node for the item.
164
+
165
+ Returns:
166
+ The text to display for the item in the TOC and sidebar.
167
+ """
168
+ return self.options.get(
169
+ "toc", f"{self.rust_item_type.display_name} {sig_node.path[-1]}"
170
+ )
171
+
172
+
173
+ class RustCrateDirective(RustDirective):
174
+ """Directive for handling crate documentation"""
175
+
176
+ @property
177
+ def rust_item_type(self) -> RustItemType:
178
+ return RustItemType.CRATE
179
+
180
+
181
+ class RustEnumDirective(RustDirective):
182
+ """Directive for handling enum documentation"""
183
+
184
+ @property
185
+ def rust_item_type(self) -> RustItemType:
186
+ return RustItemType.ENUM
187
+
188
+
189
+ class RustFunctionDirective(RustDirective):
190
+ """Directive for handling function documentation"""
191
+
192
+ @property
193
+ def rust_item_type(self) -> RustItemType:
194
+ return RustItemType.FUNCTION
195
+
196
+
197
+ class RustImplDirective(RustDirective):
198
+ """Directive for handling function documentation"""
199
+
200
+ @property
201
+ def rust_item_type(self) -> RustItemType:
202
+ return RustItemType.IMPL
203
+
204
+
205
+ class RustMacroDirective(RustDirective):
206
+ """Directive for handling function documentation"""
207
+
208
+ @property
209
+ def rust_item_type(self) -> RustItemType:
210
+ return RustItemType.MACRO
211
+
212
+
213
+ class RustModuleDirective(RustDirective):
214
+ """Directive for handling module documentation"""
215
+
216
+ @property
217
+ def rust_item_type(self) -> RustItemType:
218
+ return RustItemType.MODULE
219
+
220
+
221
+ class RustStructDirective(RustDirective):
222
+ """Directive for handling struct documentation"""
223
+
224
+ @property
225
+ def rust_item_type(self) -> RustItemType:
226
+ return RustItemType.STRUCT
227
+
228
+
229
+ class RustTraitDirective(RustDirective):
230
+ """Directive for handling trait documentation"""
231
+
232
+ @property
233
+ def rust_item_type(self) -> RustItemType:
234
+ return RustItemType.TRAIT
235
+
236
+
237
+ class RustTypeDirective(RustDirective):
238
+ """Directive for handling type documentation"""
239
+
240
+ @property
241
+ def rust_item_type(self) -> RustItemType:
242
+ return RustItemType.TYPE
243
+
244
+
245
+ class RustVariableDirective(RustDirective):
246
+ """Directive for handling type documentation"""
247
+
248
+ @property
249
+ def rust_item_type(self) -> RustItemType:
250
+ return RustItemType.VARIABLE
@@ -0,0 +1,209 @@
1
+ """
2
+ The module defines the :py:class:`RustDomain` class, which is a Sphinx domain
3
+ for the various concepts used within the Rust programming language.
4
+ """
5
+
6
+ from typing import Optional, Type, Union, Iterable
7
+
8
+ from docutils.nodes import Element
9
+ from docutils.parsers.rst import Directive
10
+ from sphinx.addnodes import pending_xref
11
+ from sphinx.builders import Builder
12
+ from sphinx.domains import Domain, Index, ObjType
13
+ from sphinx.environment import BuildEnvironment
14
+ from sphinx.roles import XRefRole
15
+ from sphinx.util import logging
16
+ from sphinx.util.nodes import make_refnode
17
+ from sphinx.util.typing import RoleFunction
18
+
19
+ from sphinxcontrib_rust.directives import RustDirective
20
+ from sphinxcontrib_rust.index import (
21
+ CrateIndex,
22
+ EnumIndex,
23
+ FunctionIndex,
24
+ ModuleIndex,
25
+ StructIndex,
26
+ TraitIndex,
27
+ TypeIndex,
28
+ VariableIndex,
29
+ )
30
+ from sphinxcontrib_rust.items import RustItem, RustItemType
31
+
32
+ LOGGER = logging.getLogger(__name__)
33
+
34
+
35
+ class RustXRefRole(XRefRole):
36
+ """An :py:class:`XRefRole` extension for Rust roles"""
37
+
38
+ def process_link(
39
+ self,
40
+ env: BuildEnvironment,
41
+ refnode: Element,
42
+ has_explicit_title: bool,
43
+ title: str,
44
+ target: str,
45
+ ) -> tuple[str, str]:
46
+ """Process the link by parsing the tile and the target"""
47
+ # pylint: disable=too-many-arguments
48
+ if not has_explicit_title:
49
+ # This is the most common case where
50
+ # only the target is specified as the title like
51
+ # `` :rust:struct:`~crate::module::Struct` ``
52
+ # title == target in this case
53
+
54
+ # Remove any leading or trailing ::s.
55
+ # Only meaningful for targets, once support
56
+ # for relative references is added.
57
+ title = title.strip("::")
58
+
59
+ # Remove the ~ from the target, only meaningful for titles.
60
+ target = target.lstrip("~")
61
+
62
+ # ~ will use only the final part of the name as the title
63
+ # instead of the full path.
64
+ if title[0:1] == "~":
65
+ title = title[1:]
66
+ sep = title.rfind("::")
67
+ if sep != -1:
68
+ title = title[sep + 2 :]
69
+
70
+ return title, target
71
+
72
+
73
+ class RustDomain(Domain):
74
+ """The Sphinx domain for the Rust programming language.
75
+
76
+ The domain provides the various roles and directives that can be used in the Sphinx
77
+ documentation for linking with Rust code.
78
+ """
79
+
80
+ name = "rust"
81
+ label = "Rust"
82
+
83
+ # The various object types provided by the domain
84
+ object_types: dict[str, ObjType] = {
85
+ t.value: t.get_sphinx_obj_type() for t in RustItemType
86
+ }
87
+
88
+ # The various directives add to Sphinx for documenting the Rust object types
89
+ directives: dict[str, Type[Directive]] = {
90
+ t.value: d for t, d in RustDirective.get_directives().items()
91
+ }
92
+
93
+ # The various roles added to Sphinx for referencing the Rust object types
94
+ roles: dict[str, Union[RoleFunction, XRefRole]] = {
95
+ # TODO: Customize this role
96
+ r: RustXRefRole()
97
+ for _, r in RustItemType.iter_roles()
98
+ }
99
+
100
+ # The indices for all the object types
101
+ indices: list[Type[Index]] = [
102
+ CrateIndex,
103
+ ModuleIndex,
104
+ StructIndex,
105
+ TraitIndex,
106
+ EnumIndex,
107
+ FunctionIndex,
108
+ VariableIndex,
109
+ TypeIndex,
110
+ ]
111
+
112
+ # The domain data created by Sphinx. This is here just for type annotation.
113
+ data: dict[RustItemType, list[RustItem]]
114
+
115
+ # Initial data for the domain, gets copied as self.data by Sphinx
116
+ initial_data: dict[RustItemType, list[RustItem]] = {
117
+ t: [] for t in RustItemType
118
+ } # XXX: Perhaps better to create dict instead of list and key by name.
119
+
120
+ # Bump this when the data format changes.
121
+ data_version = 0
122
+
123
+ def get_objects(self) -> Iterable[tuple[str, str, str, str, str, int]]:
124
+ for typ, objs in self.data.items():
125
+ if not isinstance(typ, RustItemType):
126
+ continue
127
+ for obj in objs:
128
+ yield (
129
+ obj.name,
130
+ obj.dispname,
131
+ obj.type_.value,
132
+ obj.docname,
133
+ obj.anchor,
134
+ obj.priority,
135
+ )
136
+
137
+ def clear_doc(self, docname: str) -> None:
138
+ for typ, objs in self.data.items():
139
+ if isinstance(typ, RustItemType):
140
+ self.data[typ][:] = [o for o in objs if o.docname != docname]
141
+
142
+ def _find_match(self, target: str, typ: str | None = None) -> Optional[RustItem]:
143
+ search_types = [RustItemType.from_str(typ)] if typ else self.data.keys()
144
+ matches = set()
145
+ for search_type in search_types:
146
+ matches.update(o for o in self.data[search_type] if o.name == target)
147
+ return list(matches)[0] if matches else None
148
+
149
+ def resolve_xref(
150
+ self,
151
+ env: BuildEnvironment,
152
+ fromdocname: str,
153
+ builder: Builder,
154
+ typ: str,
155
+ target: str,
156
+ node: pending_xref,
157
+ contnode: Element,
158
+ ) -> Element | None:
159
+ # pylint:disable=too-many-arguments
160
+ match = self._find_match(target, typ)
161
+ return (
162
+ make_refnode(
163
+ builder,
164
+ fromdocname,
165
+ match.docname,
166
+ (
167
+ match.name.replace("::", "-")
168
+ if match.type_ not in (RustItemType.MODULE, RustItemType.CRATE)
169
+ else None
170
+ ),
171
+ [contnode],
172
+ match.name,
173
+ )
174
+ if match
175
+ else None
176
+ )
177
+
178
+ def resolve_any_xref(
179
+ self,
180
+ env: BuildEnvironment,
181
+ fromdocname: str,
182
+ builder: Builder,
183
+ target: str,
184
+ node: pending_xref,
185
+ contnode: Element,
186
+ ) -> list[tuple[str, Element]]:
187
+ # pylint:disable=too-many-arguments
188
+ match = self._find_match(target)
189
+ return (
190
+ make_refnode(
191
+ builder,
192
+ fromdocname,
193
+ match.docname,
194
+ (
195
+ match.name.replace("::", "-")
196
+ if match.type_ not in (RustItemType.MODULE, RustItemType.CRATE)
197
+ else None
198
+ ),
199
+ [contnode],
200
+ match.name,
201
+ )
202
+ if match
203
+ else None
204
+ )
205
+
206
+ def merge_domaindata(self, docnames: list[str], otherdata: dict) -> None:
207
+ otherdata: dict[RustItemType, list[RustItem]]
208
+ for typ, objs in otherdata.items():
209
+ self.data[typ].extend(o for o in objs if o.docname in docnames)