1+ #!/usr/bin/env python
2+
3+ ##############################################################################
4+ # AUTHOR(S): Vaclav Petras <wenzeslaus gmail com>
5+ #
6+ # PURPOSE: API to call GRASS tools (modules) as Python functions
7+ #
8+ # COPYRIGHT: (C) 2023 Vaclav Petras and the GRASS Development Team
9+ #
10+ # This program is free software under the GNU General Public
11+ # License (>=v2). Read the file COPYING that comes with GRASS
12+ # for details.
13+ ##############################################################################
14+
15+ """API to call GRASS tools (modules) as Python functions"""
16+
17+ import json
118import os
2- import sys
319import shutil
420
521import grass.script as gs
622
723
824class ExecutedTool:
25+ """Result returned after executing a tool"""
26+
927 def __init__(self, name, kwargs, stdout, stderr):
1028 self._name = name
29+ self._kwargs = kwargs
1130 self._stdout = stdout
31+ self._stderr = stderr
1232 if self._stdout:
1333 self._decoded_stdout = gs.decode(self._stdout)
1434 else:
1535 self._decoded_stdout = ""
1636
1737 @property
18- def text(self):
38+ def text(self) -> str:
39+ """Text output as decoded string"""
1940 return self._decoded_stdout.strip()
2041
2142 @property
2243 def json(self):
23- import json
44+ """Text output read as JSON
2445
46+ This returns the nested structure of dictionaries and lists or fails when
47+ the output is not JSON.
48+ """
2549 return json.loads(self._stdout)
2650
2751 @property
2852 def keyval(self):
53+ """Text output read as key-value pairs separated by equal signs"""
54+
2955 def conversion(value):
56+ """Convert text to int or float if possible, otherwise return it as is"""
3057 try:
3158 return int(value)
3259 except ValueError:
@@ -41,33 +68,30 @@ def conversion(value):
4168
4269 @property
4370 def comma_items(self):
71+ """Text output read as comma-separated list"""
4472 return self.text_split(",")
4573
4674 @property
4775 def space_items(self):
76+ """Text output read as whitespace-separated list"""
4877 return self.text_split(None)
4978
5079 def text_split(self, separator=None):
80+ """Parse text output read as list separated by separators
81+
82+ Any leading or trailing newlines are removed prior to parsing.
83+ """
5184 # The use of strip is assuming that the output is one line which
5285 # ends with a newline character which is for display only.
5386 return self._decoded_stdout.strip("\n").split(separator)
5487
5588
56- class SubExecutor:
57- """use as tools().params(a="x", b="y").g_region()"""
58-
59- # a and b would be overwrite or stdin
60- # Can support other envs or all PIPE and encoding read command supports
61- def __init__(self, *, tools, env, stdin=None):
62- self._tools = tools
63- self._env = env
64- self._stdin = stdin
65-
66- def run(self, name, /, **kwargs):
67- pass
89+ class Tools:
90+ """Call GRASS tools as methods
6891
92+ GRASS tools (modules) can be executed as methods of this class.
93+ """
6994
70- class Tools:
7195 def __init__(
7296 self,
7397 *,
@@ -81,9 +105,6 @@ def __init__(
81105 stdin=None,
82106 errors=None,
83107 ):
84- # TODO: fix region, so that external g.region call in the middle
85- # is not a problem
86- # i.e. region is independent/internal/fixed
87108 if env:
88109 self._env = env.copy()
89110 elif session and hasattr(session, "env"):
@@ -122,6 +143,7 @@ def _set_stdin(self, stdin, /):
122143
123144 @property
124145 def env(self):
146+ """Internally used environment (reference to it, not a copy)"""
125147 return self._env
126148
127149 def run(self, name, /, **kwargs):
@@ -152,31 +174,21 @@ def run(self, name, /, **kwargs):
152174 stdout, stderr = process.communicate(input=stdin)
153175 stderr = gs.utils.decode(stderr)
154176 returncode = process.poll()
155- # TODO: instead of printing, do exception right away
156- # but right now, handle errors does not accept stderr
157- # or don't use handle errors and raise instead
158177 if returncode and self._errors != "ignore":
159178 raise gs.CalledModuleError(
160179 name,
161180 code=" ".join([f"{key}={value}" for key, value in kwargs.items()]),
162181 returncode=returncode,
163182 errors=stderr,
164183 )
165-
166- # Print only when we are capturing it and there was some output.
167- # (User can request ignoring the subprocess stderr and then
168- # we get only None.)
169- if stderr:
170- sys.stderr.write(stderr)
171- gs.handle_errors(returncode, stdout, [name], kwargs)
172184 return ExecutedTool(name=name, kwargs=kwargs, stdout=stdout, stderr=stderr)
173- # executor = SubExecutor(tools=self, env=self._env, stdin=self._stdin)
174- # return executor.run(name, **kwargs)
175185
176186 def feed_input_to(self, stdin, /):
187+ """Get a new object which will feed text input to a tool or tools"""
177188 return Tools(env=self._env, stdin=stdin)
178189
179190 def ignore_errors_of(self):
191+ """Get a new object which will ignore errors of the called tools"""
180192 return Tools(env=self._env, errors="ignore")
181193
182194 def __getattr__(self, name):
@@ -202,6 +214,7 @@ def wrapper(**kwargs):
202214
203215
204216def _test():
217+ """Ad-hoc tests and examples of the Tools class"""
205218 session = gs.setup.init("~/grassdata/nc_spm_08_grass7/user1")
206219
207220 tools = Tools()
@@ -227,9 +240,7 @@ def _test():
227240 env["GRASS_REGION"] = gs.region_env(res=250)
228241 coarse_computation = Tools(env=env)
229242 current_region = coarse_computation.g_region(flags="g").keyval
230- print(
231- current_region["ewres"], current_region["nsres"]
232- ) # TODO: should keyval convert?
243+ print(current_region["ewres"], current_region["nsres"])
233244 coarse_computation.r_slope_aspect(
234245 elevation="elevation", slope="slope", flags="a", overwrite=True
235246 )
0 commit comments