From 7bd41fc54b5ed4f88bf226ef4ad8bb473f89f257 Mon Sep 17 00:00:00 2001 From: Saket Kumar Mall Date: Mon, 2 Feb 2026 20:25:54 +0530 Subject: [PATCH 1/5] Add ADDONS check --- python/grass/script/core.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/python/grass/script/core.py b/python/grass/script/core.py index 6ee8e0f987f..673c2b012cc 100644 --- a/python/grass/script/core.py +++ b/python/grass/script/core.py @@ -234,8 +234,14 @@ def scan(gisbase, directory): else: cmd.append(fname) - for directory in ("bin", "scripts"): - scan(gisbase, directory) + # Add addon path if it exists + search_paths = [gisbase] + if env.get("GRASS_ADDON_BASE"): + search_paths.append(env["GRASS_ADDON_BASE"]) + + for base_dir in search_paths: + for directory in ("bin", "scripts"): + scan(base_dir, directory) return set(cmd), scripts From cdf8fa37f4a386ca01ca811d6d65e3872c7dbecb Mon Sep 17 00:00:00 2001 From: Saket Kumar Mall Date: Tue, 3 Feb 2026 11:43:01 +0530 Subject: [PATCH 2/5] Add check for addon paths --- python/grass/script/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/grass/script/core.py b/python/grass/script/core.py index 673c2b012cc..52fdf4868ea 100644 --- a/python/grass/script/core.py +++ b/python/grass/script/core.py @@ -238,6 +238,8 @@ def scan(gisbase, directory): search_paths = [gisbase] if env.get("GRASS_ADDON_BASE"): search_paths.append(env["GRASS_ADDON_BASE"]) + if env.get("GRASS_ADDON_PATH"): + search_paths.extend([p for p in env["GRASS_ADDON_PATH"].split(os.pathsep) if p]) for base_dir in search_paths: for directory in ("bin", "scripts"): From 0e44f4a38a67bd2d5f0098cdc21e092612c3e2ac Mon Sep 17 00:00:00 2001 From: Saket Kumar Mall Date: Fri, 6 Feb 2026 15:00:59 +0530 Subject: [PATCH 3/5] Add tests --- .../grass_script_core_get_commands_test.py | 34 ++++++++++++++++++- scripts/g.extension/tests/g_extension_test.py | 6 ++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/python/grass/script/tests/grass_script_core_get_commands_test.py b/python/grass/script/tests/grass_script_core_get_commands_test.py index 27c94c7f62d..59a14c0d158 100644 --- a/python/grass/script/tests/grass_script_core_get_commands_test.py +++ b/python/grass/script/tests/grass_script_core_get_commands_test.py @@ -2,7 +2,7 @@ import os import sys - +import stat import pytest import grass.script as gs @@ -65,3 +65,35 @@ def test_in_session(tmp_path): with gs.setup.init(project, env=os.environ.copy()) as session: executables_set, scripts_dict = gs.get_commands(env=session.env) common_test_code(executables_set, scripts_dict) + + +def test_addon_path_multiple_dirs(tmp_path): + """Test that get_commands scans multiple directories in GRASS_ADDON_PATH""" + + dir_a = tmp_path / "addon_a" + dir_b = tmp_path / "addon_b" + dir_a.mkdir() + dir_b.mkdir() + + bin_a = dir_a / "scripts" + bin_b = dir_b / "bin" + bin_a.mkdir() + bin_b.mkdir() + + ext = ".py" if sys.platform == "win32" else "" + script_a = f"my_custom_cmd_a{ext}" + script_b = f"my_custom_cmd_b{ext}" + + for folder, script in [(bin_a, script_a), (bin_b, script_b)]: + file_path = folder / script + file_path.write_text("#!/bin/sh\necho 'Test ADDON PATH'", encoding="utf-8") + file_path.chmod(stat.S_IRWXU) + + new_path = f"{dir_a}{os.pathsep}{dir_b}" + env = os.environ.copy() + env["GRASS_ADDON_PATH"] = new_path + + executables_set, _ = gs.get_commands(env=env) + + assert "my_custom_cmd_a" in executables_set + assert "my_custom_cmd_b" in executables_set diff --git a/scripts/g.extension/tests/g_extension_test.py b/scripts/g.extension/tests/g_extension_test.py index c6fd946e92c..1dc0938f949 100644 --- a/scripts/g.extension/tests/g_extension_test.py +++ b/scripts/g.extension/tests/g_extension_test.py @@ -1,6 +1,7 @@ """Test g.extension""" import pytest +import grass.script as gs # This should cover both C and Python tools. @@ -9,3 +10,8 @@ def test_install(tools, name): """Test that a tools installs and gives help message""" tools.g_extension(extension=name) assert tools.call_cmd([name, "--help"]).stderr + + # Check that the installed extension is in the get_commands() list + # i.e., GRASS_ADDON_BASE is accessible or not. + commands = gs.get_commands(env=tools._original_env)[0] + assert name in commands, f"Extension {name} not found in get_commands() list" From d939838ea81045f863ec679a15b4a468da71c141 Mon Sep 17 00:00:00 2001 From: Saket Kumar Mall Date: Sat, 7 Feb 2026 12:07:55 +0530 Subject: [PATCH 4/5] Implementation like runtime.py --- python/grass/script/core.py | 18 ++++++++----- .../grass_script_core_get_commands_test.py | 26 ++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/python/grass/script/core.py b/python/grass/script/core.py index 52fdf4868ea..7cc50b58733 100644 --- a/python/grass/script/core.py +++ b/python/grass/script/core.py @@ -234,16 +234,20 @@ def scan(gisbase, directory): else: cmd.append(fname) + for directory in ("bin", "scripts"): + scan(gisbase, directory) + # Add addon path if it exists - search_paths = [gisbase] if env.get("GRASS_ADDON_BASE"): - search_paths.append(env["GRASS_ADDON_BASE"]) - if env.get("GRASS_ADDON_PATH"): - search_paths.extend([p for p in env["GRASS_ADDON_PATH"].split(os.pathsep) if p]) + addon_base = env["GRASS_ADDON_BASE"] + scan(addon_base, "bin") + if not sys.platform.startswith("win"): + scan(addon_base, "scripts") - for base_dir in search_paths: - for directory in ("bin", "scripts"): - scan(base_dir, directory) + if env.get("GRASS_ADDON_PATH"): + for path in env["GRASS_ADDON_PATH"].split(os.pathsep): + if path: + scan(path, "") return set(cmd), scripts diff --git a/python/grass/script/tests/grass_script_core_get_commands_test.py b/python/grass/script/tests/grass_script_core_get_commands_test.py index 59a14c0d158..b710a65a304 100644 --- a/python/grass/script/tests/grass_script_core_get_commands_test.py +++ b/python/grass/script/tests/grass_script_core_get_commands_test.py @@ -70,26 +70,22 @@ def test_in_session(tmp_path): def test_addon_path_multiple_dirs(tmp_path): """Test that get_commands scans multiple directories in GRASS_ADDON_PATH""" - dir_a = tmp_path / "addon_a" - dir_b = tmp_path / "addon_b" - dir_a.mkdir() - dir_b.mkdir() - - bin_a = dir_a / "scripts" - bin_b = dir_b / "bin" - bin_a.mkdir() - bin_b.mkdir() + test_dirs = [ + (tmp_path / "addon_a", "my_custom_cmd_a"), + (tmp_path / "addon_b", "my_custom_cmd_b"), + ] ext = ".py" if sys.platform == "win32" else "" - script_a = f"my_custom_cmd_a{ext}" - script_b = f"my_custom_cmd_b{ext}" - for folder, script in [(bin_a, script_a), (bin_b, script_b)]: - file_path = folder / script - file_path.write_text("#!/bin/sh\necho 'Test ADDON PATH'", encoding="utf-8") + for path, cmd_name in test_dirs: + path.mkdir() + file_path = path / f"{cmd_name}{ext}" + file_path.write_text( + "#!/usr/bin/env python3\nprint('Test ADDON PATH')", encoding="utf-8" + ) file_path.chmod(stat.S_IRWXU) - new_path = f"{dir_a}{os.pathsep}{dir_b}" + new_path = os.pathsep.join(str(p) for p, _ in test_dirs) env = os.environ.copy() env["GRASS_ADDON_PATH"] = new_path From ddece17884ee499b1f929776b6561c79b5103ec4 Mon Sep 17 00:00:00 2001 From: Saket Kumar Mall Date: Sat, 7 Feb 2026 19:54:39 +0530 Subject: [PATCH 5/5] yield session --- scripts/g.extension/tests/conftest.py | 12 ++++-------- scripts/g.extension/tests/g_extension_test.py | 11 ++++++----- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/scripts/g.extension/tests/conftest.py b/scripts/g.extension/tests/conftest.py index 6fc0679c69e..7a18856f2b1 100644 --- a/scripts/g.extension/tests/conftest.py +++ b/scripts/g.extension/tests/conftest.py @@ -5,19 +5,15 @@ import pytest import grass.script as gs -from grass.tools import Tools # Using module-scoped fixture to avoid overhead of downloading addons repo. @pytest.fixture(scope="module") -def tools(tmp_path_factory): - """Tools with modified addon base directory (scope: module)""" +def session(tmp_path_factory): + """Session with modified addon base directory (scope: module)""" tmp_path = tmp_path_factory.mktemp("g_extension_tests") gs.create_project(tmp_path / "test") env = os.environ.copy() env["GRASS_ADDON_BASE"] = str(tmp_path / "addons") - with ( - gs.setup.init(tmp_path / "test", env=env) as session, - Tools(session=session, consistent_return_value=True) as tools, - ): - yield tools + with gs.setup.init(tmp_path / "test", env=env) as session: + yield session diff --git a/scripts/g.extension/tests/g_extension_test.py b/scripts/g.extension/tests/g_extension_test.py index 1dc0938f949..f0d4e5b292a 100644 --- a/scripts/g.extension/tests/g_extension_test.py +++ b/scripts/g.extension/tests/g_extension_test.py @@ -2,16 +2,17 @@ import pytest import grass.script as gs +from grass.tools import Tools # This should cover both C and Python tools. @pytest.mark.parametrize("name", ["r.stream.distance", "r.lake.series"]) -def test_install(tools, name): +def test_install(session, name): """Test that a tools installs and gives help message""" - tools.g_extension(extension=name) - assert tools.call_cmd([name, "--help"]).stderr - + with Tools(session=session, consistent_return_value=True) as tools: + tools.g_extension(extension=name) + assert tools.call_cmd([name, "--help"]).stderr # Check that the installed extension is in the get_commands() list # i.e., GRASS_ADDON_BASE is accessible or not. - commands = gs.get_commands(env=tools._original_env)[0] + commands = gs.get_commands(env=session.env)[0] assert name in commands, f"Extension {name} not found in get_commands() list"