[GRASS-dev] Python script test

I’ve done a Python version of the r.in.aster script I wrote some years back as a test for me as to what is involved in porting scripts from bash to Python. Since I started this, Glynn has suggested some changes to the parser for Python scripts along with some convenience modules. At least on my Mac, this script works very well with the existing g.parser code.

This was both a bad and good script for me to start with. It turns out that, unknown to me, recent updates to the script and gdal had made it non-functional on the Mac. Until I learned of this and how to fix it (thanks William Kyngesbury), I didn’t know whether the problems I was having were due to the underlying functions or to my Python code.

The good thing is that it was considerably easier to fix this in Python than it might have been with bash. It was also easy for me to update this script with some new and improved functionality.

My question is, would anyone like me to upload this someplace to test–and possibly modify to try out Glynn’s changes? I can put it into the SVN for GRASS 7 or elsewhere.

Michael


C. Michael Barton, Professor of Anthropology
Director of Graduate Studies
School of Human Evolution & Social Change
Center for Social Dynamics & Complexity
Arizona State University

Phone: 480-965-6262
Fax: 480-965-7671
www: <www.public.asu.edu/~cmbarton>

Michael Barton wrote:

My question is, would anyone like me to upload this someplace to test--
and possibly modify to try out Glynn's changes? I can put it into the
SVN for GRASS 7 or elsewhere.

Probably the easiest route for now is to add it as:

  scripts/r.in.aster/r.in.aster.py

But we will also need a standardised directory for Python modules (as
opposed to scripts), which will need to be added to PYTHONPATH by
Init.sh, so that e.g. "import grass" works.

--
Glynn Clements <glynn@gclements.plus.com>

I just posted a new Python version of the r.in.aster script. It is called r_in_aste.py and is currently residing in GRASS7/scripts/r.in.aster.

If you'd like to try it, it needs Python of course (2.4 or above), and wxPython (2.7 or above). There is an accompying html doc file.

Michael

On Jul 19, 2008, at 3:50 PM, Glynn Clements wrote:

Michael Barton wrote:

My question is, would anyone like me to upload this someplace to test--
and possibly modify to try out Glynn's changes? I can put it into the
SVN for GRASS 7 or elsewhere.

Probably the easiest route for now is to add it as:

  scripts/r.in.aster/r.in.aster.py

But we will also need a standardised directory for Python modules (as
opposed to scripts), which will need to be added to PYTHONPATH by
Init.sh, so that e.g. "import grass" works.

--
Glynn Clements <glynn@gclements.plus.com>

Michael Barton wrote:

I just posted a new Python version of the r.in.aster script. It is
called r_in_aste.py and is currently residing in GRASS7/scripts/
r.in.aster.

If you'd like to try it, it needs Python of course (2.4 or above), and
wxPython (2.7 or above).

AFAICT, it doesn't need wxPython.

A couple of suggestions and comments:

1. Wrap subprocess.call("g.message") in their own functions.
Ultimately, these are candidates for moving into the grass module.
Possibly, they should be replaced with Python code, or a call (via
SWIG) to the GRASS library functions.

2. Use a dictionary for the band names, rather than a conditional.

3. Don't redirect stderr to a pipe without reason. In particular,
don't redirect it if you aren't going to read it, as the child will
deadlock if it writes more than a buffer's worth of data to stderr.

4. For the equivalent of backticks, use p.communicate()[0] rather than
p.stdout.read(), as the former will consume stderr, and also wait()
for the child to terminate (otherwise you end up with zombie
processes).

5. I'm not sure that there's any point in using g.tempfile in Python
scripts.

I've attached a version which implements most of the above, and which
also uses an updated version of the grass.py module.

--
Glynn Clements <glynn@gclements.plus.com>

(attachments)

r_in_aster.py (5.87 KB)
grass.py (2.18 KB)

Thanks very much Glynn!!!

These are excellent suggestions AND shows the potential of Python for scripting.

I haven't tried this without wxPython, but I thought it was needed for the parser code. I'll test it with GRASS 6.4

I also wondered about using g.tempfile. The only reason to use it that I can think of is that the temp file ends up in a GRASS mapset rather than the global gmp directory.

Michael

On Jul 20, 2008, at 2:43 AM, Glynn Clements wrote:

Michael Barton wrote:

I just posted a new Python version of the r.in.aster script. It is
called r_in_aste.py and is currently residing in GRASS7/scripts/
r.in.aster.

If you'd like to try it, it needs Python of course (2.4 or above), and
wxPython (2.7 or above).

AFAICT, it doesn't need wxPython.

A couple of suggestions and comments:

1. Wrap subprocess.call("g.message") in their own functions.
Ultimately, these are candidates for moving into the grass module.
Possibly, they should be replaced with Python code, or a call (via
SWIG) to the GRASS library functions.

2. Use a dictionary for the band names, rather than a conditional.

3. Don't redirect stderr to a pipe without reason. In particular,
don't redirect it if you aren't going to read it, as the child will
deadlock if it writes more than a buffer's worth of data to stderr.

4. For the equivalent of backticks, use p.communicate()[0] rather than
p.stdout.read(), as the former will consume stderr, and also wait()
for the child to terminate (otherwise you end up with zombie
processes).

5. I'm not sure that there's any point in using g.tempfile in Python
scripts.

I've attached a version which implements most of the above, and which
also uses an updated version of the grass.py module.

--
Glynn Clements <glynn@gclements.plus.com>

#!/usr/bin/python
############################################################################
#
# MODULE: r_in_aster.py
# AUTHOR(S): Michael Barton (michael.barton@asu.edu)
# Based on r.in.aster bash script for GRASS
# by Michael Barton and Paul Kelly
# PURPOSE: Rectifies, georeferences, & imports Terra-ASTER imagery
# using gdalwarp
# COPYRIGHT: (C) 2008 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.
#
#############################################################################
#
# Requires:
# gdalwarp

#%Module
#% description: Georeference, rectify, and import Terra-ASTER imagery and relative DEM's using gdalwarp.
#% keywords: raster, imagery, import
#%End
#%option
#% key: input
#% type: string
#% gisprompt: old_file,file,input
#% description: Input ASTER image to be georeferenced & rectified
#% required: yes
#%end
#%option
#% key: proctype
#% type: string
#% description: ASTER imagery processing type (Level 1A, Level 1B, or relative DEM)
#% options: L1A,L1B,DEM
#% answer: L1B
#% required: yes
#%end
#%option
#% key: band
#% type: string
#% description: List L1A or L1B band to translate (1,2,3n,...), or enter 'all' to translate all bands
#% answer: all
#% required: yes
#%end
#%option
#% key: output
#% type: string
#% description: Base name for output raster map (band number will be appended to base name)
#% gisprompt: old,cell,raster
#% required: yes
#%end
#%flag
#% key: o
#% description: Overwrite existing file
#%END

import sys
import os
import subprocess
import platform
import grass

bands = {
   'L1A': {
       '1': "VNIR_Band1:ImageData",
       '2': "VNIR_Band2:ImageData",
       '3n': "VNIR_Band3N:ImageData",
       '3b': "VNIR_Band3B:ImageData",
       '4': "SWIR_Band4:ImageData",
       '5': "SWIR_Band5:ImageData",
       '6': "SWIR_Band6:ImageData",
       '7': "SWIR_Band7:ImageData",
       '8': "SWIR_Band8:ImageData",
       '9': "SWIR_Band9:ImageData",
       '10': "TIR_Band10:ImageData",
       '11': "TIR_Band11:ImageData",
       '12': "TIR_Band12:ImageData",
       '13': "TIR_Band13:ImageData",
       '14': "TIR_Band14:ImageData"
   },
   'L1B': {
       '1': "VNIR_Swath:ImageData1",
       '2': "VNIR_Swath:ImageData2",
       '3n': "VNIR_Swath:ImageData3N",
       '3b': "VNIR_Swath:ImageData3B",
       '4': "SWIR_Swath:ImageData4",
       '5': "SWIR_Swath:ImageData5",
       '6': "SWIR_Swath:ImageData6",
       '7': "SWIR_Swath:ImageData7",
       '8': "SWIR_Swath:ImageData8",
       '9': "SWIR_Swath:ImageData9",
       '10': "TIR_Swath:ImageData10",
       '11': "TIR_Swath:ImageData11",
       '12': "TIR_Swath:ImageData12",
       '13': "TIR_Swath:ImageData13",
       '14': "TIR_Swath:ImageData14"
   }
}

def _message(msg, *args):
   subprocess.call(["g.message", "message=%s" % msg] + list(args))

def debug(msg):
   _message(msg, '-d')

def message(msg):
   _message(msg)

def error(msg):
   _message(msg, '-e')
   sys.exit(1)

def main():

   #check whether gdalwarp is in path and executable
   p = None
   try:
       p = subprocess.call(['gdalwarp', '--version'])
   except:
       pass
   if p == None or p != 0:
       error("gdalwarp is not in the path and executable")

   #initialize variables
   dataset = ''
   srcfile = ''
   proj = ''
   band = ''
   outfile = ''
   bandlist =

   #create temporary file to hold gdalwarp output before importing to GRASS
   tempfile = grass.read_command("g.tempfile", pid = os.getpid()).strip() + '.tif'

   #get projection information for current GRASS location
   proj = grass.read_command('g.proj', flags = 'jf').strip()

   #currently only runs in projected location
   if "XY location" in proj:
     error ("This module needs to be run in a projected location (found: %s)" % proj)

   #process list of bands
   if options['band'].strip() == 'all':
       bandlist = ['1','2','3n','3b','4','5','6','7','8','9','10','11','12','13','14']
   else:
       bandlist = options['band'].split(',')

   print 'bandlist =',bandlist

   #initialize datasets for L1A and L1B
   #

   if options['proctype'] in ["L1A", "L1B"]:
       for band in bandlist:
      dataset = bands[options['proctype']][band]
           srcfile = "HDF4_EOS:EOS_SWATH:%s:%s" % (options['input'], dataset)
           import_aster(proj, srcfile, tempfile, band)
   elif options['proctype'] == "DEM":
       srcfile=options['input']
       import_aster(proj, srcfile, tempfile, "DEM")

   #for debugging
   #print 'source file=',srcfile
   #print 'tempfile=',tempfile
   #print 'proj =',proj

   # write cmd history: Not sure how to replicate this in Python yet
   #r.support "$GIS_OPT_OUTPUT" history="${CMDLINE}"

   #cleanup
   message("Cleaning up ...")
   os.remove(tempfile)
   message("Done.")

   return

def import_aster(proj, srcfile, tempfile, band):
   #run gdalwarp with selected options (must be in $PATH)
   #to translate aster image to geotiff
   message("Georeferencing aster image ...")
   debug("gdalwarp -t_srs %s %s %s" % (proj, srcfile, tempfile))

   if platform.system() == "Darwin":
       cmd = ["arch", "-i386", "gdalwarp", "-t_srs", proj, srcfile, tempfile ]
   else:
       cmd = ["gdalwarp", "-t_srs", proj, srcfile, tempfile ]
   p = subprocess.call(cmd)
   if p != 0:
       #check to see if gdalwarp executed properly
       return

   #p = subprocess.call(["gdal_translate", srcfile, tempfile])

   #import geotiff to GRASS
   message("Importing into GRASS ...")
   outfile = options['output'].strip()+'.'+band
   grass.run_command("r.in.gdal", overwrite = flags['o'], input = tempfile, output = outfile)

   return

if __name__ == "__main__":
   options, flags = grass.parser()
   main()

import os
import os.path
import sys
import types
import subprocess

def _make_val(val):
   if isinstance(val, types.StringType):
  return val
   if isinstance(val, types.ListType):
  return ",".join(map(_make_val, val))
   if isinstance(val, types.TupleType):
  return _make_val(list(val))
   return str(val)

def make_command(prog, flags = "", overwrite = False, quiet = False, verbose = False, **options):
   args = [prog]
   if overwrite:
  args.append("--o")
   if quiet:
  args.append("--q")
   if verbose:
  args.append("--v")
   if flags:
  args.append("-%s" % flags)
   for opt, val in options.iteritems():
  args.append("%s=%s" % (opt, _make_val(val)))
   return args

def start_command(prog, flags = "", overwrite = False, quiet = False, verbose = False, **kwargs):
   options = {}
   popts = {}
   for opt, val in kwargs.iteritems():
  if opt in ["bufsize", "executable", "stdin", "stdout", "stderr",
       "preexec_fn", "close_fds", "cwd", "env",
       "universal_newlines", "startupinfo", "creationflags"]:
      popts[opt] = val
  else:
      options[opt] = val
   args = make_command(prog, flags, overwrite, quiet, verbose, **options)
   return subprocess.Popen(args, **popts)

def run_command(*args, **kwargs):
   ps = start_command(*args, **kwargs)
   return ps.wait()

def read_command(*args, **kwargs):
   kwargs['stdout'] = subprocess.PIPE
   ps = start_command(*args, **kwargs)
   return ps.communicate()[0]

def _parse_env():
   options = {}
   flags = {}
   for var, val in os.environ.iteritems():
  if var.startswith("GIS_OPT_"):
      opt = var.replace("GIS_OPT_", "", 1).lower()
      options[opt] = val;
  if var.startswith("GIS_FLAG_"):
      flg = var.replace("GIS_FLAG_", "", 1).lower()
      flags[flg] = bool(int(val));
   return (options, flags)

def parser():
   if not os.getenv("GISBASE"):
       print >> sys.stderr, "You must be in GRASS GIS to run this program."
       sys.exit(1)

   if len(sys.argv) > 1 and sys.argv[1] == "@ARGS_PARSED@":
  return _parse_env()

   argv = sys.argv[:]
   name = argv[0]
   if not os.path.isabs(name):
  argv[0] = os.path.normpath(os.path.join(sys.path[0], name))

   os.execvp("g.parser", [name] + argv)
   raise OSError("error executing g.parser")

Michael Barton wrote:

I haven't tried this without wxPython, but I thought it was needed for
the parser code. I'll test it with GRASS 6.4

g.parser calls G_parser(); if the module has at least one required
option but you don't provide any (or if you use --ui), G_parser() will
call G_gui(), which uses either Tcl/Tk or wxPython depending upon the
setting of GRASS_GUI (if it's set to wxpython, you get the wxPython
dialogs, otherwise you get the Tcl/Tk dialogs).

If you have GRASS_GUI=wxpython, it's safe to assume that you have
wxPython installed. If you have GRASS_GUI=tcltk (or, for that matter,
GRASS_GUI=text), you'll get the Tcl/Tk GUI. The fact that the script
which calls g.parser is written in Python doesn't affect it.

I also wondered about using g.tempfile. The only reason to use it that
I can think of is that the temp file ends up in a GRASS mapset rather
than the global gmp directory.

I'm not sure if that's good or bad.

The use of <mapset>/.tmp by G_tempfile() is for a very specific
reason.

New cell/fcell are created in that directory and rename()d into place
when the map is closed. But rename() requires that the source and
destination are on the same partition (rename() just adds and removes
directory entries; it doesn't "move" the file's contents).

/tmp and /home are often separate partitions, so trying to create
temporary files in /tmp or $TMPDIR and rename() them into the mapset
directory will quite possibly fail.

The easiest way to ensure that the file is created on the same
partition as the mapset's cell/fcell directory is to create the
temporary files within the mapset directory (if you decide to mount a
separate partition at <mapset>/.tmp, you lose).

But, unless you're playing filesystem games with rename() (or link()),
there isn't actually any need for temporary files to go into the
mapset directory. They could just as easily go into e.g. the
/tmp/grass-<user>-<pid> directory. That may be more efficient, e.g. if
/tmp is a hard disk but GISDBASE is on an NFS share.

Ultimately, it doesn't particularly matter. In any case, the grass.py
module provides a tempfile() function which (currently) uses
g.tempfile, although it could be changed to use e.g. os.tempnam() if
that was preferred.

--
Glynn Clements <glynn@gclements.plus.com>

Glynn Clements wrote:

I've attached a version which implements most of the above, and which
also uses an updated version of the grass.py module.

I have now added the grass.py module to SVN trunk, at
lib/python/grass.py. It gets installed to the $GISBASE/etc/python
directory, which is added to PYTHONPATH by Init.sh, so scripts can use
"import grass" to use the functions which it provides.

Apart from the subprocess wrappers, it provides interfaces to several
common g.* modules: g.parser, g.message, g.tempfile, g.gisenv,
g.region, g.findfile, and g.list.

This should largely eliminate the need for scripts to call those
modules directly, at least for the most common usage.

--
Glynn Clements <glynn@gclements.plus.com>

Thanks again Glynn. This will be very useful. When you get a chance, can you provide a quick summary of these new python interfaces in grass.py? Once I get my system back up and running, I'll update from the SVN and look at this library.

Michael
____________________
C. Michael Barton, Professor of Anthropology
Director of Graduate Studies
School of Human Evolution & Social Change
Center for Social Dynamics & Complexity
Arizona State University

Phone: 480-965-6262
Fax: 480-965-7671
www: <www.public.asu.edu/~cmbarton>

On Jul 20, 2008, at 1:02 PM, Glynn Clements wrote:

Glynn Clements wrote:

I've attached a version which implements most of the above, and which
also uses an updated version of the grass.py module.

I have now added the grass.py module to SVN trunk, at
lib/python/grass.py. It gets installed to the $GISBASE/etc/python
directory, which is added to PYTHONPATH by Init.sh, so scripts can use
"import grass" to use the functions which it provides.

Apart from the subprocess wrappers, it provides interfaces to several
common g.* modules: g.parser, g.message, g.tempfile, g.gisenv,
g.region, g.findfile, and g.list.

This should largely eliminate the need for scripts to call those
modules directly, at least for the most common usage.

--
Glynn Clements <glynn@gclements.plus.com>

On Jul 20, 2008, at 12:33 PM, Glynn Clements wrote:

Michael Barton wrote:

I haven't tried this without wxPython, but I thought it was needed for
the parser code. I'll test it with GRASS 6.4

g.parser calls G_parser(); if the module has at least one required
option but you don't provide any (or if you use --ui), G_parser() will
call G_gui(), which uses either Tcl/Tk or wxPython depending upon the
setting of GRASS_GUI (if it's set to wxpython, you get the wxPython
dialogs, otherwise you get the Tcl/Tk dialogs).

If you have GRASS_GUI=wxpython, it's safe to assume that you have
wxPython installed. If you have GRASS_GUI=tcltk (or, for that matter,
GRASS_GUI=text), you'll get the Tcl/Tk GUI. The fact that the script
which calls g.parser is written in Python doesn't affect it.

I also wondered about using g.tempfile. The only reason to use it that
I can think of is that the temp file ends up in a GRASS mapset rather
than the global gmp directory.

I'm not sure if that's good or bad.

The use of <mapset>/.tmp by G_tempfile() is for a very specific
reason.

New cell/fcell are created in that directory and rename()d into place
when the map is closed. But rename() requires that the source and
destination are on the same partition (rename() just adds and removes
directory entries; it doesn't "move" the file's contents).

/tmp and /home are often separate partitions, so trying to create
temporary files in /tmp or $TMPDIR and rename() them into the mapset
directory will quite possibly fail.

The easiest way to ensure that the file is created on the same
partition as the mapset's cell/fcell directory is to create the
temporary files within the mapset directory (if you decide to mount a
separate partition at <mapset>/.tmp, you lose).

But, unless you're playing filesystem games with rename() (or link()),
there isn't actually any need for temporary files to go into the
mapset directory. They could just as easily go into e.g. the
/tmp/grass-<user>-<pid> directory. That may be more efficient, e.g. if
/tmp is a hard disk but GISDBASE is on an NFS share.

Ultimately, it doesn't particularly matter. In any case, the grass.py
module provides a tempfile() function which (currently) uses
g.tempfile, although it could be changed to use e.g. os.tempnam() if
that was preferred.

I committed your changes to the r_in_aster.py script and did some tests.

Init.sh is not updating PYTHONPATH on my Mac for some reason. I've looked at the code and it seems fine. I DO have a PYTHONPATH. So I'm not sure why it is not updating it. If I change it manually, by simply pasting your code (export PYTHONPATH="$GISBASE/etc/python:$PYTHONPATH") into the GRASS prompt, PYTHONPATH IS modified appropriately and r_in_aster.py works fine with the new grass.py library.

The only odd difference that I notice is that the source file is init.sh and the startup in the binary is Init.sh. This is especially odd since case doesn't matter when running programs on a Mac. I'm copying William Kyngesbury in case he has some insight.

I set GRASS_GUI=tcltk and restarted GRASS. r_in_aster.py launches with a TclTk GUI, even though it is a Python script. What you said made sense, but I wanted to test to make sure that something odd didn't happen.

With respect to the tempfile question, what happens on Windows when you use g.tempfile vs. Python's tempfile.TemporaryFile()? Overall, it seems like the Python tempfile module is easier to work with and potentially more secure.

Michael

Michael Barton wrote:

Init.sh is not updating PYTHONPATH on my Mac for some reason. I've
looked at the code and it seems fine. I DO have a PYTHONPATH. So I'm
not sure why it is not updating it. If I change it manually, by simply
pasting your code (export PYTHONPATH="$GISBASE/etc/python:
$PYTHONPATH") into the GRASS prompt, PYTHONPATH IS modified
appropriately and r_in_aster.py works fine with the new grass.py
library.

I suspect that /etc/profile resets PYTHONPATH. Init.sh creates a
.bashrc script in the mapset directory (which, at the point that the
session shell is started, is $HOME), and that script sources
/etc/profile.

That's almost certainly bogus. /etc/profile is only meant to be
sourced by login shells, not by subshells, largely for reasons such as
this. I have fixed this in SVN trunk.

AFAICT, you can get around this by setting the environment variable
PROFILEREAD (to anything) before starting GRASS (this looks backwards;
I would expect such a variable to cause /etc/profile to be read, not
inhibit it).

Really, Init.sh does way too much. Over the years, it has accumulated
all manner of "neat tricks" (where "neat" is from the perspective of
whoever decided to add them).

The only odd difference that I notice is that the source file is
init.sh and the startup in the binary is Init.sh. This is especially
odd since case doesn't matter when running programs on a Mac. I'm
copying William Kyngesbury in case he has some insight.

Init.sh is generated from init.sh by substitution:

  $(ETC)/Init.sh: init.sh
    rm -f $@
    $(SHELL) -c "sed \
    -e \"s#GRASS_VERSION_NUMBER#$(GRASS_VERSION_NUMBER)#\" \
    -e \"s#GRASS_VERSION_DATE#$(GRASS_VERSION_DATE)#\" \
    -e \"s#GRASS_VERSION_UPDATE_PKG#$(GRASS_VERSION_UPDATE)#\" \
    -e \"s#LD_LIBRARY_PATH_VAR#$(LD_LIBRARY_PATH_VAR)#g\" \
    -e \"s#PERL_COMMAND#$(PERL)#\" \
    -e \"s#START_UP#$(START_UP)#\" \
    -e \"s#CONFIG_PROJSHARE#$(PROJSHARE)#\" \
    init.sh > $@"
    chmod +x $@

With respect to the tempfile question, what happens on Windows when
you use g.tempfile vs. Python's tempfile.TemporaryFile()? Overall, it
seems like the Python tempfile module is easier to work with and
potentially more secure.

AFAICT, tempfile.TemporaryFile() won't suffice for most uses, as the
filename may be removed as soon as it is created. More below.

The main security risk is with temporary files is symlink attacks. You
choose a temporary filename, then open the file for write, but before
you have chance to do so, an attacker creates a symlink with the
chosen name, pointing at some existing file, which you then end up
overwriting.

That's only a risk because temporary files are traditionally created
in the /tmp directory, which is world-writable. If you create
temporary files in a directory for which only you have write
permission, it isn't a problem.

When tempfile functions are described as insecure, this is the issue
in question. The "secure" alternatives invariably open the temporary
file for write (without truncating the file); once the file is open,
the function re-checks whether the name refers to a file or to a
symlink. If it's a symlink, it immediately closes the file and tries
again.

Such functions often return a descriptor or FILE* rather than a
filename. Python's os.tmpfile() behaves like this. Unfortunately, this
is of no use if you need to pass a filename to another program (files
created by os.tmpfile() don't have a name, and will be automatically
deleted when closed).

os.tempnam() and os.tmpnam() both return names without creating the
file. os.tempnam() allows the directory to be specified, so you could
use e.g. /tmp/grass-<user>-<pid> or <mapset>/.tmp/<hostname> for the
files. os.tmpnam() doesn't allow the directory to be specified;
according to the documentation, the Windows implementation chooses a
name in the root directory of the current driver, where you may not
even have permission to create files.

I hadn't looked at the tempfile module before now.

TemporaryFile() and NamedTemporaryFile() produce files which may not
have a name or which may not be able to be re-opened (i.e.
auto-deleted when closed), so they can't be used in the general case,
although they may be useful for scripts wanting to use temporary files
internally (although os.tmpfile() will suffice there).

mkstemp() would suffice for creating temporary files. It returns both
the handle (which you can close) and the filename, and the file isn't
autodeleted.

mktemp() is like mkstemp() but doesn't open the file, and just returns
a filename. This potentially suffers from symlink attackes, but that
isn't an issue if a private directory is used.

--
Glynn Clements <glynn@gclements.plus.com>

Michael Barton wrote:

Thanks again Glynn. This will be very useful. When you get a chance,
can you provide a quick summary of these new python interfaces in
grass.py? Once I get my system back up and running, I'll update from
the SVN and look at this library.

Most of them are simple enough to be self-explanatory, but some comments:

[I have added this as lib/python/README.txt]

def make_command(prog, flags = "", overwrite = False, quiet = False, verbose = False, **options):

Return a list of strings suitable for use as the args parameter to
Popen() or call(). Example:

grass.make_command("g.message", flags = 'w', message = 'this is a warning')

['g.message', '-w', 'message=this is a warning']

def start_command(prog, flags = "", overwrite = False, quiet = False, verbose = False, **kwargs):

Returns a Popen object with the command created by make_command.
Accepts any of the arguments which Popen() accepts apart from "args"
and "shell". Example:

p = grass.start_command("g.gisenv", stdout = subprocess.PIPE)
print p

<subprocess.Popen object at 0xb7c12f6c>

print p.communicate()[0]

GISDBASE='/opt/grass-data';
LOCATION_NAME='spearfish57';
MAPSET='glynn';
GRASS_DB_ENCODING='ascii';
GRASS_GUI='text';
MONITOR='x0';

def run_command(*args, **kwargs):

Passes all arguments to start_command, then waits for the process to
complete, returning its exit code. Similar to subprocess.call(), but
with the make_command() interface.

def read_command(*args, **kwargs):

Passes all arguments to start_command, then waits for the process to
complete, returning its stdout (i.e. similar to shell "backticks").

def message(msg, flag = None):
def debug(msg):
def verbose(msg):
def info(msg):
def warning(msg):
def error(msg):

These all run g.message, differing only in which flag (if any) is used.

def fatal(msg):

Like error(), but also calls sys.exit(1).

def parser():

Interface to g.parser, intended to be run from the top-level, e.g.:

  if __name__ == "__main__":
      options, flags = grass.parser()
      main()

Thereafter, the global variables "options" and "flags" will be
dictionaries containing option/flag values, keyed by lower-case
option/flag names. The values in "options" are strings, those in
"flags" are Python booleans.

def tempfile():

Returns the name of a temporary file, created with g.tempfile.

def gisenv():

Returns the output from running g.gisenv (with no arguments), as a
dictionary. Example:

env = grass.gisenv()
print env['GISDBASE']

/opt/grass-data

def region():

Returns the output from running "g.region -g", as a dictionary.
Example:

region = grass.region()
[region[key] for key in "nsew"]

['4928000', '4914020', '609000', '590010']

(region['nsres'], region['ewres'])

('30', '30')

def use_temp_region():

Copies the current region to a temporary region with "g.region save=",
then sets WIND_OVERRIDE to refer to that region. Installs an atexit
handler to delete the temporary region upon termination.

def del_temp_region():

Unsets WIND_OVERRIDE and removes any region named by it.

def find_file(name, element = 'cell'):

Returns the output from running g.findfile as a dictionary. Example:

result = grass.find_file('fields', element = 'vector')
print result['fullname']

fields@PERMANENT

print result['file']

/opt/grass-data/spearfish57/PERMANENT/vector/fields

def list_grouped(type):

Returns the output from running g.list, as a dictionary where the keys
are mapset names and the values are lists of maps in that mapset.
Example:

grass.list_grouped('rast')['PERMANENT']

['aspect', 'erosion1', 'quads', 'soils', 'strm.dist', ...

def list_pairs(type):

Returns the output from running g.list, as a list of (map, mapset)
pairs. Example:

grass.list_pairs('rast')

[('aspect', 'PERMANENT'), ('erosion1', 'PERMANENT'), ('quads', 'PERMANENT'), ...

def list_strings(type):

Returns the output from running g.list, as a list of qualified names.
Example:

grass.list_strings('rast')

['aspect@PERMANENT', 'erosion1@PERMANENT', 'quads@PERMANENT', 'soils@PERMANENT', ...

--
Glynn Clements <glynn@gclements.plus.com>

On Jul 21, 2008, at 4:17 AM, Glynn Clements wrote:

Michael Barton wrote:

Init.sh is not updating PYTHONPATH on my Mac for some reason. I've
looked at the code and it seems fine. I DO have a PYTHONPATH. So I'm
not sure why it is not updating it. If I change it manually, by simply
pasting your code (export PYTHONPATH="$GISBASE/etc/python:
$PYTHONPATH") into the GRASS prompt, PYTHONPATH IS modified
appropriately and r_in_aster.py works fine with the new grass.py
library.

I suspect that /etc/profile resets PYTHONPATH. Init.sh creates a
.bashrc script in the mapset directory (which, at the point that the
session shell is started, is $HOME), and that script sources
/etc/profile.

That's almost certainly bogus. /etc/profile is only meant to be
sourced by login shells, not by subshells, largely for reasons such as
this. I have fixed this in SVN trunk.

As far as I can tell, the OSX /etc/profile only *adds* to PATH and MANPATH, and doesn't touch python stuff.

Are you sure you added PYTHONPATH code to init.sh? I don't see anything that sets PYTHONPATH. (just updated SVN, checked trunk also)

-----
William Kyngesburye <kyngchaos*at*kyngchaos*dot*com>
http://www.kyngchaos.com/

"This is a question about the past, is it? ... How can I tell that the past isn't a fiction designed to account for the discrepancy between my immediate physical sensations and my state of mind?"

- The Ruler of the Universe

William Kyngesburye wrote:

Are you sure you added PYTHONPATH code to init.sh? I don't see
anything that sets PYTHONPATH. (just updated SVN, checked trunk also)

http://trac.osgeo.org/grass/browser/grass/trunk/lib/init/init.sh#L299

--
Glynn Clements <glynn@gclements.plus.com>

On Jul 21, 2008, at 10:47 AM, Glynn Clements wrote:

William Kyngesburye wrote:

Are you sure you added PYTHONPATH code to init.sh? I don't see
anything that sets PYTHONPATH. (just updated SVN, checked trunk also)

http://trac.osgeo.org/grass/browser/grass/trunk/lib/init/init.sh#L299

Hehe, I said I checked trunk, but I forgot to update first (I updated on dev branch). oops, coffee hadn't kicked in yet :wink:

-----
William Kyngesburye <kyngchaos*at*kyngchaos*dot*com>
http://www.kyngchaos.com/

Earth: "Mostly harmless"

- revised entry in the HitchHiker's Guide to the Galaxy

On Jul 21, 2008, at 2:17 AM, Glynn Clements wrote:

Michael Barton wrote:

Init.sh is not updating PYTHONPATH on my Mac for some reason. I've
looked at the code and it seems fine. I DO have a PYTHONPATH. So I'm
not sure why it is not updating it. If I change it manually, by simply
pasting your code (export PYTHONPATH="$GISBASE/etc/python:
$PYTHONPATH") into the GRASS prompt, PYTHONPATH IS modified
appropriately and r_in_aster.py works fine with the new grass.py
library.

I suspect that /etc/profile resets PYTHONPATH. Init.sh creates a
.bashrc script in the mapset directory (which, at the point that the
session shell is started, is $HOME), and that script sources
/etc/profile.

That's it. PYTHONPATH is set in my .profile. So it overwrites whatever GRASS has set. I can fix this for me, but yours fixes this more generally.

Michael

That's almost certainly bogus. /etc/profile is only meant to be
sourced by login shells, not by subshells, largely for reasons such as
this. I have fixed this in SVN trunk.

AFAICT, you can get around this by setting the environment variable
PROFILEREAD (to anything) before starting GRASS (this looks backwards;
I would expect such a variable to cause /etc/profile to be read, not
inhibit it).

Really, Init.sh does way too much. Over the years, it has accumulated
all manner of "neat tricks" (where "neat" is from the perspective of
whoever decided to add them).

The only odd difference that I notice is that the source file is
init.sh and the startup in the binary is Init.sh. This is especially
odd since case doesn't matter when running programs on a Mac. I'm
copying William Kyngesbury in case he has some insight.

Init.sh is generated from init.sh by substitution:

  $(ETC)/Init.sh: init.sh
    rm -f $@
    $(SHELL) -c "sed \
    -e \"s#GRASS_VERSION_NUMBER#$(GRASS_VERSION_NUMBER)#\" \
    -e \"s#GRASS_VERSION_DATE#$(GRASS_VERSION_DATE)#\" \
    -e \"s#GRASS_VERSION_UPDATE_PKG#$(GRASS_VERSION_UPDATE)#\" \
    -e \"s#LD_LIBRARY_PATH_VAR#$(LD_LIBRARY_PATH_VAR)#g\" \
    -e \"s#PERL_COMMAND#$(PERL)#\" \
    -e \"s#START_UP#$(START_UP)#\" \
    -e \"s#CONFIG_PROJSHARE#$(PROJSHARE)#\" \
    init.sh > $@"
    chmod +x $@

With respect to the tempfile question, what happens on Windows when
you use g.tempfile vs. Python's tempfile.TemporaryFile()? Overall, it
seems like the Python tempfile module is easier to work with and
potentially more secure.

AFAICT, tempfile.TemporaryFile() won't suffice for most uses, as the
filename may be removed as soon as it is created. More below.

The main security risk is with temporary files is symlink attacks. You
choose a temporary filename, then open the file for write, but before
you have chance to do so, an attacker creates a symlink with the
chosen name, pointing at some existing file, which you then end up
overwriting.

That's only a risk because temporary files are traditionally created
in the /tmp directory, which is world-writable. If you create
temporary files in a directory for which only you have write
permission, it isn't a problem.

When tempfile functions are described as insecure, this is the issue
in question. The "secure" alternatives invariably open the temporary
file for write (without truncating the file); once the file is open,
the function re-checks whether the name refers to a file or to a
symlink. If it's a symlink, it immediately closes the file and tries
again.

Such functions often return a descriptor or FILE* rather than a
filename. Python's os.tmpfile() behaves like this. Unfortunately, this
is of no use if you need to pass a filename to another program (files
created by os.tmpfile() don't have a name, and will be automatically
deleted when closed).

os.tempnam() and os.tmpnam() both return names without creating the
file. os.tempnam() allows the directory to be specified, so you could
use e.g. /tmp/grass-<user>-<pid> or <mapset>/.tmp/<hostname> for the
files. os.tmpnam() doesn't allow the directory to be specified;
according to the documentation, the Windows implementation chooses a
name in the root directory of the current driver, where you may not
even have permission to create files.

I hadn't looked at the tempfile module before now.

TemporaryFile() and NamedTemporaryFile() produce files which may not
have a name or which may not be able to be re-opened (i.e.
auto-deleted when closed), so they can't be used in the general case,
although they may be useful for scripts wanting to use temporary files
internally (although os.tmpfile() will suffice there).

mkstemp() would suffice for creating temporary files. It returns both
the handle (which you can close) and the filename, and the file isn't
autodeleted.

mktemp() is like mkstemp() but doesn't open the file, and just returns
a filename. This potentially suffers from symlink attackes, but that
isn't an issue if a private directory is used.

--
Glynn Clements <glynn@gclements.plus.com>

Thanks Glynn. These will make all Python scripting easier.

Michael

Michael Barton wrote:

Thanks again Glynn. This will be very useful. When you get a chance,
can you provide a quick summary of these new python interfaces in
grass.py? Once I get my system back up and running, I'll update from
the SVN and look at this library.

Most of them are simple enough to be self-explanatory, but some comments:

[I have added this as lib/python/README.txt]

def make_command(prog, flags = "", overwrite = False, quiet = False, verbose = False, **options):

Return a list of strings suitable for use as the args parameter to
Popen() or call(). Example:

grass.make_command("g.message", flags = 'w', message = 'this is a warning')

['g.message', '-w', 'message=this is a warning']

def start_command(prog, flags = "", overwrite = False, quiet = False, verbose = False, **kwargs):

Returns a Popen object with the command created by make_command.
Accepts any of the arguments which Popen() accepts apart from "args"
and "shell". Example:

p = grass.start_command("g.gisenv", stdout = subprocess.PIPE)
print p

<subprocess.Popen object at 0xb7c12f6c>

print p.communicate()[0]

GISDBASE='/opt/grass-data';
LOCATION_NAME='spearfish57';
MAPSET='glynn';
GRASS_DB_ENCODING='ascii';
GRASS_GUI='text';
MONITOR='x0';

def run_command(*args, **kwargs):

Passes all arguments to start_command, then waits for the process to
complete, returning its exit code. Similar to subprocess.call(), but
with the make_command() interface.

def read_command(*args, **kwargs):

Passes all arguments to start_command, then waits for the process to
complete, returning its stdout (i.e. similar to shell "backticks").

def message(msg, flag = None):
def debug(msg):
def verbose(msg):
def info(msg):
def warning(msg):
def error(msg):

These all run g.message, differing only in which flag (if any) is used.

def fatal(msg):

Like error(), but also calls sys.exit(1).

def parser():

Interface to g.parser, intended to be run from the top-level, e.g.:

  if __name__ == "__main__":
      options, flags = grass.parser()
      main()

Thereafter, the global variables "options" and "flags" will be
dictionaries containing option/flag values, keyed by lower-case
option/flag names. The values in "options" are strings, those in
"flags" are Python booleans.

def tempfile():

Returns the name of a temporary file, created with g.tempfile.

def gisenv():

Returns the output from running g.gisenv (with no arguments), as a
dictionary. Example:

env = grass.gisenv()
print env['GISDBASE']

/opt/grass-data

def region():

Returns the output from running "g.region -g", as a dictionary.
Example:

region = grass.region()
[region[key] for key in "nsew"]

['4928000', '4914020', '609000', '590010']

(region['nsres'], region['ewres'])

('30', '30')

def use_temp_region():

Copies the current region to a temporary region with "g.region save=",
then sets WIND_OVERRIDE to refer to that region. Installs an atexit
handler to delete the temporary region upon termination.

def del_temp_region():

Unsets WIND_OVERRIDE and removes any region named by it.

def find_file(name, element = 'cell'):

Returns the output from running g.findfile as a dictionary. Example:

result = grass.find_file('fields', element = 'vector')
print result['fullname']

fields@PERMANENT

print result['file']

/opt/grass-data/spearfish57/PERMANENT/vector/fields

def list_grouped(type):

Returns the output from running g.list, as a dictionary where the keys
are mapset names and the values are lists of maps in that mapset.
Example:

grass.list_grouped('rast')['PERMANENT']

['aspect', 'erosion1', 'quads', 'soils', 'strm.dist', ...

def list_pairs(type):

Returns the output from running g.list, as a list of (map, mapset)
pairs. Example:

grass.list_pairs('rast')

[('aspect', 'PERMANENT'), ('erosion1', 'PERMANENT'), ('quads', 'PERMANENT'), ...

def list_strings(type):

Returns the output from running g.list, as a list of qualified names.
Example:

grass.list_strings('rast')

['aspect@PERMANENT', 'erosion1@PERMANENT', 'quads@PERMANENT', 'soils@PERMANENT', ...

--
Glynn Clements <glynn@gclements.plus.com>

On Jul 21, 2008, at 12:14 PM, Michael Barton wrote:

On Jul 21, 2008, at 2:17 AM, Glynn Clements wrote:

I suspect that /etc/profile resets PYTHONPATH. Init.sh creates a
.bashrc script in the mapset directory (which, at the point that the
session shell is started, is $HOME), and that script sources
/etc/profile.

That's it. PYTHONPATH is set in my .profile. So it overwrites whatever GRASS has set. I can fix this for me, but yours fixes this more generally.

See my other reply - /etc/profile doesn't touch PYTHONPATH. ~/.profile is not /etc/profile, and /etc/profile doesn't load ~/.profile, as far as I can tell.

-----
William Kyngesburye <kyngchaos*at*kyngchaos*dot*com>
http://www.kyngchaos.com/

"History is an illusion caused by the passage of time, and time is an illusion caused by the passage of history."

- Hitchhiker's Guide to the Galaxy

Michael Barton wrote:

>> Init.sh is not updating PYTHONPATH on my Mac for some reason. I've
>> looked at the code and it seems fine. I DO have a PYTHONPATH. So I'm
>> not sure why it is not updating it. If I change it manually, by
>> simply
>> pasting your code (export PYTHONPATH="$GISBASE/etc/python:
>> $PYTHONPATH") into the GRASS prompt, PYTHONPATH IS modified
>> appropriately and r_in_aster.py works fine with the new grass.py
>> library.
>
> I suspect that /etc/profile resets PYTHONPATH. Init.sh creates a
> .bashrc script in the mapset directory (which, at the point that the
> session shell is started, is $HOME), and that script sources
> /etc/profile.
>

That's it. PYTHONPATH is set in my .profile.

~/.profile is only read by login shells. ~/.alias is sourced if it
exists, as well as ~/.grass.bashrc.

--
Glynn Clements <glynn@gclements.plus.com>

I just found out that .profile (and I suppose other shell configuration scripts) won't recognize backslashes as a line continuation in PATH or PYTHONPATH statements.

Michael

Michael Barton wrote:

Init.sh is not updating PYTHONPATH on my Mac for some reason. I've
looked at the code and it seems fine. I DO have a PYTHONPATH. So I'm
not sure why it is not updating it. If I change it manually, by
simply
pasting your code (export PYTHONPATH="$GISBASE/etc/python:
$PYTHONPATH") into the GRASS prompt, PYTHONPATH IS modified
appropriately and r_in_aster.py works fine with the new grass.py
library.

I suspect that /etc/profile resets PYTHONPATH. Init.sh creates a
.bashrc script in the mapset directory (which, at the point that the
session shell is started, is $HOME), and that script sources
/etc/profile.

That's it. PYTHONPATH is set in my .profile.

~/.profile is only read by login shells. ~/.alias is sourced if it
exists, as well as ~/.grass.bashrc.

--
Glynn Clements <glynn@gclements.plus.com>

Michael Barton wrote:

I just found out that .profile (and I suppose other shell
configuration scripts) won't recognize backslashes as a line
continuation in PATH or PYTHONPATH statements.

Are you quoting the value? Within single quotes, all characters are
treated literally, including backslash and newline characters. Other
than that, backslash-newline is always discarded.

Also, ensure that you don't have CRLF line terminators. CR is treated
as whitespace, and trailing whitespace is normally harmless, but
backslash-newline is only recognised if the newline immediately
follows the backslash, without any intervening whitespace.

--
Glynn Clements <glynn@gclements.plus.com>