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