Repository: kedro-org/kedro
Pull request: Remove the `init_kedro` line magic from the Kedro IPython extension
## Description
<!-- Why was this PR created? -->
Fixes https://github.com/kedro-org/kedro/issues/1336

`reload_kedro` called with a path now also sets that path as default. `reload_kedro` called on its own relies on the previously set default. The very first default is either `None` (if the extension was loaded in a notebook started in a non-kedro location) or the path to a kedro project recursively discovered from cwd (through `_find_kedro_project`).

## Development notes
<!-- What have you changed, and how has this been tested? -->
* Removed `startup_path` global
* Renamed `project_path` to `default_project_path` so that it's more obvious why we use if everywhere instead of `path`
* Removed `init_kedro` and merged its defaulting mechanism into `reload_kedro`

## Testing
Running 

```ipython
%load_ext kedro.extras.extensions.ipython
```

in a location that's not a kedro project gives the following output

![Screenshot 2022-03-15 at 16 42 32](https://user-images.githubusercontent.com/6642900/158428563-b3e751c8-f80f-4cc5-954a-70de35af1f33.png)

Running `%reload_kedro <path_to_a_non_kedro_project>` (and `%reload_kedro` after that in the same session) gives the following error:

```
RuntimeError: Could not find the project configuration file 'pyproject.toml' in /Users/Lorena_balan/Code/playground. If you have created your project with Kedro version <0.17.0, make sure to update your project template. See https://github.com/kedro-org/kedro/blob/main/RELEASE.md#migration-guide-from-kedro-016-to-kedro-0170 for how to migrate your Kedro project.

```


## Checklist

- [x] Read the [contributing](https://github.com/kedro-org/kedro/blob/main/CONTRIBUTING.md) guidelines
- [x] Opened this PR as a 'Draft Pull Request' if it is work-in-progress
- [x] Updated the documentation to reflect the code changes
- [x] Added a description of this change in the [`RELEASE.md`](https://github.com/kedro-org/kedro/blob/main/RELEASE.md) file
- [x] Added tests to cover my changes


<a href="https://gitpod.io/#https://github.com/kedro-org/kedro/pull/1349"><img src="https://gitpod.io/button/open-in-gitpod.svg"/></a>



Problem statement:
Remove the `init_kedro` line magic from the Kedro IPython extension
Currently the Kedro IPython extension has two line magic commands, `reload_kedro` and `init_kedro`. The way their are implemented makes `init_kedro` useless and entirely subsumed by `reload_kedro`, so we should drop it. When doing this, we should make sure:

- All `init_kedro` references in the docs are replaced with `reload_kedro`
- All `init_kedro` use cases are still possible with `reload_kedro`

Hints from issue discussion:

Commit message: Remove global

Signed-off-by: lorenabalan <lorena.balan@quantumblack.com>

Patch to review:
diff --git a/RELEASE.md b/RELEASE.md
index f106f35b9e..bf0c53d2cc 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -79,6 +79,7 @@
 * Added the `PluginManager` `hook_manager` argument to `KedroContext` and the `Runner.run()` method, which will be provided by the `KedroSession`.
 * Removed the public method `get_hook_manager()` and replaced its functionality by `_create_hook_manager()`.
 * Enforced that only one run can be successfully executed as part of a `KedroSession`. `run_id` has been renamed to `session_id` as a result of that.
+* Removed `%init_kedro` IPython line magic, with its functionality incorporated into `%reload_kedro`. This means that if `%reload_kedro` is called with a filepath, that will be set as default for subsequent calls.
 
 ## Thanks for supporting contributions
 
diff --git a/docs/source/tools_integration/ipython.md b/docs/source/tools_integration/ipython.md
index 2ab8e97023..f41bc6f4e2 100644
--- a/docs/source/tools_integration/ipython.md
+++ b/docs/source/tools_integration/ipython.md
@@ -275,7 +275,7 @@ In [1]: %load_ext kedro.extras.extensions.ipython
 ```
 
 When you start an `ipython` shell outside a project root and load the extension the variables won't be loaded.
-Run `%reload_kedro <path_to_project_root>` to get the variables, or `%init_kedro <path_to_project_root>` to set the project path for subsequent calls and then call simply `%reload_kedro` after that without having to specify the path.
+Run `%reload_kedro <path_to_project_root>` to get the variables, and to set the project path for subsequent calls. You can then call just `%reload_kedro` without having to specify the path.
 
 ```ipython
 In [1]: %load_ext kedro.extras.extensions.ipython
@@ -286,7 +286,7 @@ or
 
 ```ipython
 In [1]: %load_ext kedro.extras.extensions.ipython
-In [2]: %init_kedro <path_to_project_root>
+In [2]: %reload_kedro <path_to_project_root>
 In [3]: %reload_kedro
 ```
 
diff --git a/kedro/extras/extensions/ipython.py b/kedro/extras/extensions/ipython.py
index 4c2fe098fe..7cfef1f6e6 100644
--- a/kedro/extras/extensions/ipython.py
+++ b/kedro/extras/extensions/ipython.py
@@ -11,8 +11,7 @@
 from IPython import get_ipython
 from IPython.core.magic import needs_local_scope, register_line_magic
 
-startup_path = Path.cwd()
-project_path = startup_path
+default_project_path = Path.cwd()
 
 
 def _remove_cached_modules(package_name):
@@ -23,7 +22,7 @@ def _remove_cached_modules(package_name):
         del sys.modules[module]  # pragma: no cover
 
 
-def _find_kedro_project(current_dir):  # pragma: no cover
+def _find_kedro_project(current_dir: Path):  # pragma: no cover
     from kedro.framework.startup import _is_project
 
     while current_dir != current_dir.parent:
@@ -34,8 +33,12 @@ def _find_kedro_project(current_dir):  # pragma: no cover
     return None
 
 
-def reload_kedro(path, env: str = None, extra_params: Dict[str, Any] = None):
-    """Line magic which reloads all Kedro default variables."""
+def reload_kedro(
+    path: str = None, env: str = None, extra_params: Dict[str, Any] = None
+):
+    """Line magic which reloads all Kedro default variables.
+    Setting the path will also make it default for subsequent calls.
+    """
 
     import kedro.config.default_logger  # noqa: F401 # pylint: disable=unused-import
     from kedro.framework.cli import load_entry_points
@@ -44,17 +47,26 @@ def reload_kedro(path, env: str = None, extra_params: Dict[str, Any] = None):
     from kedro.framework.session.session import _activate_session
     from kedro.framework.startup import bootstrap_project
 
-    path = path or project_path
-    metadata = bootstrap_project(path)
+    # If a path is provided, set it as default for subsequent calls
+    global default_project_path
+    if path:
+        default_project_path = Path(path).expanduser().resolve()
+        logging.info("Updated path to Kedro project: %s", str(default_project_path))
+    else:
+        logging.info(
+            "No path argument was provided. Using: %s", str(default_project_path)
+        )
+
+    metadata = bootstrap_project(default_project_path)
 
     _remove_cached_modules(metadata.package_name)
 
     configure_project(metadata.package_name)
     session = KedroSession.create(
-        metadata.package_name, path, env=env, extra_params=extra_params
+        metadata.package_name, default_project_path, env=env, extra_params=extra_params
     )
     _activate_session(session, force=True)
-    logging.debug("Loading the context from %s", str(path))
+    logging.debug("Loading the context from %s", str(default_project_path))
     context = session.load_context()
     catalog = context.catalog
 
@@ -77,35 +89,21 @@ def reload_kedro(path, env: str = None, extra_params: Dict[str, Any] = None):
         logging.info("Registered line magic `%s`", line_magic.__name__)  # type: ignore
 
 
-def init_kedro(path=""):
-    """Line magic to set path to Kedro project.
-    `%reload_kedro` will default to this location.
-    """
-    global project_path
-    if path:
-        project_path = Path(path).expanduser().resolve()
-        logging.info("Updated path to Kedro project: %s", str(project_path))
-    else:
-        logging.info("No path argument was provided. Using: %s", str(project_path))
-
-
 def load_ipython_extension(ipython):
     """Main entry point when %load_ext is executed"""
 
-    global project_path
-    global startup_path  # pylint:disable=global-variable-not-assigned
+    global default_project_path
 
-    ipython.register_magic_function(init_kedro, "line")
     ipython.register_magic_function(reload_kedro, "line", "reload_kedro")
 
-    project_path = _find_kedro_project(startup_path)
+    default_project_path = _find_kedro_project(Path.cwd())
 
     try:
-        reload_kedro(project_path)
+        reload_kedro(default_project_path)
     except (ImportError, ModuleNotFoundError):
         logging.error("Kedro appears not to be installed in your current environment.")
     except Exception:  # pylint: disable=broad-except
         logging.warning(
-            "Kedro extension was registered. Make sure you pass the project path to "
-            "`%reload_kedro` or set it using `%init_kedro`."
+            "Kedro extension was registered but couldn't find a Kedro project. "
+            "Make sure you run `%reload_kedro <path_to_kedro_project>`."
         )
diff --git a/tests/extras/extensions/test_ipython.py b/tests/extras/extensions/test_ipython.py
index 1ffd973c0a..6771c04b01 100644
--- a/tests/extras/extensions/test_ipython.py
+++ b/tests/extras/extensions/test_ipython.py
@@ -1,11 +1,7 @@
 # pylint: disable=import-outside-toplevel,reimported
 import pytest
 
-from kedro.extras.extensions.ipython import (
-    init_kedro,
-    load_ipython_extension,
-    reload_kedro,
-)
+from kedro.extras.extensions.ipython import load_ipython_extension, reload_kedro
 from kedro.framework.session.session import _deactivate_session
 from kedro.framework.startup import ProjectMetadata
 from kedro.pipeline import Pipeline
@@ -14,7 +10,7 @@
 @pytest.fixture(autouse=True)
 def project_path(mocker, tmp_path):
     path = tmp_path
-    mocker.patch("kedro.extras.extensions.ipython.project_path", path)
+    mocker.patch("kedro.extras.extensions.ipython.default_project_path", path)
 
 
 @pytest.fixture(autouse=True)
@@ -30,40 +26,6 @@ def cleanup_session():
     _deactivate_session()
 
 
-class TestInitKedro:
-    def test_init_kedro(self, tmp_path, caplog):
-        from kedro.extras.extensions.ipython import project_path
-
-        assert project_path == tmp_path
-
-        kedro_path = tmp_path / "here"
-        init_kedro(str(kedro_path))
-        expected_path = kedro_path.expanduser().resolve()
-        expected_message = f"Updated path to Kedro project: {expected_path}"
-
-        log_messages = [record.getMessage() for record in caplog.records]
-        assert expected_message in log_messages
-        from kedro.extras.extensions.ipython import project_path
-
-        # make sure global variable updated
-        assert project_path == expected_path
-
-    def test_init_kedro_no_path(self, tmp_path, caplog):
-        from kedro.extras.extensions.ipython import project_path
-
-        assert project_path == tmp_path
-
-        init_kedro()
-        expected_message = f"No path argument was provided. Using: {tmp_path}"
-
-        log_messages = [record.getMessage() for record in caplog.records]
-        assert expected_message in log_messages
-        from kedro.extras.extensions.ipython import project_path
-
-        # make sure global variable stayed the same
-        assert project_path == tmp_path
-
-
 @pytest.fixture()
 def pipeline_cleanup():
     yield
@@ -74,7 +36,15 @@ def pipeline_cleanup():
 
 class TestLoadKedroObjects:
     @pytest.mark.usefixtures("pipeline_cleanup")
-    def test_load_kedro_objects(self, tmp_path, mocker):
+    def test_load_kedro_objects(
+        self, tmp_path, mocker, caplog
+    ):  # pylint: disable=too-many-locals
+        from kedro.extras.extensions.ipython import default_project_path
+
+        assert default_project_path == tmp_path
+
+        kedro_path = tmp_path / "here"
+
         fake_metadata = ProjectMetadata(
             source_dir=tmp_path / "src",  # default
             config_file=tmp_path / "pyproject.toml",
@@ -96,8 +66,7 @@ def my_register_pipeline():
         mocker.patch("kedro.framework.session.session.validate_settings")
         mocker.patch("kedro.framework.session.KedroSession._get_config_loader")
         mocker.patch(
-            "kedro.framework.startup.bootstrap_project",
-            return_value=fake_metadata,
+            "kedro.framework.startup.bootstrap_project", return_value=fake_metadata
         )
         mock_line_magic = mocker.MagicMock()
         mock_line_magic.__name__ = "abc"
@@ -110,7 +79,7 @@ def my_register_pipeline():
         mock_context = mocker.patch("kedro.framework.session.KedroSession.load_context")
         mock_ipython = mocker.patch("kedro.extras.extensions.ipython.get_ipython")
 
-        reload_kedro(tmp_path)
+        reload_kedro(kedro_path)
 
         mock_ipython().push.assert_called_once_with(
             variables={
@@ -122,6 +91,16 @@ def my_register_pipeline():
         )
         mock_register_line_magic.assert_called_once()
 
+        expected_path = kedro_path.expanduser().resolve()
+        expected_message = f"Updated path to Kedro project: {expected_path}"
+
+        log_messages = [record.getMessage() for record in caplog.records]
+        assert expected_message in log_messages
+        from kedro.extras.extensions.ipython import default_project_path
+
+        # make sure global variable updated
+        assert default_project_path == expected_path
+
     def test_load_kedro_objects_extra_args(self, tmp_path, mocker):
         fake_metadata = ProjectMetadata(
             source_dir=tmp_path / "src",  # default
@@ -133,8 +112,7 @@ def test_load_kedro_objects_extra_args(self, tmp_path, mocker):
         )
         mocker.patch("kedro.framework.project.configure_project")
         mocker.patch(
-            "kedro.framework.startup.bootstrap_project",
-            return_value=fake_metadata,
+            "kedro.framework.startup.bootstrap_project", return_value=fake_metadata
         )
         mock_line_magic = mocker.MagicMock()
         mock_line_magic.__name__ = "abc"
@@ -166,8 +144,7 @@ def test_load_kedro_objects_extra_args(self, tmp_path, mocker):
 
     def test_load_kedro_objects_not_in_kedro_project(self, tmp_path, mocker):
         mocker.patch(
-            "kedro.framework.startup._get_project_metadata",
-            side_effect=RuntimeError,
+            "kedro.framework.startup._get_project_metadata", side_effect=RuntimeError
         )
         mock_ipython = mocker.patch("kedro.extras.extensions.ipython.get_ipython")
 
@@ -176,6 +153,55 @@ def test_load_kedro_objects_not_in_kedro_project(self, tmp_path, mocker):
         assert not mock_ipython().called
         assert not mock_ipython().push.called
 
+    @pytest.mark.usefixtures("pipeline_cleanup")
+    def test_load_kedro_objects_no_path(self, tmp_path, caplog, mocker):
+        from kedro.extras.extensions.ipython import default_project_path
+
+        assert default_project_path == tmp_path
+
+        fake_metadata = ProjectMetadata(
+            source_dir=tmp_path / "src",  # default
+            config_file=tmp_path / "pyproject.toml",
+            package_name="fake_package_name",
+            project_name="fake_project_name",
+            project_version="0.1",
+            project_path=tmp_path,
+        )
+        my_pipelines = {"ds": Pipeline([])}
+
+        def my_register_pipeline():
+            return my_pipelines
+
+        mocker.patch(
+            "kedro.framework.project._ProjectPipelines._get_pipelines_registry_callable",
+            return_value=my_register_pipeline,
+        )
+        mocker.patch("kedro.framework.project.settings.configure")
+        mocker.patch("kedro.framework.session.session.validate_settings")
+        mocker.patch("kedro.framework.session.KedroSession._get_config_loader")
+        mocker.patch(
+            "kedro.framework.startup.bootstrap_project", return_value=fake_metadata
+        )
+        mock_line_magic = mocker.MagicMock()
+        mock_line_magic.__name__ = "abc"
+        mocker.patch(
+            "kedro.framework.cli.load_entry_points", return_value=[mock_line_magic]
+        )
+        mocker.patch("kedro.extras.extensions.ipython.register_line_magic")
+        mocker.patch("kedro.framework.session.KedroSession.load_context")
+        mocker.patch("kedro.extras.extensions.ipython.get_ipython")
+
+        reload_kedro()
+
+        expected_message = f"No path argument was provided. Using: {tmp_path}"
+        log_messages = [record.getMessage() for record in caplog.records]
+        assert expected_message in log_messages
+
+        from kedro.extras.extensions.ipython import default_project_path
+
+        # make sure global variable stayed the same
+        assert default_project_path == tmp_path
+
 
 class TestLoadIPythonExtension:
     @pytest.mark.parametrize(
@@ -188,8 +214,8 @@ class TestLoadIPythonExtension:
             ),
             (
                 RuntimeError,
-                "Kedro extension was registered. Make sure you pass the project path to "
-                "`%reload_kedro` or set it using `%init_kedro`.",
+                "Kedro extension was registered but couldn't find a Kedro project. "
+                "Make sure you run `%reload_kedro <path_to_kedro_project>`.",
                 "WARNING",
             ),
         ],
@@ -197,10 +223,7 @@ class TestLoadIPythonExtension:
     def test_load_extension_not_in_kedro_env_or_project(
         self, error, expected_log_message, level, mocker, caplog
     ):
-        mocker.patch(
-            "kedro.framework.startup._get_project_metadata",
-            side_effect=error,
-        )
+        mocker.patch("kedro.framework.startup._get_project_metadata", side_effect=error)
         mock_ipython = mocker.patch("kedro.extras.extensions.ipython.get_ipython")
 
         load_ipython_extension(mocker.MagicMock())


Code review comments:
diff_hunk: @@ -44,17 +47,26 @@ def reload_kedro(path, env: str = None, extra_params: Dict[str, Any] = None):
     from kedro.framework.session.session import _activate_session
     from kedro.framework.startup import bootstrap_project
 
-    path = path or project_path
-    metadata = bootstrap_project(path)
+    # If a path is provided, set it as default for subsequent calls
+    global default_project_path
+    if path:
+        default_project_path = Path(path).expanduser().resolve()
+        logging.info("Updated path to Kedro project: %s", str(default_project_path))
+    else:
+        logging.info(
+            "No path argument was provided. Using: %s", str(default_project_path)
path: kedro/extras/extensions/ipython.py
text: @user1:
Extremely minor 😅  I suspect these `str` calls could be removed as the logging `%s` would coerce to string anyway (done in several places throughout file).