junifer/docs/extending/dependencies.rst
2026-03-31 11:37:03 +02:00

152 lines
5.4 KiB
ReStructuredText

.. include:: ../links.inc
.. _specifying_dependencies:
Specifying Dependencies
=======================
This section describes how you can tackle different situations when writing
your custom Marker and / or Preprocessor and take care of the dependencies for
them.
You might have already come across listing out dependencies for your
:ref:`custom Markers <extending_markers>` and / or
:ref:`custom Preprocessors <extending_preprocessors>`, if not, check them out
first. If you have already gone through them, you are already familiar with using
class attribute ``_DEPENDENCIES`` to keep track of its dependencies. ``junifer``
is a bit more sophisticated about them and we will see here how you can make the
best use of them.
.. _component_dependencies:
Handling dependencies that come as Python packages
--------------------------------------------------
You have already seen this case handled by having a class attribute
``_DEPENDENCIES`` whose value is a set of all the package names that the
component depends on. For example, for :class:`.RSSETSMarker`, we have:
.. code-block:: python
_DEPENDENCIES: ClassVar[Dependencies] = {"nilearn"}
The type annotation is for documentation and static type checking purposes.
Although not required, we highly recommend you use them, your future self
and others who use it will thank you.
.. _component_external_dependencies:
Handling external dependencies from toolboxes
---------------------------------------------
You can also specify dependencies of external toolboxes like AFNI, FSL and ANTs,
by having a class attribute like so:
.. code-block:: python
_EXT_DEPENDENCIES: ClassVar[ExternalDependencies] = [
{
"name": ExtDep.AFNI,
"commands": ["3dReHo", "3dAFNItoNIFTI"],
},
]
The above example is taken from the class which computes regional homogeneity
(ReHo) using AFNI. The general pattern is that you need to have the value of
``_EXT_DEPENDENCIES`` as a list of dictionary with two keys:
* ``name`` (:enum:`.ExtDep`) : name of the toolbox
* ``commands`` (list of str) : actual names of the commands you need to use
This is simple but powerful as we will see in the following sub-sections.
.. _component_conditional_dependencies:
Handling conditional dependencies
---------------------------------
You might encounter situations where your Marker or Preprocessor needs to have
option for the user to either use a dependency that comes as a package or
use a dependency that relies on external toolboxes. With the foundation we laid
above, it is really simple to solve it while having validation before running
and letting the user know if some dependency is missing.
Let's look at an actual implementation, in this case :class:`.SpaceWarper`, so
that it shows the problem a bit better and how we solve it:
.. code-block:: python
class SpaceWarper(BasePreprocessor):
# docstring
_CONDITIONAL_DEPENDENCIES: ClassVar[ConditionalDependencies] = [
{
"using": "fsl",
"depends_on": [FSLWarper],
},
{
"using": "ants",
"depends_on": [ANTSWarper],
},
{
"using": "auto",
"depends_on": [FSLWarper, ANTsWarper],
},
]
using: str
reference: str
on: List[DataType]
Here, you see a new class attribute ``_CONDITIONAL_DEPENDENCIES`` which is a
list of dictionaries with two keys:
* ``using`` (str) : lowercased name of the toolbox
* ``depends_on`` (list of objects) : list of classes which \
implements the particular tool's use
It is mandatory to have the ``using`` positional argument in the constructor in
this case as the validation starts with this and moves further. It is also
mandatory to only allow the value of ``using`` argument to be one of them
specified in the ``using`` key of ``_CONDITIONAL_DEPENDENCIES`` entries.
For some cases, like we have here, it might be worth to have ``"auto"`` for
``"using"`` which eases the choice of a particular tool by the user and
instead lets ``junifer`` do it automatically. In that case, ``"depends_on"``
needs to be a list of the tool implementation classes. This also requires the
user to have all the tools in the ``PATH``.
For brevity, we only show the ``FSLWarper`` here but ``ANTsWarper`` looks very
similar. ``FSLWarper`` looks like this (only the relevant part is shown here):
.. code-block:: python
class FSLWarper:
# docstring
_EXT_DEPENDENCIES: ClassVar[ExternalDependencies] = [
{
"name": ExtDep.FSL,
"commands": ["flirt", "applywarp"],
},
]
_DEPENDENCIES: ClassVar[Dependencies] = {"numpy", "nibabel"}
def preprocess(
self,
input: Dict[str, Any],
extra_input: Dict[str, Any],
) -> Dict[str, Any]:
# implementation
...
Here you can see the familiar ``_DEPENDENCIES`` and ``_EXT_DEPENDENCIES`` class
attributes. The validation process starts by looking up the ``using`` value of
the ``_CONDITIONAL_DEPENDENCIES`` entries and then retrieves the object pointed
by ``depends_on``. After that, the ``_DEPENDENCIES`` and ``_EXT_DEPENDENCIES``
class attributes are checked.
This might be a bit too much to get it right away so feel free to check the code
for a better understanding. You can also check ``ALFFBase`` for a Marker
having this pattern.