Skip to content

Commit 24b82d9

Browse files
authored
Merge pull request #97 from OpenMS/docstring_to_table
Docstring to table
2 parents 6029ea9 + 43d013d commit 24b82d9

11 files changed

Lines changed: 290 additions & 117 deletions

File tree

docs/Parameters/Chromatogram.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ Chromatograms can be plotted using kind = chromatogram. In this plot, retention
66
Parameters
77
----------
88

9-
.. csv-table::
10-
:file: chromatogramPlot.tsv
11-
:header-rows: 1
12-
:delim: tab
9+
.. docstring_to_table::
10+
:docstring: pyopenms_viz._config.ChromatogramConfig
11+
:parent_depth: 1
12+
:default_docstring:
1313

1414
Example Usage
1515
-------------

docs/Parameters/Mobilogram.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
Mobilogram
22
==========
33

4-
Chromatograms can be plotted using kind = mobilogram. In this plot, ion mobility is on the x-axis and intensity is on the y-axis. The by parameter can be used to separate out different mass traces. Functionally these plots have very similar to chromatograms
4+
Mobilograms are a type of plot used to visualize ion mobility data. In this plot, ion mobility is represented on the x-axis, while intensity is shown on the y-axis. The `by` parameter can be utilized to separate different mass traces. Functionally, mobilograms are similar to chromatograms.
55

66
Parameters
77
----------
88

9-
.. csv-table::
10-
:file: mobilogramPlot.tsv
11-
:header-rows: 1
12-
:delim: tab
9+
.. docstring_to_table::
10+
:docstring: pyopenms_viz._config.MobilogramConfig
11+
:parent_depth: 1
12+
:default_docstring:
1313

1414
Example Usage
1515
-------------

docs/Parameters/Parameters.rst

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ Pyopenms-viz plotting occurs by calling the `.plot()` method on a pandas datafra
55

66
General Options include (mandatory fields are starred)
77

8-
.. csv-table:: General Options
9-
:file: basePlot.tsv
10-
:header-rows: 1
11-
:delim: tab
8+
.. docstring_to_table::
9+
:docstring: pyopenms_viz._config.BasePlotConfig
10+
:title: Core Options
1211

12+
.. docstring_to_table::
13+
:docstring: pyopenms_viz._config.LegendConfig
14+
:title: Legend Options
1315

1416
Please click on a kind of plot below for more details on their specific parameters:
1517

docs/Parameters/PeakMap.rst

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@ Note: y_kind / x_kind is only relevant if add_marginals is set to True.
88
Parameters
99
----------
1010

11-
.. csv-table::
12-
:file: peakMapPlot.tsv
13-
:header-rows: 1
14-
:delim: tab
15-
11+
.. docstring_to_table::
12+
:docstring: pyopenms_viz._config.PeakMapConfig
13+
:parent_depth: 1
14+
:default_docstring:
1615

1716
Example Usage
1817
-------------

docs/Parameters/Spectrum.rst

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@ A spectrum can be plot using kind = "spectrum". In this plot, mass-to-charge rat
77
Parameters
88
----------
99

10-
.. csv-table:: Chromatogram Options
11-
:file: spectrumPlot.tsv
12-
:header-rows: 1
13-
:delim: tab
14-
10+
.. docstring_to_table::
11+
:docstring: pyopenms_viz._config.SpectrumConfig
12+
:parent_depth: 1
13+
:default_docstring:
1514

1615
Example Usage
1716
-------------

docs/_ext/docstring_to_table.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
from docutils.parsers.rst import Directive, directives
2+
from docutils import nodes
3+
import importlib
4+
import inspect
5+
import docstring_parser
6+
7+
8+
DEFAULT_DOCSTRING = """
9+
Default configuration for pyopenms_viz
10+
11+
Attributes:
12+
x (str): The column name for the X-axis data. Required.
13+
y (str): The column name for the Y-axis data. Required.
14+
by (str): The column name for the grouping variable.
15+
canvas (Any): Canvas for the plot. For Bokeh, this is a bokeh.plotting.Figure object. For Matplotlib, this is an Axes object, and for Plotly, this is a plotly.graph_objects.Figure object. If none, axis will be created Defaults to None.
16+
show_plot (bool): Whether to display the plot. Defaults to True.
17+
"""
18+
19+
20+
class DocstringToTableDirective(Directive):
21+
has_content = False
22+
required_arguments = 0
23+
optional_arguments = 0
24+
option_spec = {
25+
"docstring": str,
26+
"title": str,
27+
"parent_depth": int, # Number of parent classes to include
28+
"default_docstring": directives.flag, # Flag, no argument required
29+
}
30+
31+
def run(self):
32+
docstring_path = self.options.get("docstring")
33+
table_title = self.options.get("title")
34+
# If parent_depth is not specified, only include the base class (depth=0)
35+
parent_depth = self.options.get("parent_depth")
36+
if parent_depth is None:
37+
parent_depth = 0
38+
else:
39+
parent_depth = int(parent_depth)
40+
if not docstring_path:
41+
error = self.state_machine.reporter.error(
42+
"No :docstring: option provided to docstring_to_table directive.",
43+
line=self.lineno,
44+
)
45+
return [error]
46+
47+
# Split module and object
48+
mod_name, _, obj_path = docstring_path.partition(".")
49+
if not obj_path:
50+
error = self.state_machine.reporter.error(
51+
f"Invalid docstring path: {docstring_path}", line=self.lineno
52+
)
53+
return [error]
54+
55+
# Import module and get object
56+
try:
57+
mod = importlib.import_module(mod_name)
58+
obj = mod
59+
for attr in docstring_path.split(".")[1:]:
60+
obj = getattr(obj, attr)
61+
except Exception as e:
62+
error = self.state_machine.reporter.error(
63+
f"Could not import object '{docstring_path}': {e}", line=self.lineno
64+
)
65+
return [error]
66+
67+
# Collect docstrings from parent classes up to parent_depth
68+
docstrings = []
69+
current_obj = obj
70+
for i in range(parent_depth + 1):
71+
docstring = inspect.getdoc(current_obj)
72+
if docstring:
73+
docstrings.append(docstring)
74+
bases = getattr(current_obj, "__bases__", ())
75+
if bases and i < parent_depth:
76+
current_obj = bases[0]
77+
else:
78+
break
79+
80+
# Parse all collected docstrings
81+
params = []
82+
param_names = []
83+
# If :default_docstring: is present (flag), prepend its params
84+
if "default_docstring" in self.options:
85+
default_parsed = docstring_parser.parse(DEFAULT_DOCSTRING)
86+
for param in default_parsed.params:
87+
name = param.arg_name or ""
88+
default = param.default or ""
89+
typ = param.type_name or ""
90+
desc = param.description or ""
91+
if not default:
92+
name = f"{name}*"
93+
params.append((name, typ, desc, default))
94+
for docstring in reversed(docstrings): # Start from base class
95+
parsed = docstring_parser.parse(docstring)
96+
for param in parsed.params:
97+
name = param.arg_name or ""
98+
default = param.default or ""
99+
typ = param.type_name or ""
100+
desc = param.description or ""
101+
# Mark required parameters (no default) with '*'
102+
if not default:
103+
name_out = f"{name}*"
104+
else:
105+
name_out = name
106+
# Only keep the most "child" definition of each parameter
107+
if name in param_names:
108+
# Find and remove the old parameter from the list
109+
# It could be with or without a star
110+
for i, (p_name, _, _, _) in enumerate(params):
111+
if p_name.strip("*") == name:
112+
params.pop(i)
113+
break
114+
params.append((name_out, typ, desc, default))
115+
if name not in param_names:
116+
param_names.append(name)
117+
118+
# Build table
119+
table = nodes.table()
120+
if table_title:
121+
title_node = nodes.title(text=table_title)
122+
table += title_node
123+
tgroup = nodes.tgroup(cols=4)
124+
table += tgroup
125+
for width in [1, 1, 3, 1]:
126+
tgroup += nodes.colspec(colwidth=width)
127+
thead = nodes.thead()
128+
tgroup += thead
129+
header_row = nodes.row()
130+
for h in ["Parameter", "Type", "Description", "Default"]:
131+
entry = nodes.entry()
132+
entry += nodes.paragraph(text=h)
133+
header_row += entry
134+
thead += header_row
135+
tbody = nodes.tbody()
136+
tgroup += tbody
137+
for param in params:
138+
row = nodes.row()
139+
for cell in param:
140+
entry = nodes.entry()
141+
entry += nodes.paragraph(text=cell)
142+
row += entry
143+
tbody += row
144+
return [table]
145+
146+
147+
def setup(app):
148+
app.add_directive("docstring_to_table", DocstringToTableDirective)

docs/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def find_git_directory(start_path):
8989
# Add any Sphinx extension module names here, as strings. They can be
9090
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
9191
# ones.
92+
sys.path.insert(0, os.path.abspath(".")) # Add docs/ to path
9293
extensions = [
9394
"sphinx.ext.napoleon",
9495
"sphinx.ext.autodoc",
@@ -99,6 +100,7 @@ def find_git_directory(start_path):
99100
"sphinx_gallery.gen_gallery",
100101
"bokeh.sphinxext.bokeh_plot",
101102
"nbsphinx",
103+
"docs._ext.docstring_to_table",
102104
]
103105

104106
# Add any paths that contain templates here, relative to this directory.

docs/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ defusedxml==0.7.1
5454
# via
5555
# cairosvg
5656
# nbconvert
57+
docstring-parser==0.17.0
58+
# via pyopenms_viz (pyproject.toml)
5759
docutils==0.21.2
5860
# via
5961
# nbsphinx

0 commit comments

Comments
 (0)