diff --git a/src/ert/plugins/__init__.py b/src/ert/plugins/__init__.py index 7ea1f61235b..a55c1831d14 100644 --- a/src/ert/plugins/__init__.py +++ b/src/ert/plugins/__init__.py @@ -27,6 +27,7 @@ def inner(*args: P.args, **kwargs: P.kwargs) -> Any: "installable_workflow_jobs", "help_links", "installable_forward_model_steps", + "forward_model_paths", "ecl100_config_path", "ecl300_config_path", "flow_config_path", diff --git a/src/ert/plugins/hook_specifications/__init__.py b/src/ert/plugins/hook_specifications/__init__.py index 0f76b317564..7f49621b838 100644 --- a/src/ert/plugins/hook_specifications/__init__.py +++ b/src/ert/plugins/hook_specifications/__init__.py @@ -3,9 +3,7 @@ ecl300_config_path, flow_config_path, ) -from .forward_model_steps import ( - installable_forward_model_steps, -) +from .forward_model_steps import forward_model_paths, installable_forward_model_steps from .help_resources import help_links from .jobs import ( installable_jobs, @@ -21,6 +19,7 @@ "ecl100_config_path", "ecl300_config_path", "flow_config_path", + "forward_model_paths", "help_links", "installable_forward_model_steps", "installable_jobs", diff --git a/src/ert/plugins/hook_specifications/forward_model_steps.py b/src/ert/plugins/hook_specifications/forward_model_steps.py index f710803cfc9..a4542581b30 100644 --- a/src/ert/plugins/hook_specifications/forward_model_steps.py +++ b/src/ert/plugins/hook_specifications/forward_model_steps.py @@ -18,3 +18,11 @@ def installable_forward_model_steps() -> ( :return: List of forward model step plugins in the form of subclasses of the ForwardModelStepPlugin class """ + + +@no_type_check +@hook_specification +def forward_model_paths() -> PluginResponse[List[str]]: + """ + :return: List of paths that can be used by forward model steps to locate executables + """ diff --git a/src/ert/plugins/plugin_manager.py b/src/ert/plugins/plugin_manager.py index 78e341f260e..ba064ea33b6 100644 --- a/src/ert/plugins/plugin_manager.py +++ b/src/ert/plugins/plugin_manager.py @@ -124,6 +124,25 @@ def _evaluate_job_doc_hook( return response.data + def get_forward_model_paths(self) -> List[str]: + response: List[PluginResponse[List[str]]] = self.hook.forward_model_paths() + if response == []: + return [] + + paths = [] + for res in response: + if not isinstance(res.data, List): + raise TypeError( + f"{res.plugin_metadata.plugin_name} did not provide list[str]" + ) + for element in res.data: + if not isinstance(element, str): + raise TypeError( + f"{res.plugin_metadata.plugin_name} gave a list of nonstrings" + ) + paths.extend(res.data) + return paths + def get_ecl100_config_path(self) -> Optional[str]: return ErtPluginManager._evaluate_config_hook( hook=self.hook.ecl100_config_path, config_name="ecl100" diff --git a/tests/ert/unit_tests/plugins/dummy_plugins.py b/tests/ert/unit_tests/plugins/dummy_plugins.py index 7eda3bcc899..289b9e3d0eb 100644 --- a/tests/ert/unit_tests/plugins/dummy_plugins.py +++ b/tests/ert/unit_tests/plugins/dummy_plugins.py @@ -8,6 +8,11 @@ def help_links(): return {"test": "test", "test2": "test"} +@plugin(name="dummy") +def forward_model_paths(): + return ["/foo/bin", "/bar/bin"] + + @plugin(name="dummy") def ecl100_config_path(): return "/dummy/path/ecl100_config.yml" diff --git a/tests/ert/unit_tests/plugins/test_plugin_manager.py b/tests/ert/unit_tests/plugins/test_plugin_manager.py index 3c000cf33a8..704065a22da 100644 --- a/tests/ert/unit_tests/plugins/test_plugin_manager.py +++ b/tests/ert/unit_tests/plugins/test_plugin_manager.py @@ -2,7 +2,10 @@ import tempfile from unittest.mock import Mock +import pytest + import ert.plugins.hook_implementations +from ert import plugin from ert.plugins import ErtPluginManager from tests.ert.unit_tests.plugins import dummy_plugins from tests.ert.unit_tests.plugins.dummy_plugins import ( @@ -13,6 +16,7 @@ def test_no_plugins(): pm = ErtPluginManager(plugins=[ert.plugins.hook_implementations]) assert pm.get_help_links() == {"GitHub page": "https://github.com/equinor/ert"} + assert pm.get_forward_model_paths() == [] assert pm.get_flow_config_path() is None assert pm.get_ecl100_config_path() is None assert pm.get_ecl300_config_path() is None @@ -35,6 +39,7 @@ def test_with_plugins(): "test": "test", "test2": "test", } + assert pm.get_forward_model_paths() == ["/foo/bin", "/bar/bin"] assert pm.get_flow_config_path() == "/dummy/path/flow_config.yml" assert pm.get_ecl100_config_path() == "/dummy/path/ecl100_config.yml" assert pm.get_ecl300_config_path() == "/dummy/path/ecl300_config.yml" @@ -55,6 +60,69 @@ def test_with_plugins(): ] +def test_plugin_with_removed_hook_is_pass(): + class LegacyPlugin: + @plugin(name="dummy2") + def legacy_hook_that_is_now_removed(): + return "my name is legacy" + + # no error, no effect. + ErtPluginManager(plugins=[LegacyPlugin]) + + +def test_lifo_path_order_for_forward_model_paths(): + class OtherPlugin: + @plugin(name="dummy2") + def forward_model_paths(): + return ["firstinpath", "secondinpath"] + + assert ErtPluginManager( + plugins=[dummy_plugins, OtherPlugin] + ).get_forward_model_paths() == [ + "firstinpath", + "secondinpath", + "/foo/bin", + "/bar/bin", + ] + assert ErtPluginManager( + plugins=[OtherPlugin, dummy_plugins] + ).get_forward_model_paths() == [ + "/foo/bin", + "/bar/bin", + "firstinpath", + "secondinpath", + ] + + +def test_path_plugin_returning_empty_pathlist(): + class OtherPlugin: + @plugin(name="dummy2") + def forward_model_paths(): + return [] + + assert ErtPluginManager(plugins=[OtherPlugin]).get_forward_model_paths() == [] + + +def test_path_plugin_returning_nonstrings(): + class OtherPlugin: + @plugin(name="dummy2") + def forward_model_paths(): + return [0] + + with pytest.raises(TypeError, match="str"): + ErtPluginManager(plugins=[OtherPlugin]).get_forward_model_paths() + + +def test_path_plugin_returning_nonlist(): + class OtherPlugin: + @plugin(name="dummy2") + def forward_model_paths(): + return "firstinpath" + + with pytest.raises(TypeError, match="list"): + ErtPluginManager(plugins=[dummy_plugins, OtherPlugin]).get_forward_model_paths() + + def test_job_documentation(): pm = ErtPluginManager(plugins=[dummy_plugins]) expected = {