Skip to content

Commit

Permalink
Interface creators (#150)
Browse files Browse the repository at this point in the history
* [BUG] inside unit_tests workflow

* [DOC] runner help

* Creating entry-points for the project

* [DOC] command line tools

* [DOC] command line tools

* Adding a tester command line tool

* [DATALAD] change results url

* Runner configuration

* Remove dir func in

* Runner always stops on first crash

* [TEST][helpers] not failing test if correlation under threshold

* [DOC] narps_open.core.common

* [ENH][TEST] narps_open.core.nodes module

* [ENH][DOC] node generators in core module

* [PEP8][SPELL] node generators in core module

* Add a remove parent dir node generator

* [REFAC][DOC] Creators at interface level instead of nodes

* Creating an interface factory

* Remove node modules after merging
  • Loading branch information
bclenet authored Jan 26, 2024
1 parent 58afd4c commit 59bb46c
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 120 deletions.
18 changes: 10 additions & 8 deletions docs/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,27 +124,29 @@ This module contains a set of functions dedicated to computations on images.
# Get dimensions of voxels along x, y, and z in mm (returns e.g.: [1.0, 1.0, 1.0]).
get_voxel_dimensions('/path/to/the/image.nii.gz')
```
## narps_open.core.interfaces

## narps_open.core.nodes
This module contains a set of interface creators inheriting form the `narps_open.core.interfaces.InterfaceCreator` abstract class.
These are responsible for creating nipype `Interface` objects (for now, only `Function` interfaces are used, with functions defined in the `narps_open.core.common` module) to be used inside pipeline code.
The module also provide an `InterfaceFactory` to easily create the available interface, without knowing which creator is responsible for that.

This module contains a set of node creators inheriting form the `narps_open.core.nodes.NodeCreator` abstract class.
These are responsible for creating nipype `Node` objects (for now, only based on the `Function` interface, with functions defined in the `narps_open.core.common` module) to be used inside pipeline code. This allows to factorize code, hence making code simpler to read inside pipeline definition.
The overall allows to factorize code, hence making it simpler to read inside pipeline definition.

Here is an example how to use the node creators :
Here is an example how to use the interface creators :

```python
from narps_open.core.nodes import RemoveDirectoryNodeCreator, RemoveFileNodeCreator
from narps_open.core.interfaces import InterfaceFactory

# Create a Node to remove a directory
remove_smoothed = RemoveDirectoryNodeCreator.create_node('remove_smoothed')
remove_smoothed = Node(InterfaceFactory.create('remove_directory'), name = 'remove_smoothed')
remove_smoothed.inputs.directory_name = 'my_directory'

# Create a Node to remove a file
remove_gunzip = RemoveFileNodeCreator.create_node('remove_gunzip')
remove_gunzip = Node(InterfaceFactory.create('remove_file'), name = 'remove_gunzip')
remove_gunzip.inputs.file_name = 'my_file'
```

For your information, this is how an equivalent code would look like without node creators.
For your information, this is how an equivalent code would look like without interface creators.

```python
from nipype import Node
Expand Down
15 changes: 15 additions & 0 deletions narps_open/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ def remove_directory(_, directory_name: str) -> None:

rmtree(directory_name, ignore_errors = True)

def remove_parent_directory(_, file_name: str) -> None:
"""
Fully remove directory generated by a Node, once it is not needed anymore.
This function is meant to be used in a Nipype Function Node.
Parameters:
- _: input only used for triggering the Node
- file_name: str, a single absolute path of a file : its parent directory is to remove
"""
# This import must stay inside the function, as required by Nipype
from pathlib import Path
from shutil import rmtree

rmtree(Path(file_name).parent.absolute(), ignore_errors = True)

def elements_in_string(input_str: str, elements: list) -> str: #| None:
"""
Return input_str if it contains one element of the elements list.
Expand Down
74 changes: 74 additions & 0 deletions narps_open/core/interfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/python
# coding: utf-8

""" Generate useful and recurrent interfaces to write pipelines """

from abc import ABC, abstractmethod

from nipype.interfaces.base.core import Interface
from nipype.interfaces.utility import Function

from narps_open.core.common import remove_directory, remove_parent_directory, remove_file

class InterfaceCreator(ABC):
""" An abstract class to shape what interface creators must provide """

@staticmethod
@abstractmethod
def create_interface() -> Interface:
""" Return a new interface (to be defined by specialized classes) """

class RemoveParentDirectoryInterfaceCreator(InterfaceCreator):
""" An interface creator that provides an interface allowing to remove a directory,
given one of its child's file name.
"""

@staticmethod
def create_interface() -> Function:
return Function(
function = remove_parent_directory,
input_names = ['_', 'file_name'],
output_names = []
)

class RemoveDirectoryInterfaceCreator(InterfaceCreator):
""" An interface creator that provides an interface allowing to remove a directory """

@staticmethod
def create_interface() -> Function:
return Function(
function = remove_directory,
input_names = ['_', 'directory_name'],
output_names = []
)

class RemoveFileInterfaceCreator(InterfaceCreator):
""" An interface creator that provides an interface allowing to remove a file """

@staticmethod
def create_interface() -> Function:
return Function(
function = remove_file,
input_names = ['_', 'file_name'],
output_names = []
)

class InterfaceFactory():
""" A class to generate interfaces from narps_open.core functions """

# A list of creators, one for each function
creators = {
'remove_directory' : RemoveDirectoryInterfaceCreator,
'remove_parent_directory' : RemoveParentDirectoryInterfaceCreator,
'remove_file' : RemoveFileInterfaceCreator
}

@classmethod
def create(cls, creator_name: str):
""" Return a new Function interface
Arguments :
creator_name, str : the key for the creator to be used
"""
# Actually create the interface, using a creator
creator = cls.creators[creator_name]
return creator.create_interface()
44 changes: 0 additions & 44 deletions narps_open/core/nodes.py

This file was deleted.

27 changes: 27 additions & 0 deletions tests/core/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,33 @@ def test_remove_directory(remove_test_dir):
# Check file is removed
assert not exists(test_dir_path)

@staticmethod
@mark.unit_test
def test_remove_parent_directory(remove_test_dir):
""" Test the remove_parent_directory function """

# Create a single inside dir tree
dir_path = abspath(join(TEMPORARY_DIR, 'dir_1', 'dir_2'))
makedirs(dir_path)
file_path = abspath(join(TEMPORARY_DIR, 'dir_1', 'dir_2', 'file1.txt'))
Path(file_path).touch()

# Check file exist
assert exists(file_path)

# Create a Nipype Node using remove_files
test_remove_dir_node = Node(Function(
function = co.remove_parent_directory,
input_names = ['_', 'file_name'],
output_names = []
), name = 'test_remove_dir_node')
test_remove_dir_node.inputs._ = ''
test_remove_dir_node.inputs.file_name = file_path
test_remove_dir_node.run()

# Check file is removed
assert not exists(dir_path)

@staticmethod
@mark.unit_test
def test_node_elements_in_string():
Expand Down
103 changes: 103 additions & 0 deletions tests/core/test_interfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/python
# coding: utf-8

""" Tests of the 'narps_open.core.interfaces' module.
Launch this test with PyTest
Usage:
======
pytest -q test_interfaces.py
pytest -q test_interfaces.py -k <selected_test>
"""

from pytest import mark, raises

from nipype.interfaces.base.core import Interface
from nipype.interfaces.utility import Select, Function

from narps_open.core import interfaces

class ValidNC(interfaces.InterfaceCreator):
""" A valid implementation of a InterfaceCreator, for test purposes """

@staticmethod
def create_interface() -> Interface:
""" Return a Interface, as expected """
return Select()

class TestInterfaceCreator:
""" A class that contains all the unit tests for the InterfaceCreator class."""

@staticmethod
@mark.unit_test
def test_create_interface():
""" Test the create_interface method """

test_interface = ValidNC.create_interface()
assert isinstance(test_interface, Select)

class TestRemoveParentDirectoryInterfaceCreator:
""" A class that contains all the unit tests for the
RemoveParentDirectoryInterfaceCreator class.
"""

@staticmethod
@mark.unit_test
def test_create_interface():
""" Test the create_interface method """

test_interface = interfaces.RemoveParentDirectoryInterfaceCreator.create_interface()
assert isinstance(test_interface, Function)
inputs = str(test_interface.inputs)
assert '_ = <undefined>' in inputs
assert 'file_name = <undefined>' in inputs
assert 'function_str = def remove_parent_directory(_, file_name: str) -> None:' in inputs

class TestRemoveDirectoryInterfaceCreator:
""" A class that contains all the unit tests for the RemoveDirectoryInterfaceCreator class."""

@staticmethod
@mark.unit_test
def test_create_interface():
""" Test the create_interface method """

test_interface = interfaces.RemoveDirectoryInterfaceCreator.create_interface()
assert isinstance(test_interface, Function)
inputs = str(test_interface.inputs)
assert '_ = <undefined>' in inputs
assert 'directory_name = <undefined>' in inputs
assert 'function_str = def remove_directory(_, directory_name: str) -> None:' in inputs

class TestRemoveFileInterfaceCreator:
""" A class that contains all the unit tests for the RemoveFileInterfaceCreator class."""

@staticmethod
@mark.unit_test
def test_create_interface():
""" Test the create_interface method """

test_interface = interfaces.RemoveFileInterfaceCreator.create_interface()
assert isinstance(test_interface, Function)
inputs = str(test_interface.inputs)
assert '_ = <undefined>' in inputs
assert 'file_name = <undefined>' in inputs
assert 'function_str = def remove_file(_, file_name: str) -> None:' in inputs

class TestInterfaceFactory:
""" A class that contains all the unit tests for the InterfaceFactory class."""

@staticmethod
@mark.unit_test
def test_create():
""" Test the create method """

with raises(KeyError):
interfaces.InterfaceFactory.create('fake_function')

test_interface = interfaces.InterfaceFactory.create('remove_file')
assert isinstance(test_interface, Function)
inputs = str(test_interface.inputs)
assert '_ = <undefined>' in inputs
assert 'file_name = <undefined>' in inputs
assert 'function_str = def remove_file(_, file_name: str) -> None:' in inputs
68 changes: 0 additions & 68 deletions tests/core/test_nodes.py

This file was deleted.

0 comments on commit 59bb46c

Please sign in to comment.