Skip to content

Commit e89c033

Browse files
authored
Merge pull request #278 from fooof-tools/docstrings
[MNT] - Update docstring handling to do more copying around / reduce copied text
2 parents f01399c + b85138f commit e89c033

File tree

5 files changed

+261
-60
lines changed

5 files changed

+261
-60
lines changed

fooof/core/modutils.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,38 @@ def wrapped_func(*args, **kwargs):
7777
return wrap
7878

7979

80+
DOCSTRING_SECTIONS = ['Parameters', 'Returns', 'Yields', 'Raises',
81+
'Warns', 'Examples', 'References', 'Notes',
82+
'Attributes', 'Methods']
83+
84+
85+
def get_docs_indices(docstring, sections=DOCSTRING_SECTIONS):
86+
"""Get the indices of each section within a docstring.
87+
88+
Parameters
89+
----------
90+
docstring : str
91+
Docstring to check indices for.
92+
sections : list of str, optional
93+
List of sections to check and get indices for.
94+
If not provided, uses the default set of
95+
96+
Returns
97+
-------
98+
inds : dict
99+
Dictionary in which each key is a section label, and each value is the corresponding index.
100+
"""
101+
102+
inds = {label : None for label in DOCSTRING_SECTIONS}
103+
104+
for ind, line in enumerate(docstring.split('\n')):
105+
for key, val in inds.items():
106+
if key in line:
107+
inds[key] = ind
108+
109+
return inds
110+
111+
80112
def docs_drop_param(docstring):
81113
"""Drop the first parameter description for a string representation of a docstring.
82114
@@ -132,6 +164,91 @@ def docs_append_to_section(docstring, section, add):
132164
for split in docstring.split('\n\n')])
133165

134166

167+
def docs_get_section(docstring, section, output='extract'):
168+
"""Extract and/or remove a specified section from a docstring.
169+
170+
Parameters
171+
----------
172+
docstring : str
173+
Docstring to extract / remove a section from.
174+
section : str
175+
Label of the section to extract / remove.
176+
mode : {'extract', 'remove'}
177+
Run mode, options:
178+
'extract' - returns the extracted section from the docstring.
179+
'remove' - returns the docstring after removing the specified section.
180+
181+
Returns
182+
-------
183+
out_docstring : str
184+
Extracted / updated docstring.
185+
"""
186+
187+
outs = []
188+
in_section = False
189+
190+
docstring_split = docstring.split('\n')
191+
for ind, line in enumerate(docstring_split):
192+
193+
# Track whether in the desired section
194+
if section in line and '--' in docstring_split[ind + 1]:
195+
in_section = True
196+
if in_section and line == '':
197+
in_section = False
198+
199+
# Collect desired outputs based on whether extracting or removing section
200+
if output == 'extract' and in_section:
201+
outs.append(line)
202+
if output == 'remove' and not in_section:
203+
outs.append(line)
204+
205+
# As a special case, when removing section, end section marker if there is a '%' line
206+
if in_section and output == 'remove' and not line.isspace() and line.strip()[0] == '%':
207+
in_section = False
208+
209+
out_docstring = '\n'.join(outs)
210+
211+
return out_docstring
212+
213+
214+
def docs_add_section(docstring, section):
215+
"""Add a section to a specified index of a docstring.
216+
217+
Parameters
218+
----------
219+
docstring : str
220+
Docstring to add section to.
221+
section : str
222+
New section to add to docstring.
223+
224+
Returns
225+
-------
226+
out_docstring : str
227+
Updated docstring, with the new section added.
228+
"""
229+
230+
inds = get_docs_indices(docstring)
231+
232+
# Split the section, extract the label, and check it's a known docstring section
233+
split_section = section.split('\n')
234+
section_label = split_section[0].strip()
235+
assert section_label in inds, 'Section label does not match expected list.'
236+
237+
# Remove the header section from the docstring (to replace it)
238+
docstring = docs_get_section(docstring, section_label, 'remove')
239+
240+
# Check for and drop leading and trailing empty lines
241+
split_section = split_section[1:] if split_section[0] == '' else split_section
242+
split_section = split_section[:-1] if split_section[-1] == ' ' else split_section
243+
244+
# Insert the new section into the docstring and rejoin it together
245+
split_docstring = docstring.split('\n')
246+
split_docstring[inds[section_label]:inds[section_label]] = split_section
247+
new_docstring = '\n'.join(split_docstring)
248+
249+
return new_docstring
250+
251+
135252
def copy_doc_func_to_method(source):
136253
"""Decorator that copies method docstring from function, dropping first parameter.
137254
@@ -180,3 +297,26 @@ def wrapper(func):
180297
return func
181298

182299
return wrapper
300+
301+
302+
def replace_docstring_sections(replacements):
303+
"""Decorator to drop in docstring sections
304+
305+
Parameters
306+
----------
307+
replacements : str or list of str
308+
Section(s) to drop into the decorated function's docstring.
309+
"""
310+
311+
def wrapper(func):
312+
313+
docstring = func.__doc__
314+
315+
for replacement in [replacements] if isinstance(replacements, str) else replacements:
316+
docstring = docs_add_section(docstring, replacement)
317+
318+
func.__doc__ = docstring
319+
320+
return func
321+
322+
return wrapper

fooof/objs/group.py

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
from fooof.core.reports import save_report_fg
2121
from fooof.core.strings import gen_results_fg_str
2222
from fooof.core.io import save_fg, load_jsonlines
23-
from fooof.core.modutils import copy_doc_func_to_method, safe_import
23+
from fooof.core.modutils import (copy_doc_func_to_method, safe_import,
24+
docs_get_section, replace_docstring_sections)
2425
from fooof.data.conversions import group_to_dataframe
2526

2627
###################################################################################################
2728
###################################################################################################
2829

30+
@replace_docstring_sections([docs_get_section(FOOOF.__doc__, 'Parameters'),
31+
docs_get_section(FOOOF.__doc__, 'Notes')])
2932
class FOOOFGroup(FOOOF):
3033
"""Model a group of power spectra as a combination of aperiodic and periodic components.
3134
@@ -36,18 +39,7 @@ class FOOOFGroup(FOOOF):
3639
3740
Parameters
3841
----------
39-
peak_width_limits : tuple of (float, float), optional, default: (0.5, 12.0)
40-
Limits on possible peak width, as (lower_bound, upper_bound).
41-
max_n_peaks : int, optional, default: inf
42-
Maximum number of gaussians to be fit in a single spectrum.
43-
min_peak_height : float, optional, default: 0
44-
Absolute threshold for detecting peaks, in units of the input data.
45-
peak_threshold : float, optional, default: 2.0
46-
Relative threshold for detecting peaks, in units of standard deviation of the input data.
47-
aperiodic_mode : {'fixed', 'knee'}
48-
Which approach to take for fitting the aperiodic component.
49-
verbose : bool, optional, default: True
50-
Verbosity mode. If True, prints out warnings and general status updates.
42+
%copied in from FOOOF object
5143
5244
Attributes
5345
----------
@@ -75,18 +67,7 @@ class FOOOFGroup(FOOOF):
7567
7668
Notes
7769
-----
78-
- Commonly used abbreviations used in this module include:
79-
CF: center frequency, PW: power, BW: Bandwidth, AP: aperiodic
80-
- Input power spectra must be provided in linear scale.
81-
Internally they are stored in log10 scale, as this is what the model operates upon.
82-
- Input power spectra should be smooth, as overly noisy power spectra may lead to bad fits.
83-
For example, raw FFT inputs are not appropriate. Where possible and appropriate, use
84-
longer time segments for power spectrum calculation to get smoother power spectra,
85-
as this will give better model fits.
86-
- The gaussian params are those that define the gaussian of the fit, where as the peak
87-
params are a modified version, in which the CF of the peak is the mean of the gaussian,
88-
the PW of the peak is the height of the gaussian over and above the aperiodic component,
89-
and the BW of the peak, is 2*std of the gaussian (as 'two sided' bandwidth).
70+
%copied in from FOOOF object
9071
- The FOOOFGroup object inherits from the FOOOF object. As such it also has data
9172
attributes (`power_spectrum` & `fooofed_spectrum_`), and parameter attributes
9273
(`aperiodic_params_`, `peak_params_`, `gaussian_params_`, `r_squared_`, `error_`)

fooof/tests/conftest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from fooof.core.modutils import safe_import
1010

11-
from fooof.tests.tutils import get_tfm, get_tfg, get_tbands, get_tresults
11+
from fooof.tests.tutils import get_tfm, get_tfg, get_tbands, get_tresults, get_tdocstring
1212
from fooof.tests.settings import (BASE_TEST_FILE_PATH, TEST_DATA_PATH,
1313
TEST_REPORTS_PATH, TEST_PLOTS_PATH)
1414

@@ -52,6 +52,10 @@ def tbands():
5252
def tresults():
5353
yield get_tresults()
5454

55+
@pytest.fixture(scope='function')
56+
def tdocstring():
57+
yield get_tdocstring()
58+
5559
@pytest.fixture(scope='session')
5660
def skip_if_no_mpl():
5761
if not safe_import('matplotlib'):

fooof/tests/core/test_modutils.py

Lines changed: 89 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,43 +33,13 @@ def subfunc_bad():
3333
with raises(ImportError):
3434
subfunc_bad()
3535

36-
def test_docs_drop_param():
36+
def test_docs_drop_param(tdocstring):
3737

38-
ds = """STUFF
39-
40-
Parameters
41-
----------
42-
first : thing
43-
Words, words, words.
44-
second : stuff
45-
Words, words, words.
46-
47-
Returns
48-
-------
49-
out : yay
50-
Words, words, words.
51-
"""
52-
53-
out = docs_drop_param(ds)
38+
out = docs_drop_param(tdocstring)
5439
assert 'first' not in out
5540
assert 'second' in out
5641

57-
def test_docs_append_to_section():
58-
59-
ds = """STUFF
60-
61-
Parameters
62-
----------
63-
first : thing
64-
Words, words, words.
65-
second : stuff
66-
Words, words, words.
67-
68-
Returns
69-
-------
70-
out : yay
71-
Words, words, words.
72-
"""
42+
def test_docs_append_to_section(tdocstring):
7343

7444
section = 'Parameters'
7545
add = \
@@ -78,7 +48,92 @@ def test_docs_append_to_section():
7848
Added description.
7949
"""
8050

81-
new_ds = docs_append_to_section(ds, section, add)
51+
new_ds = docs_append_to_section(tdocstring, section, add)
8252

8353
assert 'third' in new_ds
8454
assert 'Added description' in new_ds
55+
56+
def test_get_docs_indices(tdocstring):
57+
58+
inds = get_docs_indices(tdocstring)
59+
60+
for el in DOCSTRING_SECTIONS:
61+
assert el in inds.keys()
62+
63+
assert inds['Parameters'] == 2
64+
assert inds['Returns'] == 9
65+
66+
def test_docs_get_section(tdocstring):
67+
68+
out1 = docs_get_section(tdocstring, 'Parameters', output='extract')
69+
assert 'Parameters' in out1
70+
assert 'Returns' not in out1
71+
72+
out2 = docs_get_section(tdocstring, 'Parameters', output='remove')
73+
assert 'Parameters' not in out2
74+
assert 'Returns' in out2
75+
76+
def test_docs_add_section(tdocstring):
77+
78+
tdocstring = tdocstring + \
79+
"""\nNotes\n-----\n % copied in"""
80+
81+
new_section = \
82+
"""Notes\n-----\n \nThis is a new note."""
83+
new_docstring = docs_add_section(tdocstring, new_section)
84+
85+
assert 'Notes' in new_docstring
86+
assert '%' not in new_docstring
87+
assert 'new note' in new_docstring
88+
89+
def test_copy_doc_func_to_method(tdocstring):
90+
91+
def tfunc(): pass
92+
tfunc.__doc__ = tdocstring
93+
94+
class tObj():
95+
96+
@copy_doc_func_to_method(tfunc)
97+
def tmethod():
98+
pass
99+
100+
assert tObj.tmethod.__doc__
101+
assert 'first' not in tObj.tmethod.__doc__
102+
assert 'second' in tObj.tmethod.__doc__
103+
104+
105+
def test_copy_doc_class(tdocstring):
106+
107+
class tObj1():
108+
pass
109+
tObj1.__doc__ = tdocstring
110+
111+
new_section = \
112+
"""
113+
third : stuff
114+
Words, words, words.
115+
"""
116+
@copy_doc_class(tObj1, 'Parameters', new_section)
117+
class tObj2():
118+
pass
119+
120+
assert 'third' in tObj2.__doc__
121+
assert 'third' not in tObj1.__doc__
122+
123+
def test_replace_docstring_sections(tdocstring):
124+
125+
# Extract just the parameters section from general test docstring
126+
new_parameters = '\n'.join(tdocstring.split('\n')[2:8])
127+
128+
@replace_docstring_sections(new_parameters)
129+
def tfunc():
130+
"""Test function docstring
131+
132+
Parameters
133+
----------
134+
% copied in
135+
"""
136+
pass
137+
138+
assert 'first' in tfunc.__doc__
139+
assert 'second' in tfunc.__doc__

0 commit comments

Comments
 (0)