Repository: kedro-org/kedro

Issue:
Revisit: Make KedroContext a dataclass and add `config_loader` as a property
# Description
As `KedroSession` now control the lifecycle of Kedro's run, `KedroContext` act like a container and it stores important attributes. Since we now dropped Python 3.6 support, we can make use of Python's `dataclass` to further tidy up `KedroContext`'s constructor code.

todo:
- [x] Make `KedroContext` a dataclass(https://github.com/kedro-org/kedro/pull/1465)
- [x] Add `config_loader` as @property instead of relying the private `context._get_config_loader()` https://github.com/kedro-org/kedro/pull/1505
- [x] Update corresponding document in Starter/Kedro



Discussion:

Resolving patch:
diff --git a/kedro/framework/context/context.py b/kedro/framework/context/context.py
--- a/kedro/framework/context/context.py
+++ b/kedro/framework/context/context.py
@@ -1,11 +1,11 @@
 """This module provides context for Kedro project."""
-
 from copy import deepcopy
 from pathlib import Path, PurePosixPath, PureWindowsPath
 from typing import Any, Dict, Optional, Union
 from urllib.parse import urlparse
 from warnings import warn
 
+from attrs import field, frozen
 from pluggy import PluginManager
 
 from kedro.config import ConfigLoader, MissingConfigException
@@ -159,65 +159,42 @@ def _update_nested_dict(old_dict: Dict[Any, Any], new_dict: Dict[Any, Any]) -> N
                 old_dict[key] = value
 
 
+def _expand_full_path(project_path: Union[str, Path]) -> Path:
+    return Path(project_path).expanduser().resolve()
+
+
+@frozen
 class KedroContext:
     """``KedroContext`` is the base class which holds the configuration and
     Kedro's main functionality.
     """
 
-    def __init__(
-        self,
-        package_name: str,
-        project_path: Union[Path, str],
-        config_loader: ConfigLoader,
-        hook_manager: PluginManager,
-        env: str = None,
-        extra_params: Dict[str, Any] = None,
-    ):  # pylint: disable=too-many-arguments
-        """Create a context object by providing the root of a Kedro project and
-        the environment configuration subfolders
-        (see ``kedro.config.ConfigLoader``)
-
-        Raises:
-            KedroContextError: If there is a mismatch
-                between Kedro project version and package version.
-
-        Args:
-            package_name: Package name for the Kedro project the context is
-                created for.
-            project_path: Project path to define the context for.
-            hook_manager: The ``PluginManager`` to activate hooks, supplied by the session.
-            env: Optional argument for configuration default environment to be used
-                for running the pipeline. If not specified, it defaults to "local".
-            extra_params: Optional dictionary containing extra project parameters.
-                If specified, will update (and therefore take precedence over)
-                the parameters retrieved from the project configuration.
-        """
-        self._project_path = Path(project_path).expanduser().resolve()
-        self._package_name = package_name
-        self._config_loader = config_loader
-        self._env = env
-        self._extra_params = deepcopy(extra_params)
-        self._hook_manager = hook_manager
-
-    @property  # type: ignore
-    def env(self) -> Optional[str]:
-        """Property for the current Kedro environment.
-
-        Returns:
-            Name of the current Kedro environment.
+    _package_name: str
+    project_path: Path = field(converter=_expand_full_path)
+    config_loader: ConfigLoader
+    _hook_manager: PluginManager
+    env: Optional[str] = None
+    _extra_params: Optional[Dict[str, Any]] = field(default=None, converter=deepcopy)
 
-        """
-        return self._env
+    """Create a context object by providing the root of a Kedro project and
+    the environment configuration subfolders (see ``kedro.config.ConfigLoader``)
 
-    @property
-    def project_path(self) -> Path:
-        """Read-only property containing Kedro's root project directory.
-
-        Returns:
-            Project directory.
+    Raises:
+        KedroContextError: If there is a mismatch
+            between Kedro project version and package version.
 
-        """
-        return self._project_path
+    Args:
+        package_name: Package name for the Kedro project the context is
+            created for.
+        project_path: Project path to define the context for.
+        config_loader: Kedro's ``ConfigLoader`` for loading the configuration files.
+        hook_manager: The ``PluginManager`` to activate hooks, supplied by the session.
+        env: Optional argument for configuration default environment to be used
+            for running the pipeline. If not specified, it defaults to "local".
+        extra_params: Optional dictionary containing extra project parameters.
+            If specified, will update (and therefore take precedence over)
+            the parameters retrieved from the project configuration.
+    """
 
     @property
     def catalog(self) -> DataCatalog:
@@ -250,17 +227,6 @@ def params(self) -> Dict[str, Any]:
         _update_nested_dict(params, self._extra_params or {})
         return params
 
-    @property
-    def config_loader(self):
-        """Read-only property referring to Kedro's ``ConfigLoader`` for this
-        context.
-        Returns:
-            Instance of `ConfigLoader`.
-        Raises:
-            KedroContextError: Incorrect ``ConfigLoader`` registered for the project.
-        """
-        return self._config_loader
-
     def _get_catalog(
         self,
         save_version: str = None,


Test patch:
diff --git a/test_requirements.txt b/test_requirements.txt
--- a/test_requirements.txt
+++ b/test_requirements.txt
@@ -13,6 +13,7 @@ dill~=0.3.1
 filelock>=3.4.0, <4.0
 gcsfs>=2021.4, <=2022.1
 geopandas>=0.6.0, <1.0
+h5py>3.5.0
 hdfs>=2.5.8, <3.0
 holoviews~=1.13.0
 import-linter[toml]==1.2.6
diff --git a/tests/framework/context/test_context.py b/tests/framework/context/test_context.py
--- a/tests/framework/context/test_context.py
+++ b/tests/framework/context/test_context.py
@@ -9,6 +9,7 @@
 import pytest
 import toml
 import yaml
+from attrs.exceptions import FrozenInstanceError
 from pandas.util.testing import assert_frame_equal
 
 from kedro import __version__ as kedro_version
@@ -209,6 +210,10 @@ def test_attributes(self, tmp_path, dummy_context):
         assert isinstance(dummy_context.project_path, Path)
         assert dummy_context.project_path == tmp_path.resolve()
 
+    def test_immutable_instance(self, dummy_context):
+        with pytest.raises(FrozenInstanceError):
+            dummy_context.catalog = 1
+
     def test_get_catalog_always_using_absolute_path(self, dummy_context):
         config_loader = dummy_context.config_loader
         conf_catalog = config_loader.get("catalog*")
@@ -222,8 +227,7 @@ def test_get_catalog_always_using_absolute_path(self, dummy_context):
         ds_path = catalog._data_sets["horses"]._filepath
         assert PurePath(ds_path.as_posix()).is_absolute()
         assert (
-            ds_path.as_posix()
-            == (dummy_context._project_path / "horses.csv").as_posix()
+            ds_path.as_posix() == (dummy_context.project_path / "horses.csv").as_posix()
         )
 
     def test_get_catalog_validates_layers(self, dummy_context, mocker):