Skip to content

Commit

Permalink
Support main function return data
Browse files Browse the repository at this point in the history
  • Loading branch information
jennydaman committed Dec 23, 2022
1 parent 5e9a1fd commit 88821c4
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 36 deletions.
53 changes: 33 additions & 20 deletions chris_plugin/chris_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Callable, Optional

from chris_plugin._registration import register, PluginDetails
from chris_plugin.main_function import MainFunction, is_plugin_main, is_fs
from chris_plugin.main_function import MainFunction, is_plugin_main, is_fs, T
from chris_plugin.types import ChrisPluginType


Expand Down Expand Up @@ -62,6 +62,7 @@ def chris_plugin(
max_cpu_limit: str = "",
min_gpu_limit: int = 0,
max_gpu_limit: int = 0,
singleton: bool = True
):
"""
Creates a decorator which identifies a *ChRIS* plugin main function
Expand Down Expand Up @@ -151,6 +152,13 @@ def main(options, outputdir):
Parameters
----------
main: Callable
The main function of this *ChRIS* plugin.
It either accepts (`argparse.Namespace`, `pathlib.Path`) for *fs*-type plugins,
or (`argparse.Namespace`, `pathlib.Path`, `pathlib.Path`) for *ds*-type plugins.
Its return type can be anything, but it's recommended that it returns `None` or
perhaps `int` (implementing a C convention where `main` returns the exit code of
your program: 0 -> success, 1 or anything else -> failure).
parser : argparse.ArgumentParser
A parser defining the arguments of this *ChRIS* plugin.
The parser must only define arguments which satisfy the
Expand Down Expand Up @@ -186,9 +194,13 @@ def main(options, outputdir):
0: GPU is disabled. If min_gpu_limit > 1, GPU is enabled.
max_gpu_limit: int
maximum number of GPUs the plugin may use
singleton: bool
Indicates whether to register the given main function to a global mutable
variable so that it can be located by the `chris_plugin_info` command.
Used for internal testing, set `singleton=False`.
"""

def wrap(main: MainFunction) -> Callable[[], None]:
def wrap(main: MainFunction) -> Callable[[], T]:
nonlocal parser
if parser is None:
parser = argparse.ArgumentParser()
Expand All @@ -209,26 +221,27 @@ def wrap(main: MainFunction) -> Callable[[], None]:
parser.add_argument("inputdir", help="directory containing input files")
parser.add_argument("outputdir", help="directory containing output files")

register(
PluginDetails(
parser=parser,
type=verified_type,
category=category,
icon=icon,
title=title,
min_number_of_workers=min_number_of_workers,
max_number_of_workers=max_number_of_workers,
min_memory_limit=min_memory_limit,
max_memory_limit=max_memory_limit,
min_cpu_limit=min_cpu_limit,
max_cpu_limit=max_cpu_limit,
min_gpu_limit=min_gpu_limit,
max_gpu_limit=max_gpu_limit,
if singleton:
register(
PluginDetails(
parser=parser,
type=verified_type,
category=category,
icon=icon,
title=title,
min_number_of_workers=min_number_of_workers,
max_number_of_workers=max_number_of_workers,
min_memory_limit=min_memory_limit,
max_memory_limit=max_memory_limit,
min_cpu_limit=min_cpu_limit,
max_cpu_limit=max_cpu_limit,
min_gpu_limit=min_gpu_limit,
max_gpu_limit=max_gpu_limit,
)
)
)

@functools.wraps(main)
def wrapper(*args):
def wrapper(*args) -> T:
if args:
options, inputdir, outputdir = _call_from_python(args)
else:
Expand All @@ -248,7 +261,7 @@ def wrapper(*args):

input_path = Path(inputdir)
_check_is_dir(input_path)
main(options, input_path, output_path)
return main(options, input_path, output_path)

return wrapper

Expand Down
11 changes: 5 additions & 6 deletions chris_plugin/main_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@

from pathlib import Path
from argparse import Namespace
from typing import Union, Callable, Tuple
from typing import Union, Callable, Tuple, TypeVar
import inspect
from inspect import Signature, Parameter

FsMainFunction = Callable[[Namespace, Path], None]
DsMainFunction = Callable[[Namespace, Path, Path], None]
MainFunction = Union[FsMainFunction, DsMainFunction]
T = TypeVar('T')
FsMainFunction = Callable[[Namespace, Path], T]
DsMainFunction = Callable[[Namespace, Path, Path], T]
MainFunction = Union[FsMainFunction[T], DsMainFunction[T]]


def get_function_params(_s: Signature) -> Tuple[type, ...]:
Expand Down Expand Up @@ -42,8 +43,6 @@ def is_plugin_main(_f: Callable) -> bool: # -> TypeGuard[MainFunction]:
raise ValueError(
"A ChRIS plugin's data directory arguments " "must have type pathlib.Path"
)
if s.return_annotation is not None and s.return_annotation is not Signature.empty:
raise ValueError("A ChRIS plugin's main function must be void")
return True


Expand Down
9 changes: 0 additions & 9 deletions chris_plugin/tests/test_function_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,6 @@ def bad_path_type2(a: Namespace, b: Path, c: str):
is_plugin_main(example)


def test_bad_return_type():
def bad_return_type(a: Namespace, b: Path) -> int:
pass

em = "A ChRIS plugin's main function must be void"
with pytest.raises(ValueError, match=em):
is_plugin_main(bad_return_type)


def test_is_good_main():
assert is_plugin_main(examples.ok_ds1)
assert is_plugin_main(examples.ok_ds2)
Expand Down
40 changes: 40 additions & 0 deletions chris_plugin/tests/test_main_return.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from argparse import Namespace
from pathlib import Path

import pytest

from chris_plugin import chris_plugin


@chris_plugin(singleton=False)
def __returns_none_fs(_options, _path) -> None:
pass


@chris_plugin(singleton=False)
def __returns_int_fs(_options, _path) -> int:
return 400


@chris_plugin(singleton=False)
def __returns_none_ds(_options, _p1, _p2) -> None:
pass


@chris_plugin(singleton=False)
def __returns_int_ds(_options, _p1, _p2) -> int:
return 500


_options = Namespace()
_path = Path('/tmp')


@pytest.mark.parametrize('result, expected', [
(__returns_none_fs(_options, _path), None),
(__returns_int_fs(_options, _path), 400),
(__returns_none_ds(_options, _path, _path), None),
(__returns_int_ds(_options, _path, _path), 500),
])
def test_main_return(result, expected):
...
11 changes: 10 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setup(
name="chris_plugin",
version="0.1.1-2",
version="0.1.2",
packages=["chris_plugin"],
url="https://github.com/FNNDSC/chris_plugin",
project_urls={
Expand All @@ -24,12 +24,21 @@
long_description_content_type="text/markdown",
python_requires=">= 3.8",
install_requires=['importlib-metadata; python_version<"3.10"'],
extras_require={
'none': [],
'dev': [
'pytest~=7.2',
'pytest-mock~=3.10'
]
},
entry_points={
"console_scripts": ["chris_plugin_info = chris_plugin.chris_plugin_info:main"]
},
classifiers=[
"Development Status :: 1 - Planning",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Software Development",
"Topic :: Scientific/Engineering",
Expand Down

0 comments on commit 88821c4

Please sign in to comment.