Skip to content

Commit 55fb343

Browse files
committed
Raise exception when more than 1 run executed within the same session
Signed-off-by: Merel Theisen <merel.theisen@quantumblack.com>
1 parent 1af6cce commit 55fb343

File tree

2 files changed

+88
-1
lines changed

2 files changed

+88
-1
lines changed

kedro/framework/session/session.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ def _jsonify_cli_context(ctx: click.core.Context) -> Dict[str, Any]:
7575
}
7676

7777

78+
class KedroSessionError(Exception):
79+
"""``KedroSessionError`` raised by ``KedroSession``
80+
in case of run failure as part of a session.
81+
"""
82+
83+
pass
84+
85+
7886
class KedroSession:
7987
"""``KedroSession`` is the object that is responsible for managing the lifecycle
8088
of a Kedro run.
@@ -94,18 +102,20 @@ class KedroSession:
94102
>>>
95103
"""
96104

97-
def __init__(
105+
def __init__( # pylint: disable=too-many-arguments
98106
self,
99107
session_id: str,
100108
package_name: str = None,
101109
project_path: Union[Path, str] = None,
102110
save_on_close: bool = False,
111+
run_called: bool = False,
103112
):
104113
self._project_path = Path(project_path or Path.cwd()).resolve()
105114
self.session_id = session_id
106115
self.save_on_close = save_on_close
107116
self._package_name = package_name
108117
self._store = self._init_store()
118+
self._run_called = run_called
109119

110120
hook_manager = _create_hook_manager()
111121
_register_hooks(hook_manager, settings.HOOKS)
@@ -318,6 +328,8 @@ def run( # pylint: disable=too-many-arguments,too-many-locals
318328
defined by `register_pipelines`.
319329
Exception: Any uncaught exception during the run will be re-raised
320330
after being passed to ``on_pipeline_error`` hook.
331+
KedroSessionError: If more than one run is attempted to be executed during
332+
a single session.
321333
Returns:
322334
Any node outputs that cannot be processed by the ``DataCatalog``.
323335
These are returned in a dictionary, where the keys are defined
@@ -327,6 +339,13 @@ def run( # pylint: disable=too-many-arguments,too-many-locals
327339
# Report project name
328340
self._logger.info("** Kedro project %s", self._project_path.name)
329341

342+
if self._run_called:
343+
raise KedroSessionError(
344+
"A run has already been executed as part of the"
345+
" active KedroSession. KedroSession has a 1-1 mapping with"
346+
" runs, and thus only one run should be executed per session."
347+
)
348+
330349
save_version = self.store["session_id"]
331350
extra_params = self.store.get("extra_params") or {}
332351
context = self.load_context()
@@ -380,6 +399,7 @@ def run( # pylint: disable=too-many-arguments,too-many-locals
380399

381400
try:
382401
run_result = runner.run(filtered_pipeline, catalog, hook_manager)
402+
self._run_called = True
383403
except Exception as error:
384404
hook_manager.hook.on_pipeline_error(
385405
error=error,

tests/framework/session/test_session.py

+67
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,73 @@ def test_run(
576576
catalog=mock_catalog,
577577
)
578578

579+
@pytest.mark.usefixtures("mock_settings_context_class")
580+
@pytest.mark.parametrize("fake_pipeline_name", [None, _FAKE_PIPELINE_NAME])
581+
def test_run_multiple_times( # pylint: disable=too-many-locals
582+
self,
583+
fake_project,
584+
fake_session_id,
585+
fake_pipeline_name,
586+
mock_context_class,
587+
mock_package_name,
588+
mocker,
589+
):
590+
"""Test running the project more than once via the session"""
591+
592+
mock_hook = mocker.patch(
593+
"kedro.framework.session.session._create_hook_manager"
594+
).return_value.hook
595+
mock_pipelines = mocker.patch(
596+
"kedro.framework.session.session.pipelines",
597+
return_value={
598+
_FAKE_PIPELINE_NAME: mocker.Mock(),
599+
"__default__": mocker.Mock(),
600+
},
601+
)
602+
mock_context = mock_context_class.return_value
603+
mock_catalog = mock_context._get_catalog.return_value
604+
mock_runner = mocker.Mock()
605+
mock_pipeline = mock_pipelines.__getitem__.return_value.filter.return_value
606+
607+
message = (
608+
"A run has already been executed as part of the active KedroSession. "
609+
"KedroSession has a 1-1 mapping with runs, and thus only one run should be"
610+
" executed per session."
611+
)
612+
with pytest.raises(Exception, match=message):
613+
with KedroSession.create(mock_package_name, fake_project) as session:
614+
session.run(runner=mock_runner, pipeline_name=fake_pipeline_name)
615+
session.run(runner=mock_runner, pipeline_name=fake_pipeline_name)
616+
617+
record_data = {
618+
"session_id": fake_session_id,
619+
"project_path": fake_project.as_posix(),
620+
"env": mock_context.env,
621+
"kedro_version": kedro_version,
622+
"tags": None,
623+
"from_nodes": None,
624+
"to_nodes": None,
625+
"node_names": None,
626+
"from_inputs": None,
627+
"to_outputs": None,
628+
"load_versions": None,
629+
"extra_params": {},
630+
"pipeline_name": fake_pipeline_name,
631+
}
632+
633+
mock_hook.before_pipeline_run.assert_called_once_with(
634+
run_params=record_data, pipeline=mock_pipeline, catalog=mock_catalog
635+
)
636+
mock_runner.run.assert_called_once_with(
637+
mock_pipeline, mock_catalog, session._hook_manager
638+
)
639+
mock_hook.after_pipeline_run.assert_called_once_with(
640+
run_params=record_data,
641+
run_result=mock_runner.run.return_value,
642+
pipeline=mock_pipeline,
643+
catalog=mock_catalog,
644+
)
645+
579646
@pytest.mark.usefixtures("mock_settings_context_class")
580647
def test_run_non_existent_pipeline(self, fake_project, mock_package_name, mocker):
581648
mock_runner = mocker.Mock()

0 commit comments

Comments
 (0)