From 57beede6cb464232452890a22fa2d1fd37d8b65e Mon Sep 17 00:00:00 2001 From: Anna Petrasova Date: Fri, 29 May 2026 23:14:15 -0400 Subject: [PATCH 1/4] temporal: add test for tgis.init() across a session change --- ..._temporal_core_init_session_change_test.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 python/grass/temporal/tests/grass_temporal_core_init_session_change_test.py diff --git a/python/grass/temporal/tests/grass_temporal_core_init_session_change_test.py b/python/grass/temporal/tests/grass_temporal_core_init_session_change_test.py new file mode 100644 index 00000000000..968e5040958 --- /dev/null +++ b/python/grass/temporal/tests/grass_temporal_core_init_session_change_test.py @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +"""Regression test for tgis.init() across a GRASS session change. + +tgis.init() spawns message and C-library subprocesses that capture +GISRC at spawn time. When the parent process switches to a different +project (different LOCATION_NAME or GISDBASE) and calls tgis.init() +again, those subprocesses must be restarted; otherwise they remain +bound to the previous session's GISRC, which may have been deleted. +""" + +import os + +import pytest + +from grass.grassdb.create import create_mapset +import grass.script as gs +import grass.temporal as tgis + + +@pytest.fixture +def two_projects(tmp_path): + """Create two empty projects and yield their paths.""" + project_a = tmp_path / "a" + project_b = tmp_path / "b" + gs.create_project(project_a) + create_mapset(project_a / "a") + gs.create_project(project_b) + create_mapset(project_b / "b") + return project_a, project_b + + +def _promote_session_env(session, monkeypatch): + """Copy session.env entries into os.environ so tgis (which reads + os.environ directly) sees the active session. + """ + for key, value in session.env.items(): + monkeypatch.setenv(key, value) + + +def test_init_restarts_subprocesses_on_session_change(two_projects, monkeypatch): + """tgis.init() must respawn its subprocesses after a session change.""" + project_a, project_b = two_projects + with gs.setup.init(project_a / "a", env=os.environ.copy()) as session_a: + _promote_session_env(session_a, monkeypatch) + tgis.init() + first_ciface = tgis.get_tgis_c_library_interface() + assert first_ciface.available_mapsets() == ["a", "PERMANENT"] + + with gs.setup.init(project_b / "b", env=os.environ.copy()) as session_b: + _promote_session_env(session_b, monkeypatch) + tgis.init() + second_ciface = tgis.get_tgis_c_library_interface() + + # The C-library interface must have been replaced. Without this, + # the subprocess is still bound to session A's deleted GISRC and + # the call below fails. + assert second_ciface is not first_ciface + assert second_ciface.available_mapsets() == ["b", "PERMANENT"] From df750fd800f88b19fa0fdf82a21ae36bebc66c15 Mon Sep 17 00:00:00 2001 From: Anna Petrasova Date: Fri, 29 May 2026 23:43:31 -0400 Subject: [PATCH 2/4] restart the c library and messanger interface --- python/grass/temporal/core.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/python/grass/temporal/core.py b/python/grass/temporal/core.py index efec4d8b66d..67d26a8b1d6 100644 --- a/python/grass/temporal/core.py +++ b/python/grass/temporal/core.py @@ -596,6 +596,7 @@ def init(raise_fatal_error: bool = False, skip_db_version_check: bool = False): global raise_on_error # noqa: FURB154 global enable_mapset_check, enable_timestamp_write # noqa: FURB154 global current_mapset, current_location, current_gisdbase # noqa: FURB154 + global message_interface, c_library_interface # noqa: FURB154 raise_on_error = raise_fatal_error @@ -604,10 +605,30 @@ def init(raise_fatal_error: bool = False, skip_db_version_check: bool = False): gs.run_command("t.connect", flags="c") grassenv = gs.gisenv() + new_mapset = grassenv["MAPSET"] + new_location = grassenv["LOCATION_NAME"] + new_gisdbase = grassenv["GISDBASE"] + + # The message and C-library subprocesses inherit GISRC at spawn time, + # so a session change leaves them bound to the old (possibly deleted) + # session. Stop them so the _init_* helpers below respawn them in the + # current environment. + if current_mapset is not None and ( + current_mapset != new_mapset + or current_location != new_location + or current_gisdbase != new_gisdbase + ): + if message_interface is not None: + message_interface.stop() + message_interface = None + if c_library_interface is not None: + c_library_interface.stop() + c_library_interface = None + # Set the global variable for faster access - current_mapset = grassenv["MAPSET"] - current_location = grassenv["LOCATION_NAME"] - current_gisdbase = grassenv["GISDBASE"] + current_mapset = new_mapset + current_location = new_location + current_gisdbase = new_gisdbase # Check environment variable GRASS_TGIS_RAISE_ON_ERROR if ( From d321a9d820fceb15b789356d52bb4de8e190b0b9 Mon Sep 17 00:00:00 2001 From: Anna Petrasova Date: Sat, 30 May 2026 00:08:31 -0400 Subject: [PATCH 3/4] simplify test --- ...s_temporal_core_init_session_change_test.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/python/grass/temporal/tests/grass_temporal_core_init_session_change_test.py b/python/grass/temporal/tests/grass_temporal_core_init_session_change_test.py index 968e5040958..e07fd019014 100644 --- a/python/grass/temporal/tests/grass_temporal_core_init_session_change_test.py +++ b/python/grass/temporal/tests/grass_temporal_core_init_session_change_test.py @@ -9,8 +9,6 @@ bound to the previous session's GISRC, which may have been deleted. """ -import os - import pytest from grass.grassdb.create import create_mapset @@ -30,25 +28,15 @@ def two_projects(tmp_path): return project_a, project_b -def _promote_session_env(session, monkeypatch): - """Copy session.env entries into os.environ so tgis (which reads - os.environ directly) sees the active session. - """ - for key, value in session.env.items(): - monkeypatch.setenv(key, value) - - -def test_init_restarts_subprocesses_on_session_change(two_projects, monkeypatch): +def test_init_restarts_subprocesses_on_session_change(two_projects): """tgis.init() must respawn its subprocesses after a session change.""" project_a, project_b = two_projects - with gs.setup.init(project_a / "a", env=os.environ.copy()) as session_a: - _promote_session_env(session_a, monkeypatch) + with gs.setup.init(project_a / "a"): tgis.init() first_ciface = tgis.get_tgis_c_library_interface() assert first_ciface.available_mapsets() == ["a", "PERMANENT"] - with gs.setup.init(project_b / "b", env=os.environ.copy()) as session_b: - _promote_session_env(session_b, monkeypatch) + with gs.setup.init(project_b / "b"): tgis.init() second_ciface = tgis.get_tgis_c_library_interface() From fc297ec9643f2d3acfb32d1b77b78c379aa1a0b8 Mon Sep 17 00:00:00 2001 From: Anna Petrasova Date: Mon, 1 Jun 2026 15:53:58 -0400 Subject: [PATCH 4/4] test importlib pytest mode --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index fb2681eda28..7ca68e6f4b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -522,6 +522,7 @@ docstring-code-format = true [tool.pytest.ini_options] addopts = """ + --import-mode=importlib --ignore-glob='dist.*' --ignore-glob='bin.*' --ignore-glob='*/tests/data/*'