|
| 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