Repository: kedro-org/kedro

Issue:
Adding a new `after_context_created` hook into Kedro's hooks collection
# Description
Related: #506 
Prerequisite of: #1431 
The new hook `after_context_created` will be the solution for #506, by exposing the `context`, it will provide a public API for developers to access `catalog`, `config_loader`, `credentials` etc. The `KedroContext` will gradually become a container while the `KedroSession` controls the lifecycle of Kedro's run


todo:
- [ ] Create new `after_context_created` hook
- [ ] Update Runners
- [ ] Update corresponding tests
- [ ] Add the lifecycle hooks chart to illustrate the order of hook being executed (seems datasets specs is missing from test previously)


Discussion:
@Raalsky @takikadiri Tagging you here since it may be a new useful hook for your plugins. Let us know if you are happy with the change.

Resolving patch:
diff --git a/kedro/framework/hooks/manager.py b/kedro/framework/hooks/manager.py
--- a/kedro/framework/hooks/manager.py
+++ b/kedro/framework/hooks/manager.py
@@ -7,7 +7,13 @@
 from pluggy import PluginManager
 
 from .markers import HOOK_NAMESPACE
-from .specs import DataCatalogSpecs, DatasetSpecs, NodeSpecs, PipelineSpecs
+from .specs import (
+    DataCatalogSpecs,
+    DatasetSpecs,
+    KedroContextSpecs,
+    NodeSpecs,
+    PipelineSpecs,
+)
 
 _PLUGIN_HOOKS = "kedro.hooks"  # entry-point to load hooks from for installed plugins
 
@@ -21,6 +27,7 @@ def _create_hook_manager() -> PluginManager:
     manager.add_hookspecs(PipelineSpecs)
     manager.add_hookspecs(DataCatalogSpecs)
     manager.add_hookspecs(DatasetSpecs)
+    manager.add_hookspecs(KedroContextSpecs)
     return manager
 
 
diff --git a/kedro/framework/hooks/specs.py b/kedro/framework/hooks/specs.py
--- a/kedro/framework/hooks/specs.py
+++ b/kedro/framework/hooks/specs.py
@@ -4,6 +4,7 @@
 """
 from typing import Any, Dict, Optional
 
+from kedro.framework.context import KedroContext
 from kedro.io import DataCatalog
 from kedro.pipeline import Pipeline
 from kedro.pipeline.node import Node
@@ -279,3 +280,19 @@ def after_dataset_saved(self, dataset_name: str, data: Any) -> None:
             data: the actual data that was saved to the catalog.
         """
         pass
+
+
+class KedroContextSpecs:
+    """Namespace that defines all specifications for a Kedro context's lifecycle hooks."""
+
+    @hook_spec
+    def after_context_created(
+        self,
+        context: KedroContext,
+    ) -> None:
+        """Hooks to be invoked after a `KedroContext` is created. This is the earliest
+        hook triggered within a Kedro run. The `KedroContext` stores useful information
+        such as `credentials`, `config_loader` and `env`.
+        Args:
+            context: The context that was created.
+        """
diff --git a/kedro/framework/session/session.py b/kedro/framework/session/session.py
--- a/kedro/framework/session/session.py
+++ b/kedro/framework/session/session.py
@@ -242,6 +242,10 @@ def load_context(self) -> KedroContext:
             extra_params=extra_params,
             hook_manager=self._hook_manager,
         )
+        self._hook_manager.hook.after_context_created(  # pylint: disable=no-member
+            context=context
+        )
+
         return context
 
     def _get_config_loader(self) -> ConfigLoader:
@@ -377,8 +381,8 @@ def run(  # pylint: disable=too-many-arguments,too-many-locals
         )
 
         # Run the runner
-        runner = runner or SequentialRunner()
         hook_manager = self._hook_manager
+        runner = runner or SequentialRunner()
         hook_manager.hook.before_pipeline_run(  # pylint: disable=no-member
             run_params=record_data, pipeline=filtered_pipeline, catalog=catalog
         )


Test patch:
diff --git a/tests/framework/hooks/test_manager.py b/tests/framework/hooks/test_manager.py
--- a/tests/framework/hooks/test_manager.py
+++ b/tests/framework/hooks/test_manager.py
@@ -1,12 +1,19 @@
 import pytest
 
 from kedro.framework.hooks.manager import _create_hook_manager, _NullPluginManager
-from kedro.framework.hooks.specs import DataCatalogSpecs, NodeSpecs, PipelineSpecs
+from kedro.framework.hooks.specs import (
+    DataCatalogSpecs,
+    DatasetSpecs,
+    KedroContextSpecs,
+    NodeSpecs,
+    PipelineSpecs,
+)
 
 
 @pytest.mark.parametrize(
     "hook_specs,hook_name,hook_params",
     [
+        (KedroContextSpecs, "after_context_created", ("context")),
         (
             DataCatalogSpecs,
             "after_catalog_created",
@@ -41,6 +48,10 @@
             "on_pipeline_error",
             ("error", "run_params", "pipeline", "catalog"),
         ),
+        (DatasetSpecs, "before_dataset_loaded", ("dataset_name")),
+        (DatasetSpecs, "after_dataset_loaded", ("dataset_name", "data")),
+        (DatasetSpecs, "before_dataset_saved", ("dataset_name", "data")),
+        (DatasetSpecs, "after_dataset_saved", ("dataset_name", "data")),
     ],
 )
 def test_hook_manager_can_call_hooks_defined_in_specs(
diff --git a/tests/framework/session/conftest.py b/tests/framework/session/conftest.py
--- a/tests/framework/session/conftest.py
+++ b/tests/framework/session/conftest.py
@@ -11,6 +11,7 @@
 from dynaconf.validator import Validator
 
 from kedro import __version__ as kedro_version
+from kedro.framework.context.context import KedroContext
 from kedro.framework.hooks import hook_impl
 from kedro.framework.project import (
     _ProjectPipelines,
@@ -337,6 +338,10 @@ def after_dataset_saved(self, dataset_name: str, data: Any) -> None:
             "After dataset saved", extra={"dataset_name": dataset_name, "data": data}
         )
 
+    @hook_impl
+    def after_context_created(self, context: KedroContext) -> None:
+        logger.info("After context created", extra={"context": context})
+
 
 @pytest.fixture
 def project_hooks():
diff --git a/tests/framework/session/test_session_extension_hooks.py b/tests/framework/session/test_session_extension_hooks.py
--- a/tests/framework/session/test_session_extension_hooks.py
+++ b/tests/framework/session/test_session_extension_hooks.py
@@ -560,3 +560,16 @@ def test_after_dataset_load_hook_async(
         assert str(
             ["Before dataset loaded", "Catalog load", "After dataset loaded"]
         ).strip("[]") in str(hooks_log_messages).strip("[]")
+
+
+class TestKedroContextSpecsHook:
+    """Test the behavior of `after_context_created` when updating node inputs."""
+
+    def test_after_context_created_hook(self, mock_session, caplog):
+        context = mock_session.load_context()
+        relevant_records = [
+            r for r in caplog.records if r.getMessage() == "After context created"
+        ]
+        assert len(relevant_records) == 1
+        record = relevant_records[0]
+        assert record.context is context