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

Reply via email to