On Mon, Jul 17, 2017 at 12:36 AM, Pietro <peter.z...@gmail.com> wrote:
> > On Fri, Jul 14, 2017 at 6:00 PM, Vaclav Petras <wenzesl...@gmail.com> > wrote: > >> This is exactly what I had in my mind when doing the last major changes >> in the grass.py file. >> > I generally like the layout you suggested. It seems to me that choosing a >> good name for the whole module will be a bit tricky. >> > > This is intended as a proof of concept to see the feasibility. > I've try to found a better name but didn't come up to my mind... > Perhaps: session instead of init? > > My final objective is to be able to do something like: > That makes sense. In fact, that's very similar to a file I drafted some time ago splitting the data initialization and runtime in grass.script.setup and adding Session (see the attached file). Another example, for a different case, is here: https://github.com/wenzeslaus/g.remote/blob/master/grasssession.py *# Perhaps in GRASS8 we will be able to skip this! ;-)* > sys.append(os.environ.get('GISBASE', '/home/pietro/my/gisbase')) > Added to the list, but how to do it remains unclear (many different discussions in Trac and ML): https://trac.osgeo.org/grass/wiki/Grass8Planning > > from grass.init import Session > > # open - close mode > session = Session('mygisdbase/location/mapset') > session.open() > # do my stuff here... > session.close() > > # with statement > with Session('mygisdbase/location/mapset') as session: > # do my stuff here > ``` > > Unfortunately, here is where the trouble begins. The above leads to the following: with Session as session: session.run_command(...) which fits with API which already exists for Ruby: https://github.com/jgoizueta/grassgis/ GrassGis.session configuration do+ r.info 'slope' g.region '-p' end The trouble is that session (at least in Python) needs to depend on the rest of the library because it is the interface for the rest (on demand imports may help here). So perhaps having grass.init or grass.setup with the low level functions and then a separate grass.session with a nice interface which may depend on all other modules may be a better way. (Although having each function from the library as a method of Session calls for more thinking about grass.session.Session. Just to be clear: I definitively think this should be done. I'm just not sure what is the right way. Vaclav
"""Setup and initialization functions Function can be used in Python scripts to setup a GRASS environment without starting an actual GRASS session. Usage:: On Linux when GRASS GIS executable on path, use:: export LD_LIBRARY_PATH=$(grass --config path)/lib export PYTHONPATH=$(grass --config path)/etc/python When GRASS GIS executable is not on path, you have spaces in paths and you want to preserve the context of the variables, use:: export GRASS_EXECUTABLE="/path/to/grass" export LD_LIBRARY_PATH="$($GRASS_EXECUTABLE --config path)/lib:$LD_LIBRARY_PATH" export PYTHONPATH="$($GRASS_EXECUTABLE --config path)/etc/python:$PYTHONPATH" On MS Windows, use:: set GRASS_EXECUTABLE="C:\path\to\grass\grass70.bat" set PATH="C:\path\to\grass\lib;%PATH%" set PYTHONPATH="C:\path\to\grass\etc\python;%PYTHONPATH%" On Mac OS X, use instruction for Linux and use ``DYLD_LIBRARY_PATH`` instead of ``LD_LIBRARY_PATH``. For other systems, use instructions for Linux and replace ``LD_LIBRARY_PATH`` and colon by their equivalent if needed. (C) 2010-2012 by the GRASS Development Team This program is free software under the GNU General Public License (>=v2). Read the file COPYING that comes with GRASS for details. @author Martin Landa <landa.martin gmail.com> @author Vaclav Petras <wenzeslaus gmail.com> """ # TODO: this should share code from lib/init/grass.py # perhaps grass.py can import without much trouble once GISBASE # is known, this would allow moving things from there, here # then this could even do locking import os import sys import subprocess import tempfile as tmpfile # this file does not depend on anything in script and we should # keep it this way, except for creating location and this should be done # by lazy import # TODO: move this file from grass.script.setup to grass.setup def write_gisrc(dbase, location, mapset): """Write the ``gisrc`` file and return its path.""" gisrc = tmpfile.mktemp() with open(gisrc, 'w') as rc: rc.write("GISDBASE: %s\n" % dbase) rc.write("LOCATION_NAME: %s\n" % location) rc.write("MAPSET: %s\n" % mapset) return gisrc def set_gui_path(): """Insert wxPython GRASS path to sys.path.""" gui_path = os.path.join(os.environ['GISBASE'], 'gui', 'wxpython') if gui_path and gui_path not in sys.path: sys.path.insert(0, gui_path) # TODO: EPSG supports two colons but the meaning is EPSG:version:code, not EPSG:code:datum_trans def create_location(gisdbase, location, geostring): """Create GRASS Location using georeferenced file or EPSG EPSG code format is ``EPSG:code`` or ``EPSG:code:datum_trans``. :param gisdbase: Path to GRASS GIS database directory :param location: name of new Location :param geostring: path to a georeferenced file or EPSG code """ # runtime is full, so this should work smoothly from grass.script import core as gcore # pylint: disable=E0611 try: if geostring.upper().find('EPSG:') > -1: # create location using EPSG code epsg = geostring.split(':', 1)[1] if ':' in epsg: epsg, datum_trans = epsg.split(':', 1) else: datum_trans = None gcore.create_location(gisdbase, location, epsg=epsg, datum_trans=datum_trans, call_g_gisenv=False) elif geostring.upper() == 'XY': gcore.create_location(gisdbase, location, call_g_gisenv=False) elif os.path.isfile(geostring): if geostring.lower().endswith(".prj"): gcore.create_location(gisdbase, location, wkt=geostring, call_g_gisenv=False) else: gcore.create_location(gisdbase, location, filename=geostring, call_g_gisenv=False) else: # more complicated test, perhaps too fuzzy import re if re.search(r"\+[a-z_0-9]+=[-0-9a-z]", geostring): print "PROJ4" gcore.create_location(gisdbase, location, proj4=geostring, call_g_gisenv=False) else: raise RuntimeError("Cannot create Location based on <{}>".format(geostring)) except gcore.ScriptError as err: raise RuntimeError(err.value.strip('"').strip("'").replace('\\n', os.linesep)) def create_mapset(gisdbase, location, mapset): def readfile(path): f = open(path, 'r') s = f.read() f.close() return s def writefile(path, s): f = open(path, 'w') f.write(s) f.close() mapset_path = os.path.join(gisdbase, location, mapset) os.mkdir(mapset_path) s = readfile(os.path.join(gisdbase, location, "PERMANENT", "DEFAULT_WIND")) writefile(os.path.join(mapset_path, "WIND"), s) def is_mapset_valid(gisdbase, location, mapset): """Return True if GRASS Mapset is valid""" return os.access(os.path.join(gisdbase, location, mapset, "WIND"), os.R_OK) def is_location_valid(gisdbase, location): """Return True if GRASS Location is valid :param gisdbase: Path to GRASS GIS database directory :param location: name of a Location """ return os.access(os.path.join(gisdbase, location, "PERMANENT", "DEFAULT_WIND"), os.F_OK) def is_gisdbase_valid(gisdbase): """Return True if GRASS GIS database directory is valid""" return os.access(gisdbase, os.R_OK) # TODO: deal with translations here def fake_translate(text): return text _ = fake_translate # TODO: solve the _ function here # basically checking location, possibly split into two functions # (mapset one can call location one) # TODO: def get_mapset_invalid_reason(gisdbase, location, mapset): """Returns a message describing what is wrong with the Mapset :param gisdbase: Path to GRASS GIS database directory :param location: name of a Location :param mapset: name of a Mapset :returns: translated message """ full_location = os.path.join(gisdbase, location) if not os.path.exists(full_location): return _("Location <%s> doesn't exist") % full_location elif 'PERMANENT' not in os.listdir(full_location): return _("<%s> is not a valid GRASS Location" " because PERMANENT Mapset is missing") % full_location elif not os.path.isdir(os.path.join(full_location, 'PERMANENT')): return _("<%s> is not a valid GRASS Location" " because PERMANENT is not a directory") % full_location elif not os.path.isfile((os.path.join(full_location, 'PERMANENT', 'DEFAULT_WIND'))): return _("<%s> is not a valid GRASS Location" " because PERMANENT Mapset does not have a DEFAULT_WIND file" " (default computational region)") % full_location else: # TODO: check for WIND in the mapset # TODO: removed grass.py specific notes, can grass.py live without them? return _("Mapset <{mapset}> doesn't exist in GRASS Location <{loc}>").format( mapset=mapset, loc=location) def _get_grass_executable(): package_grass_version = ('@GRASS_VERSION_MAJOR@', '@GRASS_VERSION_MINOR@', '@GRASS_VERSION_RELEASE@') if 'GRASS_EXECUTABLE' in os.environ: executable = os.environ['GRASS_EXECUTABLE'] if executable: # we don't check for existence because a command is OK too # TODO: check that the version/revision fits the package? return executable # grass version is needed to find executable (esp. Win and Mac) # TODO: check revision or release/patch of executable vs package (similarly to C)? # ideally, we just ensure that GRASS is on path during installation # and we don't have to do the special checks Win and Mac if sys.platform.startswith('win'): paths = [ r'C:\Program Files (x86)\GRASS GIS {major}.{minor}.{release}\{major}{minor}.bat', r'C:\Program Files\GRASS GIS {major}.{minor}.{release}\grass{major}{minor}.bat', r'C:\OSGeo4W\grass{major}{minor}.bat', ] for path in paths: if os.path.isfile(fname): return path elif 'darwin' in sys.platform: paths = [ '/Applications/GRASS/GRASS-7.0.app/', ] for path in paths: if os.path.isfile(fname): return path # if nothing found or standard OS, use what is on path # (paths hardcoded here precedes whatever is on path on Win and Mac) # we require grass executable with version in name return 'grass{major}{minor}'.format( major=package_grass_version[0], minor=package_grass_version[1]) def _get_gisbase(grass_executable=None): if grass_executable is None: grass_executable = _get_grass_executable() else: grass_executable = os.path.expanduser(grass_executable) # query GRASS GIS itself for its GISBASE startcmd = [grass_executable, '--config', 'path'] try: p = subprocess.Popen(startcmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() except OSError as error: # TODO: use exception sys.exit("ERROR: Cannot find GRASS GIS start script" " {cmd}: {error}".format(cmd=startcmd[0], error=error)) if p.returncode != 0: sys.exit("ERROR: Issues running GRASS GIS start script" " {cmd}: {error}" .format(cmd=' '.join(startcmd), error=err)) gisbase = out.strip(os.linesep) return gisbase def init_runtime(path=None, executable=None): if path: gisbase = os.path.expanduser(path) if not os.path.exists(path): # TODO: using general exception and non-translatable string, OK? raise RuntimeError("GRASS GIS directory <{}> does not exist".format(path)) else: gisbase = _get_gisbase(executable) mswin = sys.platform.startswith('win') # define PATH os.environ['PATH'] += os.pathsep + os.path.join(gisbase, 'bin') os.environ['PATH'] += os.pathsep + os.path.join(gisbase, 'scripts') if mswin: # added for winGRASS os.environ['PATH'] += os.pathsep + os.path.join(gisbase, 'extrabin') # add addons to the PATH # copied and simplified from lib/init/grass.py if mswin: config_dirname = "GRASS7" config_dir = os.path.join(os.getenv('APPDATA'), config_dirname) else: config_dirname = ".grass7" config_dir = os.path.join(os.getenv('HOME'), config_dirname) addon_base = os.path.join(config_dir, 'addons') os.environ['GRASS_ADDON_BASE'] = addon_base if not mswin: os.environ['PATH'] += os.pathsep + os.path.join(addon_base, 'scripts') # define LD_LIBRARY_PATH if '@LD_LIBRARY_PATH_VAR@' not in os.environ: os.environ['@LD_LIBRARY_PATH_VAR@'] = '' os.environ['@LD_LIBRARY_PATH_VAR@'] += os.pathsep + os.path.join(gisbase, 'lib') os.environ['GIS_LOCK'] = str(os.getpid()) # Set GRASS_PYTHON and PYTHONPATH to find GRASS Python modules if not os.getenv('GRASS_PYTHON'): if mswin: os.environ['GRASS_PYTHON'] = "python.exe" else: os.environ['GRASS_PYTHON'] = "python" # TODO: does this make any sense, we are already in the package? path = os.getenv('PYTHONPATH') etcpy = os.path.join(gisbase, 'etc', 'python') if path: path = etcpy + os.pathsep + path else: path = etcpy os.environ['PYTHONPATH'] = path os.environ['GISBASE'] = gisbase class GrassSession(): def __init__(self, rcfile): self.rcfile = rcfile # TODO: we might want to register cleaning to atexit but preserve also the explicit option def close(self): # TODO: clean tmp # TODO: unset the env var (we won't unset the runtime, just data) os.remove(self.rcfile) # TODO: when create or geostring are required is bit complicated although it might yield intuitive behavior # TODO: name of geostring parameter # TODO: additional datum parameters for creating the location? # TODO: PERMENENT as default for mapset? # TODO: support mapset_path # when all exist the create and geostring params are ignored def init_data(gisdbase=None, location=None, mapset=None, geostring=None, overwrite=False): r"""Prepare the environment to use GRASS functions Creates Mapset automatically when it does not exist. Location is created when it does not exist and the *geostring* parameter is provided. If the GRASS Database directory does not exist, it is created automatically when a new Location is created. If Location exists and the *geostring* parameter is ignored regardless its value and coordinate system of the Location. When *overwrite* parameter is ``True``, the *geostring* parameter parameter is used to create a new Location after deleting the existing one. When *overwrite* parameter is ``True`` but the *geostring* parameter is not set, existing Mapset is deleted and new one is created. Basic usage:: session = init_data("~/grassdata", "nc_spm", "user1") # call GRASS functions here session.close() To make this work you need to set some variables ahead in the command line, your system or whatever is appropriate in your case. """ # assuming that the runtime environment does not exist if not 'GISBASE' in os.environ: init_runtime() # TODO: without a parameter create var is not so useful, remove it if geostring: create = True else: create = False # define GRASS Database if not set, a little bit questionable # especially when it does not exist if gisdbase: gisdbase = os.path.expanduser(gisdbase) else: if sys.platform.startswith('win'): # the following path is the default path on MS Windows gisdbase = os.path.join(os.path.expanduser("~"), "Documents", "grassdata") else: gisdbase = os.path.join(os.path.expanduser("~"), "grassdata") if not os.path.isdir(gisdbase): if create: os.mkdir(gisdbase) else: # TODO: using general exception and non-translatable string, OK? raise RuntimeError("GRASS GIS Database directory <{}> does not exist".format(gisdbase)) location_valid = is_location_valid(gisdbase, location) mapset_valid = is_mapset_valid(gisdbase, location, mapset) if overwrite and location_valid and geostring: import shutil shutil.rmtree(os.path.join(gisdbase, location)) location_valid = False mapset_valid = False elif overwrite and mapset_valid: import shutil shutil.rmtree(os.path.join(gisdbase, location, mapset)) mapset_valid = False # some operations require GISRC # we create the ahead only when really needed # TODO: but we leave behind the file on some exceptions gisrc = None if not mapset_valid and location_valid: create_mapset(gisdbase, location, mapset) elif not mapset_valid and not location_valid and (create or geostring): # TODO: confusing, bring here some order gisrc = write_gisrc(gisdbase, location, mapset) os.environ['GISRC'] = gisrc create_location(gisdbase, location, geostring) if mapset != 'PERMANENT': create_mapset(gisdbase, location, mapset) elif not mapset_valid or not location_valid: raise RuntimeError(get_mapset_invalid_reason(gisdbase, location, mapset)) if not gisrc: gisrc = write_gisrc(gisdbase, location, mapset) os.environ['GISRC'] = gisrc # this could return some object which would have close, and del, # this would nicely hide whatever we add in the future # consequently this function could be also a method/constructor # of that class but that might not be that advantageous # issue is when we do del and user won't save the object # returning a simple object with close for now # TODO: add (optional) locking return GrassSession(gisrc)
_______________________________________________ grass-dev mailing list grass-dev@lists.osgeo.org https://lists.osgeo.org/mailman/listinfo/grass-dev