"""Sphinx extension.
To enable dargs Sphinx extension, add :mod:`dargs.sphinx` to the extensions of conf.py:
.. code-block:: python
extensions = [
'dargs.sphinx',
]
Then `dargs` directive will be added:
.. code-block:: rst
.. dargs::
:module: dargs.sphinx
:func: _test_argument
where `_test_argument` returns an :class:`Argument <dargs.Argument>`. A :class:`list` of :class:`Argument <dargs.Argument>` is also accepted.
"""
import sys
from typing import ClassVar, List
from docutils.parsers.rst import Directive
from docutils.parsers.rst.directives import unchanged
from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType
from sphinx.roles import XRefRole
from sphinx.util.nodes import make_refnode
from .dargs import Argument, Variant
[docs]
class DargsDirective(Directive):
"""dargs directive."""
has_content: ClassVar[bool] = True
option_spec: ClassVar[dict] = {
"module": unchanged,
"func": unchanged,
}
[docs]
def run(self):
if "module" in self.options and "func" in self.options:
module_name = self.options["module"]
attr_name = self.options["func"]
else:
raise self.error(":module: and :func: should be specified")
try:
mod = __import__(module_name, globals(), locals(), [attr_name])
except ImportError:
raise self.error(
f'Failed to import "{attr_name}" from "{module_name}".\n{sys.exc_info()[1]}'
)
if not hasattr(mod, attr_name):
raise self.error(
f'Module "{module_name}" has no attribute "{attr_name}"\n'
"Incorrect argparse :module: or :func: values?"
)
func = getattr(mod, attr_name)
arguments = func()
if not isinstance(arguments, (list, tuple)):
arguments = [arguments]
rsts = []
for argument in arguments:
if not isinstance(argument, (Argument, Variant)):
raise RuntimeError("The function doesn't return Argument")
rst = argument.gen_doc(
make_anchor=True, make_link=True, use_sphinx_domain=True
)
rsts.extend(rst.split("\n"))
self.state_machine.insert_input(rsts, f"{module_name}:{attr_name}")
return []
[docs]
class DargsObject(ObjectDescription):
"""dargs::argument directive.
This directive creates a signature node for an argument.
"""
option_spec: ClassVar[dict] = {
"path": unchanged,
}
[docs]
def handle_signature(self, sig, signode):
signode += addnodes.desc_name(sig, sig)
return sig
[docs]
def add_target_and_index(self, name, sig, signode):
path = self.options["path"]
targetid = f"{self.objtype}:{path}"
if targetid not in self.state.document.ids:
signode["names"].append(targetid)
signode["ids"].append(targetid)
signode["first"] = not self.names
self.state.document.note_explicit_target(signode)
# for cross-references
inv = self.env.domaindata["dargs"]["arguments"]
if targetid in inv:
self.state.document.reporter.warning(
f'Duplicated argument "{targetid}" described in "{self.env.doc2path(inv[targetid][0])}".',
line=self.lineno,
)
inv[targetid] = (self.env.docname, self.objtype)
self.indexnode["entries"].append(
(
"pair",
f"{name}; {path} ({self.objtype.title()})",
targetid,
"main",
None,
)
)
[docs]
class DargsDomain(Domain):
"""Dargs domain.
Includes:
- dargs::argument directive
- dargs::argument role
"""
name: ClassVar[str] = "dargs"
label: ClassVar[str] = "dargs"
object_types: ClassVar[dict] = {
"argument": ObjType("argument", "argument"),
}
directives: ClassVar[dict] = {
"argument": DargsObject,
}
roles: ClassVar[dict] = {
"argument": XRefRole(),
}
initial_data: ClassVar[dict] = {
"arguments": {}, # fullname -> docname, objtype
}
[docs]
def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
"""Resolve cross-references."""
targetid = f"{typ}:{target}"
obj = self.data["arguments"].get(targetid)
if obj is None:
return None
return make_refnode(builder, fromdocname, obj[0], targetid, contnode, target)
[docs]
def setup(app):
"""Setup sphinx app."""
app.add_directive("dargs", DargsDirective)
app.add_domain(DargsDomain)
return {"parallel_read_safe": True}
def _test_argument() -> Argument:
"""This internal function is used to generate docs of dargs."""
doc_test = "This argument/variant is only used to test."
return Argument(
name="test",
dtype=str,
doc=doc_test,
sub_fields=[
Argument("test_argument", dtype=str, doc=doc_test, default="test"),
Argument("test_list", dtype=List[int], optional=True),
],
sub_variants=[
Variant(
"test_variant",
doc=doc_test,
choices=[
Argument(
"test_variant_argument",
dtype=dict,
optional=True,
doc=doc_test,
sub_fields=[
Argument(
"test_repeat",
dtype=list,
repeat=True,
doc=doc_test,
sub_fields=[
Argument(
"test_repeat_item", dtype=bool, doc=doc_test
),
],
)
],
),
],
),
],
)
def _test_arguments() -> List[Argument]:
"""Returns a list of arguments."""
return [
Argument(name="test1", dtype=int, doc="Argument 1"),
Argument(name="test2", dtype=[float, None], doc="Argument 2"),
Argument(
name="test3",
dtype=List[str],
default=["test"],
optional=True,
doc="Argument 3",
),
]