Skip to content

Commit c181c93

Browse files
committed
ENH: Parse Boutiques input structure
1 parent f31416a commit c181c93

File tree

1 file changed

+173
-0
lines changed

1 file changed

+173
-0
lines changed

nipype/interfaces/base/boutiques.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# -*- coding: utf-8 -*-
2+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
3+
# vi: set ft=python sts=4 ts=4 sw=4 et:
4+
import os
5+
import json
6+
from collections import defaultdict
7+
8+
from ... import logging
9+
from ..base import (
10+
traits, File, Str, isdefined, Undefined,
11+
DynamicTraitedSpec, CommandLine)
12+
13+
iflogger = logging.getLogger('nipype.interface')
14+
15+
16+
class BoutiqueInterface(CommandLine):
17+
"""Convert Boutique specification to Nipype interface
18+
19+
"""
20+
21+
input_spec = DynamicTraitedSpec
22+
output_spec = DynamicTraitedSpec
23+
24+
# Subclasses may override the trait_map to provide better traits
25+
# for various types
26+
trait_map = {
27+
'File': File,
28+
'String': Str,
29+
'Number': traits.Float,
30+
'Flag': traits.Bool,
31+
}
32+
33+
def __init__(self, boutique_spec, **inputs):
34+
if os.path.exists(boutique_spec):
35+
with open(boutique_spec, 'r') as fobj:
36+
boutique_spec = json.load(fobj)
37+
38+
self.boutique_spec = boutique_spec
39+
split_cmd = boutique_spec['command-line'].split(None, 1)
40+
self._cmd = split_cmd[0]
41+
self._argspec = split_cmd[1] if len(split_cmd) > 1 else None
42+
43+
super().__init__()
44+
45+
self._xors = defaultdict(list)
46+
self._requires = defaultdict(list)
47+
self._one_required = defaultdict(list)
48+
self._load_groups(boutique_spec.get('groups', []))
49+
50+
self._populate_input_spec(boutique_spec.get('inputs', []))
51+
#self._populate_output_spec(boutique_spec.get('output-files', []))
52+
self.inputs.trait_set(trait_change_notify=False, **inputs)
53+
54+
def _load_groups(self, groups):
55+
for group in groups:
56+
members = group['members']
57+
if group['all-or-none']:
58+
for member in members:
59+
self._requires[member].extend(members)
60+
elif group['mutually-exclusive']:
61+
for member in members:
62+
self._xors[member].extend(members)
63+
elif group['one-is-required']:
64+
for member in members:
65+
self._one_required[member].extend(members)
66+
67+
def _populate_input_spec(self, input_list):
68+
value_keys = {}
69+
undefined_traits = {}
70+
for input_dict in input_list:
71+
trait_name = input_dict['id']
72+
args = []
73+
metadata = {}
74+
75+
# Establish trait type
76+
77+
typestr = input_dict['type']
78+
if 'value-choices' in input_dict:
79+
ttype = traits.Enum
80+
args = input_dict['value-choices']
81+
elif typestr == 'Number' and ('maximum' in input_dict or
82+
'minimum' in input_dict):
83+
ttype = traits.Range
84+
elif typestr == 'Number' and input_dict.get('integer'):
85+
ttype = traits.Int
86+
else:
87+
ttype = self.trait_map[typestr]
88+
89+
if 'default-value' in input_dict:
90+
nipype_key = 'default_value'
91+
default_value = input_dict['default-value']
92+
if ttype in (traits.Range, traits.List):
93+
nipype_key = 'value'
94+
elif ttype is traits.Enum:
95+
args.remove(default_value)
96+
args.insert(0, default_value)
97+
98+
if ttype is not traits.Enum:
99+
metadata[nipype_key] = default_value
100+
metadata['usedefault'] = True
101+
102+
if input_dict.get('list'):
103+
if args:
104+
ttype = ttype(*args)
105+
args = []
106+
metadata['trait'] = ttype
107+
ttype = traits.List
108+
109+
# Complete metadata
110+
if 'command-line-flag' in input_dict:
111+
argstr = input_dict['command-line-flag']
112+
if typestr != 'Flag':
113+
argstr += input_dict.get('command-line-flag-separator',
114+
' ')
115+
argstr += '%s' # May be insufficient for some
116+
metadata['argstr'] = argstr
117+
118+
direct_mappings = {
119+
# Boutiques: Nipype
120+
'description': 'desc',
121+
'disables-inputs': 'xor',
122+
'exclusive-maximum': 'exclude_high',
123+
'exclusive-minimum': 'exclude_low',
124+
'max-list-entries': 'maxlen',
125+
'maximum': 'high',
126+
'min-list-entries': 'minlen',
127+
'minimum': 'low',
128+
'requires-inputs': 'requires',
129+
}
130+
131+
for boutique_key, nipype_key in direct_mappings.items():
132+
if boutique_key in input_dict:
133+
metadata[nipype_key] = input_dict[boutique_key]
134+
135+
# Unsupported:
136+
# * uses-absolute-path
137+
# * value-disables
138+
# * value-requires
139+
140+
metadata['mandatory'] = not input_dict.get('optional', False)
141+
142+
# This is a little weird and hacky, and could probably be done
143+
# better.
144+
if trait_name in self._requires:
145+
metadata.setdefault('requires',
146+
[]).extend(self._requires[trait_name])
147+
if trait_name in self._xors:
148+
metadata.setdefault('xor',
149+
[]).extend(self._xor[trait_name])
150+
151+
trait = ttype(*args, **metadata)
152+
self.inputs.add_trait(trait_name, trait)
153+
if not trait.usedefault:
154+
undefined_traits[trait_name] = Undefined
155+
value_keys[input_dict['value-key']] = trait_name
156+
157+
self.inputs.trait_set(trait_change_notify=False,
158+
**undefined_traits)
159+
self.value_keys = value_keys
160+
161+
def cmdline(self):
162+
args = self._argspec
163+
inputs = self.inputs.trait_get()
164+
for valkey, name in self.value_keys.items():
165+
spec = self.inputs.traits()[name]
166+
value = inputs[name]
167+
168+
if not isdefined(value):
169+
value = ''
170+
elif spec.argstr:
171+
value = self._format_arg(name, spec, value)
172+
args = args.replace(valkey, value)
173+
return self._cmd + ' ' + args

0 commit comments

Comments
 (0)