Repository: kedro-org/kedro

Issue:
Support `%load_ext kedro`
## Description
We have discussion in https://github.com/kedro-org/kedro/issues/1763, the desired options are
`%load_ext kedro` or `%load_ext kedro.ipython`, at the end we settle with `%load_ext kedro.ipython` as we try to avoid Python importing the `ipython` module at the top-level.

## Context
A shorter alias is always appreciated. It's also not just "ipython" now as we introduce some Jupyter features. It feels like an overkill if we separate them into `kedro.ipython` and `kedro.jupyter` as long as we keep the dependencies minimal.

In `kedro` codebase, we should keep using `kedro.ipython`, the `%load_ext kedro` is only an alias for the end user. This makes mocking easier to work with.

![image](https://github.com/user-attachments/assets/a6c64fca-0136-4ba4-b1d4-1c6c1eff393f)

## Possible Implementation
I just realise we can get away with these dependency with a thin wrapper in `kedro/__init__.py`. We can keep `%load_ext kedro.ipython`, or slowly replace this later in 0.20.

I tested this today and it works perfectly fine.

```
def load_ipython_extension(ipython) -> None:
    import kedro.ipython
    kedro.ipython.load_ipython_extension(ipython)
```


Discussion:

Patch:
diff --git a/RELEASE.md b/RELEASE.md
index 49fb42d9..0e132d42 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -4,6 +4,7 @@
 * Implemented `KedroDataCatalog.to_config()` method that converts the catalog instance into a configuration format suitable for serialization.
 * Improve OmegaConfigLoader performance.
 * Replaced `trufflehog` with `detect-secrets` for detecting secrets within a code base.
+* Added support for `%load_ext kedro`.
 
 ## Bug fixes and other changes
 * Added validation to ensure dataset versions consistency across catalog.
diff --git a/kedro/__init__.py b/kedro/__init__.py
index 0a0d0500..4e42f039 100644
--- a/kedro/__init__.py
+++ b/kedro/__init__.py
@@ -3,6 +3,13 @@ data pipelines by providing uniform project templates, data abstraction,
 configuration and pipeline assembly.
 """
 
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from IPython.core.interactiveshell import InteractiveShell
+
 import sys
 import warnings
 
@@ -29,3 +36,9 @@ run Kedro with `python -W "default:Kedro is not yet fully compatible" -m kedro .
 or set the PYTHONWARNINGS environment variable accordingly.""",
         KedroPythonVersionWarning,
     )
+
+
+def load_ipython_extension(ipython: InteractiveShell) -> None:
+    import kedro.ipython
+
+    kedro.ipython.load_ipython_extension(ipython)
diff --git a/kedro/ipython/__init__.py b/kedro/ipython/__init__.py
index 0e479e8a..20578a3e 100644
--- a/kedro/ipython/__init__.py
+++ b/kedro/ipython/__init__.py
@@ -19,6 +19,8 @@ from typing import TYPE_CHECKING, Any, Callable
 if TYPE_CHECKING:
     from collections import OrderedDict
 
+    from IPython.core.interactiveshell import InteractiveShell
+
 from IPython.core.getipython import get_ipython
 from IPython.core.magic import needs_local_scope, register_line_magic
 from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
@@ -50,16 +52,16 @@ FunctionParameters = MappingProxyType
 RICH_INSTALLED = True if importlib.util.find_spec("rich") is not None else False
 
 
-def load_ipython_extension(ipython: Any) -> None:
+def load_ipython_extension(ipython: InteractiveShell) -> None:
     """
     Main entry point when %load_ext kedro.ipython is executed, either manually or
     automatically through `kedro ipython` or `kedro jupyter lab/notebook`.
     IPython will look for this function specifically.
     See https://ipython.readthedocs.io/en/stable/config/extensions/index.html
     """
-    ipython.register_magic_function(magic_reload_kedro, magic_name="reload_kedro")
+    ipython.register_magic_function(func=magic_reload_kedro, magic_name="reload_kedro")  # type: ignore[call-arg]
     logger.info("Registered line magic '%reload_kedro'")
-    ipython.register_magic_function(magic_load_node, magic_name="load_node")
+    ipython.register_magic_function(func=magic_load_node, magic_name="load_node")  # type: ignore[call-arg]
     logger.info("Registered line magic '%load_node'")
 
     if _find_kedro_project(Path.cwd()) is None:
diff --git a/pyproject.toml b/pyproject.toml
index f98d2ebe..eac6b8b4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -178,7 +178,8 @@ layers = [
 ]
 ignore_imports = [
     "kedro.runner.task -> kedro.framework.project",
-    "kedro.framework.hooks.specs -> kedro.framework.context"
+    "kedro.framework.hooks.specs -> kedro.framework.context",
+    "kedro -> kedro.ipython"
 ]
 
 [[tool.importlinter.contracts]]
@@ -188,6 +189,9 @@ modules = [
     "kedro.pipeline",
     "kedro.io"
 ]
+ignore_imports = [
+    "kedro -> kedro.ipython"
+]
 
 [[tool.importlinter.contracts]]
 name = "Config cannot import Runner et al"


Test patch:
diff --git a/tests/ipython/test_ipython.py b/tests/ipython/test_ipython.py
index 4228e9e6..8cc6089f 100644
--- a/tests/ipython/test_ipython.py
+++ b/tests/ipython/test_ipython.py
@@ -222,6 +222,17 @@ class TestLoadIPythonExtension:
         ):
             ipython.magic("reload_kedro --invalid_arg=dummy")
 
+    def test_ipython_kedro_extension_alias(self, mocker, ipython):
+        mock_ipython_extension = mocker.patch(
+            "kedro.ipython.load_ipython_extension", autospec=True
+        )
+        # Ensure that `kedro` is not loaded initially
+        assert "kedro" not in ipython.extension_manager.loaded
+        ipython.magic("load_ext kedro")
+        mock_ipython_extension.assert_called_once_with(ipython)
+        # Ensure that `kedro` extension has been loaded
+        assert "kedro" in ipython.extension_manager.loaded
+
 
 class TestProjectPathResolution:
     def test_only_path_specified(self):