Skip to content

Commit f0d0089

Browse files
authored
Merge pull request #1118 from fls-bioinformatics-core/add-utilities-for-documentation
New utilities to help with creating Sphinx documentation
2 parents 53b3771 + de55879 commit f0d0089

3 files changed

Lines changed: 502 additions & 61 deletions

File tree

auto_process_ngs/docs.py

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
#!/usr/bin/env python
2+
#
3+
# docs: helpers for auto-process-ngs Sphinx documentation generation
4+
# Copyright (C) University of Manchester 2025 Peter Briggs
5+
#
6+
7+
"""
8+
Provides utility classes and functions to help with generating Sphinx
9+
documentation for the ``auto_process_ngs`` package.
10+
11+
The helpers are intended to be called from the Sphinx ``conf.py`` file,
12+
to generate content such as tables of applications and protocols which
13+
are then included in the documentation source files.
14+
15+
Classes:
16+
17+
* ``RstSimpleTable``: class for making reStructuredText 'simple' tables
18+
* ``RstGridTable``: class for making reStructuredText 'grid' tables
19+
20+
"""
21+
22+
23+
class RstSimpleTable:
24+
"""
25+
Class for making simple reStructuredText tables.
26+
27+
See documentation for reStructureText simple tables at:
28+
https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#simple-tables
29+
30+
Usage:
31+
32+
>>> table_data = [
33+
... ['col1_row1','col2_row1','col3_row1'],
34+
... ['col1_row2','col2_row2','col3_row2'],
35+
... ['col1_row3','col2_row3','col3_row3'],
36+
... ]
37+
>>> header = ['Column 1','Column 2','Column 3']
38+
>>> table = RstSimpleTable(table_data)
39+
>>> for line in table.construct_table(header=header):
40+
... print(line)
41+
========= ========= =========
42+
Column 1 Column 2 Column 3
43+
========= ========= =========
44+
col1_row1 col2_row1 col3_row1
45+
col1_row2 col2_row2 col3_row2
46+
col1_row3 col2_row3 col3_row3
47+
========= ========= =========
48+
49+
Arguments:
50+
table_data (list): list of table rows, where each
51+
row is a list of column values.
52+
"""
53+
def __init__(self, table_data):
54+
self._table_data = table_data
55+
56+
def get_field_widths(self, header=None):
57+
"""
58+
Returns a list of field widths.
59+
"""
60+
field_widths = []
61+
for row in self._table_data:
62+
for i, col in enumerate(row):
63+
try:
64+
field_widths[i] = max(field_widths[i], len(col))
65+
except IndexError:
66+
field_widths.append(len(col))
67+
if header:
68+
for i, title in enumerate(header):
69+
field_widths[i] = max(field_widths[i], len(title))
70+
return field_widths
71+
72+
def make_divider(self, field_widths):
73+
"""
74+
Make a row divider
75+
"""
76+
divider = []
77+
for width in field_widths:
78+
divider.append("="*width)
79+
return " ".join(divider)
80+
81+
def construct_table(self, header=None, indent=""):
82+
"""
83+
Returns reStructuredText simple table
84+
85+
Arguments:
86+
header (list): list of column titles to use in
87+
the table header (otherwise no header is made)
88+
indent (str): string to use for indenting each
89+
line of the table (default: no indentation)
90+
"""
91+
# Collect the field widths
92+
field_widths = self.get_field_widths(header=header)
93+
# Start constructing the table
94+
table = []
95+
# Add top divider
96+
table.append(indent + self.make_divider(field_widths))
97+
# Add table header
98+
if header:
99+
line = []
100+
for title, width in zip(header, field_widths):
101+
line.append(title + " "*(width - len(title)))
102+
table.append(indent + " ".join(line))
103+
# Add header divider
104+
table.append(indent + self.make_divider(field_widths))
105+
# Add the table contents
106+
for row in self._table_data:
107+
line = []
108+
for col, width in zip(row, field_widths):
109+
line.append(col + " "*(width - len(col)))
110+
table.append(indent + " ".join(line))
111+
# Closing divider
112+
table.append(indent + self.make_divider(field_widths))
113+
return table
114+
115+
116+
class RstGridTable:
117+
"""
118+
Class for making reStructuredText 'grid' tables.
119+
120+
See documentation for reStructureText grid tables at:
121+
https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#grid-tables
122+
123+
Usage:
124+
125+
>>> table_data = [
126+
... ['col1_row1','col2_row1','col3_row1'],
127+
... ['col1_row2','col2_row2','col3_row2'],
128+
... ['col1_row3','col2_row3','col3_row3'],
129+
... ]
130+
>>> header = ['Column 1','Column 2','Column 3']
131+
>>> table = RstGridTable(table_data)
132+
>>> for line in table.construct_table(header=header):
133+
... print(line)
134+
+-----------+-----------+-----------+
135+
| Column 1 | Column 2 | Column 3 |
136+
+===========+===========+===========+
137+
| col1_row1 | col2_row1 | col3_row1 |
138+
+-----------+-----------+-----------+
139+
| col1_row2 | col2_row2 | col3_row2 |
140+
+-----------+-----------+-----------+
141+
| col1_row3 | col2_row3 | col3_row3 |
142+
+-----------+-----------+-----------+
143+
144+
Where columns with identical values in adjacent rows are merged:
145+
146+
>>> table_data = [
147+
... ['block1','col2_row1','col3_row1'],
148+
... ['block1','col2_row2','col3_row2'],
149+
... ['block2','col2_row3','col3_row3'],
150+
... ]
151+
>>> header = ['Block','Column 2','Column 3']
152+
>>> table = RstGridTable(table_data)
153+
>>> for line in table.construct_table(header=header):
154+
... print(line)
155+
+--------+-----------+-----------+
156+
| Block | Column 2 | Column 3 |
157+
+========+===========+===========+
158+
| block1 | col2_row1 | col3_row1 |
159+
| +-----------+-----------+
160+
| | col2_row2 | col3_row2 |
161+
+--------+-----------+-----------+
162+
| block2 | col2_row3 | col3_row3 |
163+
+--------+-----------+-----------+
164+
165+
Arguments:
166+
table_data (list): list of table rows, where each
167+
row is a list of column values.
168+
"""
169+
def __init__(self, table_data):
170+
self._table_data = table_data
171+
172+
def get_field_widths(self, header=None):
173+
"""
174+
Returns a list of field widths.
175+
"""
176+
field_widths = []
177+
for row in self._table_data:
178+
for i, col in enumerate(row):
179+
try:
180+
field_widths[i] = max(field_widths[i], len(col))
181+
except IndexError:
182+
field_widths.append(len(col))
183+
if header:
184+
for i, title in enumerate(header):
185+
field_widths[i] = max(field_widths[i], len(title))
186+
return field_widths
187+
188+
def make_header(self, header, field_widths):
189+
"""
190+
Make the table header
191+
"""
192+
table_header = [self.make_divider(field_widths)]
193+
line = []
194+
for title, width in zip(header, field_widths):
195+
line.append(title + " "*(width - len(title)))
196+
table_header.append("| " + " | ".join(line) + " |")
197+
return table_header
198+
199+
def make_divider(self, field_widths, divider_char="-"):
200+
"""
201+
Make a row divider
202+
"""
203+
divider = []
204+
for width in field_widths:
205+
divider.extend(["+", divider_char*(width + 2)])
206+
return "".join(divider) + "+"
207+
208+
def construct_table(self, header=None, indent=""):
209+
"""
210+
Returns reStructuredText table
211+
212+
Arguments:
213+
header (list): list of column titles to use in
214+
the table header (otherwise no header is made)
215+
indent (str): string to use for indenting each
216+
line of the table (default: no indentation)
217+
"""
218+
# Collect the field widths
219+
field_widths = self.get_field_widths(header=header)
220+
# Start constructing the table
221+
table = []
222+
# Add table header
223+
if header:
224+
for line in self.make_header(header, field_widths):
225+
table.append(indent + line)
226+
# Add the table contents
227+
previous_row = None
228+
for row in self._table_data:
229+
if previous_row is None:
230+
# First line
231+
previous_row = row
232+
# Add a divider
233+
if header:
234+
# Separate from header
235+
divider = self.make_divider(field_widths, divider_char="=")
236+
else:
237+
# No header
238+
divider = self.make_divider(field_widths)
239+
else:
240+
# Inside table body
241+
if previous_row[0] != row[0]:
242+
# First column data differs from previous row,
243+
# so treat as a new "block"
244+
# Store this row for next round
245+
previous_row = row
246+
divider = self.make_divider(field_widths)
247+
else:
248+
# Inside a "block": look at merging rows where values match
249+
# within this column
250+
new_row = []
251+
for col, prev_col, width in zip(row, previous_row, field_widths):
252+
if col != prev_col:
253+
# Column contents differ between rows so
254+
# insert explicit value
255+
new_row.append(col)
256+
else:
257+
# Column contents are the same so insert
258+
# 'None' to indicate merged cell
259+
new_row.append(None)
260+
previous_row = row
261+
row = new_row
262+
# Create divider
263+
divider = ""
264+
for i, col in enumerate(row):
265+
if col or (i>0 and row[i-1]):
266+
divider += "+"
267+
else:
268+
divider += "|"
269+
if col:
270+
divider += "-"*(field_widths[i]+2)
271+
else:
272+
divider += " "*(field_widths[i]+2)
273+
if divider.endswith("-"):
274+
divider += "+"
275+
else:
276+
divider += "|"
277+
# Append the row
278+
table.append(indent + divider)
279+
line = ""
280+
for i, col in enumerate(row):
281+
if col:
282+
# Pad value
283+
line += "| " + col + " "*(field_widths[i]-len(col)+1)
284+
else:
285+
# Empty cell
286+
line += "| " + " "*(field_widths[i]+1)
287+
line += "|"
288+
table.append(indent + line)
289+
# Closing divider
290+
table.append(indent + self.make_divider(field_widths))
291+
return table

0 commit comments

Comments
 (0)