Skip to content

Commit 53872a7

Browse files
committed
enh: added script to check metadata
1 parent 6ebbcef commit 53872a7

File tree

5 files changed

+348
-0
lines changed

5 files changed

+348
-0
lines changed

nipype/interfaces/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ class BaseInterface(Interface):
699699
"""
700700
input_spec = BaseInterfaceInputSpec
701701
_version = None
702+
_additional_metadata = []
702703

703704
def __init__(self, **inputs):
704705
if not self.input_spec:

nipype/interfaces/freesurfer/preprocess.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,7 @@ class ReconAll(CommandLine):
626626
"""
627627

628628
_cmd = 'recon-all'
629+
_additional_metadata = ['loc', 'altkey']
629630
input_spec = ReconAllInputSpec
630631
output_spec = ReconAllIOutputSpec
631632

nipype/interfaces/io.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,7 @@ class FreeSurferSource(IOBase):
943943
input_spec = FSSourceInputSpec
944944
output_spec = FSSourceOutputSpec
945945
_always_run = True
946+
_additional_metadata = ['loc', 'altkey']
946947

947948
def _get_files(self, path, key, dirval, altkey=None):
948949
globsuffix = ''

nipype/interfaces/spm/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ class SPMCommand(BaseInterface):
209209
WARNING: Pseudo prototype class, meant to be subclassed
210210
"""
211211
input_spec = SPMCommandInputSpec
212+
_additional_metadata = ['field']
212213

213214
_jobtype = 'basetype'
214215
_jobname = 'basename'

tools/checkspecs.py

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
4+
"""Attempt to check each interface in nipype
5+
"""
6+
7+
# Stdlib imports
8+
import inspect
9+
import os
10+
import re
11+
import sys
12+
import tempfile
13+
import warnings
14+
15+
from nipype.interfaces.base import BaseInterface
16+
17+
# Functions and classes
18+
class InterfaceChecker(object):
19+
"""Class for checking all interface specifications
20+
"""
21+
22+
def __init__(self,
23+
package_name,
24+
package_skip_patterns=None,
25+
module_skip_patterns=None,
26+
class_skip_patterns=None
27+
):
28+
''' Initialize package for parsing
29+
30+
Parameters
31+
----------
32+
package_name : string
33+
Name of the top-level package. *package_name* must be the
34+
name of an importable package
35+
package_skip_patterns : None or sequence of {strings, regexps}
36+
Sequence of strings giving URIs of packages to be excluded
37+
Operates on the package path, starting at (including) the
38+
first dot in the package path, after *package_name* - so,
39+
if *package_name* is ``sphinx``, then ``sphinx.util`` will
40+
result in ``.util`` being passed for earching by these
41+
regexps. If is None, gives default. Default is:
42+
['\.tests$']
43+
module_skip_patterns : None or sequence
44+
Sequence of strings giving URIs of modules to be excluded
45+
Operates on the module name including preceding URI path,
46+
back to the first dot after *package_name*. For example
47+
``sphinx.util.console`` results in the string to search of
48+
``.util.console``
49+
If is None, gives default. Default is:
50+
['\.setup$', '\._']
51+
class_skip_patterns : None or sequence
52+
Sequence of strings giving classes to be excluded
53+
Default is: None
54+
55+
'''
56+
if package_skip_patterns is None:
57+
package_skip_patterns = ['\\.tests$']
58+
if module_skip_patterns is None:
59+
module_skip_patterns = ['\\.setup$', '\\._']
60+
if class_skip_patterns:
61+
self.class_skip_patterns = class_skip_patterns
62+
else:
63+
self.class_skip_patterns = []
64+
self.package_name = package_name
65+
self.package_skip_patterns = package_skip_patterns
66+
self.module_skip_patterns = module_skip_patterns
67+
68+
def get_package_name(self):
69+
return self._package_name
70+
71+
def set_package_name(self, package_name):
72+
"""Set package_name"""
73+
# It's also possible to imagine caching the module parsing here
74+
self._package_name = package_name
75+
self.root_module = __import__(package_name)
76+
self.root_path = self.root_module.__path__[0]
77+
78+
package_name = property(get_package_name, set_package_name, None,
79+
'get/set package_name')
80+
81+
def _get_object_name(self, line):
82+
name = line.split()[1].split('(')[0].strip()
83+
# in case we have classes which are not derived from object
84+
# ie. old style classes
85+
return name.rstrip(':')
86+
87+
def _uri2path(self, uri):
88+
"""Convert uri to absolute filepath
89+
90+
Parameters
91+
----------
92+
uri : string
93+
URI of python module to return path for
94+
95+
Returns
96+
-------
97+
path : None or string
98+
Returns None if there is no valid path for this URI
99+
Otherwise returns absolute file system path for URI
100+
101+
"""
102+
if uri == self.package_name:
103+
return os.path.join(self.root_path, '__init__.py')
104+
path = uri.replace('.', os.path.sep)
105+
path = path.replace(self.package_name + os.path.sep, '')
106+
path = os.path.join(self.root_path, path)
107+
# XXX maybe check for extensions as well?
108+
if os.path.exists(path + '.py'): # file
109+
path += '.py'
110+
elif os.path.exists(os.path.join(path, '__init__.py')):
111+
path = os.path.join(path, '__init__.py')
112+
else:
113+
return None
114+
return path
115+
116+
def _path2uri(self, dirpath):
117+
''' Convert directory path to uri '''
118+
relpath = dirpath.replace(self.root_path, self.package_name)
119+
if relpath.startswith(os.path.sep):
120+
relpath = relpath[1:]
121+
return relpath.replace(os.path.sep, '.')
122+
123+
def _parse_module(self, uri):
124+
''' Parse module defined in *uri* '''
125+
filename = self._uri2path(uri)
126+
if filename is None:
127+
# nothing that we could handle here.
128+
return ([],[])
129+
f = open(filename, 'rt')
130+
functions, classes = self._parse_lines(f, uri)
131+
f.close()
132+
return functions, classes
133+
134+
def _parse_lines(self, linesource, module):
135+
''' Parse lines of text for functions and classes '''
136+
functions = []
137+
classes = []
138+
for line in linesource:
139+
if line.startswith('def ') and line.count('('):
140+
# exclude private stuff
141+
name = self._get_object_name(line)
142+
if not name.startswith('_'):
143+
functions.append(name)
144+
elif line.startswith('class '):
145+
# exclude private stuff
146+
name = self._get_object_name(line)
147+
if not name.startswith('_') and \
148+
self._survives_exclude('.'.join((module, name)),
149+
'class'):
150+
classes.append(name)
151+
else:
152+
pass
153+
functions.sort()
154+
classes.sort()
155+
return functions, classes
156+
157+
def test_specs(self, uri):
158+
"""Check input and output specs in an uri
159+
160+
Parameters
161+
----------
162+
uri : string
163+
python location of module - e.g 'sphinx.builder'
164+
165+
Returns
166+
-------
167+
"""
168+
# get the names of all classes and functions
169+
_, classes = self._parse_module(uri)
170+
if not classes:
171+
#print 'WARNING: Empty -',uri # dbg
172+
return None
173+
174+
# Make a shorter version of the uri that omits the package name for
175+
# titles
176+
uri_short = re.sub(r'^%s\.' % self.package_name, '', uri)
177+
allowed_keys = ['desc', 'genfile', 'xor', 'requires', 'desc',
178+
'nohash', 'argstr', 'position', 'mandatory',
179+
'copyfile', 'usedefault', 'sep', 'hash_files',
180+
'deprecated', 'default', 'min_ver', 'max_ver',
181+
'name_source', 'units']
182+
in_built = ['type', 'copy', 'parent', 'instance_handler']
183+
bad_specs = []
184+
for c in classes:
185+
__import__(uri)
186+
try:
187+
with warnings.catch_warnings():
188+
warnings.simplefilter("ignore")
189+
classinst = sys.modules[uri].__dict__[c]
190+
except Exception as inst:
191+
print inst
192+
continue
193+
194+
if not issubclass(classinst, BaseInterface):
195+
continue
196+
197+
for traitname, trait in classinst.input_spec().traits(transient=None).items():
198+
for key in trait.__dict__:
199+
if key in in_built:
200+
continue
201+
if key not in allowed_keys + classinst._additional_metadata:
202+
bad_specs.append([uri, c, traitname, key])
203+
if not classinst.output_spec:
204+
continue
205+
for traitname, trait in classinst.output_spec().traits(transient=None).items():
206+
for key in trait.__dict__:
207+
if key in in_built:
208+
continue
209+
if key not in allowed_keys + classinst._additional_metadata:
210+
bad_specs.append([uri, c, traitname, key])
211+
return bad_specs
212+
213+
214+
def _survives_exclude(self, matchstr, match_type):
215+
''' Returns True if *matchstr* does not match patterns
216+
217+
``self.package_name`` removed from front of string if present
218+
219+
Examples
220+
--------
221+
>>> dw = ApiDocWriter('sphinx')
222+
>>> dw._survives_exclude('sphinx.okpkg', 'package')
223+
True
224+
>>> dw.package_skip_patterns.append('^\\.badpkg$')
225+
>>> dw._survives_exclude('sphinx.badpkg', 'package')
226+
False
227+
>>> dw._survives_exclude('sphinx.badpkg', 'module')
228+
True
229+
>>> dw._survives_exclude('sphinx.badmod', 'module')
230+
True
231+
>>> dw.module_skip_patterns.append('^\\.badmod$')
232+
>>> dw._survives_exclude('sphinx.badmod', 'module')
233+
False
234+
'''
235+
if match_type == 'module':
236+
patterns = self.module_skip_patterns
237+
elif match_type == 'package':
238+
patterns = self.package_skip_patterns
239+
elif match_type == 'class':
240+
patterns = self.class_skip_patterns
241+
else:
242+
raise ValueError('Cannot interpret match type "%s"'
243+
% match_type)
244+
# Match to URI without package name
245+
L = len(self.package_name)
246+
if matchstr[:L] == self.package_name:
247+
matchstr = matchstr[L:]
248+
for pat in patterns:
249+
try:
250+
pat.search
251+
except AttributeError:
252+
pat = re.compile(pat)
253+
if pat.search(matchstr):
254+
return False
255+
return True
256+
257+
def discover_modules(self):
258+
''' Return module sequence discovered from ``self.package_name``
259+
260+
261+
Parameters
262+
----------
263+
None
264+
265+
Returns
266+
-------
267+
mods : sequence
268+
Sequence of module names within ``self.package_name``
269+
270+
Examples
271+
--------
272+
'''
273+
modules = [self.package_name]
274+
# raw directory parsing
275+
for dirpath, dirnames, filenames in os.walk(self.root_path):
276+
# Check directory names for packages
277+
root_uri = self._path2uri(os.path.join(self.root_path,
278+
dirpath))
279+
for dirname in dirnames[:]: # copy list - we modify inplace
280+
package_uri = '.'.join((root_uri, dirname))
281+
if (self._uri2path(package_uri) and
282+
self._survives_exclude(package_uri, 'package')):
283+
modules.append(package_uri)
284+
else:
285+
dirnames.remove(dirname)
286+
# Check filenames for modules
287+
for filename in filenames:
288+
module_name = filename[:-3]
289+
module_uri = '.'.join((root_uri, module_name))
290+
if (self._uri2path(module_uri) and
291+
self._survives_exclude(module_uri, 'module')):
292+
modules.append(module_uri)
293+
return sorted(modules)
294+
295+
def check_modules(self):
296+
# write the list
297+
modules = self.discover_modules()
298+
checked_modules = []
299+
for m in modules:
300+
bad_specs = self.test_specs(m)
301+
if bad_specs:
302+
checked_modules.extend(bad_specs)
303+
for bad_spec in checked_modules:
304+
print ':'.join(bad_spec)
305+
306+
if __name__ == "__main__":
307+
package = 'nipype'
308+
ic = InterfaceChecker(package)
309+
# Packages that should not be included in generated API docs.
310+
ic.package_skip_patterns += ['\.external$',
311+
'\.fixes$',
312+
'\.utils$',
313+
'\.pipeline',
314+
'\.testing',
315+
'\.caching',
316+
'\.workflows',
317+
]
318+
"""
319+
# Modules that should not be included in generated API docs.
320+
ic.module_skip_patterns += ['\.version$',
321+
'\.interfaces\.base$',
322+
'\.interfaces\.matlab$',
323+
'\.interfaces\.rest$',
324+
'\.interfaces\.pymvpa$',
325+
'\.interfaces\.slicer\.generate_classes$',
326+
'\.interfaces\.spm\.base$',
327+
'\.interfaces\.traits',
328+
'\.pipeline\.alloy$',
329+
'\.pipeline\.s3_node_wrapper$',
330+
'.\testing',
331+
]
332+
ic.class_skip_patterns += ['AFNI',
333+
'ANTS',
334+
'FSL',
335+
'FS',
336+
'Info',
337+
'^SPM',
338+
'Tester',
339+
'Spec$',
340+
'Numpy',
341+
'NipypeTester',
342+
]
343+
"""
344+
ic.check_modules()

0 commit comments

Comments
 (0)