From 607ffe9c7ac23d3e0f27f917761f23449e2446b1 Mon Sep 17 00:00:00 2001 From: Jason Kai <21226986+kaitj@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:33:59 -0500 Subject: [PATCH 1/9] Reorganize legacy tests - Update .gitignore to ignore .vscode/ --- .gitignore | 1 + tests/legacy/__init__.py | 0 tests/{ => legacy}/test_carg_building.py | 37 ++++++++++++------- tests/{ => legacy}/test_default_values.py | 8 ++-- tests/{ => legacy}/test_numeric_ranges.py | 16 ++++---- tests/{ => legacy}/test_output_files.py | 12 +++--- tests/{ => legacy}/utils/__init__.py | 0 tests/{ => legacy}/utils/compile_boutiques.py | 0 tests/{ => legacy}/utils/dummy_runner.py | 0 tests/{ => legacy}/utils/dynmodule.py | 0 10 files changed, 42 insertions(+), 32 deletions(-) create mode 100644 tests/legacy/__init__.py rename tests/{ => legacy}/test_carg_building.py (86%) rename tests/{ => legacy}/test_default_values.py (78%) rename tests/{ => legacy}/test_numeric_ranges.py (87%) rename tests/{ => legacy}/test_output_files.py (89%) rename tests/{ => legacy}/utils/__init__.py (100%) rename tests/{ => legacy}/utils/compile_boutiques.py (100%) rename tests/{ => legacy}/utils/dummy_runner.py (100%) rename tests/{ => legacy}/utils/dynmodule.py (100%) diff --git a/.gitignore b/.gitignore index 1e64e01..d86cd1b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .ruff_cache +.vscode # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/tests/legacy/__init__.py b/tests/legacy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_carg_building.py b/tests/legacy/test_carg_building.py similarity index 86% rename from tests/test_carg_building.py rename to tests/legacy/test_carg_building.py index 109bde7..0797722 100644 --- a/tests/test_carg_building.py +++ b/tests/legacy/test_carg_building.py @@ -1,8 +1,8 @@ """Test command line argument building.""" -import tests.utils.dummy_runner -from tests.utils.compile_boutiques import boutiques2python -from tests.utils.dynmodule import ( +import tests.legacy.utils.dummy_runner +from tests.legacy.utils.compile_boutiques import boutiques2python +from tests.legacy.utils.dynmodule import ( BT_TYPE_FILE, BT_TYPE_FLAG, BT_TYPE_NUMBER, @@ -29,7 +29,7 @@ def test_positional_string_arg() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() test_module.dummy(runner=dummy_runner, x="my_string") assert dummy_runner.last_cargs is not None @@ -53,7 +53,7 @@ def test_positional_number_arg() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() test_module.dummy(runner=dummy_runner, x="123") assert dummy_runner.last_cargs is not None @@ -77,7 +77,7 @@ def test_positional_file_arg() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() test_module.dummy(runner=dummy_runner, x="/my/file.txt") assert dummy_runner.last_cargs is not None @@ -102,7 +102,7 @@ def test_flag_arg() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() test_module.dummy(runner=dummy_runner, x="my_string") assert dummy_runner.last_cargs is not None @@ -127,7 +127,7 @@ def test_named_arg() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() test_module.dummy(runner=dummy_runner, x="my_string") assert dummy_runner.last_cargs is not None @@ -161,11 +161,20 @@ def test_list_of_strings_arg() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() - test_module.dummy(runner=dummy_runner, x=["my_string1", "my_string2"], y=["my_string3", "my_string4"]) + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() + test_module.dummy( + runner=dummy_runner, + x=["my_string1", "my_string2"], + y=["my_string3", "my_string4"], + ) assert dummy_runner.last_cargs is not None - assert dummy_runner.last_cargs == ["dummy", "my_string1", "my_string2", "my_string3 my_string4"] + assert dummy_runner.last_cargs == [ + "dummy", + "my_string1", + "my_string2", + "my_string3 my_string4", + ] def test_list_of_numbers_arg() -> None: @@ -195,7 +204,7 @@ def test_list_of_numbers_arg() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() test_module.dummy(runner=dummy_runner, x=[1, 2], y=[3, 4]) assert dummy_runner.last_cargs is not None @@ -220,7 +229,7 @@ def test_static_args() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() test_module.dummy(runner=dummy_runner, x="my_string") assert dummy_runner.last_cargs is not None @@ -265,7 +274,7 @@ def test_arg_order() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() test_module.dummy(a="aaa", b="bbb", runner=dummy_runner) assert dummy_runner.last_cargs is not None diff --git a/tests/test_default_values.py b/tests/legacy/test_default_values.py similarity index 78% rename from tests/test_default_values.py rename to tests/legacy/test_default_values.py index 5e85f35..23cbe92 100644 --- a/tests/test_default_values.py +++ b/tests/legacy/test_default_values.py @@ -1,8 +1,8 @@ """Input argument default value tests.""" -import tests.utils.dummy_runner -from tests.utils.compile_boutiques import boutiques2python -from tests.utils.dynmodule import ( +import tests.legacy.utils.dummy_runner +from tests.legacy.utils.compile_boutiques import boutiques2python +from tests.legacy.utils.dynmodule import ( BT_TYPE_STRING, boutiques_dummy, dynamic_module, @@ -27,7 +27,7 @@ def test_default_string_arg() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() test_module.dummy(runner=dummy_runner) assert dummy_runner.last_cargs is not None diff --git a/tests/test_numeric_ranges.py b/tests/legacy/test_numeric_ranges.py similarity index 87% rename from tests/test_numeric_ranges.py rename to tests/legacy/test_numeric_ranges.py index a331244..1c87476 100644 --- a/tests/test_numeric_ranges.py +++ b/tests/legacy/test_numeric_ranges.py @@ -2,9 +2,9 @@ import pytest -import tests.utils.dummy_runner -from tests.utils.compile_boutiques import boutiques2python -from tests.utils.dynmodule import ( +import tests.legacy.utils.dummy_runner +from tests.legacy.utils.compile_boutiques import boutiques2python +from tests.legacy.utils.dynmodule import ( BT_TYPE_NUMBER, boutiques_dummy, dynamic_module, @@ -30,7 +30,7 @@ def test_below_range_minimum_inclusive() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() with pytest.raises(ValueError): test_module.dummy(runner=dummy_runner, x=4) @@ -54,7 +54,7 @@ def test_above_range_maximum_inclusive() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() with pytest.raises(ValueError): test_module.dummy(runner=dummy_runner, x=6) @@ -79,7 +79,7 @@ def test_above_range_maximum_exclusive() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() with pytest.raises(ValueError): test_module.dummy(runner=dummy_runner, x=5) @@ -104,7 +104,7 @@ def test_below_range_minimum_exclusive() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() with pytest.raises(ValueError): test_module.dummy(runner=dummy_runner, x=5) @@ -129,7 +129,7 @@ def test_outside_range() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() with pytest.raises(ValueError): test_module.dummy(runner=dummy_runner, x=11) diff --git a/tests/test_output_files.py b/tests/legacy/test_output_files.py similarity index 89% rename from tests/test_output_files.py rename to tests/legacy/test_output_files.py index 7123dab..cde64ed 100644 --- a/tests/test_output_files.py +++ b/tests/legacy/test_output_files.py @@ -1,8 +1,8 @@ """Test output file paths.""" -import tests.utils.dummy_runner -from tests.utils.compile_boutiques import boutiques2python -from tests.utils.dynmodule import ( +import tests.legacy.utils.dummy_runner +from tests.legacy.utils.compile_boutiques import boutiques2python +from tests.legacy.utils.dynmodule import ( BT_TYPE_FILE, BT_TYPE_NUMBER, boutiques_dummy, @@ -34,7 +34,7 @@ def test_output_file() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() out = test_module.dummy(runner=dummy_runner, x=5) assert dummy_runner.last_cargs is not None @@ -67,7 +67,7 @@ def test_output_file_with_template() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() out = test_module.dummy(runner=dummy_runner, x=5) assert dummy_runner.last_cargs is not None @@ -101,7 +101,7 @@ def test_output_file_with_template_and_stripped_extensions() -> None: compiled_module = boutiques2python(model) test_module = dynamic_module(compiled_module, "test_module") - dummy_runner = tests.utils.dummy_runner.DummyRunner() + dummy_runner = tests.legacy.utils.dummy_runner.DummyRunner() out = test_module.dummy(runner=dummy_runner, x="in.txt") assert dummy_runner.last_cargs is not None diff --git a/tests/utils/__init__.py b/tests/legacy/utils/__init__.py similarity index 100% rename from tests/utils/__init__.py rename to tests/legacy/utils/__init__.py diff --git a/tests/utils/compile_boutiques.py b/tests/legacy/utils/compile_boutiques.py similarity index 100% rename from tests/utils/compile_boutiques.py rename to tests/legacy/utils/compile_boutiques.py diff --git a/tests/utils/dummy_runner.py b/tests/legacy/utils/dummy_runner.py similarity index 100% rename from tests/utils/dummy_runner.py rename to tests/legacy/utils/dummy_runner.py diff --git a/tests/utils/dynmodule.py b/tests/legacy/utils/dynmodule.py similarity index 100% rename from tests/utils/dynmodule.py rename to tests/legacy/utils/dynmodule.py From d2c3c2349065515344343c39680bec3c07b6632c Mon Sep 17 00:00:00 2001 From: Jason Kai <21226986+kaitj@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:35:03 -0500 Subject: [PATCH 2/9] Add tests for package dataclass - Currently skipping tests for invalid inputs, which need to be fixed in code base first --- tests/frontend/boutiques/test_descriptor.py | 60 +++++++++++++++++++++ tests/frontend/boutiques/test_docs.py | 50 +++++++++++++++++ tests/frontend/boutiques/test_inputs.py | 0 tests/frontend/boutiques/test_metadata.py | 0 tests/frontend/boutiques/test_outputs.py | 0 5 files changed, 110 insertions(+) create mode 100644 tests/frontend/boutiques/test_descriptor.py create mode 100644 tests/frontend/boutiques/test_docs.py create mode 100644 tests/frontend/boutiques/test_inputs.py create mode 100644 tests/frontend/boutiques/test_metadata.py create mode 100644 tests/frontend/boutiques/test_outputs.py diff --git a/tests/frontend/boutiques/test_descriptor.py b/tests/frontend/boutiques/test_descriptor.py new file mode 100644 index 0000000..279de56 --- /dev/null +++ b/tests/frontend/boutiques/test_descriptor.py @@ -0,0 +1,60 @@ +from typing import Any + +import pytest + +import styx.ir.core as ir +from styx.frontend.boutiques.core import from_boutiques + + +class TestPackage: + package_name = "My package" + descriptor_name = "My descriptor" + + def test_descriptor(self) -> None: + bt = {"name": self.descriptor_name} + out = from_boutiques(bt, self.package_name) + + assert isinstance(out, ir.Interface) + assert out.package.name == self.package_name + assert isinstance(out.command.body, ir.Param.Struct) + assert out.command.base.name == self.descriptor_name + + @pytest.mark.parametrize("descriptor_name", ([123], [["list of str"]])) + @pytest.mark.skip + def test_invalid_descriptor(self, descriptor_name: Any) -> None: # noqa: ANN401 + bt = {"name": descriptor_name} + with pytest.raises(TypeError): + from_boutiques(bt, self.package_name) + + @pytest.mark.parametrize("version", (["0.0.0"], [None])) + def test_valid_version(self, version: str | None) -> None: + bt = {"name": self.descriptor_name, "tool-version": version} + out = from_boutiques(bt, self.package_name) + + assert isinstance(out.package, ir.Package) + assert out.package.name == self.package_name + assert out.package.version == version + + @pytest.mark.parametrize("version", ([1.23], [["version"]])) + @pytest.mark.skip + def test_invalid_version_type(self, version: Any) -> None: # noqa: ANN401 + bt = {"name": self.descriptor_name, "tool-version": version} + + with pytest.raises(TypeError): + from_boutiques(bt, self.package_name) + + @pytest.mark.parametrize("image", (["container:version"], [None])) + def test_valid_docker(self, image: str | None) -> None: + bt = {"name": self.descriptor_name, "container-image": {"image": image}} + out = from_boutiques(bt, self.package_name) + assert isinstance(out.package, ir.Package) + assert out.package.name == self.package_name + assert out.package.docker == image + + @pytest.mark.parametrize("image", ([123], [["list of str"]])) + @pytest.mark.skip + def test_invalid_docker_type(self, image: Any) -> None: # noqa: ANN401 + bt = {"name": self.descriptor_name, "container-image": {"image": image}} + + with pytest.raises(TypeError): + from_boutiques(bt, self.package_name) diff --git a/tests/frontend/boutiques/test_docs.py b/tests/frontend/boutiques/test_docs.py new file mode 100644 index 0000000..60cfe61 --- /dev/null +++ b/tests/frontend/boutiques/test_docs.py @@ -0,0 +1,50 @@ +from typing import Any + +import pytest + +import styx.ir.core as ir +from styx.frontend.boutiques.core import from_boutiques + + +class TestDocumentation: + package_name = "My package" + descriptor_name = "My descriptor" + + # "name": "ANTSIntegrateVectorField", + # "command-line": "ANTSIntegrateVectorField [VECTOR_FIELD_INPUT] [ROI_MASK_INPUT] [FIBERS_OUTPUT] [LENGTH_IMAGE_OUTPUT]", + # "author": "ANTs Developers", + # "description": "This tool integrates a vector field, where vectors are voxels, using a region of interest (ROI) mask. The ROI mask controls where the integration is performed and specifies the starting point region.", + # "url": "https://github.com/ANTsX/ANTs", + # "tool-version": "2.5.3", + # "schema-version": "0.5", + # "inputs": + + @pytest.mark.parametrize("desc", [["A short description"], [None]]) + def test_valid_description(self, desc: str | None) -> None: + bt = {"name": self.descriptor_name, "description": desc} + out = from_boutiques(bt, self.package_name) + + assert isinstance(out.package.docs, ir.Documentation) + assert out.package.docs.description == desc + + @pytest.mark.parametrize("desc", [[["A short description"]], [123]]) + @pytest.mark.skip + def test_invalid_description_type(self, desc: Any) -> None: + bt = {"name": self.descriptor_name, "description": desc} + with pytest.raises(TypeError): + from_boutiques(bt, self.package_name) + + def test_valid_authors(self) -> None: + bt = {"name": self.descriptor_name, "authors": ["Author 1", "Author 2"]} + out = from_boutiques(bt, self.package_name) + + assert isinstance(out.package.docs.authors, list) + + # def test_descriptor(self) -> None: + # bt = {"name": self.descriptor_name} + # out = from_boutiques(bt, self.package_name) + + # assert isinstance(out, ir.Interface) + # assert out.package.name == self.package_name + # assert isinstance(out.command.body, ir.Param.Struct) + # assert out.command.base.name == self.descriptor_name diff --git a/tests/frontend/boutiques/test_inputs.py b/tests/frontend/boutiques/test_inputs.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/frontend/boutiques/test_metadata.py b/tests/frontend/boutiques/test_metadata.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/frontend/boutiques/test_outputs.py b/tests/frontend/boutiques/test_outputs.py new file mode 100644 index 0000000..e69de29 From 4e53436bfb3867ea6c183901a68010de686aeb2c Mon Sep 17 00:00:00 2001 From: Florian Rupprecht Date: Fri, 10 Jan 2025 15:30:51 -0500 Subject: [PATCH 3/9] Fix IR pretty_print ir.Param --- src/styx/ir/pretty_print.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/styx/ir/pretty_print.py b/src/styx/ir/pretty_print.py index 1194f2c..9030fcd 100644 --- a/src/styx/ir/pretty_print.py +++ b/src/styx/ir/pretty_print.py @@ -68,6 +68,17 @@ def field_is_default(obj: Any, field_: dataclasses.Field) -> bool: # noqa: ANN4 ), ")", ]) + if hasattr(obj, "__dict__"): + return f"\n{_indentation(ind)}".join([ + f"{obj.__class__.__name__}(", + *_expand( + ",\n".join([ + f" {field_name}={_pretty_print(field_value, 1)}" + for field_name, field_value in obj.__dict__.items() + ]) + ), + ")", + ]) else: return str(obj) From 475058310ad065d3e122dd3f8b2bb483b78f8f23 Mon Sep 17 00:00:00 2001 From: Jason Kai <21226986+kaitj@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:23:02 -0500 Subject: [PATCH 4/9] Fix brackets for descriptor tests --- tests/frontend/boutiques/test_descriptor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/frontend/boutiques/test_descriptor.py b/tests/frontend/boutiques/test_descriptor.py index 279de56..382d8af 100644 --- a/tests/frontend/boutiques/test_descriptor.py +++ b/tests/frontend/boutiques/test_descriptor.py @@ -19,14 +19,14 @@ def test_descriptor(self) -> None: assert isinstance(out.command.body, ir.Param.Struct) assert out.command.base.name == self.descriptor_name - @pytest.mark.parametrize("descriptor_name", ([123], [["list of str"]])) + @pytest.mark.parametrize("descriptor_name", (123, ["list of str"])) @pytest.mark.skip def test_invalid_descriptor(self, descriptor_name: Any) -> None: # noqa: ANN401 bt = {"name": descriptor_name} with pytest.raises(TypeError): from_boutiques(bt, self.package_name) - @pytest.mark.parametrize("version", (["0.0.0"], [None])) + @pytest.mark.parametrize("version", ("0.0.0", None)) def test_valid_version(self, version: str | None) -> None: bt = {"name": self.descriptor_name, "tool-version": version} out = from_boutiques(bt, self.package_name) @@ -35,7 +35,7 @@ def test_valid_version(self, version: str | None) -> None: assert out.package.name == self.package_name assert out.package.version == version - @pytest.mark.parametrize("version", ([1.23], [["version"]])) + @pytest.mark.parametrize("version", (1.23, ["version"])) @pytest.mark.skip def test_invalid_version_type(self, version: Any) -> None: # noqa: ANN401 bt = {"name": self.descriptor_name, "tool-version": version} @@ -43,7 +43,7 @@ def test_invalid_version_type(self, version: Any) -> None: # noqa: ANN401 with pytest.raises(TypeError): from_boutiques(bt, self.package_name) - @pytest.mark.parametrize("image", (["container:version"], [None])) + @pytest.mark.parametrize("image", ("container:version", None)) def test_valid_docker(self, image: str | None) -> None: bt = {"name": self.descriptor_name, "container-image": {"image": image}} out = from_boutiques(bt, self.package_name) @@ -51,7 +51,7 @@ def test_valid_docker(self, image: str | None) -> None: assert out.package.name == self.package_name assert out.package.docker == image - @pytest.mark.parametrize("image", ([123], [["list of str"]])) + @pytest.mark.parametrize("image", (123, ["list of str"])) @pytest.mark.skip def test_invalid_docker_type(self, image: Any) -> None: # noqa: ANN401 bt = {"name": self.descriptor_name, "container-image": {"image": image}} From 7059f66c568b0955d5e817b7c1d15070bda3394e Mon Sep 17 00:00:00 2001 From: Jason Kai <21226986+kaitj@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:23:07 -0500 Subject: [PATCH 5/9] Add tests for documentation - Unable to test 'literature', which also isn't currently being used - Also add per-file ignore for 'ANN401' --- pyproject.toml | 25 +++++----- tests/frontend/boutiques/test_docs.py | 67 ++++++++++++++++++--------- 2 files changed, 56 insertions(+), 36 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 496463e..1e4b4fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Boutiques code generator" authors = ["Florian Rupprecht "] license = "LGPL-2.1" readme = "README.md" -packages = [{include = "styx", from = "src"}] +packages = [{ include = "styx", from = "src" }] [tool.poetry.dependencies] python = "^3.11" @@ -28,19 +28,14 @@ docs = ["pdoc"] styx = "styx.main:main" [tool.pytest.ini_options] -pythonpath = [ - "src" -] +pythonpath = ["src"] [tool.mypy] ignore_missing_imports = true [tool.ruff] preview = true -extend-exclude = [ - "examples", - "src/styx/boutiques/model.py" -] +extend-exclude = ["examples", "src/styx/boutiques/model.py"] line-length = 120 indent-width = 4 src = ["src"] @@ -49,11 +44,11 @@ target-version = "py311" [tool.ruff.lint] select = ["ANN", "D", "E", "F", "I"] ignore = [ - "D100", # Missing docstring in public module. - "D101", # Missing docstring in public class. - "D102", # Missing docstring in public method. - "D103", # Missing docstring in public function. - "D107" # Missing docstring in __init__. + "D100", # Missing docstring in public module. + "D101", # Missing docstring in public class. + "D102", # Missing docstring in public method. + "D103", # Missing docstring in public function. + "D107", # Missing docstring in __init__. ] fixable = ["ALL"] unfixable = [] @@ -62,7 +57,9 @@ unfixable = [] convention = "google" [tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [] +"tests/**/*.py" = [ + "ANN401", # Dynamic type expression ('Any') +] [build-system] requires = ["poetry-core>=1.2.0"] diff --git a/tests/frontend/boutiques/test_docs.py b/tests/frontend/boutiques/test_docs.py index 60cfe61..4b921f5 100644 --- a/tests/frontend/boutiques/test_docs.py +++ b/tests/frontend/boutiques/test_docs.py @@ -7,44 +7,67 @@ class TestDocumentation: + # NOTE: 'literature' unable to be tested package_name = "My package" descriptor_name = "My descriptor" - # "name": "ANTSIntegrateVectorField", - # "command-line": "ANTSIntegrateVectorField [VECTOR_FIELD_INPUT] [ROI_MASK_INPUT] [FIBERS_OUTPUT] [LENGTH_IMAGE_OUTPUT]", - # "author": "ANTs Developers", - # "description": "This tool integrates a vector field, where vectors are voxels, using a region of interest (ROI) mask. The ROI mask controls where the integration is performed and specifies the starting point region.", - # "url": "https://github.com/ANTsX/ANTs", - # "tool-version": "2.5.3", - # "schema-version": "0.5", - # "inputs": + @pytest.mark.parametrize("title", ("Title", None)) + def test_valid_title(self, title: str | None) -> None: + bt = {"name": self.descriptor_name} + out = from_boutiques(bt, self.package_name, ir.Documentation(title=title)) + assert out.package.docs.title == title - @pytest.mark.parametrize("desc", [["A short description"], [None]]) + @pytest.mark.parametrize("title", (["Title"], 123)) + @pytest.mark.skip + def test_invalid_title_type(self, title: Any) -> None: + bt = {"name": self.descriptor_name} + with pytest.raises(TypeError): + from_boutiques(bt, self.package_name, ir.Documentation(title=title)) + + @pytest.mark.parametrize("desc", ("A short description", None)) def test_valid_description(self, desc: str | None) -> None: bt = {"name": self.descriptor_name, "description": desc} out = from_boutiques(bt, self.package_name) + assert isinstance(out.command.base.docs, ir.Documentation) + assert out.command.base.docs.description == desc - assert isinstance(out.package.docs, ir.Documentation) - assert out.package.docs.description == desc - - @pytest.mark.parametrize("desc", [[["A short description"]], [123]]) + @pytest.mark.parametrize("desc", (["A short description"], 123)) @pytest.mark.skip def test_invalid_description_type(self, desc: Any) -> None: bt = {"name": self.descriptor_name, "description": desc} with pytest.raises(TypeError): from_boutiques(bt, self.package_name) + # NOTE: Can only pass a single string of author(s) via boutiques def test_valid_authors(self) -> None: - bt = {"name": self.descriptor_name, "authors": ["Author 1", "Author 2"]} + authors = "Author One" + bt = {"name": self.descriptor_name, "author": authors} out = from_boutiques(bt, self.package_name) - assert isinstance(out.package.docs.authors, list) + assert isinstance(out.command.base.docs.authors, list) + assert out.command.base.docs.authors == [authors] - # def test_descriptor(self) -> None: - # bt = {"name": self.descriptor_name} - # out = from_boutiques(bt, self.package_name) + @pytest.mark.parametrize("authors", (["Author One"], 123)) + @pytest.mark.skip + def test_invalid_author_type(self, authors: Any) -> None: + bt = {"name": self.descriptor_name, "author": authors} + + with pytest.raises(TypeError): + from_boutiques(bt, self.package_name) - # assert isinstance(out, ir.Interface) - # assert out.package.name == self.package_name - # assert isinstance(out.command.body, ir.Param.Struct) - # assert out.command.base.name == self.descriptor_name + # NOTE: Can only pass a single string of url(s) via boutiques + def test_valid_urls(self) -> None: + urls = "https://url.com" + bt = {"name": self.descriptor_name, "url": urls} + out = from_boutiques(bt, self.package_name) + + assert isinstance(out.command.base.docs.urls, list) + assert out.command.base.docs.urls == [urls] + + @pytest.mark.parametrize("urls", (["https://url.com"], 123)) + @pytest.mark.skip + def test_invalid_urls_type(self, urls: Any) -> None: + bt = {"name": self.descriptor_name, "url": urls} + + with pytest.raises(TypeError): + from_boutiques(bt, self.package_name) From 9a5c27df54f09ed9ef81f0f4632f787de34dfa6a Mon Sep 17 00:00:00 2001 From: Jason Kai <21226986+kaitj@users.noreply.github.com> Date: Sun, 12 Jan 2025 15:31:00 -0500 Subject: [PATCH 6/9] Remove tests for title --- tests/frontend/boutiques/test_docs.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/frontend/boutiques/test_docs.py b/tests/frontend/boutiques/test_docs.py index 4b921f5..7794960 100644 --- a/tests/frontend/boutiques/test_docs.py +++ b/tests/frontend/boutiques/test_docs.py @@ -11,19 +11,6 @@ class TestDocumentation: package_name = "My package" descriptor_name = "My descriptor" - @pytest.mark.parametrize("title", ("Title", None)) - def test_valid_title(self, title: str | None) -> None: - bt = {"name": self.descriptor_name} - out = from_boutiques(bt, self.package_name, ir.Documentation(title=title)) - assert out.package.docs.title == title - - @pytest.mark.parametrize("title", (["Title"], 123)) - @pytest.mark.skip - def test_invalid_title_type(self, title: Any) -> None: - bt = {"name": self.descriptor_name} - with pytest.raises(TypeError): - from_boutiques(bt, self.package_name, ir.Documentation(title=title)) - @pytest.mark.parametrize("desc", ("A short description", None)) def test_valid_description(self, desc: str | None) -> None: bt = {"name": self.descriptor_name, "description": desc} From ebe93a56c0e3a6c28800805d9c02e60d967ece9d Mon Sep 17 00:00:00 2001 From: Florian Rupprecht <33600480+nx10@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:39:59 -0500 Subject: [PATCH 7/9] Add basic command line arg tests (#53) --- tests/frontend/boutiques/test_command_line.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/frontend/boutiques/test_command_line.py diff --git a/tests/frontend/boutiques/test_command_line.py b/tests/frontend/boutiques/test_command_line.py new file mode 100644 index 0000000..1dfbb0e --- /dev/null +++ b/tests/frontend/boutiques/test_command_line.py @@ -0,0 +1,37 @@ +import styx.ir.core as ir +from styx.frontend.boutiques.core import from_boutiques + + +class TestCommandLine: + package_name = "My package" + descriptor_name = "My descriptor" + + def test_basic(self) -> None: + bt = { + "name": self.descriptor_name, + "command-line": "hello world", + } + out = from_boutiques(bt, self.package_name) + + assert isinstance(out.command.body, ir.Param.Struct) + groups = out.command.body.groups + assert len(groups) == 2 + assert isinstance(groups[0].cargs[0].tokens[0], str) + assert groups[0].cargs[0].tokens[0] == "hello" + assert isinstance(groups[1].cargs[0].tokens[0], str) + assert groups[1].cargs[0].tokens[0] == "world" + + def test_quoting(self) -> None: + bt = { + "name": self.descriptor_name, + "command-line": 'hello "string with whitespace"', + } + out = from_boutiques(bt, self.package_name) + + assert isinstance(out.command.body, ir.Param.Struct) + groups = out.command.body.groups + assert len(groups) == 2 + assert isinstance(groups[0].cargs[0].tokens[0], str) + assert groups[0].cargs[0].tokens[0] == "hello" + assert isinstance(groups[1].cargs[0].tokens[0], str) + assert groups[1].cargs[0].tokens[0] == "string with whitespace" From 11e7d4bea7179561175c270b78d90d29602c2ac1 Mon Sep 17 00:00:00 2001 From: Jason Kai <21226986+kaitj@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:18:49 -0500 Subject: [PATCH 8/9] Add metadata tests (#57) * Move metadata tests from descriptor - Also remove the "# noqa: ANN401" * Add docstring for legacy tests' init --- tests/frontend/boutiques/test_descriptor.py | 37 +---------------- tests/frontend/boutiques/test_metadata.py | 44 +++++++++++++++++++++ tests/legacy/__init__.py | 1 + 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/tests/frontend/boutiques/test_descriptor.py b/tests/frontend/boutiques/test_descriptor.py index 382d8af..f410935 100644 --- a/tests/frontend/boutiques/test_descriptor.py +++ b/tests/frontend/boutiques/test_descriptor.py @@ -6,7 +6,7 @@ from styx.frontend.boutiques.core import from_boutiques -class TestPackage: +class TestDescriptor: package_name = "My package" descriptor_name = "My descriptor" @@ -21,40 +21,7 @@ def test_descriptor(self) -> None: @pytest.mark.parametrize("descriptor_name", (123, ["list of str"])) @pytest.mark.skip - def test_invalid_descriptor(self, descriptor_name: Any) -> None: # noqa: ANN401 + def test_invalid_descriptor(self, descriptor_name: Any) -> None: bt = {"name": descriptor_name} with pytest.raises(TypeError): from_boutiques(bt, self.package_name) - - @pytest.mark.parametrize("version", ("0.0.0", None)) - def test_valid_version(self, version: str | None) -> None: - bt = {"name": self.descriptor_name, "tool-version": version} - out = from_boutiques(bt, self.package_name) - - assert isinstance(out.package, ir.Package) - assert out.package.name == self.package_name - assert out.package.version == version - - @pytest.mark.parametrize("version", (1.23, ["version"])) - @pytest.mark.skip - def test_invalid_version_type(self, version: Any) -> None: # noqa: ANN401 - bt = {"name": self.descriptor_name, "tool-version": version} - - with pytest.raises(TypeError): - from_boutiques(bt, self.package_name) - - @pytest.mark.parametrize("image", ("container:version", None)) - def test_valid_docker(self, image: str | None) -> None: - bt = {"name": self.descriptor_name, "container-image": {"image": image}} - out = from_boutiques(bt, self.package_name) - assert isinstance(out.package, ir.Package) - assert out.package.name == self.package_name - assert out.package.docker == image - - @pytest.mark.parametrize("image", (123, ["list of str"])) - @pytest.mark.skip - def test_invalid_docker_type(self, image: Any) -> None: # noqa: ANN401 - bt = {"name": self.descriptor_name, "container-image": {"image": image}} - - with pytest.raises(TypeError): - from_boutiques(bt, self.package_name) diff --git a/tests/frontend/boutiques/test_metadata.py b/tests/frontend/boutiques/test_metadata.py index e69de29..e925251 100644 --- a/tests/frontend/boutiques/test_metadata.py +++ b/tests/frontend/boutiques/test_metadata.py @@ -0,0 +1,44 @@ +from typing import Any + +import pytest + +import styx.ir.core as ir +from styx.frontend.boutiques.core import from_boutiques + + +class TestMetadata: + package_name = "My package" + descriptor_name = "My descriptor" + + @pytest.mark.parametrize("version", ("0.0.0", None)) + def test_valid_version(self, version: str | None) -> None: + bt = {"name": self.descriptor_name, "tool-version": version} + out = from_boutiques(bt, self.package_name) + + assert isinstance(out.package, ir.Package) + assert out.package.name == self.package_name + assert out.package.version == version + + @pytest.mark.parametrize("version", (1.23, ["version"])) + @pytest.mark.skip + def test_invalid_version_type(self, version: Any) -> None: + bt = {"name": self.descriptor_name, "tool-version": version} + + with pytest.raises(TypeError): + from_boutiques(bt, self.package_name) + + @pytest.mark.parametrize("image", ("container:version", None)) + def test_valid_docker(self, image: str | None) -> None: + bt = {"name": self.descriptor_name, "container-image": {"image": image}} + out = from_boutiques(bt, self.package_name) + assert isinstance(out.package, ir.Package) + assert out.package.name == self.package_name + assert out.package.docker == image + + @pytest.mark.parametrize("image", (123, ["list of str"])) + @pytest.mark.skip + def test_invalid_docker_type(self, image: Any) -> None: + bt = {"name": self.descriptor_name, "container-image": {"image": image}} + + with pytest.raises(TypeError): + from_boutiques(bt, self.package_name) diff --git a/tests/legacy/__init__.py b/tests/legacy/__init__.py index e69de29..0ecdab8 100644 --- a/tests/legacy/__init__.py +++ b/tests/legacy/__init__.py @@ -0,0 +1 @@ +"""Legacy tests.""" From 6ff8cfed2da949f62da40b48f3e093cef633aade Mon Sep 17 00:00:00 2001 From: Florian Rupprecht <33600480+nx10@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:25:32 -0500 Subject: [PATCH 9/9] Add basic primitive params tests (#58) * Add basic primitive params tests * Update tests/frontend/boutiques/test_inputs.py * add clarifying comment * Minor changes --- pyproject.toml | 14 +-- tests/frontend/boutiques/test_inputs.py | 127 ++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1e4b4fa..10743c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Boutiques code generator" authors = ["Florian Rupprecht "] license = "LGPL-2.1" readme = "README.md" -packages = [{ include = "styx", from = "src" }] +packages = [{include = "styx", from = "src"}] [tool.poetry.dependencies] python = "^3.11" @@ -44,11 +44,11 @@ target-version = "py311" [tool.ruff.lint] select = ["ANN", "D", "E", "F", "I"] ignore = [ - "D100", # Missing docstring in public module. - "D101", # Missing docstring in public class. - "D102", # Missing docstring in public method. - "D103", # Missing docstring in public function. - "D107", # Missing docstring in __init__. + "D100", # Missing docstring in public module. + "D101", # Missing docstring in public class. + "D102", # Missing docstring in public method. + "D103", # Missing docstring in public function. + "D107" # Missing docstring in __init__. ] fixable = ["ALL"] unfixable = [] @@ -58,7 +58,7 @@ convention = "google" [tool.ruff.lint.per-file-ignores] "tests/**/*.py" = [ - "ANN401", # Dynamic type expression ('Any') + "ANN401" # Dynamic type expression ('Any') ] [build-system] diff --git a/tests/frontend/boutiques/test_inputs.py b/tests/frontend/boutiques/test_inputs.py index e69de29..330e364 100644 --- a/tests/frontend/boutiques/test_inputs.py +++ b/tests/frontend/boutiques/test_inputs.py @@ -0,0 +1,127 @@ +import styx.ir.core as ir +from styx.frontend.boutiques.core import from_boutiques + + +class TestPrimitiveParams: + """Test primitive param types. + + Main difference in primitive types between boutiques and IR are: + + - IR has separate int and float types, boutiques just "Number" + - Boutiques "Flag" type maps to IR bool which is encoded differently. + """ + + package_name = "My package" + descriptor_name = "My descriptor" + + def test_int(self) -> None: + bt = { + "name": self.descriptor_name, + "command-line": "dummy [X]", + "inputs": [ + { + "id": "x", + "value-key": "[X]", + "type": "Number", + "integer": True, + } + ], + } + out = from_boutiques(bt, self.package_name) + + assert isinstance(out.command.body, ir.Param.Struct) + groups = out.command.body.groups + assert len(groups) == 2 + param = groups[1].cargs[0].tokens[0] + assert isinstance(param, ir.Param) + assert isinstance(param.body, ir.Param.Int) + + def test_float(self) -> None: + bt = { + "name": self.descriptor_name, + "command-line": "dummy [X]", + "inputs": [ + { + "id": "x", + "value-key": "[X]", + "type": "Number", + # "integer": False, # Default is float + } + ], + } + out = from_boutiques(bt, self.package_name) + + assert isinstance(out.command.body, ir.Param.Struct) + groups = out.command.body.groups + assert len(groups) == 2 + param = groups[1].cargs[0].tokens[0] + assert isinstance(param, ir.Param) + assert isinstance(param.body, ir.Param.Float) + + def test_file(self) -> None: + bt = { + "name": self.descriptor_name, + "command-line": "dummy [X]", + "inputs": [ + { + "id": "x", + "value-key": "[X]", + "type": "File", + } + ], + } + out = from_boutiques(bt, self.package_name) + + assert isinstance(out.command.body, ir.Param.Struct) + groups = out.command.body.groups + assert len(groups) == 2 + param = groups[1].cargs[0].tokens[0] + assert isinstance(param, ir.Param) + assert isinstance(param.body, ir.Param.File) + + def test_string(self) -> None: + bt = { + "name": self.descriptor_name, + "command-line": "dummy [X]", + "inputs": [ + { + "id": "x", + "value-key": "[X]", + "type": "String", + } + ], + } + out = from_boutiques(bt, self.package_name) + + assert isinstance(out.command.body, ir.Param.Struct) + groups = out.command.body.groups + assert len(groups) == 2 + param = groups[1].cargs[0].tokens[0] + assert isinstance(param, ir.Param) + assert isinstance(param.body, ir.Param.String) + + def test_bool(self) -> None: + bt = { + "name": self.descriptor_name, + "command-line": "dummy [X]", + "inputs": [ + { + "id": "x", + "value-key": "[X]", + "type": "Flag", + "command-line-flag": "--x", + } + ], + } + out = from_boutiques(bt, self.package_name) + + assert isinstance(out.command.body, ir.Param.Struct) + groups = out.command.body.groups + assert len(groups) == 2 + param = groups[1].cargs[0].tokens[0] + assert isinstance(param, ir.Param) + assert isinstance(param.body, ir.Param.Bool) + assert param.body.value_true == ["--x"] + assert param.body.value_false == [] + assert not param.nullable + assert param.default_value is False # check identity to ensure it's False and not None