Repository: kedro-org/kedro

Issue:
Provide better error message when you try to use a project command from outside a project
> **Note**. This ticket does not handle the question of what happens if you're in a subdirectory of the project. That is done in #1831 . This ticket is just about what happens if you're outside the project altogether (e.g. the level above the project).

When a kedro project command (e.g. `kedro run`) is run from outside a project then currently you get the very uninformative message `run command is not found`. We should change this so that it gives a much clearer message that you are outside a kedro project and should `cd` into the project before executing the command.

This has been started in #1720 @noklam . You could either continue working on that PR or start a new one. Relevant modifications to make:
* since we're not interested in iterating upwards through parent directories here, the existing upwards iteration using `find_project_root` is no longer relevant. That leaves open the question of whether and how we might suggest what the correct kedro project directory is. It's definitely a bad idea to iterate _downwards_ through directory structure arbitrarily deep (could take a long time). We could go one directory deep so long as it's not too expensive an operation (maybe need to check there aren't too many folders in the current directory first? Needs some quick time profiling to see). Or just not make any suggestion as to the right `cd` command. 
* if we're totally outside a kedro project, a helpful error message is great but:
  * is there some better way to check if the command is a project one without overriding `get_command`, e.g. check if it's in `self.project_groups`?
  * I don't think this formatting is "on brand" yet since we haven't moved the rest of the CLI to rich yet. Let's just do it as `click.secho` for now. Doing rich styling for CLI messages is a separate issue for another time (e.g. we might move to https://github.com/ewels/rich-click)


Discussion:

Patch:
diff --git a/RELEASE.md b/RELEASE.md
index 182aa16a02..e4b293342f 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,6 +1,7 @@
 # Upcoming Release 0.19.4
 
 ## Major features and improvements
+* Kedro CLI now provides a better error message when project commands are run outside of a project i.e. `kedro run`
 
 ## Bug fixes and other changes
 * Updated `kedro pipeline create` and `kedro pipeline delete` to read the base environment from the project settings.
diff --git a/kedro/framework/cli/__init__.py b/kedro/framework/cli/__init__.py
index fcbb427ef7..8816ace3ea 100644
--- a/kedro/framework/cli/__init__.py
+++ b/kedro/framework/cli/__init__.py
@@ -1,7 +1,11 @@
 """``kedro.framework.cli`` implements commands available from Kedro's CLI.
 """
 
-from .cli import main
-from .utils import command_with_verbosity, load_entry_points
+# The constant need to be defined first otherwise it causes circular depdencies
+ORANGE = (255, 175, 0)
+BRIGHT_BLACK = (128, 128, 128)
+
+from .cli import main  # noqa: E402
+from .utils import command_with_verbosity, load_entry_points  # noqa: E402
 
 __all__ = ["main", "command_with_verbosity", "load_entry_points"]
diff --git a/kedro/framework/cli/cli.py b/kedro/framework/cli/cli.py
index 5ecd5b9f9c..8ee3567b7a 100644
--- a/kedro/framework/cli/cli.py
+++ b/kedro/framework/cli/cli.py
@@ -6,6 +6,7 @@
 
 import importlib
 import sys
+import traceback
 from collections import defaultdict
 from pathlib import Path
 from typing import Any, Sequence
@@ -13,6 +14,7 @@
 import click
 
 from kedro import __version__ as version
+from kedro.framework.cli import BRIGHT_BLACK, ORANGE
 from kedro.framework.cli.catalog import catalog_cli
 from kedro.framework.cli.hooks import get_cli_hook_manager
 from kedro.framework.cli.jupyter import jupyter_cli
@@ -133,10 +135,40 @@ def main(
             )
         # click.core.main() method exits by default, we capture this and then
         # exit as originally intended
+
         except SystemExit as exc:
             self._cli_hook_manager.hook.after_command_run(
                 project_metadata=self._metadata, command_args=args, exit_code=exc.code
             )
+            # When CLI is run outside of a project, project_groups are not registered
+            catch_exception = "click.exceptions.UsageError: No such command"
+            # click convert exception handles to error message
+            if catch_exception in traceback.format_exc() and not self.project_groups:
+                warn = click.style(
+                    "\nKedro project not found in this directory. ",
+                    fg=ORANGE,
+                    bold=True,
+                )
+                result = (
+                    click.style("Project specific commands such as ")
+                    + click.style("'run' ", fg="cyan")
+                    + "or "
+                    + click.style("'jupyter' ", fg="cyan")
+                    + "are only available within a project directory."
+                )
+                message = warn + result
+                hint = (
+                    click.style(
+                        "\nHint: Kedro is looking for a file called ", fg=BRIGHT_BLACK
+                    )
+                    + click.style("'pyproject.toml", fg="magenta")
+                    + click.style(
+                        ", is one present in your current working directory?",
+                        fg=BRIGHT_BLACK,
+                    )
+                )
+                click.echo(message)
+                click.echo(hint)
             sys.exit(exc.code)
 
     @property


Test patch:
diff --git a/tests/framework/cli/test_cli.py b/tests/framework/cli/test_cli.py
index c1d348e8b2..c1e5ce51ec 100644
--- a/tests/framework/cli/test_cli.py
+++ b/tests/framework/cli/test_cli.py
@@ -386,6 +386,21 @@ def test_kedro_cli_no_project(self, mocker, tmp_path):
         assert "Global commands from Kedro" in result.output
         assert "Project specific commands from Kedro" not in result.output
 
+    def test_kedro_run_no_project(self, mocker, tmp_path):
+        mocker.patch("kedro.framework.cli.cli._is_project", return_value=False)
+        kedro_cli = KedroCLI(tmp_path)
+
+        result = CliRunner().invoke(kedro_cli, ["run"])
+        assert (
+            "Kedro project not found in this directory. Project specific commands such as 'run' or "
+            "'jupyter' are only available within a project directory." in result.output
+        )
+
+        assert (
+            "Hint: Kedro is looking for a file called 'pyproject.toml, is one present in"
+            " your current working directory?" in result.output
+        )
+
     def test_kedro_cli_with_project(self, mocker, fake_metadata):
         Module = namedtuple("Module", ["cli"])
         mocker.patch("kedro.framework.cli.cli._is_project", return_value=True)