From e6252eef7c1e2a71b5a71e460267f2f9e04c9331 Mon Sep 17 00:00:00 2001 From: Tom Close Date: Fri, 4 Apr 2025 12:18:57 +1100 Subject: [PATCH 1/9] adapting to 1.0alpha syntax --- .vscode/launch.json | 31 +++ conftest.py | 18 ++ pydra/compose/nipype1/__init__.py | 11 + pydra/compose/nipype1/builder.py | 218 ++++++++++++++++++ .../nipype1/tests/__init__.py | 0 .../compose/nipype1/tests/test_nipype1task.py | 46 ++++ pydra/tasks/nipype1/__init__.py | 13 -- pydra/tasks/nipype1/tests/test_nipype1task.py | 41 ---- pydra/tasks/nipype1/utils.py | 76 ------ pyproject.toml | 20 +- 10 files changed, 336 insertions(+), 138 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 conftest.py create mode 100644 pydra/compose/nipype1/__init__.py create mode 100644 pydra/compose/nipype1/builder.py rename pydra/{tasks => compose}/nipype1/tests/__init__.py (100%) create mode 100644 pydra/compose/nipype1/tests/test_nipype1task.py delete mode 100644 pydra/tasks/nipype1/__init__.py delete mode 100644 pydra/tasks/nipype1/tests/test_nipype1task.py delete mode 100644 pydra/tasks/nipype1/utils.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..97210f6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + }, + { + "name": "Test Config", + "type": "python", + "request": "launch", + "purpose": [ + "debug-test" + ], + "justMyCode": false, + "console": "internalConsole", + "env": { + "_PYTEST_RAISE": "1" + }, + "args": [ + "--capture=no", + ] + }, + ] +} \ No newline at end of file diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..a8d84bf --- /dev/null +++ b/conftest.py @@ -0,0 +1,18 @@ +import os +import typing as ty +import pytest + +# For debugging in IDE's don't catch raised exceptions and let the IDE +# break at it +if os.getenv("_PYTEST_RAISE", "0") != "0": + + @pytest.hookimpl(tryfirst=True) + def pytest_exception_interact(call: pytest.CallInfo[ty.Any]) -> None: + if call.excinfo is not None: + raise call.excinfo.value + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror( + excinfo: pytest.ExceptionInfo[BaseException] + ) -> None: + raise excinfo.value diff --git a/pydra/compose/nipype1/__init__.py b/pydra/compose/nipype1/__init__.py new file mode 100644 index 0000000..26d0f33 --- /dev/null +++ b/pydra/compose/nipype1/__init__.py @@ -0,0 +1,11 @@ +from .builder import ( + Task, + Outputs, + define, + arg, + out, +) +from ._version import __version__ + + +__all__ = ["Task", "Outputs", "define", "arg", "out", "__version__"] diff --git a/pydra/compose/nipype1/builder.py b/pydra/compose/nipype1/builder.py new file mode 100644 index 0000000..1dd3e1b --- /dev/null +++ b/pydra/compose/nipype1/builder.py @@ -0,0 +1,218 @@ +import nipype +import attrs +import typing as ty +from pydra.compose import base +from pydra.compose.base.builder import build_task_class +from pydra.utils.general import task_fields, task_dict +from fileformats.generic import File, Directory, FileSet +import nipype.interfaces.base.traits_extension +from pydra.engine.job import Job +from pydra.utils.typing import is_fileset_or_union + + +__all__ = ["define", "arg", "out", "Task", "Outputs"] + + +class arg(base.Arg): + """Argument of a Python task + + Parameters + ---------- + help: str + A short description of the input field. + default : Any, optional + the default value for the argument + allowed_values: list, optional + List of allowed values for the field. + requires: list, optional + Names of the inputs that are required together with the field. + copy_mode: File.CopyMode, optional + The mode of copying the file, by default it is File.CopyMode.any + copy_collation: File.CopyCollation, optional + The collation of the file, by default it is File.CopyCollation.any + copy_ext_decomp: File.ExtensionDecomposition, optional + The extension decomposition of the file, by default it is + File.ExtensionDecomposition.single + readonly: bool, optional + If True the input field can’t be provided by the user but it aggregates other + input fields (for example the fields with argstr: -o {fldA} {fldB}), by default + it is False + type: type, optional + The type of the field, by default it is Any + name: str, optional + The name of the field, used when specifying a list of fields instead of a mapping + from name to field, by default it is None + """ + + +class out(base.Out): + """Output of a Python task + + Parameters + ---------- + name: str, optional + The name of the field, used when specifying a list of fields instead of a mapping + from name to field, by default it is None + type: type, optional + The type of the field, by default it is Any + help: str, optional + A short description of the input field. + requires: list, optional + Names of the inputs that are required together with the field. + converter: callable, optional + The converter for the field passed through to the attrs.field, by default it is None + validator: callable | iterable[callable], optional + The validator(s) for the field passed through to the attrs.field, by default it is None + position : int + The position of the output in the output list, allows for tuple unpacking of + outputs + """ + + +def define(interface: nipype.interfaces.base.BaseInterface) -> "Task": + """ + Create an interface for a function or a class. + + Parameters + ---------- + wrapped : type | callable | None + The function or class to create an interface for. + inputs : list[str | Arg] | dict[str, Arg | type] | None + The inputs to the function or class. + outputs : list[str | base.Out] | dict[str, base.Out | type] | type | None + The outputs of the function or class. + auto_attribs : bool + Whether to use auto_attribs mode when creating the class. + xor: Sequence[str | None] | Sequence[Sequence[str | None]], optional + Names of args that are exclusive mutually exclusive, which must include + the name of the current field. If this list includes None, then none of the + fields need to be set. + + Returns + ------- + Task + The task class for the Python function + """ + inputs = traitedspec_to_fields( + interface.inputs, arg, skip_fields={"interface", "function_str"} + ) + outputs = traitedspec_to_fields(interface._outputs(), out) + + task_class = build_task_class( + Nipype1Task, + Nipype1Outputs, + inputs, + outputs, + name=type(interface).__name__, + klass=None, + bases=(), + outputs_bases=(), + ) + + task_class._interface = interface + + return task_class + + +class Nipype1Outputs(base.Outputs): + + @classmethod + def _from_job(cls, job: "Job[Nipype1Outputs]") -> ty.Self: + """Collect the outputs of a job from a combination of the provided inputs, + the objects in the output directory, and the stdout and stderr of the process. + + Parameters + ---------- + job : Job[Task] + The job whose outputs are being collected. + outputs_dict : dict[str, ty.Any] + The outputs of the job, as a dictionary + + Returns + ------- + outputs : Outputs + The outputs of the job in dataclass + """ + outputs = super()._from_task(job) + for name, val in job.return_values.items(): + setattr(outputs, name, val) + return outputs + + @classmethod + def _from_task(cls, job: "Job[Nipype1Outputs]") -> ty.Self: + # Added for backwards compatibility + return cls._from_job(job) + + +class Nipype1Task(base.Task): + """Wrap a Nipype 1.x Interface as a Pydra Task + + This utility translates the Nipype 1 input and output specs to + Pydra-style specs, wraps the run command, and exposes the output + in Pydra Task outputs. + + >>> import pytest + >>> from pydra.tasks.nipype1.tests import load_resource + >>> from nipype.interfaces import fsl + >>> if fsl.Info.version() is None: + ... pytest.skip() + >>> img = load_resource('nipype', 'testing/data/tpms_msk.nii.gz') + + >>> from pydra.tasks.nipype1.utils import Nipype1Task + >>> thresh = Nipype1Task(fsl.Threshold()) + >>> thresh.inputs.in_file = img + >>> thresh.inputs.thresh = 0.5 + >>> res = thresh() + >>> res.output.out_file # DOCTEST: +ELLIPSIS + '.../tpms_msk_thresh.nii.gz' + """ + + _task_type = "nipype1" + + def _run(self, job: "Job[Nipype1Task]", rerun: bool = False) -> None: + fields = task_fields(self) + inputs = { + n: v if not isinstance(v, FileSet) else str(v) + for n, v in task_dict(self).items() + if v is not None or fields[n].mandatory + } + node = nipype.Node( + self._interface, base_dir=job.cache_dir, name=type(self).__name__ + ) + node.inputs.trait_set(**inputs) + res = node.run() + job.return_values = res.outputs.get() + + +FieldType = ty.TypeVar("FieldType", bound=arg | out) + + +def traitedspec_to_fields( + traitedspec, field_type: type[FieldType], skip_fields: set[str] = set() +) -> dict[str, FieldType]: + trait_names = set(traitedspec.copyable_trait_names()) + fields = {} + for name, trait in traitedspec.traits().items(): + if name in skip_fields: + continue + type_ = TYPE_CONVERSIONS.get(type(trait.trait_type), ty.Any) + if not trait.mandatory: + type_ = type_ | None + default = None + else: + default = base.NO_DEFAULT + if name in trait_names: + fields[name] = field_type( + name=name, help=trait.desc, type=type_, default=default + ) + return fields + + +Task = Nipype1Task +Outputs = Nipype1Outputs + + +TYPE_CONVERSIONS = { + nipype.interfaces.base.traits_extension.File: File, + nipype.interfaces.base.traits_extension.Directory: Directory, +} diff --git a/pydra/tasks/nipype1/tests/__init__.py b/pydra/compose/nipype1/tests/__init__.py similarity index 100% rename from pydra/tasks/nipype1/tests/__init__.py rename to pydra/compose/nipype1/tests/__init__.py diff --git a/pydra/compose/nipype1/tests/test_nipype1task.py b/pydra/compose/nipype1/tests/test_nipype1task.py new file mode 100644 index 0000000..bf2e695 --- /dev/null +++ b/pydra/compose/nipype1/tests/test_nipype1task.py @@ -0,0 +1,46 @@ +import pytest +import shutil +from fileformats.generic import File +from pydra.compose import nipype1 +from nipype.interfaces import fsl +import nipype.interfaces.utility as nutil +from . import load_resource + + +@pytest.mark.skipif(fsl.Info.version() is None, reason="Test requires FSL") +def test_isolation(tmp_path): + in_file = tmp_path / "orig/tpms_msk.nii.gz" + in_file.parent.mkdir() + shutil.copyfile( + load_resource("nipype", "testing/data/tpms_msk.nii.gz"), + in_file + ) + + out_dir = tmp_path / "output" + out_dir.mkdir() + + Slicer = nipype1.define(fsl.Slice()) + slicer = Slicer(in_file=File(in_file)) + + outputs = slicer(cache_root=out_dir) + assert outputs.out_files + assert all(fname.startswith(str(out_dir)) for fname in outputs.out_files) + + +def test_preserve_input_types(): + def with_tuple(in_param: tuple): + out_param = in_param + return out_param + + tuple_interface = nutil.Function( + input_names=["in_param"], + output_names=["out_param"], + function=with_tuple + ) + + TaskTuple = nipype1.define(tuple_interface) + nipype1_task_tuple = TaskTuple(in_param=tuple(["test"])) + + outputs = nipype1_task_tuple() + + assert isinstance(outputs.out_param, tuple) diff --git a/pydra/tasks/nipype1/__init__.py b/pydra/tasks/nipype1/__init__.py deleted file mode 100644 index 94ee228..0000000 --- a/pydra/tasks/nipype1/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -""" ->>> from pydra import ShellCommandTask ->>> import pydra.tasks.nipype1 -""" - -try: - from ._version import __version__ -except ImportError: # pragma: no cover - __version__ = "0+unknown" - -from .utils import Nipype1Task - -__all__ = ["Nipype1Task"] diff --git a/pydra/tasks/nipype1/tests/test_nipype1task.py b/pydra/tasks/nipype1/tests/test_nipype1task.py deleted file mode 100644 index 5ceeb16..0000000 --- a/pydra/tasks/nipype1/tests/test_nipype1task.py +++ /dev/null @@ -1,41 +0,0 @@ -import pytest -import shutil -from . import load_resource - -from nipype.interfaces import fsl -import nipype.interfaces.utility as nutil - -from pydra.tasks.nipype1 import Nipype1Task - - -@pytest.mark.skipif(fsl.Info.version() is None, reason="Test requires FSL") -def test_isolation(tmp_path): - in_file = tmp_path / "orig/tpms_msk.nii.gz" - in_file.parent.mkdir() - shutil.copyfile(load_resource("nipype", "testing/data/tpms_msk.nii.gz"), in_file) - - out_dir = tmp_path / "output" - out_dir.mkdir() - - slicer = Nipype1Task(fsl.Slice(), cache_dir=str(out_dir)) - slicer.inputs.in_file = in_file - - res = slicer() - assert res.output.out_files - assert all(fname.startswith(str(out_dir)) for fname in res.output.out_files) - - -def test_preserve_input_types(): - def with_tuple(in_param: tuple): - out_param = in_param - return out_param - - tuple_interface = nutil.Function( - input_names=["in_param"], output_names=["out_param"], function=with_tuple - ) - - nipype1_task_tuple = Nipype1Task(interface=tuple_interface, in_param=tuple(["test"])) - - nipype1_task_tuple() - - assert isinstance(nipype1_task_tuple._interface._list_outputs()["out_param"], tuple) diff --git a/pydra/tasks/nipype1/utils.py b/pydra/tasks/nipype1/utils.py deleted file mode 100644 index 913403b..0000000 --- a/pydra/tasks/nipype1/utils.py +++ /dev/null @@ -1,76 +0,0 @@ -import pydra -import nipype -import attrs -import typing as ty - -__all__ = ["Nipype1Task"] - - -def traitedspec_to_specinfo(traitedspec): - trait_names = set(traitedspec.copyable_trait_names()) - return pydra.specs.SpecInfo( - name="Inputs", - fields=[ - (name, attrs.field(metadata={"help_string": trait.desc}, type=ty.Any)) - for name, trait in traitedspec.traits().items() - if name in trait_names - ], - bases=(pydra.engine.specs.BaseSpec,), - ) - - -class Nipype1Task(pydra.engine.task.TaskBase): - """Wrap a Nipype 1.x Interface as a Pydra Task - - This utility translates the Nipype 1 input and output specs to - Pydra-style specs, wraps the run command, and exposes the output - in Pydra Task outputs. - - >>> import pytest - >>> from pydra.tasks.nipype1.tests import load_resource - >>> from nipype.interfaces import fsl - >>> if fsl.Info.version() is None: - ... pytest.skip() - >>> img = load_resource('nipype', 'testing/data/tpms_msk.nii.gz') - - >>> from pydra.tasks.nipype1.utils import Nipype1Task - >>> thresh = Nipype1Task(fsl.Threshold()) - >>> thresh.inputs.in_file = img - >>> thresh.inputs.thresh = 0.5 - >>> res = thresh() - >>> res.output.out_file # DOCTEST: +ELLIPSIS - '.../tpms_msk_thresh.nii.gz' - """ - - def __init__( - self, - interface: nipype.interfaces.base.BaseInterface, - audit_flags: pydra.AuditFlag = pydra.AuditFlag.NONE, - cache_dir=None, - cache_locations=None, - messenger_args=None, - messengers=None, - name=None, - **kwargs, - ): - self.input_spec = traitedspec_to_specinfo(interface.inputs) - self._interface = interface - if name is None: - name = interface.__class__.__name__ - super(Nipype1Task, self).__init__( - name, - inputs=kwargs, - audit_flags=audit_flags, - messengers=messengers, - messenger_args=messenger_args, - cache_dir=cache_dir, - cache_locations=cache_locations, - ) - self.output_spec = traitedspec_to_specinfo(interface._outputs()) - - def _run_task(self, environment=None): - inputs = attrs.asdict(self.inputs, filter=lambda a, v: v is not attrs.NOTHING) - node = nipype.Node(self._interface, base_dir=self.output_dir, name=self.name) - node.inputs.trait_set(**inputs) - res = node.run() - self.output_ = res.outputs.get() diff --git a/pyproject.toml b/pyproject.toml index 7fe2461..8906098 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["flit_scm"] -build-backend = "flit_scm:buildapi" +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" [project] name = "pydra-nipype1" @@ -49,16 +49,20 @@ test = [ homepage = "https://github.com/nipype/pydra-nipype1" repository = "https://github.com/nipype/pydra-nipype1" -[tool.flit.module] -name = "pydra.tasks.nipype1" +[tool.hatch.version] +source = "vcs" -[tool.flit.sdist] -exclude = [".gitignore"] +[tool.hatch.build.hooks.vcs] +version-file = "pydra/compose/nipype1/_version.py" + +[tool.hatch.build.targets.wheel] +packages = ["pydra"] +include-only = ["pydra/compose/nipype1"] [tool.setuptools_scm] -write_to = "pydra/tasks/nipype1/_version.py" +write_to = "pydra/compose/nipype1/_version.py" [tool.black] line-length = 99 -target-version = ["py37"] +target-version = ["py311"] exclude = "_version.py" From 50bf5c89a99a535bb996c0e6de36cd614dabcaa3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 01:25:01 +0000 Subject: [PATCH 2/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .vscode/launch.json | 2 +- conftest.py | 4 +--- pydra/compose/nipype1/builder.py | 8 ++------ pydra/compose/nipype1/tests/test_nipype1task.py | 9 ++------- 4 files changed, 6 insertions(+), 17 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 97210f6..48bc99c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -28,4 +28,4 @@ ] }, ] -} \ No newline at end of file +} diff --git a/conftest.py b/conftest.py index a8d84bf..391704e 100644 --- a/conftest.py +++ b/conftest.py @@ -12,7 +12,5 @@ def pytest_exception_interact(call: pytest.CallInfo[ty.Any]) -> None: raise call.excinfo.value @pytest.hookimpl(tryfirst=True) - def pytest_internalerror( - excinfo: pytest.ExceptionInfo[BaseException] - ) -> None: + def pytest_internalerror(excinfo: pytest.ExceptionInfo[BaseException]) -> None: raise excinfo.value diff --git a/pydra/compose/nipype1/builder.py b/pydra/compose/nipype1/builder.py index 1dd3e1b..c799afb 100644 --- a/pydra/compose/nipype1/builder.py +++ b/pydra/compose/nipype1/builder.py @@ -176,9 +176,7 @@ def _run(self, job: "Job[Nipype1Task]", rerun: bool = False) -> None: for n, v in task_dict(self).items() if v is not None or fields[n].mandatory } - node = nipype.Node( - self._interface, base_dir=job.cache_dir, name=type(self).__name__ - ) + node = nipype.Node(self._interface, base_dir=job.cache_dir, name=type(self).__name__) node.inputs.trait_set(**inputs) res = node.run() job.return_values = res.outputs.get() @@ -202,9 +200,7 @@ def traitedspec_to_fields( else: default = base.NO_DEFAULT if name in trait_names: - fields[name] = field_type( - name=name, help=trait.desc, type=type_, default=default - ) + fields[name] = field_type(name=name, help=trait.desc, type=type_, default=default) return fields diff --git a/pydra/compose/nipype1/tests/test_nipype1task.py b/pydra/compose/nipype1/tests/test_nipype1task.py index bf2e695..f54ffde 100644 --- a/pydra/compose/nipype1/tests/test_nipype1task.py +++ b/pydra/compose/nipype1/tests/test_nipype1task.py @@ -11,10 +11,7 @@ def test_isolation(tmp_path): in_file = tmp_path / "orig/tpms_msk.nii.gz" in_file.parent.mkdir() - shutil.copyfile( - load_resource("nipype", "testing/data/tpms_msk.nii.gz"), - in_file - ) + shutil.copyfile(load_resource("nipype", "testing/data/tpms_msk.nii.gz"), in_file) out_dir = tmp_path / "output" out_dir.mkdir() @@ -33,9 +30,7 @@ def with_tuple(in_param: tuple): return out_param tuple_interface = nutil.Function( - input_names=["in_param"], - output_names=["out_param"], - function=with_tuple + input_names=["in_param"], output_names=["out_param"], function=with_tuple ) TaskTuple = nipype1.define(tuple_interface) From 2da9e067a89cc52b8d41f7854450e2ae1e041518 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 4 Apr 2025 15:44:11 -0400 Subject: [PATCH 3/9] Set minimum Python to 3.11 --- .github/workflows/pythonpackage.yml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 47984d1..cdd5ba7 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.12"] # Check oldest and newest versions + python-version: ["3.11", "3.12"] # Check oldest and newest versions pip-flags: ["", "--editable"] pydra: - "pydra" @@ -60,7 +60,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/pyproject.toml b/pyproject.toml index 8906098..b864170 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "pydra-nipype1" description = "Tools for importing nipype 1.x interfaces into Pydra" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.11" dependencies = [ "pydra >=0.6.2", "nipype", From 665562698d7ab4eec74e76717f0d4818b5f2f13a Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 4 Apr 2025 15:45:00 -0400 Subject: [PATCH 4/9] Update pydra minimum, version location --- .github/workflows/pythonpackage.yml | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index cdd5ba7..0bc9ff2 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -49,12 +49,12 @@ jobs: - name: Install Pydra run: | pip install ${{ matrix.pydra }} - python -c "import pydra as m; print(f'{m.__name__} {m.__version__} @ {m.__file__}')" + python -c "import pydra.utils as m; print(f'{m.__name__} {m.__version__} @ {m.__file__}')" - name: Install task package run: | pip install ${{ matrix.pip-flags }} ".[dev]" - python -c "import pydra.tasks.$SUBPACKAGE as m; print(f'{m.__name__} {m.__version__} @ {m.__file__}')" - python -c "import pydra as m; print(f'{m.__name__} {m.__version__} @ {m.__file__}')" + python -c "import pydra.compose.$SUBPACKAGE as m; print(f'{m.__name__} {m.__version__} @ {m.__file__}')" + python -c "import pydra.utils as m; print(f'{m.__name__} {m.__version__} @ {m.__file__}')" test: runs-on: ubuntu-latest @@ -83,8 +83,8 @@ jobs: - name: Install task package run: | pip install ".[test]" - python -c "import pydra.tasks.$SUBPACKAGE as m; print(f'{m.__name__} {m.__version__} @ {m.__file__}')" - python -c "import pydra as m; print(f'{m.__name__} {m.__version__} @ {m.__file__}')" + python -c "import pydra.compose.$SUBPACKAGE as m; print(f'{m.__name__} {m.__version__} @ {m.__file__}')" + python -c "import pydra.utils as m; print(f'{m.__name__} {m.__version__} @ {m.__file__}')" - name: Test with pytest run: | pytest -sv --doctest-modules --pyargs pydra.tasks.$SUBPACKAGE \ diff --git a/pyproject.toml b/pyproject.toml index b864170..75d3cf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "Tools for importing nipype 1.x interfaces into Pydra" readme = "README.md" requires-python = ">=3.11" dependencies = [ - "pydra >=0.6.2", + "pydra >=1.0a1", "nipype", "attrs >=21.3.0", ] From 7f72f7fb8a6afc35678196d35fef82d43066c3d3 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 4 Apr 2025 15:50:27 -0400 Subject: [PATCH 5/9] Install alpha --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 0bc9ff2..f852943 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -34,7 +34,7 @@ jobs: python-version: ["3.11", "3.12"] # Check oldest and newest versions pip-flags: ["", "--editable"] pydra: - - "pydra" + - "'pydra>=1.0a0'" - "--editable git+https://github.com/nipype/pydra.git#egg=pydra" steps: From a536e3b18fec3553539110a7db3b6e52f397000a Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 4 Apr 2025 15:53:36 -0400 Subject: [PATCH 6/9] alpha0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 75d3cf7..feed842 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "Tools for importing nipype 1.x interfaces into Pydra" readme = "README.md" requires-python = ">=3.11" dependencies = [ - "pydra >=1.0a1", + "pydra >=1.0a0", "nipype", "attrs >=21.3.0", ] From f1ce8d0f3f53c7d02b98f88968a705f05dbfaa3a Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 4 Apr 2025 16:00:22 -0400 Subject: [PATCH 7/9] chore(ci): Set FSLDIR in main environment --- .github/workflows/pythonpackage.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index f852943..f80c00f 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -69,14 +69,15 @@ jobs: with: python-version: ${{ matrix.python-version }} mamba-version: "*" - channels: ${{ env.FSLCONDA }},conda-forge,defaults + channels: ${{ env.FSLCONDA }},conda-forge channel-priority: true - name: Install FSL run: | mamba install fsl-avwutils - mamba env config vars set FSLDIR="$CONDA_PREFIX" FSLOUTPUTTYPE="NIFTI_GZ" # Hack because we're not doing a full FSL install echo "6.0.7.9" > $CONDA_PREFIX/etc/fslversion + - name: Set FSLDIR + run: echo FSLDIR=$CONDA_PREFIX >> $GITHUB_ENV - name: Upgrade pip run: | python -m pip install --upgrade pip From 36df8b9a7b14bfc2db5975238089c5dfb415b45b Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 4 Apr 2025 16:07:24 -0400 Subject: [PATCH 8/9] chore(ci): Test pydra.compose.nipype1 --- .github/workflows/pythonpackage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index f80c00f..cd21abe 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -88,8 +88,8 @@ jobs: python -c "import pydra.utils as m; print(f'{m.__name__} {m.__version__} @ {m.__file__}')" - name: Test with pytest run: | - pytest -sv --doctest-modules --pyargs pydra.tasks.$SUBPACKAGE \ - --cov pydra.tasks.$SUBPACKAGE --cov-report xml --cov-report term-missing + pytest -sv --doctest-modules --pyargs pydra.compose.$SUBPACKAGE \ + --cov pydra.compose.$SUBPACKAGE --cov-report xml --cov-report term-missing - uses: codecov/codecov-action@v4 if: ${{ always() }} with: From f5a775e535cfed3adff5b4144fe291ae1b56b701 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Fri, 4 Apr 2025 17:06:15 -0400 Subject: [PATCH 9/9] test: Get test failures down to 1 --- pydra/compose/nipype1/builder.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pydra/compose/nipype1/builder.py b/pydra/compose/nipype1/builder.py index c799afb..dbf992e 100644 --- a/pydra/compose/nipype1/builder.py +++ b/pydra/compose/nipype1/builder.py @@ -152,19 +152,18 @@ class Nipype1Task(base.Task): in Pydra Task outputs. >>> import pytest - >>> from pydra.tasks.nipype1.tests import load_resource + >>> from pydra.compose.nipype1.tests import load_resource >>> from nipype.interfaces import fsl >>> if fsl.Info.version() is None: ... pytest.skip() >>> img = load_resource('nipype', 'testing/data/tpms_msk.nii.gz') - >>> from pydra.tasks.nipype1.utils import Nipype1Task - >>> thresh = Nipype1Task(fsl.Threshold()) - >>> thresh.inputs.in_file = img - >>> thresh.inputs.thresh = 0.5 + >>> from pydra.compose.nipype1.builder import define + >>> Threshold = define(fsl.Threshold()) + >>> thresh = Threshold(in_file=img, thresh=0.5) >>> res = thresh() - >>> res.output.out_file # DOCTEST: +ELLIPSIS - '.../tpms_msk_thresh.nii.gz' + >>> res.out_file # DOCTEST: +ELLIPSIS + File('.../tpms_msk_thresh.nii.gz') """ _task_type = "nipype1"