From 3ebd73cfaf31a54415fc85e364c70ef1af393a52 Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Mon, 4 Mar 2024 15:25:20 +0000 Subject: [PATCH 01/10] provide error message outside of kedro project with rich style Signed-off-by: Nok Lam Chan --- kedro/framework/cli/cli.py | 40 +++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/kedro/framework/cli/cli.py b/kedro/framework/cli/cli.py index 5ecd5b9f9c..2ac1793f16 100644 --- a/kedro/framework/cli/cli.py +++ b/kedro/framework/cli/cli.py @@ -8,9 +8,13 @@ import sys from collections import defaultdict from pathlib import Path -from typing import Any, Sequence +from typing import Any, Optional, Sequence import click +from black import find_project_root +from click.core import Command, Context +from rich.console import Console +from rich.panel import Panel from kedro import __version__ as version from kedro.framework.cli.catalog import catalog_cli @@ -95,8 +99,13 @@ class KedroCLI(CommandCollection): def __init__(self, project_path: Path): self._metadata = None # running in package mode + self._guessed_project_root = None if _is_project(project_path): self._metadata = bootstrap_project(project_path) + else: + guessed_project_root, _ = find_project_root(None) + if _is_project(guessed_project_root): + self._guessed_project_root = guessed_project_root self._cli_hook_manager = get_cli_hook_manager() super().__init__( @@ -188,6 +197,35 @@ def project_groups(self) -> Sequence[click.MultiCommand]: # (overriding happens as follows built-in < plugins < cli.py) return [*built_in, *plugins, user_defined] + def get_command(self, ctx: Context, cmd_name: str) -> Optional[Command]: + """ + Add more useful help message when command is not found. i.e. try to find where + the kedro project root is. + """ + command = super().get_command(ctx, cmd_name) + + if not command: + warn = "[orange1][b]You are not in a Kedro project[/]![/]" + result = "Project specific commands such as '[bright_cyan]run[/]' or \ +'[bright_cyan]jupyter[/]' are only available within a project directory." + if self._guessed_project_root: + solution = f"[bright_black][b]Hint:[/] [i]Kedro is looking for a file called \ +'[magenta]pyproject.toml[/]', Is this your working directory?[/]\ +\n{self._guessed_project_root}[/]" + else: + solution = "[bright_black][b]Hint:[/] [i]Kedro is looking for a file called \ +'[magenta]pyproject.toml[/]', is one present in your current working directory?[/][/]" + msg = f"{warn} {result}\n\n{solution}" + console = Console() + panel = Panel( + msg, + title=f"Command '{cmd_name}' not found", + expand=False, + border_style="dim", + title_align="left", + ) + console.print("\n", panel, "\n") + def main() -> None: # pragma: no cover """Main entry point. Look for a ``cli.py``, and, if found, add its From b434fef38f08a91ae3e705344ef224abb6d9572e Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Mon, 4 Mar 2024 16:40:35 +0000 Subject: [PATCH 02/10] change to add custom click message on error instead of overriding get_commands Signed-off-by: Nok Lam Chan --- kedro/framework/cli/cli.py | 61 +++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/kedro/framework/cli/cli.py b/kedro/framework/cli/cli.py index 2ac1793f16..6c55486d67 100644 --- a/kedro/framework/cli/cli.py +++ b/kedro/framework/cli/cli.py @@ -8,13 +8,10 @@ import sys from collections import defaultdict from pathlib import Path -from typing import Any, Optional, Sequence +from typing import Any, Sequence import click from black import find_project_root -from click.core import Command, Context -from rich.console import Console -from rich.panel import Panel from kedro import __version__ as version from kedro.framework.cli.catalog import catalog_cli @@ -112,6 +109,7 @@ def __init__(self, project_path: Path): ("Global commands", self.global_groups), ("Project specific commands", self.project_groups), ) + # print("KEDRO CLI************") def main( self, @@ -146,6 +144,32 @@ def main( self._cli_hook_manager.hook.after_command_run( project_metadata=self._metadata, command_args=args, exit_code=exc.code ) + if not self.project_groups: + ORANGE = (255, 175, 0) + BRIGHT_BLACK = (128, 128, 128) + warn = click.style( + "\nYou are not in a Kedro project! ", 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 @@ -197,35 +221,6 @@ def project_groups(self) -> Sequence[click.MultiCommand]: # (overriding happens as follows built-in < plugins < cli.py) return [*built_in, *plugins, user_defined] - def get_command(self, ctx: Context, cmd_name: str) -> Optional[Command]: - """ - Add more useful help message when command is not found. i.e. try to find where - the kedro project root is. - """ - command = super().get_command(ctx, cmd_name) - - if not command: - warn = "[orange1][b]You are not in a Kedro project[/]![/]" - result = "Project specific commands such as '[bright_cyan]run[/]' or \ -'[bright_cyan]jupyter[/]' are only available within a project directory." - if self._guessed_project_root: - solution = f"[bright_black][b]Hint:[/] [i]Kedro is looking for a file called \ -'[magenta]pyproject.toml[/]', Is this your working directory?[/]\ -\n{self._guessed_project_root}[/]" - else: - solution = "[bright_black][b]Hint:[/] [i]Kedro is looking for a file called \ -'[magenta]pyproject.toml[/]', is one present in your current working directory?[/][/]" - msg = f"{warn} {result}\n\n{solution}" - console = Console() - panel = Panel( - msg, - title=f"Command '{cmd_name}' not found", - expand=False, - border_style="dim", - title_align="left", - ) - console.print("\n", panel, "\n") - def main() -> None: # pragma: no cover """Main entry point. Look for a ``cli.py``, and, if found, add its From 302fe7b14fad65d58e3ba97d5e30410dbbaf52cd Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Mon, 4 Mar 2024 16:57:52 +0000 Subject: [PATCH 03/10] add test Signed-off-by: Nok Lam Chan --- kedro/framework/cli/cli.py | 1 + tests/framework/cli/test_cli.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/kedro/framework/cli/cli.py b/kedro/framework/cli/cli.py index 6c55486d67..02e2143b00 100644 --- a/kedro/framework/cli/cli.py +++ b/kedro/framework/cli/cli.py @@ -144,6 +144,7 @@ def main( self._cli_hook_manager.hook.after_command_run( project_metadata=self._metadata, command_args=args, exit_code=exc.code ) + # When the CLI is run outside of a project, project_groups are not registered if not self.project_groups: ORANGE = (255, 175, 0) BRIGHT_BLACK = (128, 128, 128) diff --git a/tests/framework/cli/test_cli.py b/tests/framework/cli/test_cli.py index c1d348e8b2..f2328e7f52 100644 --- a/tests/framework/cli/test_cli.py +++ b/tests/framework/cli/test_cli.py @@ -385,6 +385,15 @@ def test_kedro_cli_no_project(self, mocker, tmp_path): assert result.exit_code == 0 assert "Global commands from Kedro" in result.output assert "Project specific commands from Kedro" not in result.output + assert ( + "You are not in a Kedro project! 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"]) From 7eff977506ef95dd92ffd12c007ada9b0730cb89 Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Mon, 4 Mar 2024 17:07:10 +0000 Subject: [PATCH 04/10] clean up Signed-off-by: Nok Lam Chan --- kedro/framework/cli/cli.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/kedro/framework/cli/cli.py b/kedro/framework/cli/cli.py index 02e2143b00..47e7200eb1 100644 --- a/kedro/framework/cli/cli.py +++ b/kedro/framework/cli/cli.py @@ -11,7 +11,6 @@ from typing import Any, Sequence import click -from black import find_project_root from kedro import __version__ as version from kedro.framework.cli.catalog import catalog_cli @@ -96,20 +95,14 @@ class KedroCLI(CommandCollection): def __init__(self, project_path: Path): self._metadata = None # running in package mode - self._guessed_project_root = None if _is_project(project_path): self._metadata = bootstrap_project(project_path) - else: - guessed_project_root, _ = find_project_root(None) - if _is_project(guessed_project_root): - self._guessed_project_root = guessed_project_root self._cli_hook_manager = get_cli_hook_manager() super().__init__( ("Global commands", self.global_groups), ("Project specific commands", self.project_groups), ) - # print("KEDRO CLI************") def main( self, From c528711d416a4fa0c8e657e3138b07278717a9d3 Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Mon, 4 Mar 2024 22:40:40 +0000 Subject: [PATCH 05/10] catch the error message from click Signed-off-by: Nok Lam Chan --- kedro/framework/cli/cli.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/kedro/framework/cli/cli.py b/kedro/framework/cli/cli.py index 47e7200eb1..0622ee985f 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 @@ -133,12 +134,15 @@ 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 the CLI is run outside of a project, project_groups are not registered - if not self.project_groups: + 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: ORANGE = (255, 175, 0) BRIGHT_BLACK = (128, 128, 128) warn = click.style( From 79ef7d1d2e76d6310c7fd7ea911897f69040c776 Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Mon, 4 Mar 2024 23:00:35 +0000 Subject: [PATCH 06/10] fix unitests Signed-off-by: Nok Lam Chan --- tests/framework/cli/test_cli.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/framework/cli/test_cli.py b/tests/framework/cli/test_cli.py index f2328e7f52..d9f20e450d 100644 --- a/tests/framework/cli/test_cli.py +++ b/tests/framework/cli/test_cli.py @@ -385,6 +385,12 @@ def test_kedro_cli_no_project(self, mocker, tmp_path): assert result.exit_code == 0 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 ( "You are not in a Kedro project! Project specific commands such as 'run' or " "'jupyter' are only available within a project directory." in result.output From 194dfd1ba8fe27779f710ed468d506836c03ea23 Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Tue, 5 Mar 2024 12:35:47 +0000 Subject: [PATCH 07/10] Update error message Signed-off-by: Nok Lam Chan --- kedro/framework/cli/cli.py | 4 +++- tests/framework/cli/test_cli.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/kedro/framework/cli/cli.py b/kedro/framework/cli/cli.py index 0622ee985f..93d4790438 100644 --- a/kedro/framework/cli/cli.py +++ b/kedro/framework/cli/cli.py @@ -146,7 +146,9 @@ def main( ORANGE = (255, 175, 0) BRIGHT_BLACK = (128, 128, 128) warn = click.style( - "\nYou are not in a Kedro project! ", fg=ORANGE, bold=True + "\nKedro project not found in this directory. ", + fg=ORANGE, + bold=True, ) result = ( click.style("Project specific commands such as ") diff --git a/tests/framework/cli/test_cli.py b/tests/framework/cli/test_cli.py index d9f20e450d..c1e5ce51ec 100644 --- a/tests/framework/cli/test_cli.py +++ b/tests/framework/cli/test_cli.py @@ -392,7 +392,7 @@ def test_kedro_run_no_project(self, mocker, tmp_path): result = CliRunner().invoke(kedro_cli, ["run"]) assert ( - "You are not in a Kedro project! Project specific commands such as 'run' or " + "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 ) From 07f54720b53d8a0e41cbed1bcf6052f4cb970be2 Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Tue, 5 Mar 2024 13:01:12 +0000 Subject: [PATCH 08/10] move color constant to the upper module Signed-off-by: Nok Lam Chan --- kedro/framework/cli/__init__.py | 3 +++ kedro/framework/cli/cli.py | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/kedro/framework/cli/__init__.py b/kedro/framework/cli/__init__.py index fcbb427ef7..0c610bde15 100644 --- a/kedro/framework/cli/__init__.py +++ b/kedro/framework/cli/__init__.py @@ -5,3 +5,6 @@ from .utils import command_with_verbosity, load_entry_points __all__ = ["main", "command_with_verbosity", "load_entry_points"] + +ORANGE = (255, 175, 0) +BRIGHT_BLACK = (128, 128, 128) diff --git a/kedro/framework/cli/cli.py b/kedro/framework/cli/cli.py index 93d4790438..8ee3567b7a 100644 --- a/kedro/framework/cli/cli.py +++ b/kedro/framework/cli/cli.py @@ -14,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 @@ -139,12 +140,10 @@ def main( self._cli_hook_manager.hook.after_command_run( project_metadata=self._metadata, command_args=args, exit_code=exc.code ) - # When the CLI is run outside of a project, project_groups are not registered + # 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: - ORANGE = (255, 175, 0) - BRIGHT_BLACK = (128, 128, 128) warn = click.style( "\nKedro project not found in this directory. ", fg=ORANGE, From b3c39495f486aa0d91be4adce4725fe9ca212758 Mon Sep 17 00:00:00 2001 From: Nok Lam Chan Date: Tue, 5 Mar 2024 13:14:01 +0000 Subject: [PATCH 09/10] fix imports Signed-off-by: Nok Lam Chan --- kedro/framework/cli/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/kedro/framework/cli/__init__.py b/kedro/framework/cli/__init__.py index 0c610bde15..8816ace3ea 100644 --- a/kedro/framework/cli/__init__.py +++ b/kedro/framework/cli/__init__.py @@ -1,10 +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 - -__all__ = ["main", "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"] From 520d8dab266e31eafa88a3903b71fb5a32424ae4 Mon Sep 17 00:00:00 2001 From: Nok Date: Wed, 6 Mar 2024 15:16:45 +0000 Subject: [PATCH 10/10] release note Signed-off-by: Nok --- RELEASE.md | 1 + 1 file changed, 1 insertion(+) 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.