setup.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. R"""Setup, initialization, and clean-up functions
  2. Functions can be used in Python scripts to setup a GRASS environment
  3. and session without using grassXY.
  4. Usage::
  5. import os
  6. import sys
  7. import subprocess
  8. # define GRASS Database
  9. # add your path to grassdata (GRASS GIS database) directory
  10. gisdb = "~/grassdata"
  11. # the following path is the default path on MS Windows
  12. # gisdb = "~/Documents/grassdata"
  13. # specify (existing) Location and Mapset
  14. location = "nc_spm_08"
  15. mapset = "user1"
  16. # path to the GRASS GIS launch script
  17. # we assume that the GRASS GIS start script is available and on PATH
  18. # query GRASS itself for its GISBASE
  19. # (with fixes for specific platforms)
  20. # needs to be edited by the user
  21. grass8bin = 'grass'
  22. if sys.platform.startswith('win'):
  23. # MS Windows
  24. grass8bin = r'C:\OSGeo4W\bin\grass.bat'
  25. # uncomment when using standalone WinGRASS installer
  26. # grass8bin = r'C:\Program Files (x86)\GRASS GIS 8.0.0\grass.bat'
  27. # this can be avoided if GRASS executable is added to PATH
  28. elif sys.platform == 'darwin':
  29. # Mac OS X
  30. # TODO: this have to be checked, maybe unix way is good enough
  31. grass8bin = '/Applications/GRASS/GRASS-8.0.app/'
  32. # query GRASS GIS itself for its GISBASE
  33. startcmd = [grass8bin, '--config', 'path']
  34. try:
  35. p = subprocess.Popen(startcmd, shell=False,
  36. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  37. out, err = p.communicate()
  38. except OSError as error:
  39. sys.exit("ERROR: Cannot find GRASS GIS start script"
  40. " {cmd}: {error}".format(cmd=startcmd[0], error=error))
  41. if p.returncode != 0:
  42. sys.exit("ERROR: Issues running GRASS GIS start script"
  43. " {cmd}: {error}"
  44. .format(cmd=' '.join(startcmd), error=err))
  45. gisbase = out.strip(os.linesep)
  46. # set GISBASE environment variable
  47. os.environ['GISBASE'] = gisbase
  48. # define GRASS-Python environment
  49. grass_pydir = os.path.join(gisbase, "etc", "python")
  50. sys.path.append(grass_pydir)
  51. # import (some) GRASS Python bindings
  52. import grass.script as gs
  53. import grass.script.setup as gsetup
  54. # launch session
  55. rcfile = gsetup.init(gisdb, location, mapset, grass_path=gisbase)
  56. # example calls
  57. gs.message('Current GRASS GIS 8 environment:')
  58. print(gs.gisenv())
  59. gs.message('Available raster maps:')
  60. for rast in gs.list_strings(type='raster'):
  61. print(rast)
  62. gs.message('Available vector maps:')
  63. for vect in gs.list_strings(type='vector'):
  64. print(vect)
  65. # clean up at the end
  66. gsetup.finish()
  67. (C) 2010-2021 by the GRASS Development Team
  68. This program is free software under the GNU General Public
  69. License (>=v2). Read the file COPYING that comes with GRASS
  70. for details.
  71. @author Martin Landa <landa.martin gmail.com>
  72. @author Vaclav Petras <wenzeslaus gmail.com>
  73. @author Markus Metz
  74. """
  75. # TODO: this should share code from lib/init/grass.py
  76. # perhaps grass.py can import without much trouble once GISBASE
  77. # is known, this would allow moving things from there, here
  78. # then this could even do locking
  79. from pathlib import Path
  80. import os
  81. import shutil
  82. import subprocess
  83. import sys
  84. import tempfile as tmpfile
  85. windows = sys.platform == "win32"
  86. def write_gisrc(dbase, location, mapset):
  87. """Write the ``gisrc`` file and return its path."""
  88. gisrc = tmpfile.mktemp()
  89. with open(gisrc, "w") as rc:
  90. rc.write("GISDBASE: %s\n" % dbase)
  91. rc.write("LOCATION_NAME: %s\n" % location)
  92. rc.write("MAPSET: %s\n" % mapset)
  93. return gisrc
  94. def set_gui_path():
  95. """Insert wxPython GRASS path to sys.path."""
  96. gui_path = os.path.join(os.environ["GISBASE"], "gui", "wxpython")
  97. if gui_path and gui_path not in sys.path:
  98. sys.path.insert(0, gui_path)
  99. def get_install_path(path=None):
  100. """Get path to GRASS installation usable for setup of environmental variables.
  101. The function tries to determine path tp GRASS GIS installation so that the
  102. returned path can be used for setup of environmental variable for GRASS runtime.
  103. If the search fails, None is returned.
  104. By default, the resulting path is derived relatively from the location of the
  105. Python package (specifically this module) in the file system. This derived path
  106. is returned only if it has subdirectories called ``bin`` and ``lib``.
  107. If the parameter or certain environmental variables are set, the following
  108. attempts are made to find the path.
  109. If *path* is provided and it is an existing executable, the executable is queried
  110. for the path. Otherwise, provided *path* is returned as is.
  111. If *path* is not provided, the GISBASE environmental variable is used as the path
  112. if it exists. If GRASSBIN environmental variable exists and it is an existing
  113. executable, the executable is queried for the path.
  114. If *path* is not provided and no relevant environmental variables are set, the
  115. default relative path search is performed.
  116. If that fails and executable called ``grass`` exists, it is queried for the path.
  117. None is returned if all the attempts failed.
  118. If an existing executable is called as a subprocess is called during the search
  119. and it fails, the CalledProcessError exception is propagated from the subprocess
  120. call.
  121. """
  122. def ask_executable(arg):
  123. """Query the GRASS exectable for the path"""
  124. return subprocess.run(
  125. [arg, "--config", "path"], text=True, check=True, capture_output=True
  126. ).stdout.strip()
  127. # Exectable was provided as parameter.
  128. if path and shutil.which(path):
  129. # The path was provided by the user and it is an executable
  130. # (on path or provided with full path), so raise exception on failure.
  131. ask_executable(path)
  132. # Presumably directory was provided.
  133. if path:
  134. return path
  135. # GISBASE is already set.
  136. env_gisbase = os.environ.get("GISBASE")
  137. if env_gisbase:
  138. return env_gisbase
  139. # Executable provided in environment (name is from grass-session).
  140. # The variable is supported (here), documented, but not widely promoted
  141. # at this point (to be re-evaluated).
  142. grass_bin = os.environ.get("GRASSBIN")
  143. if grass_bin and shutil.which(grass_bin):
  144. ask_executable(grass_bin)
  145. # Derive the path from path to this file (Python module).
  146. # This is the standard way when there is no user-provided settings.
  147. # Uses relative path to find the right parent and then tests presence of lib
  148. # and bin. Removing 5 parts from the path works for
  149. # .../grass_install_prefix/etc/python/grass and also .../python3/dist-packages/.
  150. install_path = Path(*Path(__file__).parts[:-5])
  151. bin_path = install_path / "bin"
  152. lib_path = install_path / "lib"
  153. if bin_path.is_dir() and lib_path.is_dir():
  154. path = install_path
  155. # As a last resort, try running grass command if it exists.
  156. # This is less likely give the right result than the relative path on systems
  157. # with multiple installations (where an explicit setup is likely required).
  158. # However, it allows for non-standard installations with standard command.
  159. grass_bin = "grass"
  160. if grass_bin and shutil.which(grass_bin):
  161. ask_executable(grass_bin)
  162. return None
  163. def setup_runtime_env(gisbase):
  164. """Setup the runtime environment.
  165. Modifies the global environment (os.environ) so that GRASS modules can run.
  166. """
  167. # Set GISBASE
  168. os.environ["GISBASE"] = gisbase
  169. mswin = sys.platform.startswith("win")
  170. # define PATH
  171. os.environ["PATH"] += os.pathsep + os.path.join(gisbase, "bin")
  172. os.environ["PATH"] += os.pathsep + os.path.join(gisbase, "scripts")
  173. if mswin: # added for winGRASS
  174. os.environ["PATH"] += os.pathsep + os.path.join(gisbase, "extrabin")
  175. # add addons to the PATH
  176. # copied and simplified from lib/init/grass.py
  177. if mswin:
  178. config_dirname = "GRASS8"
  179. config_dir = os.path.join(os.getenv("APPDATA"), config_dirname)
  180. else:
  181. config_dirname = ".grass8"
  182. config_dir = os.path.join(os.getenv("HOME"), config_dirname)
  183. addon_base = os.path.join(config_dir, "addons")
  184. os.environ["GRASS_ADDON_BASE"] = addon_base
  185. if not mswin:
  186. os.environ["PATH"] += os.pathsep + os.path.join(addon_base, "scripts")
  187. os.environ["PATH"] += os.pathsep + os.path.join(addon_base, "bin")
  188. # define LD_LIBRARY_PATH
  189. if "@LD_LIBRARY_PATH_VAR@" not in os.environ:
  190. os.environ["@LD_LIBRARY_PATH_VAR@"] = ""
  191. os.environ["@LD_LIBRARY_PATH_VAR@"] += os.pathsep + os.path.join(gisbase, "lib")
  192. # Set GRASS_PYTHON and PYTHONPATH to find GRASS Python modules
  193. if not os.getenv("GRASS_PYTHON"):
  194. if sys.platform == "win32":
  195. os.environ["GRASS_PYTHON"] = "python3.exe"
  196. else:
  197. os.environ["GRASS_PYTHON"] = "python3"
  198. path = os.getenv("PYTHONPATH")
  199. etcpy = os.path.join(gisbase, "etc", "python")
  200. if path:
  201. path = etcpy + os.pathsep + path
  202. else:
  203. path = etcpy
  204. os.environ["PYTHONPATH"] = path
  205. def init(path, location=None, mapset=None, grass_path=None):
  206. """Initialize system variables to run GRASS modules
  207. This function is for running GRASS GIS without starting it with the
  208. standard main executable grass. No GRASS modules shall be called before
  209. call of this function but any module or user script can be called
  210. afterwards because a GRASS session has been set up. GRASS Python
  211. libraries are usable as well in general but the ones using C
  212. libraries through ``ctypes`` are not (which is caused by library
  213. path not being updated for the current process which is a common
  214. operating system limitation).
  215. When the path or specified mapset does not exist, ValueError is raised.
  216. The :func:`get_install_path` function is used to determine where
  217. the rest of GRASS files is installed. The *grass_path* parameter is
  218. passed to it if provided. If the path cannot be determined,
  219. ValueError is raised. Exceptions from the underlying function are propagated.
  220. To create a GRASS session a session file (aka gisrc file) is created.
  221. Caller is responsible for deleting the file which is normally done
  222. with the function :func:`finish`.
  223. Basic usage::
  224. # ... setup GISBASE and sys.path before import
  225. import grass.script as gs
  226. gs.setup.init(
  227. "~/grassdata/nc_spm_08/user1",
  228. grass_path="/usr/lib/grass",
  229. )
  230. # ... use GRASS modules here
  231. # end the session
  232. gs.setup.finish()
  233. :param path: path to GRASS database
  234. :param location: location name
  235. :param mapset: mapset within given location (default: 'PERMANENT')
  236. :param grass_path: path to GRASS installation or executable
  237. :returns: path to ``gisrc`` file (may change in future versions)
  238. """
  239. grass_path = get_install_path(grass_path)
  240. if not grass_path:
  241. raise ValueError(
  242. _("Parameter grass_path or GISBASE environmental variable must be set")
  243. )
  244. # We reduce the top-level imports because this is initialization code.
  245. # pylint: disable=import-outside-toplevel
  246. from grass.grassdb.checks import get_mapset_invalid_reason, is_mapset_valid
  247. from grass.grassdb.manage import resolve_mapset_path
  248. # A simple existence test. The directory, whatever it is, should exist.
  249. if not Path(path).exists():
  250. raise ValueError(_("Path '{path}' does not exist").format(path=path))
  251. # A specific message when it exists, but it is a file.
  252. if Path(path).is_file():
  253. raise ValueError(
  254. _("Path '{path}' is a file, but a directory is needed").format(path=path)
  255. )
  256. mapset_path = resolve_mapset_path(path=path, location=location, mapset=mapset)
  257. if not is_mapset_valid(mapset_path):
  258. raise ValueError(
  259. _("Mapset {path} is not valid: {reason}").format(
  260. path=mapset_path.path,
  261. reason=get_mapset_invalid_reason(
  262. mapset_path.directory, mapset_path.location, mapset_path.mapset
  263. ),
  264. )
  265. )
  266. setup_runtime_env(grass_path)
  267. # TODO: lock the mapset?
  268. os.environ["GIS_LOCK"] = str(os.getpid())
  269. os.environ["GISRC"] = write_gisrc(
  270. mapset_path.directory, mapset_path.location, mapset_path.mapset
  271. )
  272. return os.environ["GISRC"]
  273. # clean-up functions when terminating a GRASS session
  274. # these fns can only be called within a valid GRASS session
  275. def clean_default_db():
  276. # clean the default db if it is sqlite
  277. from grass.script import core as gcore
  278. from grass.script import db as gdb
  279. conn = gdb.db_connection()
  280. if conn and conn["driver"] == "sqlite":
  281. # check if db exists
  282. gisenv = gcore.gisenv()
  283. database = conn["database"]
  284. database = database.replace("$GISDBASE", gisenv["GISDBASE"])
  285. database = database.replace("$LOCATION_NAME", gisenv["LOCATION_NAME"])
  286. database = database.replace("$MAPSET", gisenv["MAPSET"])
  287. if os.path.exists(database):
  288. gcore.message(_("Cleaning up default sqlite database ..."))
  289. gcore.start_command("db.execute", sql="VACUUM")
  290. # give it some time to start
  291. import time
  292. time.sleep(0.1)
  293. def call(cmd, **kwargs):
  294. """Wrapper for subprocess.call to deal with platform-specific issues"""
  295. if windows:
  296. kwargs["shell"] = True
  297. return subprocess.call(cmd, **kwargs)
  298. def clean_temp():
  299. from grass.script import core as gcore
  300. gcore.message(_("Cleaning up temporary files..."))
  301. nul = open(os.devnull, "w")
  302. gisbase = os.environ["GISBASE"]
  303. call([os.path.join(gisbase, "etc", "clean_temp")], stdout=nul)
  304. nul.close()
  305. def finish():
  306. """Terminate the GRASS session and clean up
  307. GRASS commands can no longer be used after this function has been
  308. called
  309. Basic usage::
  310. import grass.script as gs
  311. gs.setup.finish()
  312. The function is not completely symmetrical with :func:`init` because it only
  313. closes the mapset, but doesn't undo the runtime environment setup.
  314. """
  315. clean_default_db()
  316. clean_temp()
  317. # TODO: unlock the mapset?
  318. # unset the GISRC and delete the file
  319. from grass.script import utils as gutils
  320. gutils.try_remove(os.environ["GISRC"])
  321. os.environ.pop("GISRC")
  322. # remove gislock env var (not the gislock itself
  323. os.environ.pop("GIS_LOCK")