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:
Hi @noklam , 

I did implement your suggestion and based on the earlier discussions and comments (https://github.com/kedro-org/kedro/pull/1837#pullrequestreview-1099116841 , https://github.com/kedro-org/kedro/pull/1837#pullrequestreview-1099164821) , I think we can close this issue. Also `make lint` throws errors with breaking contracts as mentioned by Antony [here](https://github.com/kedro-org/kedro/pull/1837#issue-1364582173). Wdyt ?

Thank you
@ravi-kumar-pilla Is there a PR/branch link to this or are you suggesting to close it without implementing it?

The previous consideration as linked in your comments are dependencies problem, i.e. we want to avoid this as a top-level module because any of the the following will import `kedro/__init__.py`
- `import kedro`
- `import kedro.framework`
- `import kedro.ipython`

Thus we choose to implement to keep the implementation in `kedro/ipython/__init__.py` to keep it scoped. As a result, we currently requires user to use `%load_ext kedro.ipython` instead of the preferred `%load_ext kedro`.

The modification that I suggest keeps the implementation in the same file, but add an additional shortcut in `kedro/__init__.py`. This should not cause any dependencies issue since the imports are inside a function. It may cause the linter to complain, but I think it is a fair case to suppress it is a function called by `IPython` but not referred anywhere inside `kedro` (will be nice if there are ways to enforce this contract, but I am not too worry about this).


> The modification that I suggest keeps the implementation in the same file, but add an additional shortcut in `kedro/__init__.py`. This should not cause any dependencies issue since the imports are inside a function. It may cause the linter to complain, but I think it is a fair case to suppress it is a function called by `IPython` but not referred anywhere inside `kedro` (will be nice if there are ways to enforce this contract, but I am not too worry about this).

Oh I see. Thanks for the explanation. I did understand the main issue and I thought the linter issue mentioned by Antony (regarding the contracts) was overlooked. Since `tool.importlinter.contracts` was complaining the same, I thought it is not a good practice and we can close this issue. I will open a PR in some time with your suggestion suppressing the linter. Thank you




Patch:
diff --git a/RELEASE.md b/RELEASE.md
index 49fb42d94c..0e132d4275 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 0a0d050098..4e42f039d8 100644
--- a/kedro/__init__.py
+++ b/kedro/__init__.py
@@ -3,6 +3,13 @@
 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 @@ class KedroPythonVersionWarning(UserWarning):
 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 0e479e8a68..20578a3e7c 100644
--- a/kedro/ipython/__init__.py
+++ b/kedro/ipython/__init__.py
@@ -19,6 +19,8 @@
 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 @@
 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 f98d2ebef5..eac6b8b4c8 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 4228e9e6ae..8cc6089fd1 100644
--- a/tests/ipython/test_ipython.py
+++ b/tests/ipython/test_ipython.py
@@ -222,6 +222,17 @@ def test_line_magic_with_invalid_arguments(self, mocker, ipython):
         ):
             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):