Skip to content

Commit 0b8ada1

Browse files
authored
grass.app: Add internal CLI with argparse subcommands (#5590)
This adds a CLI to grass.app subpackage using `__main__.py` and Python argparse with subcommands. At this point, this is meant as internal and experimental. The original motivation for this is testing locking in #5443. The subcommands add a lot of flexibility in what can done with argparse supporting directly several use cases and being quite stable and standardized. Just as an example (to have this separate from #5443 while functional), this adds two subcommands, help and man, which both end up calling g.manual. These specific subcommands are not a replacement for calling g.manual inside an interactive shell, but there are a replacement for command line call with a temporary project setup.
1 parent 12f893b commit 0b8ada1

File tree

4 files changed

+115
-0
lines changed

4 files changed

+115
-0
lines changed

python/grass/app/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ include $(MODULE_TOPDIR)/include/Make/Python.make
66
DSTDIR = $(ETC)/python/grass/app
77

88
MODULES = \
9+
__main__ \
10+
cli \
911
data \
1012
runtime
1113

python/grass/app/__main__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
##############################################################################
2+
# AUTHOR(S): Vaclav Petras <wenzeslaus gmail com>
3+
#
4+
# PURPOSE: Minimal file for package execution for CLI interface
5+
#
6+
# COPYRIGHT: (C) 2025 Vaclav Petras and the GRASS Development Team
7+
#
8+
# This program is free software under the GNU General Public
9+
# License (>=v2). Read the file COPYING that comes with GRASS
10+
# for details.
11+
##############################################################################
12+
13+
"""Minimal file for package execution for low-level CLI interface
14+
15+
This is not a stable part of the API. Contact developers before using it.
16+
"""
17+
18+
import sys
19+
from .cli import main
20+
21+
sys.exit(main())

python/grass/app/cli.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
##############################################################################
2+
# AUTHOR(S): Vaclav Petras <wenzeslaus gmail com>
3+
#
4+
# PURPOSE: Python-driven CLI interface
5+
#
6+
# COPYRIGHT: (C) 2025 Vaclav Petras and the GRASS Development Team
7+
#
8+
# This program is free software under the GNU General Public
9+
# License (>=v2). Read the file COPYING that comes with GRASS
10+
# for details.
11+
##############################################################################
12+
13+
"""
14+
Experimental low-level CLI interface for the main GRASS executable functionality
15+
16+
This is not a stable part of the API. Contact developers before using it.
17+
"""
18+
19+
import argparse
20+
import tempfile
21+
import os
22+
import sys
23+
from pathlib import Path
24+
25+
import grass.script as gs
26+
27+
28+
def call_g_manual(**kwargs):
29+
with tempfile.TemporaryDirectory() as tmp_dir_name:
30+
project_name = "project"
31+
project_path = Path(tmp_dir_name) / project_name
32+
gs.create_project(project_path)
33+
with gs.setup.init(project_path) as session:
34+
return gs.run_command(
35+
"g.manual", **kwargs, env=session.env, errors="status"
36+
)
37+
38+
39+
def subcommand_show_help(args):
40+
return call_g_manual(entry=args.page)
41+
42+
43+
def subcommand_show_man(args):
44+
return call_g_manual(entry=args.page, flags="m")
45+
46+
47+
def main(args=None, program=None):
48+
# Top-level parser
49+
if program is None:
50+
program = os.path.basename(sys.argv[0])
51+
if program == "__main__.py":
52+
program = f"{Path(sys.executable).name} -m grass.app"
53+
parser = argparse.ArgumentParser(
54+
description="Experimental low-level CLI interface to GRASS. Consult developers before using it.",
55+
prog=program,
56+
)
57+
subparsers = parser.add_subparsers(
58+
title="subcommands", required=True, dest="subcommand"
59+
)
60+
61+
# Subcommand parsers
62+
63+
subparser = subparsers.add_parser(
64+
"help", help="show HTML documentation for a tool or topic"
65+
)
66+
subparser.add_argument("page", type=str)
67+
subparser.set_defaults(func=subcommand_show_help)
68+
69+
subparser = subparsers.add_parser(
70+
"man", help="show man page for a tool or topic using"
71+
)
72+
subparser.add_argument("page", type=str)
73+
subparser.set_defaults(func=subcommand_show_man)
74+
75+
parsed_args = parser.parse_args(args)
76+
return parsed_args.func(parsed_args)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import sys
2+
3+
import pytest
4+
5+
from grass.app.cli import main
6+
7+
8+
def test_cli_help_runs():
9+
with pytest.raises(SystemExit) as exception:
10+
main(["--help"])
11+
assert exception.value.code == 0
12+
13+
14+
@pytest.mark.skipif(sys.platform.startswith("win"), reason="No man on Windows")
15+
def test_man_subcommand_runs():
16+
assert main(["man", "g.region"]) == 0

0 commit comments

Comments
 (0)