Repository: kedro-org/kedro

Issue:
Provide option to raise error explicitly for `find_pipelines`
## Description
<!-- Is your feature request related to a problem? A clear and concise description of what the problem is: "I'm always frustrated when ..." -->

`find_pipelines` is added since 0.18.3 as a pipeline autodiscovery feature. While this is useful, it is sometimes hard to debug because it raises warning but not error so the program will keep running.

## Context
<!-- Why is this change important to you? How would you use it? How can it benefit other users? -->
For library developers, they may need strict control and want the pipeline fails explicitly. This features was designed for development, so any half-developed pipeline won't prevent other pipeline from running (incomplete pipeline, not installed library etc)

Some quotes from users:
> We recently have users reporting issues when running tensorflow on M1. The error was hidden in some kedro pipelines and it took a while to dig out the root cause from the user. In general, it can be difficult to predict when and where we will encounter these issue. Therefore, we are planning to build some CI workflows for checking our packages and pipelines for M1 machines in general.

> I'm noticing that warnings like SyntaxErrors and type errors are considered "warnings" by Kedro, which continues to try to run the pipeline.  Is there a setting to escalate these to Errors so they can abort the pipeline run?
  [15 days ago](https://kedro-org.slack.com/archives/C03RKP2LW64/p1690226861464369?thread_ts=1690222660.057049&cid=C03RKP2LW64)
Is there a way to mark a pipeline as a work in progress and then tell find_pipelines to error on warnings?  Or is the answer to just not use pipeline autodiscovery

## Possible Implementation
<!-- (Optional) Suggest an idea for implementing the addition or change. -->

## Possible Alternatives
<!-- (Optional) Describe any alternative solutions or features you've considered. -->


Discussion:
deepyaman: > Implementation wise looks fine. I think we should update the relevant documentation as well. I was wondering should we also update the starter to add the default to make this more discoverable.
> 
> ```python
> pipelines = find_pipelines(raise_errors=False)
> ```

Good point! See [71e0f64](https://github.com/kedro-org/kedro/pull/3823/commits/71e0f64f038929f4d127cbc9f092217894187592) for documentation added in response.
deepyaman: > My only question is whether we should keep the default value `raise_errors=False` rather than `raise_errors=True`. If I understood correctly, the feature was initially designed for developers, so any half-developed pipeline won't prevent another pipeline from running. However, general users find debugging hard if an issue occurs and the pipeline is still running. So we may consider making `raise_errors=True` the default behaviour, as the chance that developers are aware of this feature and the flag is higher than that of general users.

I'm OK with that, if that's the prevailing user feedback. @astrojuanlu @merelcht any opinion?

Also, I don't personally feel that behavior change is "breaking" (it's pretty easy to update your registry, and hopefully you're not running with a broken pipeline already in production), but would be happy to get others' view.
merelcht: > > My only question is whether we should keep the default value `raise_errors=False` rather than `raise_errors=True`. If I understood correctly, the feature was initially designed for developers, so any half-developed pipeline won't prevent another pipeline from running. However, general users find debugging hard if an issue occurs and the pipeline is still running. So we may consider making `raise_errors=True` the default behaviour, as the chance that developers are aware of this feature and the flag is higher than that of general users.
> 
> I'm OK with that, if that's the prevailing user feedback. @astrojuanlu @merelcht any opinion?
> 
> Also, I don't personally feel that behavior change is "breaking" (it's pretty easy to update your registry, and hopefully you're not running with a broken pipeline already in production), but would be happy to get others' view.

I think it's a good call to make the default `raise_errors=True`. We have indeed heard that users struggle with debugging, so raising errors explicitly by default will hopefully help.
astrojuanlu: Without a full understanding of https://github.com/kedro-org/kedro/issues/2401, I'd be wary of setting `raise_errors=True` on a micro release. We want to take a broader look at what kind of errors people find while debugging.

My recommendation would be to introduce the option but not change the behavior, so that we can take a more comprehensive look soon. That's also the safer option anyway in terms of backwards compatibility.

Patch:
diff --git a/RELEASE.md b/RELEASE.md
index b75cc57a..a74ec12c 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,6 +1,7 @@
 # Upcoming Release 0.19.6
 
 ## Major features and improvements
+* Added `raise_errors` argument to `find_pipelines`. If `True`, the first pipeline for which autodiscovery fails will cause an error to be raised. The default behaviour is still to raise a warning for each failing pipeline.
 * It is now possible to use Kedro without having `rich` installed.
 * Added a `--telemetry` flag to `kedro run`, allowing consent to data usage to be granted or revoked at the same time the command is run.
 
@@ -45,7 +46,7 @@ Many thanks to the following Kedroids for contributing PRs to this release:
 * Updated `kedro pipeline create` and `kedro pipeline delete` to read the base environment from the project settings.
 * Updated CLI command `kedro catalog resolve` to read credentials properly.
 * Changed the path of where pipeline tests generated with `kedro pipeline create` from `<project root>/src/tests/pipelines/<pipeline name>` to `<project root>/tests/pipelines/<pipeline name>`.
-* Updated ``.gitignore`` to prevent pushing Mlflow local runs folder to a remote forge when using mlflow and git.
+* Updated ``.gitignore`` to prevent pushing MLflow local runs folder to a remote forge when using MLflow and Git.
 * Fixed error handling message for malformed yaml/json files in OmegaConfigLoader.
 * Fixed a bug in `node`-creation allowing self-dependencies when using transcoding, that is datasets named like `name@format`.
 * Improved error message when passing wrong value to node.
diff --git a/docs/source/nodes_and_pipelines/pipeline_registry.md b/docs/source/nodes_and_pipelines/pipeline_registry.md
index 6b92b305..6520c06e 100644
--- a/docs/source/nodes_and_pipelines/pipeline_registry.md
+++ b/docs/source/nodes_and_pipelines/pipeline_registry.md
@@ -51,7 +51,9 @@ Under the hood, the `find_pipelines()` function traverses the `src/<package_name
 2. Calling the `create_pipeline()` function exposed by the `<package_name>.pipelines.<pipeline_name>` module
 3. Validating that the constructed object is a {py:class}`~kedro.pipeline.Pipeline`
 
-If any of these steps fail, `find_pipelines()` raises an appropriate warning and skips the current pipeline but continues traversal.
+By default, if any of these steps fail, `find_pipelines()` (or `find_pipelines(raise_errors=False)`) raises an appropriate warning and skips the current pipeline but continues traversal. During development, this enables you to run your project with some pipelines, even if other pipelines are broken or works in progress.
+
+If you specify `find_pipelines(raise_errors=True)`, the autodiscovery process will fail upon the first error. In production, this ensures errors are caught up front, and pipelines do not get excluded accidentally.
 
 The mapping returned by `find_pipelines()` can be modified, meaning you are not limited to the pipelines returned by each of the `create_pipeline()` functions found above. For example, to add a data engineering pipeline that isn't part of the default pipeline, add it to the dictionary *after* constructing the default pipeline:
 
diff --git a/kedro/framework/project/__init__.py b/kedro/framework/project/__init__.py
index 7cfd2a99..4ced05c0 100644
--- a/kedro/framework/project/__init__.py
+++ b/kedro/framework/project/__init__.py
@@ -347,7 +347,7 @@ def _create_pipeline(pipeline_module: types.ModuleType) -> Pipeline | None:
     return obj
 
 
-def find_pipelines() -> dict[str, Pipeline]:  # noqa: PLR0912
+def find_pipelines(raise_errors: bool = False) -> dict[str, Pipeline]:  # noqa: PLR0912
     """Automatically find modular pipelines having a ``create_pipeline``
     function. By default, projects created using Kedro 0.18.3 and higher
     call this function to autoregister pipelines upon creation/addition.
@@ -359,13 +359,23 @@ def find_pipelines() -> dict[str, Pipeline]:  # noqa: PLR0912
     For more information on the pipeline registry and autodiscovery, see
     https://kedro.readthedocs.io/en/stable/nodes_and_pipelines/pipeline_registry.html
 
+    Args:
+        raise_errors: If ``True``, raise an error upon failed discovery.
+
     Returns:
         A generated mapping from pipeline names to ``Pipeline`` objects.
 
+    Raises:
+        ImportError: When a module does not expose a ``create_pipeline``
+            function, the ``create_pipeline`` function does not return a
+            ``Pipeline`` object, or if the module import fails up front.
+            If ``raise_errors`` is ``False``, see Warns section instead.
+
     Warns:
         UserWarning: When a module does not expose a ``create_pipeline``
             function, the ``create_pipeline`` function does not return a
             ``Pipeline`` object, or if the module import fails up front.
+            If ``raise_errors`` is ``True``, see Raises section instead.
     """
     pipeline_obj = None
 
@@ -375,6 +385,12 @@ def find_pipelines() -> dict[str, Pipeline]:  # noqa: PLR0912
         pipeline_module = importlib.import_module(pipeline_module_name)
     except Exception as exc:
         if str(exc) != f"No module named '{pipeline_module_name}'":
+            if raise_errors:
+                raise ImportError(
+                    f"An error occurred while importing the "
+                    f"'{pipeline_module_name}' module."
+                ) from exc
+
             warnings.warn(
                 IMPORT_ERROR_MESSAGE.format(
                     module=pipeline_module_name, tb_exc=traceback.format_exc()
@@ -406,7 +422,13 @@ def find_pipelines() -> dict[str, Pipeline]:  # noqa: PLR0912
         pipeline_module_name = f"{PACKAGE_NAME}.pipelines.{pipeline_name}"
         try:
             pipeline_module = importlib.import_module(pipeline_module_name)
-        except:  # noqa: E722
+        except Exception as exc:
+            if raise_errors:
+                raise ImportError(
+                    f"An error occurred while importing the "
+                    f"'{pipeline_module_name}' module."
+                ) from exc
+
             warnings.warn(
                 IMPORT_ERROR_MESSAGE.format(
                     module=pipeline_module_name, tb_exc=traceback.format_exc()
diff --git a/kedro/pipeline/modular_pipeline.py b/kedro/pipeline/modular_pipeline.py
index 2bf672a2..9eb4caba 100644
--- a/kedro/pipeline/modular_pipeline.py
+++ b/kedro/pipeline/modular_pipeline.py
@@ -99,8 +99,10 @@ def _get_dataset_names_mapping(
             the same as they are named in the provided pipeline.
             When dict[str, str] is provided, current names will be
             mapped to new names in the resultant pipeline.
+
     Returns:
         A dictionary that maps the old dataset names to the provided ones.
+
     Examples:
         >>> _get_dataset_names_mapping("dataset_name")
         {"dataset_name": "dataset_name"}  # a str name will stay the same
@@ -138,8 +140,10 @@ def _get_param_names_mapping(
             the same as they are named in the provided pipeline.
             When dict[str, str] is provided, current names will be
             mapped to new names in the resultant pipeline.
+
     Returns:
         A dictionary that maps the old parameter names to the provided ones.
+
     Examples:
         >>> _get_param_names_mapping("param_name")
         {"params:param_name": "params:param_name"}  # a str name will stay the same


Test patch:
diff --git a/tests/framework/project/test_pipeline_discovery.py b/tests/framework/project/test_pipeline_discovery.py
index 4525144f..afffbcfd 100644
--- a/tests/framework/project/test_pipeline_discovery.py
+++ b/tests/framework/project/test_pipeline_discovery.py
@@ -177,12 +177,16 @@ def test_find_pipelines_skips_regular_files_within_the_pipelines_folder(
 
 
 @pytest.mark.parametrize(
-    "mock_package_name_with_pipelines,pipeline_names",
-    [(x, x) for x in [set(), {"my_pipeline"}]],
-    indirect=True,
+    "mock_package_name_with_pipelines,pipeline_names,raise_errors",
+    [
+        (x, x, raise_errors)
+        for x in [set(), {"my_pipeline"}]
+        for raise_errors in [True, False]
+    ],
+    indirect=["mock_package_name_with_pipelines", "pipeline_names"],
 )
 def test_find_pipelines_skips_modules_that_cause_exceptions_upon_import(
-    mock_package_name_with_pipelines, pipeline_names
+    mock_package_name_with_pipelines, pipeline_names, raise_errors
 ):
     # Create a module that will result in errors when we try to load it.
     pipelines_dir = Path(sys.path[0]) / mock_package_name_with_pipelines / "pipelines"
@@ -191,12 +195,14 @@ def test_find_pipelines_skips_modules_that_cause_exceptions_upon_import(
     (pipeline_dir / "__init__.py").write_text("I walk a lonely road...")
 
     configure_project(mock_package_name_with_pipelines)
-    with pytest.warns(
-        UserWarning, match=r"An error occurred while importing the '\S+' module."
+    with getattr(pytest, "raises" if raise_errors else "warns")(
+        ImportError if raise_errors else UserWarning,
+        match=r"An error occurred while importing the '\S+' module.",
     ):
-        pipelines = find_pipelines()
-    assert set(pipelines) == pipeline_names | {"__default__"}
-    assert sum(pipelines.values()).outputs() == pipeline_names
+        pipelines = find_pipelines(raise_errors=raise_errors)
+    if not raise_errors:
+        assert set(pipelines) == pipeline_names | {"__default__"}
+        assert sum(pipelines.values()).outputs() == pipeline_names
 
 
 @pytest.mark.parametrize(
@@ -226,12 +232,16 @@ def test_find_pipelines_handles_simplified_project_structure(
 
 
 @pytest.mark.parametrize(
-    "mock_package_name_with_pipelines,pipeline_names",
-    [(x, x) for x in [set(), {"my_pipeline"}]],
-    indirect=True,
+    "mock_package_name_with_pipelines,pipeline_names,raise_errors",
+    [
+        (x, x, raise_errors)
+        for x in [set(), {"my_pipeline"}]
+        for raise_errors in [True, False]
+    ],
+    indirect=["mock_package_name_with_pipelines", "pipeline_names"],
 )
 def test_find_pipelines_skips_unimportable_pipeline_module(
-    mock_package_name_with_pipelines, pipeline_names
+    mock_package_name_with_pipelines, pipeline_names, raise_errors
 ):
     (Path(sys.path[0]) / mock_package_name_with_pipelines / "pipeline.py").write_text(
         textwrap.dedent(
@@ -248,12 +258,14 @@ def test_find_pipelines_skips_unimportable_pipeline_module(
     )
 
     configure_project(mock_package_name_with_pipelines)
-    with pytest.warns(
-        UserWarning, match=r"An error occurred while importing the '\S+' module."
+    with getattr(pytest, "raises" if raise_errors else "warns")(
+        ImportError if raise_errors else UserWarning,
+        match=r"An error occurred while importing the '\S+' module.",
     ):
-        pipelines = find_pipelines()
-    assert set(pipelines) == pipeline_names | {"__default__"}
-    assert sum(pipelines.values()).outputs() == pipeline_names
+        pipelines = find_pipelines(raise_errors=raise_errors)
+    if not raise_errors:
+        assert set(pipelines) == pipeline_names | {"__default__"}
+        assert sum(pipelines.values()).outputs() == pipeline_names
 
 
 @pytest.mark.parametrize(