Skip to content

Commit 68a534c

Browse files
authored
Merge pull request #3 from djarecka/converter_cli
Converter cli
2 parents 4f45343 + 931b9b7 commit 68a534c

File tree

3 files changed

+87
-32
lines changed

3 files changed

+87
-32
lines changed

README.md

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,43 +24,56 @@ pip install /path/to/pydra-fsl/
2424
`FSLConverter` class (from `tools/converter.py`) requires three parts of information:
2525

2626
- Nipype spec: converter loads nipype interface and reads `_cmd`, `input_spec` and `output_spec`
27-
- yml file with additional spec: `specs/fsl_conv_params.yml` contains additional spec that are written based
27+
- yml file with additional spec: `specs/fsl_{module_name}_params.yml` contains additional spec that are written based
2828
on additional functions from nipype (including `list_outputs`), each interface can have the following fields:
29-
- inputs_metadata: additional fields for metadata for fields from input_spec,
30-
e.g., used for `FAST` to set default for `number_classes`
29+
- inputs_metadata: additional metadata for fields from input_spec
30+
(it will be included in `metadata` in pydra spec),
31+
e.g., used in `specs/fsl_preprocess_params.yml` for `FAST` to set default value for `number_classes`
3132
(it's not part of nipype's spec, but it's set in `list_output`)
3233

33-
- output_requirements: providing required field for the output to be created,
34+
- output_requirements: providing required fields for the output to be created,
3435
taken from `list_output` structure;
35-
it's a part of the `requires` field in metadata
36+
it's a part of the `requires` field in metadata in pydra spec
3637

3738
- output_templates: providing template to create the output file name,
3839
taken from `list_output` structure;
3940
it is set as `output_file_template` in metadata
4041

4142
- output_callables: providing function name that should be used to gather output,
4243
based on the `list_output` structure and used only for `FAST`;
43-
it is set as `output_file_template` in metadata
44+
it is set as `callable` in metadata
45+
46+
- tests_inputs, tests_outputs: specification for tests,
47+
the fields should have the same length and each element should contain
48+
the input fields values and list of the expected output fields names
49+
50+
- doctests: specification for doctest,
51+
should include values for input fields and the expected `cmdline`
4452

4553
- python file with functions used as callables to gather the outputs:
4654
`specs/callables.py` should contain all the functions from `output_callables`;
4755
the source code of the functions is read and written again in the pydra interface file
4856

4957

50-
### How to convert
58+
### How to use the convert
5159

52-
Currently, the converter is temporarly used in a pytest test, so can be run as `pytest tools/converter.py`
53-
in order to run `test_convert_file` (other tests should be skipped).
54-
The tests will create a converter for interfaces names provided in the
55-
`parametrize` decorator and for each interface `pydra_specs` will be run.
56-
The method creates file with the pydra task in the `preprocess` directory,
57-
and two tests in the `preprocess/tests` directory.
60+
The converter can be used by running:
61+
62+
python tools/converter.py --interface_name <name of teh interface> --module_name <module_name>
63+
64+
The pydra task will be created and saved in `pydra/tasks/fsl/{module_name}/{interface_name}.py`.
65+
Note, that the spec file has to be present for the specific module name in order to run the converter.
66+
If no `interface_name` is provided, the default value `all` will be used
67+
and the converter will be run for all interfaces from the spec file.
5868

5969
Tests are written based on the fields from the yml file:
6070
`tests_inputs` and `tests_outputs` (the lengths should be the same).
6171
One test, `test_specs_*` checks only the correctness of the spec based
6272
on the `test_inputs/outputs` pairs, i.e. predicts which output fields
6373
should be created based on the list of the input fields.
64-
The second test, `test_run_*` should run teh interfaces, this is temporary,
65-
should be removed from the final repo.
74+
The second test, `test_run_*` should run the interfaces
75+
(TODO: this is temporary, should be removed from the final repo).
76+
Tests can be run using `pytest`:
77+
78+
pytest -vs pydra/tasks/fsl/{module_name}/tests
6679

File renamed without changes.

tools/converter.py

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
from nipype.interfaces.base import traits_extension
33
from pydra.engine import specs
44

5-
import os, yaml, black, imp
5+
import os, sys, yaml, black, imp
66
import traits
77
from pathlib import Path
88
import typing as ty
99
import inspect
10-
import pytest
11-
10+
import click
11+
import warnings
12+
import functools
1213

14+
sys.path.append(str(Path(__file__).resolve().parent.parent / 'specs'))
15+
import callables
1316

1417
class FSLConverter:
1518

@@ -31,8 +34,7 @@ class FSLConverter:
3134
]
3235

3336

34-
def __init__(self, interface_name,
35-
interface_spec_file=Path(os.path.dirname(__file__)) / "../specs/fsl_conv_param.yml"):
37+
def __init__(self, interface_name, interface_spec_file):
3638
self.interface_name = interface_name
3739
with interface_spec_file.open() as f:
3840
self.interface_spec = yaml.safe_load(f)[self.interface_name]
@@ -348,7 +350,6 @@ def function_callables(self):
348350
python_functions_spec = Path(os.path.dirname(__file__)) / "../specs/callables.py"
349351
if not python_functions_spec.exists():
350352
raise Exception("specs/callables.py file is needed if output_callables in the spec files")
351-
from specs import callables
352353
fun_str = ""
353354
fun_names = list(set(self.interface_spec["output_callables"].values()))
354355
fun_names.sort()
@@ -416,14 +417,55 @@ def string_formats(self, argstr, name):
416417
return argstr_new
417418

418419

419-
420-
@pytest.mark.parametrize("interface_name",
421-
["BET", "MCFLIRT", "FLIRT", "FNIRT", "ApplyWarp", "SliceTimer",
422-
"SUSAN", "PRELUDE", "FIRST", "FAST"]
423-
)
424-
def test_convert_file(interface_name):
425-
converter = FSLConverter(interface_name=interface_name)
426-
427-
dirname_interf = Path(__file__).parent.parent / "pydra/tasks/fsl/preprocess"
428-
429-
input_spec, output_spec = converter.pydra_specs(write=True, dirname=dirname_interf)
420+
FSL_MODULES = ['aroma', 'dti', 'epi', 'fix', 'maths', 'model', 'possum', 'preprocess', 'utils']
421+
422+
@click.command()
423+
@click.option("-i", "--interface_name", required=True, default="all",
424+
help="name of the interface (name used in Nipype, e.g. BET) or all (default)"
425+
"if all is used all interfaces from the spec file will be created")
426+
@click.option("-m", "--module_name", required=True, help=f"name of the module from the list {FSL_MODULES}")
427+
def create_pydra_spec(interface_name, module_name):
428+
if module_name not in FSL_MODULES:
429+
raise Exception(f"module name {module_name} not available;"
430+
f"should be from the list {FSL_MODULES}")
431+
432+
spec_file = Path(os.path.dirname(__file__)) / f"../specs/fsl_{module_name}_param.yml"
433+
if not spec_file.exists():
434+
raise Exception(f"the specification file doesn't exist for the module {module_name},"
435+
f"create the specification file in {spec_file.parent}")
436+
437+
@functools.lru_cache()
438+
def all_interfaces(module):
439+
nipype_module = getattr(fsl, module)
440+
all_specs = [el for el in dir(nipype_module) if "InputSpec" in el]
441+
all_interf = [el.replace("InputSpec", "") for el in all_specs]
442+
443+
# interfaces in the spec file
444+
with open(spec_file) as f:
445+
spec_interf = yaml.safe_load(f).keys()
446+
447+
if set(all_interf) - set(spec_interf):
448+
warnings.warn(f"some interfaces are not in the spec file: "
449+
f"{set(all_interf) - set(spec_interf)}, "
450+
f"and pydra interfaces will not be created for them")
451+
return spec_interf
452+
453+
if interface_name == "all":
454+
interface_list = all_interfaces(module_name)
455+
elif interface_name in all_interfaces(module_name):
456+
interface_list = [interface_name]
457+
else:
458+
raise Exception(f"interface_name has to be 'all' "
459+
f"or a name from the list {all_interfaces(module_name)}")
460+
461+
dirname_interf = Path(__file__).parent.parent / f"pydra/tasks/fsl/{module_name}"
462+
dirname_interf.mkdir(exist_ok=True)
463+
464+
for interface_el in interface_list:
465+
converter = FSLConverter(
466+
interface_name=interface_el,
467+
interface_spec_file=Path(__file__).parent.parent / "specs/fsl_preprocess_param.yml")
468+
converter.pydra_specs(write=True, dirname=dirname_interf)
469+
470+
if __name__ == '__main__':
471+
create_pydra_spec()

0 commit comments

Comments
 (0)