Source code for kedro.framework.cli.cli

"""kedro is a CLI for managing Kedro projects.

This module implements commands available from the kedro CLI.
"""
from __future__ import annotations

import importlib
import sys
import traceback
from collections import defaultdict
from pathlib import Path
from typing import Any, Sequence

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
from kedro.framework.cli.micropkg import micropkg_cli
from kedro.framework.cli.pipeline import pipeline_cli
from kedro.framework.cli.project import project_group
from kedro.framework.cli.registry import registry_cli
from kedro.framework.cli.starters import create_cli
from kedro.framework.cli.utils import (
    CONTEXT_SETTINGS,
    ENTRY_POINT_GROUPS,
    CommandCollection,
    KedroCliError,
    _get_entry_points,
    load_entry_points,
)
from kedro.framework.project import LOGGING  # noqa: F401
from kedro.framework.startup import bootstrap_project
from kedro.utils import _find_kedro_project, _is_project

LOGO = rf"""
 _            _
| | _____  __| |_ __ ___
| |/ / _ \/ _` | '__/ _ \
|   <  __/ (_| | | | (_) |
|_|\_\___|\__,_|_|  \___/
v{version}
"""


@click.group(context_settings=CONTEXT_SETTINGS, name="Kedro")
@click.version_option(version, "--version", "-V", help="Show version and exit")
def cli() -> None:  # pragma: no cover
    """Kedro is a CLI for creating and using Kedro projects. For more
    information, type ``kedro info``.

    """
    pass


@cli.command()
def info() -> None:
    """Get more information about kedro."""
    click.secho(LOGO, fg="green")
    click.echo(
        "Kedro is a Python framework for\n"
        "creating reproducible, maintainable\n"
        "and modular data science code."
    )

    plugin_versions = {}
    plugin_entry_points = defaultdict(set)
    for plugin_entry_point in ENTRY_POINT_GROUPS:
        for entry_point in _get_entry_points(plugin_entry_point):
            module_name = entry_point.module.split(".")[0]
            plugin_versions[module_name] = entry_point.dist.version
            plugin_entry_points[module_name].add(plugin_entry_point)

    click.echo()
    if plugin_versions:
        click.echo("Installed plugins:")
        for plugin_name, plugin_version in sorted(plugin_versions.items()):
            entrypoints_str = ",".join(sorted(plugin_entry_points[plugin_name]))
            click.echo(
                f"{plugin_name}: {plugin_version} (entry points:{entrypoints_str})"
            )
    else:
        click.echo("No plugins installed")


def _init_plugins() -> None:
    init_hooks = load_entry_points("init")
    for init_hook in init_hooks:
        init_hook()


[docs] class KedroCLI(CommandCollection): """A CommandCollection class to encapsulate the KedroCLI command loading. """ def __init__(self, project_path: Path): self._metadata = None # running in package mode if _is_project(project_path): self._metadata = bootstrap_project(project_path) self._cli_hook_manager = get_cli_hook_manager() super().__init__( ("Global commands", self.global_groups), ("Project specific commands", self.project_groups), ) def main( self, args: Any | None = None, prog_name: Any | None = None, complete_var: Any | None = None, standalone_mode: bool = True, **extra: Any, ) -> Any: if self._metadata: extra.update(obj=self._metadata) # This is how click's internals parse sys.argv, which include the command, # subcommand, arguments and options. click doesn't store this information anywhere # so we have to re-do it. args = sys.argv[1:] if args is None else list(args) self._cli_hook_manager.hook.before_command_run( project_metadata=self._metadata, command_args=args ) try: super().main( args=args, prog_name=prog_name, complete_var=complete_var, standalone_mode=standalone_mode, **extra, ) # 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 def global_groups(self) -> Sequence[click.MultiCommand]: """Property which loads all global command groups from plugins and combines them with the built-in ones (eventually overriding the built-in ones if they are redefined by plugins). """ return [cli, create_cli, *load_entry_points("global")] @property def project_groups(self) -> Sequence[click.MultiCommand]: """Property which loads all project command groups from the project and the plugins, then combines them with the built-in ones. Built-in commands can be overridden by plugins, which can be overridden by a custom project cli.py. See https://kedro.readthedocs.io/en/stable/extend_kedro/common_use_cases.html#use-case-3-how-to-add-or-modify-cli-commands on how to add this. """ if not self._metadata: return [] built_in = [ catalog_cli, jupyter_cli, pipeline_cli, micropkg_cli, project_group, registry_cli, ] plugins = load_entry_points("project") try: project_cli = importlib.import_module(f"{self._metadata.package_name}.cli") # fail gracefully if cli.py does not exist except ModuleNotFoundError: # return only built-in commands and commands from plugins # (plugins can override built-in commands) return [*built_in, *plugins] # fail badly if cli.py exists, but has no `cli` in it if not hasattr(project_cli, "cli"): raise KedroCliError( f"Cannot load commands from {self._metadata.package_name}.cli" ) user_defined = project_cli.cli # return built-in commands, plugin commands and user defined commands # (overriding happens as follows built-in < plugins < cli.py) return [*built_in, *plugins, user_defined]
[docs] def main() -> None: # pragma: no cover """Main entry point. Look for a ``cli.py``, and, if found, add its commands to `kedro`'s before invoking the CLI. """ _init_plugins() cli_collection = KedroCLI( project_path=_find_kedro_project(Path.cwd()) or Path.cwd() ) cli_collection()