from __future__ import annotations
import os
import re
from pathlib import Path
from docutils import nodes
from docutils.parsers.rst import Directive
from docutils.parsers.rst import directives as rst_directives
from docutils.statemachine import StringList
from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.util import logging
from sphinx.util.parsing import nested_parse_to_nodes
from sphinx_fortran_domain.utils import (
collect_fortran_source_files_from_config,
doc_markers_from_doc_chars,
extract_predoc_before_line,
extract_use_dependencies,
read_lines_utf8,
read_text_utf8,
)
logger = logging.getLogger(__name__)
_RE_DOC_SECTION = re.compile(r"^\s*##\s+(?P<title>\S.*?\S)\s*$")
_RE_FOOTNOTE_DEF = re.compile(r"^\s*\.\.\s*\[(?P<label>\d+|#)\]\s+")
_RE_END_PROGRAM = re.compile(r"^\s*end\s*program\b", re.IGNORECASE)
def _doc_markers_from_env(env) -> list[str]:
"""Return configured Fortran doc markers (e.g. ['!>'])."""
app = getattr(env, "app", None)
config = getattr(app, "config", None) if app is not None else None
try:
return doc_markers_from_doc_chars(getattr(config, "fortran_doc_chars", None))
except Exception:
# Keep directives resilient even if config is malformed.
return ["!>"]
def _collect_fortran_files_from_env(env) -> list[str]:
app = getattr(env, "app", None)
if app is None:
return []
confdir = Path(getattr(app, "confdir", os.getcwd()))
config = getattr(app, "config", None)
return collect_fortran_source_files_from_config(confdir=confdir, config=config)
def _find_program_in_file(lines: list[str], progname: str, *, start_at: int = 0) -> int | None:
pat = re.compile(rf"^\s*program\s+{re.escape(str(progname))}\b", re.IGNORECASE)
for i in range(max(start_at, 0), len(lines)):
if pat.match(lines[i]):
return i
return None
def _read_program_source_by_search(env, progname: str, *, doc_markers: list[str]) -> tuple[str | None, str | None]:
"""Find and read a program unit by scanning configured fortran_sources.
Returns (source, predoc) where predoc is doc lines immediately preceding
the program statement (using configured doc_markers).
"""
files = _collect_fortran_files_from_env(env)
if not files:
return (None, None)
for path in files:
try:
text = read_text_utf8(path)
except OSError:
continue
lines = text.splitlines()
start = _find_program_in_file(lines, progname)
if start is None:
continue
predoc = extract_predoc_before_line(lines, start, doc_markers=doc_markers)
buf: list[str] = []
for i in range(start, len(lines)):
buf.append(lines[i])
if _RE_END_PROGRAM.match(lines[i]):
break
return ("\n".join(buf), predoc)
return (None, None)
def _split_out_doc_section_blocks(text: str | None) -> tuple[str | None, str | None]:
"""Split a docstring into (preamble, sections) based on our "## Title" markers.
- preamble: everything before the first "##" section marker
- sections: from the first "##" marker to the end
This is used to control placement: the preamble stays near the top of object
documentation, while section blocks (Notes/References/See Also/...) can be
placed after intrinsic blocks (Arguments/Returns/Attributes/Procedures).
"""
if not text:
return None, None
lines = str(text).splitlines()
first = None
for i, line in enumerate(lines):
if _RE_DOC_SECTION.match(line):
first = i
break
if first is None:
preamble = "\n".join(lines).strip() or None
return preamble, None
preamble = "\n".join(lines[:first]).strip() or None
sections = "\n".join(lines[first:]).strip() or None
return preamble, sections
def _preprocess_fortran_docstring(text: str) -> str:
"""Normalize a lightweight docstring convention into valid reST.
Supported patterns:
- Section markers: "## References" -> ".. rubric:: References"
- Example blocks: contiguous lines starting with ">>>" -> ".. code-block:: fortran"
"""
lines = (text or "").splitlines()
out: list[str] = []
i = 0
while i < len(lines):
line = lines[i]
m = _RE_DOC_SECTION.match(line)
if m:
title = m.group("title")
normalized = (title or "").strip().lower()
# Match NumpyDoc-style rendering for "See Also" by emitting a real
# Sphinx seealso directive (renders as an admonition with special styling).
if normalized in {"see also", "seealso"}:
if out and out[-1].strip() != "":
out.append("")
out.append(".. seealso::")
out.append("")
i += 1
# Consume the See Also body until the next "##" section marker.
# Support a lightweight "term : description" syntax (with spaces
# around the colon) to produce a definition-list style layout.
while i < len(lines) and not _RE_DOC_SECTION.match(lines[i]):
body_line = lines[i]
if not body_line.strip():
out.append("")
i += 1
continue
# Split only on " : " (spaces required) so domain roles like
# ":f:func:`name`" are not misinterpreted.
parts = re.split(r"\s+:\s+", body_line.strip(), maxsplit=1)
if len(parts) == 2:
term, desc = parts
out.append(" " + term.strip())
out.append(" " + desc.strip())
out.append("")
else:
out.append(" " + body_line.strip())
i += 1
continue
i += 1
# Avoid accumulating extra blank lines at the end of the directive.
while out and out[-1] == "":
out.pop()
out.append("")
continue
if out and out[-1].strip() != "":
out.append("")
out.append(f".. rubric:: {title}")
out.append("")
i += 1
continue
if line.lstrip().startswith(">>>"):
block: list[str] = []
while i < len(lines) and lines[i].lstrip().startswith(">>>"):
s = lines[i].lstrip()[3:]
block.append(s.lstrip(" \t"))
i += 1
if out and out[-1].strip() != "":
out.append("")
out.append(".. code-block:: fortran")
out.append("")
for b in block:
out.append(" " + b)
out.append("")
continue
stripped = line.lstrip()
if stripped.startswith("```"):
# Extract language if provided
fence = stripped[3:].strip() # after "```"
language = fence if fence else "fortran"
block: list[str] = []
i += 1
while i < len(lines):
s = lines[i].lstrip()
if s.startswith("```"):
i += 1
break
block.append(s.rstrip("\t"))
i += 1
if out and out[-1].strip() != "":
out.append("")
out.append(f".. code-block:: {language}")
out.append("")
for b in block:
out.append(" " + b)
out.append("")
continue
out.append(line)
i += 1
# Make footnote/citation definitions robust in docstring fragments.
# In reST, a footnote definition must be preceded by a blank line.
# Many docstrings omit that blank line, which causes undefined refs like [1]_.
# We insert it here for common `.. [n] ...` patterns.
if _RE_FOOTNOTE_DEF.match(line):
# If the previous non-empty line isn't blank, insert a blank line *before*
# this definition. Because we've already appended the line, adjust in-place.
if len(out) >= 2 and out[-2].strip() != "":
out.insert(len(out) - 1, "")
return "\n".join(out).rstrip()
def _make_object_id(objtype: str, fullname: str) -> str:
# ``nodes.make_id`` produces a valid HTML id fragment.
return nodes.make_id(f"f-{objtype}-{fullname}")
def _append_doc(section: nodes.Element, doc: str | None, state) -> None:
if not doc:
return
text = _preprocess_fortran_docstring(str(doc))
content = StringList(text.splitlines(), source="<fortran-doc>")
container: nodes.Element = nodes.container()
# Parse doc as a reST fragment so Sphinx roles/directives work (e.g. .. math::).
for n in nested_parse_to_nodes(
state,
content,
source="<fortran-doc>",
offset=0,
allow_section_headings=True,
keep_title_context=True,
):
container += n
section += container
def _read_program_source_from_location(location) -> str | None:
"""Best-effort read of a program unit source from its file location."""
if location is None:
return None
path = getattr(location, "path", None)
lineno = getattr(location, "lineno", None)
if not path or not lineno:
return None
try:
lines = read_lines_utf8(path)
except OSError:
return None
start = max(int(lineno) - 1, 0)
buf: list[str] = []
for i in range(start, len(lines)):
buf.append(lines[i])
if re.match(r"^\s*end\s*program\b", lines[i], flags=re.IGNORECASE):
break
return "\n".join(buf)
def _append_fortran_code_block(section: nodes.Element, *, source: str) -> None:
text = (source or "").rstrip() + "\n"
lit = nodes.literal_block(text, text)
lit["language"] = "fortran"
section += lit
def _append_program_dependencies(section: nodes.Element, *, dependencies, state) -> None:
deps = [str(d).strip() for d in (dependencies or []) if str(d).strip()]
if not deps:
return
items = nodes.bullet_list()
for dep in deps:
xref = addnodes.pending_xref(
"",
refdomain="f",
reftype="module",
reftarget=dep,
refexplicit=True,
)
xref += nodes.literal(text=dep)
items += nodes.list_item("", nodes.paragraph("", "", xref))
section += nodes.subtitle(text="Dependencies")
section += items
def _parse_doc_fragment(doc: str | None, state) -> list[nodes.Node]:
if not doc:
return []
text = _preprocess_fortran_docstring(str(doc))
content = StringList(text.splitlines(), source="<fortran-doc>")
return list(
nested_parse_to_nodes(
state,
content,
source="<fortran-doc>",
offset=0,
allow_section_headings=True,
keep_title_context=True,
)
)
def _field_list(title: str, body: nodes.Element) -> nodes.field_list:
fl = nodes.field_list()
field = nodes.field()
field += nodes.field_name(text=title)
fbody = nodes.field_body()
fbody += body
field += fbody
fl += field
return fl
def _stamp_source_line(node: nodes.Node, *, source: str = "<fortran>", line: int = 1) -> None:
# Sphinx expects certain nodes (notably definition_list_item) to have
# a non-None .line during HTML builds.
if getattr(node, "source", None) is None:
node.source = source # type: ignore[attr-defined]
if getattr(node, "line", None) is None:
node.line = line # type: ignore[attr-defined]
def _append_named_decl_docs(
section: nodes.Element,
title: str,
items,
state,
*,
anchors_by_name: dict[str, str] | None = None,
as_field_list: bool = True,
) -> None:
"""Render a standard name/decl/doc definition list inside a field list.
This is the canonical rendering used for procedure Arguments and module Variables.
"""
if not items:
return
rows = []
for a in items:
name = getattr(a, "name", "")
decl = getattr(a, "decl", None) or ""
doc = getattr(a, "doc", None)
if not name:
continue
rows.append((name, decl, doc))
if not rows:
return
dl = nodes.definition_list()
dl["classes"].append("simple")
_stamp_source_line(dl)
for name, decl, doc in rows:
item = nodes.definition_list_item()
_stamp_source_line(item)
term = nodes.term()
_stamp_source_line(term)
anchor = (anchors_by_name or {}).get(str(name))
if anchor:
# Add an invisible target so xrefs land on the correct row without
# changing the visual rendering.
term += nodes.target(ids=[anchor])
term += nodes.strong(text=str(name))
item += term
if decl:
classifier = nodes.classifier(text=str(decl))
_stamp_source_line(classifier)
item += classifier
definition = nodes.definition()
_stamp_source_line(definition)
for n in _parse_doc_fragment(doc, state):
definition += n
item += definition
dl += item
if as_field_list:
section += _field_list(title, dl)
else:
section += nodes.subtitle(text=str(title))
section += dl
def _append_return_docs(section: nodes.Element, result, state) -> None:
if not result:
return
name = getattr(result, "name", "")
decl = getattr(result, "decl", None) or ""
doc = getattr(result, "doc", None)
if not name:
return
dl = nodes.definition_list()
dl["classes"].append("simple")
_stamp_source_line(dl)
item = nodes.definition_list_item()
_stamp_source_line(item)
term = nodes.term()
_stamp_source_line(term)
term += nodes.strong(text=str(name))
item += term
if decl:
classifier = nodes.classifier(text=str(decl))
_stamp_source_line(classifier)
item += classifier
definition = nodes.definition()
_stamp_source_line(definition)
for n in _parse_doc_fragment(doc, state):
definition += n
item += definition
dl += item
section += _field_list("Returns", dl)
def _append_component_docs(section: nodes.Element, components, state) -> None:
if not components:
return
rows = []
for c in components:
name = getattr(c, "name", "")
decl = getattr(c, "decl", None) or ""
doc = getattr(c, "doc", None)
if not name:
continue
rows.append((name, decl, doc))
if not rows:
return
dl = nodes.definition_list()
dl["classes"].append("simple")
_stamp_source_line(dl)
for name, decl, doc in rows:
item = nodes.definition_list_item()
_stamp_source_line(item)
term = nodes.term()
_stamp_source_line(term)
term += nodes.strong(text=str(name))
item += term
if decl:
classifier = nodes.classifier(text=str(decl))
_stamp_source_line(classifier)
item += classifier
definition = nodes.definition()
_stamp_source_line(definition)
for n in _parse_doc_fragment(doc, state):
definition += n
item += definition
dl += item
section += _field_list("Attributes", dl)
def _find_proc_by_name(procedures, name: str):
for p in procedures or []:
if getattr(p, "name", None) == name:
return p
return None
def _proc_ref_node(target: str, all_procedures, *, text: str | None = None) -> nodes.Node:
"""Return a cross-reference to ``target`` if it is a known procedure, else a plain literal."""
label = nodes.literal(text=str(text or target))
proc = _find_proc_by_name(all_procedures, target)
kind = getattr(proc, "kind", None) if proc is not None else None
if kind not in {"function", "subroutine"}:
return label
xref = addnodes.pending_xref(
"",
refdomain="f",
reftype=str(kind),
reftarget=str(target),
refexplicit=True,
)
xref += label
return xref
def _append_type_bound_procedures(section: nodes.Element, bindings, all_procedures, state) -> None:
if not bindings:
return
items = nodes.bullet_list()
for b in bindings:
bname = getattr(b, "name", "")
target = getattr(b, "target", None) or bname
targets = tuple(getattr(b, "targets", ()) or ())
if not targets and target:
targets = (target,)
if not bname:
continue
para = nodes.paragraph()
if len(targets) == 1:
para += _proc_ref_node(str(target), all_procedures, text=str(bname))
else:
para += nodes.literal(text=str(bname))
if targets and not (len(targets) == 1 and targets[0] == bname):
para += nodes.Text(" => ")
for index, target_name in enumerate(targets):
if index:
para += nodes.Text(", ")
para += _proc_ref_node(str(target_name), all_procedures)
item = nodes.list_item("", para)
items += item
section += _field_list("Procedures", items)
def _append_interface_procedures(section: nodes.Element, names, all_procedures) -> None:
"""Render the specific procedures lumped together by a generic interface."""
if not names:
return
items = nodes.bullet_list()
for name in names:
items += nodes.list_item("", nodes.paragraph("", "", _proc_ref_node(str(name), all_procedures)))
section += _field_list("Procedures", items)
def _append_object_description(
section: nodes.Element,
*,
domain: str,
objtype: str,
name: str,
signature: str | None,
doc: str | None,
state,
args=None,
result=None,
components=None,
bindings=None,
all_procedures=None,
specifics=None,
bodies=None,
anchor: str | None = None,
) -> None:
"""Render an object in a Sphinx-like <dl class="..."> wrapper.
For interfaces, ``specifics`` lists the lumped procedure names and ``bodies``
is a sequence of ``(procedure, anchor)`` pairs rendered as nested objects.
"""
desc = addnodes.desc()
desc["domain"] = domain
desc["objtype"] = objtype
desc["classes"].extend([domain, objtype])
signode = addnodes.desc_signature()
if anchor:
signode["ids"].append(anchor)
# Keep signature rendering simple and stable: show the parsed signature text
# as a literal inside the signature node.
text = signature if signature else f"{name} ({objtype})"
signode += nodes.literal(text=str(text))
desc += signode
content = addnodes.desc_content()
preamble_doc, section_blocks_doc = _split_out_doc_section_blocks(doc)
# Keep the opening free-text docstring at the top.
_append_doc(content, preamble_doc, state)
_append_named_decl_docs(content, "Arguments", args, state)
if objtype in {"function", "interface"}:
_append_return_docs(content, result, state)
_append_component_docs(content, components, state)
_append_type_bound_procedures(content, bindings, all_procedures, state)
_append_interface_procedures(content, specifics, all_procedures)
for body, body_anchor in bodies or ():
_append_object_description(
content,
domain=domain,
objtype=str(getattr(body, "kind", "procedure")),
name=str(body.name),
signature=getattr(body, "signature", None),
doc=getattr(body, "doc", None),
state=state,
args=getattr(body, "arguments", None),
result=getattr(body, "result", None),
anchor=body_anchor,
)
# Place all "## ..." section blocks (including "## Examples") after intrinsic blocks.
_append_doc(content, section_blocks_doc, state)
desc += content
section += desc
def _is_private_member(item) -> bool:
return bool(getattr(item, "is_private", False))
def _filter_visibility(items, *, show_private: bool):
if not items:
return []
if show_private:
return list(items)
return [item for item in items if not _is_private_member(item)]
def _binding_target_names(binding) -> list[str]:
targets = list(getattr(binding, "targets", ()) or ())
if targets:
return [str(t) for t in targets if str(t)]
target = getattr(binding, "target", None)
if target:
return [str(target)]
name = getattr(binding, "name", None)
return [str(name)] if name else []
def _collect_exported_procedure_names_from_types(types, procedures, *, show_private: bool) -> set[str]:
if show_private:
return {str(getattr(proc, "name", "")) for proc in procedures or [] if getattr(proc, "name", "")}
procedure_names = {str(getattr(proc, "name", "")) for proc in procedures or [] if getattr(proc, "name", "")}
exported: set[str] = set()
for fortran_type in _filter_visibility(types, show_private=False):
visible_bindings = _filter_visibility(getattr(fortran_type, "bound_procedures", None), show_private=False)
binding_by_name = {
str(getattr(binding, "name", "")): binding
for binding in visible_bindings
if getattr(binding, "name", "")
}
pending = [name for binding in visible_bindings for name in _binding_target_names(binding)]
seen: set[str] = set()
while pending:
name = str(pending.pop())
if not name or name in seen:
continue
seen.add(name)
if name in procedure_names:
exported.add(name)
continue
binding = binding_by_name.get(name)
if binding is not None:
pending.extend(_binding_target_names(binding))
return exported
def _collect_exported_procedure_names_from_interfaces(interfaces) -> set[str]:
# Specific procedures are reachable through a public generic interface even
# when they are private themselves.
return {
str(name)
for iface in _filter_visibility(interfaces, show_private=False)
for name in getattr(iface, "procedures", ()) or ()
if name
}
def _filter_exported_procedures(procedures, *, types, interfaces=None, show_private: bool):
all_procedures = list(procedures or [])
if show_private:
return all_procedures
exported_names = _collect_exported_procedure_names_from_types(types, all_procedures, show_private=show_private)
exported_names |= _collect_exported_procedure_names_from_interfaces(interfaces)
return [
procedure
for procedure in all_procedures
if not _is_private_member(procedure) or str(getattr(procedure, "name", "")) in exported_names
]
def _append_interface_section(section: nodes.Element, iface, *, scope_name: str, domain, index, procedures, state) -> None:
"""Render a module/submodule interface along with the procedures it lumps together."""
fullname = f"{scope_name}.{iface.name}"
obj_anchor = _make_object_id("interface", fullname)
domain.note_object(name=iface.name, objtype="interface", anchor=obj_anchor)
index["entries"].append(("single", f"{iface.name} (interface)", obj_anchor, "", None))
sub = nodes.section(ids=[obj_anchor])
sub += nodes.title(text=f"{iface.name} (interface)")
bodies = list(getattr(iface, "bodies", None) or ())
specifics = list(getattr(iface, "procedures", None) or ())
if len(bodies) == 1 and bodies[0].name == iface.name:
# A single explicit interface (e.g. from an abstract interface block):
# document the body directly as the interface.
body = bodies[0]
_append_object_description(
sub,
domain="f",
objtype="interface",
name=str(iface.name),
signature=getattr(body, "signature", None),
doc=getattr(iface, "doc", None) or getattr(body, "doc", None),
state=state,
args=getattr(body, "arguments", None),
result=getattr(body, "result", None),
)
section += sub
return
body_entries = []
for body in bodies:
kind = str(getattr(body, "kind", "procedure"))
body_anchor = _make_object_id(kind, f"{fullname}.{body.name}")
domain.note_object(name=body.name, objtype=kind, anchor=body_anchor)
index["entries"].append(("single", f"{body.name} ({kind})", body_anchor, "", None))
body_entries.append((body, body_anchor))
_append_object_description(
sub,
domain="f",
objtype="interface",
name=str(iface.name),
signature=None,
doc=getattr(iface, "doc", None),
state=state,
all_procedures=[*(procedures or ()), *bodies],
specifics=specifics,
bodies=body_entries,
)
section += sub
[docs]
class FortranObject(ObjectDescription[str]):
"""Base class for manual Fortran object directives.
Use these directives when you want to declare API objects directly in docs,
instead of relying on parsed ``fortran_sources``.
In reStructuredText:
.. code-block:: rst
.. f:function:: solve_linear_system(a, b)
Solves ``Ax = b``.
In MyST Markdown (requires MyST directive fences):
.. code-block:: md
```{f:function} solve_linear_system(a, b)
Solves `Ax = b`.
```
"""
has_content = True
required_arguments = 1
[docs]
def handle_signature(self, sig: str, signode: addnodes.desc_signature) -> str:
fullname = sig.strip()
signode += addnodes.desc_name(fullname, fullname)
return fullname
[docs]
def add_target_and_index(self, name: str, sig: str, signode: addnodes.desc_signature) -> None:
domain: FortranDomain = self.env.get_domain("f") # type: ignore[assignment]
objtype = self.objtype
anchor = _make_object_id(objtype, name)
if anchor not in signode["ids"]:
signode["ids"].append(anchor)
domain.note_object(name=name, objtype=objtype, anchor=anchor)
index_text = f"{name} ({objtype})"
self.indexnode["entries"].append(("single", index_text, anchor, "", None))
[docs]
class FortranFunction(FortranObject):
"""Manual declaration for a Fortran function.
In reStructuredText:
.. code-block:: rst
.. f:function:: det(a)
Computes the determinant of ``a``.
In MyST Markdown:
.. code-block:: md
```{f:function} det(a)
Computes the determinant of `a`.
```
"""
objtype = "function"
[docs]
class FortranSubroutine(FortranObject):
"""Manual declaration for a Fortran subroutine.
In reStructuredText:
.. code-block:: rst
.. f:subroutine:: normalize(vec)
Normalizes ``vec`` in place.
In MyST Markdown:
.. code-block:: md
```{f:subroutine} normalize(vec)
Normalizes `vec` in place.
```
"""
objtype = "subroutine"
[docs]
class FortranType(FortranObject):
"""Manual declaration for a Fortran derived type.
In reStructuredText:
.. code-block:: rst
.. f:type:: mesh_t
Unstructured mesh container.
In MyST Markdown:
.. code-block:: md
```{f:type} mesh_t
Unstructured mesh container.
```
"""
objtype = "type"
[docs]
class FortranInterface(FortranObject):
"""Manual declaration for a Fortran interface or generic interface.
In reStructuredText:
.. code-block:: rst
.. f:interface:: solve
Generic interface for linear solvers.
In MyST Markdown:
.. code-block:: md
```{f:interface} solve
Generic interface for linear solvers.
```
The specific procedures lumped together by the interface are listed under a
"Procedures" field and linked to their documentation (parsed or declared with
``f:function``/``f:subroutine``). By default they are taken from the interface
of the same name in ``fortran_sources`` (the first module, then submodule,
defining it).
Options:
* ``:module:`` module or submodule in which to look up the parsed interface,
when several of them define an interface with that name.
* ``:procedures:`` comma-separated specific procedures, overriding the parsed
list (e.g. to list only some of them, or to document an interface that is
not parsed). Names unknown to the parsed interface are reported as warnings.
.. code-block:: rst
.. f:interface:: solve
:module: linear_solvers
:procedures: solve_real, solve_complex
Generic interface for linear solvers.
"""
objtype = "interface"
option_spec = {
**FortranObject.option_spec,
"module": rst_directives.unchanged_required,
"procedures": rst_directives.unchanged,
}
[docs]
def transform_content(self, content_node: addnodes.desc_content) -> None:
name = self.names[0] if self.names else self.arguments[0].strip()
module = (self.options.get("module") or "").strip() or None
parsed = self.env.get_domain("f").find_interface(name, module=module) # type: ignore[attr-defined]
if parsed is None and module:
logger.warning(
"f:interface %s: no parsed interface of that name in module %s",
name,
module,
location=self.get_location(),
)
parsed_names = [str(n) for n in getattr(parsed, "procedures", None) or ()]
if "procedures" in self.options:
names = [n.strip() for n in re.split(r"[,\s]+", self.options["procedures"] or "") if n.strip()]
if parsed is not None:
known = {n.lower() for n in parsed_names}
unknown = [n for n in names if n.lower() not in known]
if unknown:
logger.warning(
"f:interface %s: %s not among the procedures of the parsed interface (%s)",
name,
", ".join(unknown),
", ".join(parsed_names) or "none",
location=self.get_location(),
)
else:
names = parsed_names
if not names:
return
items = nodes.bullet_list()
for name in names:
xref = addnodes.pending_xref(
"",
refdomain="f",
reftype="procedure",
reftarget=name,
refexplicit=True,
)
xref += nodes.literal(text=name)
items += nodes.list_item("", nodes.paragraph("", "", xref))
# Keep the list right after the free-text description, as in parsed interfaces.
content_node += _field_list("Procedures", items)
[docs]
class FortranProgram(Directive):
"""Render a parsed Fortran ``program``.
This directive looks up a program from the domain parsed result (typically
populated from ``fortran_sources``) and renders:
* program title and docstring block,
* optional source code,
* ``use`` dependencies,
* optional internal procedures.
Options:
* ``:no-show-code:`` hide the program source block.
* ``:procedures:`` force rendering internal procedures.
* ``:no-procedures:`` hide internal procedures.
In reStructuredText:
.. code-block:: rst
.. f:program:: simulation_main
:no-show-code:
In MyST Markdown:
.. code-block:: md
```{f:program} simulation_main
:no-show-code:
```
"""
required_arguments = 1
option_spec = {
"procedures": rst_directives.flag,
"no-procedures": rst_directives.flag,
"no-show-code": rst_directives.flag,
}
[docs]
def run(self):
progname = self.arguments[0]
env = self.state.document.settings.env
domain = env.get_domain("f")
program = getattr(domain, "get_program")(progname)
show_procedures = True
if "no-procedures" in self.options:
show_procedures = False
if "procedures" in self.options:
show_procedures = True
anchor = nodes.make_id(f"f-program-{progname}")
index = addnodes.index(entries=[("single", f"{progname} (program)", anchor, "", None)])
section = nodes.section(ids=[anchor])
section += nodes.title(text=f"{progname} (program)")
getattr(domain, "note_object")(name=progname, objtype="program", anchor=anchor)
if program is None:
return [index, nodes.warning(text=f"Fortran program '{progname}' not found (did you configure fortran_sources?)")]
markers = _doc_markers_from_env(env)
src = getattr(program, "source", None) or _read_program_source_from_location(getattr(program, "location", None))
predoc: str | None = None
if not src:
src, predoc = _read_program_source_by_search(env, progname, doc_markers=markers)
else:
# If we have a usable file location, try to re-read to extract only the doc
# block immediately preceding the program statement (important for FORD,
# which may include in-body docs in program.doc).
loc = getattr(program, "location", None)
path = getattr(loc, "path", None) if loc is not None else None
lineno = getattr(loc, "lineno", None) if loc is not None else None
if path and lineno:
try:
lines = Path(str(path)).read_text(encoding="utf-8", errors="replace").splitlines()
start = _find_program_in_file(lines, progname, start_at=max(int(lineno) - 1 - 5, 0))
if start is not None:
predoc = extract_predoc_before_line(lines, start, doc_markers=markers)
except OSError:
pass
# Render program-level docs right under the title.
# Prefer only the pre-program doc block when available (prevents in-body docs
# from being rendered separately, which is especially important for FORD).
doc = predoc if predoc is not None else getattr(program, "doc", None)
_append_doc(section, doc, self.state)
show_source_code = "no-show-code" not in self.options
if src and show_source_code:
_append_fortran_code_block(section, source=src)
deps = list(getattr(program, "dependencies", None) or [])
if not deps and src:
deps = extract_use_dependencies(src)
_append_program_dependencies(section, dependencies=deps, state=self.state)
# Internal procedures (after `contains`) are optionally rendered after the program source.
if show_procedures and getattr(program, "procedures", None):
section += nodes.subtitle(text="Procedures")
for p in program.procedures:
kind = getattr(p, "kind", "procedure")
fullname = f"{progname}.{p.name}"
obj_anchor = _make_object_id(kind, fullname)
getattr(domain, "note_object")(name=p.name, objtype=kind, anchor=obj_anchor)
index["entries"].append(("single", f"{p.name} ({kind})", obj_anchor, "", None))
sub = nodes.section(ids=[obj_anchor])
sub += nodes.title(text=f"{p.name} ({kind})")
_append_object_description(
sub,
domain="f",
objtype=str(kind),
name=str(p.name),
signature=getattr(p, "signature", None),
doc=getattr(p, "doc", None),
state=self.state,
args=getattr(p, "arguments", None),
result=getattr(p, "result", None),
)
section += sub
return [index, section]
[docs]
class FortranModule(Directive):
"""Render a parsed Fortran module and its public members.
This directive resolves a module from parsed sources and renders module docs,
variables, derived types, interfaces, and procedures in one section.
Options:
* ``:show-private:`` include private module members and private type internals
(components/type-bound procedures). By default, private elements are hidden.
In reStructuredText:
.. code-block:: rst
.. f:module:: linalg
:show-private:
In MyST Markdown:
.. code-block:: md
```{f:module} linalg
:show-private:
```
"""
required_arguments = 1
option_spec = {
"show-private": rst_directives.flag,
}
[docs]
def run(self):
modname = self.arguments[0]
env = self.state.document.settings.env
domain = env.get_domain("f")
module = getattr(domain, "get_module")(modname)
show_private = "show-private" in self.options
anchor = nodes.make_id(f"f-module-{modname}")
index = addnodes.index(entries=[("single", f"{modname} (module)", anchor, "", None)])
section = nodes.section(ids=[anchor])
section += nodes.title(text=f"{modname} (module)")
getattr(domain, "note_object")(name=modname, objtype="module", anchor=anchor)
if module is None:
return [index, nodes.warning(text=f"Fortran module '{modname}' not found (did you configure fortran_sources?)")]
_append_doc(section, getattr(module, "doc", None), self.state)
variables = _filter_visibility(getattr(module, "variables", None), show_private=show_private)
if variables:
anchors: dict[str, str] = {}
for v in variables:
name = getattr(v, "name", "")
if not name:
continue
fullname = f"{modname}.{name}"
obj_anchor = _make_object_id("variable", fullname)
anchors[str(name)] = obj_anchor
getattr(domain, "note_object")(name=name, objtype="variable", anchor=obj_anchor)
index["entries"].append(("single", f"{name} (variable)", obj_anchor, "", None))
_append_named_decl_docs(
section,
"Variables",
variables,
self.state,
anchors_by_name=anchors,
as_field_list=False,
)
types = _filter_visibility(getattr(module, "types", None), show_private=show_private)
procedures = _filter_exported_procedures(
getattr(module, "procedures", None),
types=getattr(module, "types", None),
interfaces=getattr(module, "interfaces", None),
show_private=show_private,
)
interfaces = _filter_visibility(getattr(module, "interfaces", None), show_private=show_private)
if types:
section += nodes.subtitle(text="Types")
for t in types:
fullname = f"{modname}.{t.name}"
obj_anchor = _make_object_id("type", fullname)
getattr(domain, "note_object")(name=t.name, objtype="type", anchor=obj_anchor)
index["entries"].append(("single", f"{t.name} (type)", obj_anchor, "", None))
sub = nodes.section(ids=[obj_anchor])
sub += nodes.title(text=f"{t.name} (type)")
_append_object_description(
sub,
domain="f",
objtype="type",
name=str(t.name),
signature=getattr(t, "signature", None),
doc=getattr(t, "doc", None),
state=self.state,
components=_filter_visibility(getattr(t, "components", None), show_private=show_private),
bindings=_filter_visibility(getattr(t, "bound_procedures", None), show_private=show_private),
all_procedures=procedures,
)
section += sub
if interfaces:
section += nodes.subtitle(text="Interfaces")
for g in interfaces:
_append_interface_section(
section,
g,
scope_name=modname,
domain=domain,
index=index,
procedures=procedures,
state=self.state,
)
if procedures:
section += nodes.subtitle(text="Procedures")
for p in procedures:
kind = getattr(p, "kind", "procedure")
fullname = f"{modname}.{p.name}"
obj_anchor = _make_object_id(kind, fullname)
getattr(domain, "note_object")(name=p.name, objtype=kind, anchor=obj_anchor)
index["entries"].append(("single", f"{p.name} ({kind})", obj_anchor, "", None))
sub = nodes.section(ids=[obj_anchor])
sub += nodes.title(text=f"{p.name} ({kind})")
_append_object_description(
sub,
domain="f",
objtype=str(kind),
name=str(p.name),
signature=getattr(p, "signature", None),
doc=getattr(p, "doc", None),
state=self.state,
args=getattr(p, "arguments", None),
result=getattr(p, "result", None),
)
section += sub
return [index, section]
[docs]
class FortranSubmodule(Directive):
"""Render a parsed Fortran submodule and its members.
Use this for projects that split implementation into submodules and want
automatic API pages similar to ``f:module``.
Options:
* ``:show-private:`` include private submodule members and private type internals
(components/type-bound procedures). By default, private elements are hidden.
In reStructuredText:
.. code-block:: rst
.. f:submodule:: linalg_impl
:show-private:
In MyST Markdown:
.. code-block:: md
```{f:submodule} linalg_impl
:show-private:
```
"""
required_arguments = 1
option_spec = {
"show-private": rst_directives.flag,
}
[docs]
def run(self):
submodname = self.arguments[0]
env = self.state.document.settings.env
domain = env.get_domain("f")
submodule = getattr(domain, "get_submodule")(submodname)
show_private = "show-private" in self.options
anchor = nodes.make_id(f"f-submodule-{submodname}")
index = addnodes.index(entries=[("single", f"{submodname} (submodule)", anchor, "", None)])
section = nodes.section(ids=[anchor])
section += nodes.title(text=f"{submodname} (submodule)")
getattr(domain, "note_object")(name=submodname, objtype="submodule", anchor=anchor)
if submodule is None:
return [index, nodes.warning(text=f"Fortran submodule '{submodname}' not found (did you configure fortran_sources?)")]
_append_doc(section, getattr(submodule, "doc", None), self.state)
variables = _filter_visibility(getattr(submodule, "variables", None), show_private=show_private)
if variables:
anchors: dict[str, str] = {}
for v in variables:
name = getattr(v, "name", "")
if not name:
continue
fullname = f"{submodname}.{name}"
obj_anchor = _make_object_id("variable", fullname)
anchors[str(name)] = obj_anchor
getattr(domain, "note_object")(name=name, objtype="variable", anchor=obj_anchor)
index["entries"].append(("single", f"{name} (variable)", obj_anchor, "", None))
_append_named_decl_docs(
section,
"Variables",
variables,
self.state,
anchors_by_name=anchors,
as_field_list=False,
)
types = _filter_visibility(getattr(submodule, "types", None), show_private=show_private)
procedures = _filter_exported_procedures(
getattr(submodule, "procedures", None),
types=getattr(submodule, "types", None),
interfaces=getattr(submodule, "interfaces", None),
show_private=show_private,
)
interfaces = _filter_visibility(getattr(submodule, "interfaces", None), show_private=show_private)
if types:
section += nodes.subtitle(text="Types")
for t in types:
fullname = f"{submodname}.{t.name}"
obj_anchor = _make_object_id("type", fullname)
getattr(domain, "note_object")(name=t.name, objtype="type", anchor=obj_anchor)
index["entries"].append(("single", f"{t.name} (type)", obj_anchor, "", None))
sub = nodes.section(ids=[obj_anchor])
sub += nodes.title(text=f"{t.name} (type)")
_append_object_description(
sub,
domain="f",
objtype="type",
name=str(t.name),
signature=getattr(t, "signature", None),
doc=getattr(t, "doc", None),
state=self.state,
components=_filter_visibility(getattr(t, "components", None), show_private=show_private),
bindings=_filter_visibility(getattr(t, "bound_procedures", None), show_private=show_private),
all_procedures=procedures,
)
section += sub
if interfaces:
section += nodes.subtitle(text="Interfaces")
for g in interfaces:
_append_interface_section(
section,
g,
scope_name=submodname,
domain=domain,
index=index,
procedures=procedures,
state=self.state,
)
if procedures:
section += nodes.subtitle(text="Procedures")
for p in procedures:
kind = getattr(p, "kind", "procedure")
fullname = f"{submodname}.{p.name}"
obj_anchor = _make_object_id(kind, fullname)
getattr(domain, "note_object")(name=p.name, objtype=kind, anchor=obj_anchor)
index["entries"].append(("single", f"{p.name} ({kind})", obj_anchor, "", None))
sub = nodes.section(ids=[obj_anchor])
sub += nodes.title(text=f"{p.name} ({kind})")
_append_object_description(
sub,
domain="f",
objtype=str(kind),
name=str(p.name),
signature=getattr(p, "signature", None),
doc=getattr(p, "doc", None),
state=self.state,
args=getattr(p, "arguments", None),
result=getattr(p, "result", None),
)
section += sub
return [index, section]