| 1 | """
|
|---|
| 2 | Core functions to be used in Python scripts.
|
|---|
| 3 |
|
|---|
| 4 | Usage:
|
|---|
| 5 |
|
|---|
| 6 | ::
|
|---|
| 7 |
|
|---|
| 8 | from grass.script import core as grass
|
|---|
| 9 | grass.parser()
|
|---|
| 10 |
|
|---|
| 11 | (C) 2008-2014 by the GRASS Development Team
|
|---|
| 12 | This program is free software under the GNU General Public
|
|---|
| 13 | License (>=v2). Read the file COPYING that comes with GRASS
|
|---|
| 14 | for details.
|
|---|
| 15 |
|
|---|
| 16 | .. sectionauthor:: Glynn Clements
|
|---|
| 17 | .. sectionauthor:: Martin Landa <landa.martin gmail.com>
|
|---|
| 18 | .. sectionauthor:: Michael Barton <michael.barton asu.edu>
|
|---|
| 19 | """
|
|---|
| 20 | from __future__ import absolute_import, print_function
|
|---|
| 21 |
|
|---|
| 22 | import os
|
|---|
| 23 | import sys
|
|---|
| 24 | import atexit
|
|---|
| 25 | import subprocess
|
|---|
| 26 | import shutil
|
|---|
| 27 | import codecs
|
|---|
| 28 | import string
|
|---|
| 29 | import random
|
|---|
| 30 | import types as python_types
|
|---|
| 31 |
|
|---|
| 32 | from .utils import KeyValue, parse_key_val, basename, encode
|
|---|
| 33 | from grass.exceptions import ScriptError, CalledModuleError
|
|---|
| 34 |
|
|---|
| 35 | # i18N
|
|---|
| 36 | import gettext
|
|---|
| 37 | gettext.install('grasslibs', os.path.join(os.getenv("GISBASE"), 'locale'))
|
|---|
| 38 |
|
|---|
| 39 | try:
|
|---|
| 40 | # python2
|
|---|
| 41 | import __builtin__
|
|---|
| 42 | from os import environ
|
|---|
| 43 | except ImportError:
|
|---|
| 44 | # python3
|
|---|
| 45 | import builtins as __builtin__
|
|---|
| 46 | from os import environb as environ
|
|---|
| 47 | unicode = str
|
|---|
| 48 | __builtin__.__dict__['_'] = __builtin__.__dict__['_'].__self__.lgettext
|
|---|
| 49 |
|
|---|
| 50 |
|
|---|
| 51 | # subprocess wrapper that uses shell on Windows
|
|---|
| 52 |
|
|---|
| 53 |
|
|---|
| 54 | class Popen(subprocess.Popen):
|
|---|
| 55 | _builtin_exts = set(['.com', '.exe', '.bat', '.cmd'])
|
|---|
| 56 |
|
|---|
| 57 | @staticmethod
|
|---|
| 58 | def _escape_for_shell(arg):
|
|---|
| 59 | # TODO: what are cmd.exe's parsing rules?
|
|---|
| 60 | return arg
|
|---|
| 61 |
|
|---|
| 62 | def __init__(self, args, **kwargs):
|
|---|
| 63 | if (sys.platform == 'win32'
|
|---|
| 64 | and isinstance(args, list)
|
|---|
| 65 | and not kwargs.get('shell', False)
|
|---|
| 66 | and kwargs.get('executable') is None):
|
|---|
| 67 | cmd = shutil_which(args[0])
|
|---|
| 68 | if cmd is None:
|
|---|
| 69 | raise OSError(_("Cannot find the executable {0}")
|
|---|
| 70 | .format(args[0]))
|
|---|
| 71 | args = [cmd] + args[1:]
|
|---|
| 72 | name, ext = os.path.splitext(cmd)
|
|---|
| 73 | if ext.lower() not in self._builtin_exts:
|
|---|
| 74 | kwargs['shell'] = True
|
|---|
| 75 | args = [self._escape_for_shell(arg) for arg in args]
|
|---|
| 76 | subprocess.Popen.__init__(self, args, **kwargs)
|
|---|
| 77 |
|
|---|
| 78 | PIPE = subprocess.PIPE
|
|---|
| 79 | STDOUT = subprocess.STDOUT
|
|---|
| 80 |
|
|---|
| 81 |
|
|---|
| 82 | raise_on_error = False # raise exception instead of calling fatal()
|
|---|
| 83 | _capture_stderr = False # capture stderr of subprocesses if possible
|
|---|
| 84 |
|
|---|
| 85 |
|
|---|
| 86 | def call(*args, **kwargs):
|
|---|
| 87 | return Popen(*args, **kwargs).wait()
|
|---|
| 88 |
|
|---|
| 89 | # GRASS-oriented interface to subprocess module
|
|---|
| 90 |
|
|---|
| 91 | _popen_args = ["bufsize", "executable", "stdin", "stdout", "stderr",
|
|---|
| 92 | "preexec_fn", "close_fds", "cwd", "env",
|
|---|
| 93 | "universal_newlines", "startupinfo", "creationflags"]
|
|---|
| 94 |
|
|---|
| 95 |
|
|---|
| 96 | def _make_val(val):
|
|---|
| 97 | """Convert value to bytes"""
|
|---|
| 98 | if isinstance(val, bytes):
|
|---|
| 99 | return val
|
|---|
| 100 | if isinstance(val, (str, unicode)):
|
|---|
| 101 | return encode(val)
|
|---|
| 102 | if isinstance(val, (int, float)):
|
|---|
| 103 | return encode(str(val))
|
|---|
| 104 | try:
|
|---|
| 105 | return b",".join(map(_make_val, iter(val)))
|
|---|
| 106 | except TypeError:
|
|---|
| 107 | pass
|
|---|
| 108 | return bytes(val)
|
|---|
| 109 |
|
|---|
| 110 |
|
|---|
| 111 | def get_commands():
|
|---|
| 112 | """Create list of available GRASS commands to use when parsing
|
|---|
| 113 | string from the command line
|
|---|
| 114 |
|
|---|
| 115 | :return: list of commands (set) and directory of scripts (collected
|
|---|
| 116 | by extension - MS Windows only)
|
|---|
| 117 |
|
|---|
| 118 | >>> cmds = list(get_commands()[0])
|
|---|
| 119 | >>> cmds.sort()
|
|---|
| 120 | >>> cmds[:5]
|
|---|
| 121 | ['d.barscale', 'd.colorlist', 'd.colortable', 'd.correlate', 'd.erase']
|
|---|
| 122 |
|
|---|
| 123 | """
|
|---|
| 124 | gisbase = os.environ['GISBASE']
|
|---|
| 125 | cmd = list()
|
|---|
| 126 | scripts = {'.py': list()} if sys.platform == 'win32' else {}
|
|---|
| 127 |
|
|---|
| 128 | def scan(gisbase, directory):
|
|---|
| 129 | dir_path = os.path.join(gisbase, directory)
|
|---|
| 130 | if os.path.exists(dir_path):
|
|---|
| 131 | for fname in os.listdir(os.path.join(gisbase, directory)):
|
|---|
| 132 | if scripts: # win32
|
|---|
| 133 | name, ext = os.path.splitext(fname)
|
|---|
| 134 | if ext != '.manifest':
|
|---|
| 135 | cmd.append(name)
|
|---|
| 136 | if ext in scripts.keys():
|
|---|
| 137 | scripts[ext].append(name)
|
|---|
| 138 | else:
|
|---|
| 139 | cmd.append(fname)
|
|---|
| 140 |
|
|---|
| 141 | for directory in ('bin', 'scripts'):
|
|---|
| 142 | scan(gisbase, directory)
|
|---|
| 143 |
|
|---|
| 144 | # scan gui/scripts/
|
|---|
| 145 | gui_path = os.path.join(gisbase, 'etc', 'gui', 'scripts')
|
|---|
| 146 | if os.path.exists(gui_path):
|
|---|
| 147 | os.environ["PATH"] = os.getenv("PATH") + os.pathsep + gui_path
|
|---|
| 148 | cmd = cmd + os.listdir(gui_path)
|
|---|
| 149 |
|
|---|
| 150 | return set(cmd), scripts
|
|---|
| 151 |
|
|---|
| 152 |
|
|---|
| 153 | # replacement for which function from shutil (not available in all versions)
|
|---|
| 154 | # from http://hg.python.org/cpython/file/6860263c05b3/Lib/shutil.py#l1068
|
|---|
| 155 | # added because of Python scripts running Python scripts on MS Windows
|
|---|
| 156 | # see also ticket #2008 which is unrelated but same function was proposed
|
|---|
| 157 | def shutil_which(cmd, mode=os.F_OK | os.X_OK, path=None):
|
|---|
| 158 | """Given a command, mode, and a PATH string, return the path which
|
|---|
| 159 | conforms to the given mode on the PATH, or None if there is no such
|
|---|
| 160 | file.
|
|---|
| 161 |
|
|---|
| 162 | `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
|
|---|
| 163 | of os.environ.get("PATH"), or can be overridden with a custom search
|
|---|
| 164 | path.
|
|---|
| 165 |
|
|---|
| 166 | :param cmd: the command
|
|---|
| 167 | :param mode:
|
|---|
| 168 | :param path:
|
|---|
| 169 |
|
|---|
| 170 | """
|
|---|
| 171 | # Check that a given file can be accessed with the correct mode.
|
|---|
| 172 | # Additionally check that `file` is not a directory, as on Windows
|
|---|
| 173 | # directories pass the os.access check.
|
|---|
| 174 | def _access_check(fn, mode):
|
|---|
| 175 | return (os.path.exists(fn) and os.access(fn, mode)
|
|---|
| 176 | and not os.path.isdir(fn))
|
|---|
| 177 |
|
|---|
| 178 | # If we're given a path with a directory part, look it up directly rather
|
|---|
| 179 | # than referring to PATH directories. This includes checking relative to the
|
|---|
| 180 | # current directory, e.g. ./script
|
|---|
| 181 | if os.path.dirname(cmd):
|
|---|
| 182 | if _access_check(cmd, mode):
|
|---|
| 183 | return cmd
|
|---|
| 184 | return None
|
|---|
| 185 |
|
|---|
| 186 | if path is None:
|
|---|
| 187 | path = os.environ.get("PATH", os.defpath)
|
|---|
| 188 | if not path:
|
|---|
| 189 | return None
|
|---|
| 190 | path = path.split(os.pathsep)
|
|---|
| 191 |
|
|---|
| 192 | if sys.platform == "win32":
|
|---|
| 193 | # The current directory takes precedence on Windows.
|
|---|
| 194 | if not os.curdir in path:
|
|---|
| 195 | path.insert(0, os.curdir)
|
|---|
| 196 |
|
|---|
| 197 | # PATHEXT is necessary to check on Windows.
|
|---|
| 198 | pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
|
|---|
| 199 | map(lambda x: x.lower(), pathext) # force lowercase
|
|---|
| 200 | if '.py' not in pathext: # we assume that PATHEXT contains always '.py'
|
|---|
| 201 | pathext.insert(0, '.py')
|
|---|
| 202 | # See if the given file matches any of the expected path extensions.
|
|---|
| 203 | # This will allow us to short circuit when given "python.exe".
|
|---|
| 204 | # If it does match, only test that one, otherwise we have to try
|
|---|
| 205 | # others.
|
|---|
| 206 | if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
|
|---|
| 207 | files = [cmd]
|
|---|
| 208 | else:
|
|---|
| 209 | files = [cmd + ext for ext in pathext]
|
|---|
| 210 | else:
|
|---|
| 211 | # On other platforms you don't have things like PATHEXT to tell you
|
|---|
| 212 | # what file suffixes are executable, so just pass on cmd as-is.
|
|---|
| 213 | files = [cmd]
|
|---|
| 214 |
|
|---|
| 215 | seen = set()
|
|---|
| 216 | for dir in path:
|
|---|
| 217 | normdir = os.path.normcase(dir)
|
|---|
| 218 | if not normdir in seen:
|
|---|
| 219 | seen.add(normdir)
|
|---|
| 220 | for thefile in files:
|
|---|
| 221 | name = os.path.join(dir, thefile)
|
|---|
| 222 | if _access_check(name, mode):
|
|---|
| 223 | return name
|
|---|
| 224 | return None
|
|---|
| 225 |
|
|---|
| 226 |
|
|---|
| 227 | # Added because of scripts calling scripts on MS Windows.
|
|---|
| 228 | # Module name (here cmd) differs from the file name (does not have extension).
|
|---|
| 229 | # Additionally, we don't run scripts using system executable mechanism,
|
|---|
| 230 | # so we need the full path name.
|
|---|
| 231 | # However, scripts are on the PATH and '.PY' in in PATHEXT, so we can use
|
|---|
| 232 | # shutil.which to get the full file path. Addons are on PATH too.
|
|---|
| 233 | # An alternative to which function call would be to check the script path and
|
|---|
| 234 | # addons path. This is proposed improvement for the future.
|
|---|
| 235 | # Another alternative is to check some global list of scripts but this list
|
|---|
| 236 | # needs to be created first. The question is what is less expensive.
|
|---|
| 237 | # Note that getting the full path is only part of the solution,
|
|---|
| 238 | # the other part is to use the right Python as an executable and pass the full
|
|---|
| 239 | # script path as a parameter.
|
|---|
| 240 | # Nevertheless, it is unclear on which places which extensions are added.
|
|---|
| 241 | # This function also could skip the check for platform but depends
|
|---|
| 242 | # how will be used, this is most general but not most effective.
|
|---|
| 243 | def get_real_command(cmd):
|
|---|
| 244 | """Returns the real file command for a module (cmd)
|
|---|
| 245 |
|
|---|
| 246 | For Python scripts on MS Windows it returns full path to the script
|
|---|
| 247 | and adds a '.py' extension.
|
|---|
| 248 | For other cases it just returns a module (name).
|
|---|
| 249 | So, you can just use this function for all without further check.
|
|---|
| 250 |
|
|---|
| 251 | >>> get_real_command('g.region')
|
|---|
| 252 | 'g.region'
|
|---|
| 253 |
|
|---|
| 254 | :param cmd: the command
|
|---|
| 255 | """
|
|---|
| 256 | if sys.platform == 'win32':
|
|---|
| 257 | # we in fact expect pure module name (without extension)
|
|---|
| 258 | # so, lets remove extension
|
|---|
| 259 | if os.path.splitext(cmd)[1] == '.py':
|
|---|
| 260 | cmd = cmd[:-3]
|
|---|
| 261 | full_path = shutil_which(cmd + '.py')
|
|---|
| 262 | if full_path:
|
|---|
| 263 | return full_path
|
|---|
| 264 |
|
|---|
| 265 | return cmd
|
|---|
| 266 |
|
|---|
| 267 |
|
|---|
| 268 | def make_command(prog, flags=b"", overwrite=False, quiet=False, verbose=False,
|
|---|
| 269 | superquiet=False, errors=None, **options):
|
|---|
| 270 | """Return a list of strings suitable for use as the args parameter to
|
|---|
| 271 | Popen() or call(). Example:
|
|---|
| 272 |
|
|---|
| 273 |
|
|---|
| 274 | >>> make_command("g.message", flags = 'w', message = 'this is a warning')
|
|---|
| 275 | ['g.message', '-w', 'message=this is a warning']
|
|---|
| 276 |
|
|---|
| 277 |
|
|---|
| 278 | :param str prog: GRASS module
|
|---|
| 279 | :param str flags: flags to be used (given as a string)
|
|---|
| 280 | :param bool overwrite: True to enable overwriting the output (<tt>--o</tt>)
|
|---|
| 281 | :param bool quiet: True to run quietly (<tt>--q</tt>)
|
|---|
| 282 | :param bool verbose: True to run verbosely (<tt>--v</tt>)
|
|---|
| 283 | :param options: module's parameters
|
|---|
| 284 |
|
|---|
| 285 | :return: list of arguments
|
|---|
| 286 | """
|
|---|
| 287 | args = [_make_val(prog)]
|
|---|
| 288 | if overwrite:
|
|---|
| 289 | args.append(b"--o")
|
|---|
| 290 | if quiet:
|
|---|
| 291 | args.append(b"--q")
|
|---|
| 292 | if verbose:
|
|---|
| 293 | args.append(b"--v")
|
|---|
| 294 | if superquiet:
|
|---|
| 295 | args.append(b"--qq")
|
|---|
| 296 | if flags:
|
|---|
| 297 | flags = _make_val(flags)
|
|---|
| 298 | if b'-' in flags:
|
|---|
| 299 | raise ScriptError("'-' is not a valid flag")
|
|---|
| 300 | args.append(b"-" + bytes(flags))
|
|---|
| 301 | for opt, val in options.items():
|
|---|
| 302 | if opt in _popen_args:
|
|---|
| 303 | continue
|
|---|
| 304 | # convert string to bytes
|
|---|
| 305 | opt = encode(opt)
|
|---|
| 306 | if val != None:
|
|---|
| 307 | if opt.startswith(b'_'):
|
|---|
| 308 | opt = opt[1:]
|
|---|
| 309 | warning(_("To run the module <%s> add underscore at the end"
|
|---|
| 310 | " of the option <%s> to avoid conflict with Python"
|
|---|
| 311 | " keywords. Underscore at the beginning is"
|
|---|
| 312 | " depreciated in GRASS GIS 7.0 and will be removed"
|
|---|
| 313 | " in version 7.1.") % (prog, opt))
|
|---|
| 314 | elif opt.endswith(b'_'):
|
|---|
| 315 | opt = opt[:-1]
|
|---|
| 316 | args.append(opt + b'=' + _make_val(val))
|
|---|
| 317 | return args
|
|---|
| 318 |
|
|---|
| 319 |
|
|---|
| 320 | def handle_errors(returncode, result, args, kwargs):
|
|---|
| 321 | if returncode == 0:
|
|---|
| 322 | return result
|
|---|
| 323 | handler = kwargs.get('errors', 'raise')
|
|---|
| 324 | if handler.lower() == 'ignore':
|
|---|
| 325 | return result
|
|---|
| 326 | elif handler.lower() == 'status':
|
|---|
| 327 | return returncode
|
|---|
| 328 | elif handler.lower() == 'exit':
|
|---|
| 329 | sys.exit(1)
|
|---|
| 330 | else:
|
|---|
| 331 | # TODO: construction of the whole command is far from perfect
|
|---|
| 332 | args = make_command(*args, **kwargs)
|
|---|
| 333 | raise CalledModuleError(module=None, code=repr(args),
|
|---|
| 334 | returncode=returncode)
|
|---|
| 335 |
|
|---|
| 336 | def start_command(prog, flags=b"", overwrite=False, quiet=False,
|
|---|
| 337 | verbose=False, superquiet=False, **kwargs):
|
|---|
| 338 | """Returns a Popen object with the command created by make_command.
|
|---|
| 339 | Accepts any of the arguments which Popen() accepts apart from "args"
|
|---|
| 340 | and "shell".
|
|---|
| 341 |
|
|---|
| 342 | >>> p = start_command("g.gisenv", stdout=subprocess.PIPE)
|
|---|
| 343 | >>> print(p) # doctest: +ELLIPSIS
|
|---|
| 344 | <...Popen object at 0x...>
|
|---|
| 345 | >>> print(p.communicate()[0]) # doctest: +SKIP
|
|---|
| 346 | GISDBASE='/opt/grass-data';
|
|---|
| 347 | LOCATION_NAME='spearfish60';
|
|---|
| 348 | MAPSET='glynn';
|
|---|
| 349 | GUI='text';
|
|---|
| 350 | MONITOR='x0';
|
|---|
| 351 |
|
|---|
| 352 | If the module parameter is the same as Python keyword, add
|
|---|
| 353 | underscore at the end of the parameter. For example, use
|
|---|
| 354 | ``lambda_=1.6`` instead of ``lambda=1.6``.
|
|---|
| 355 |
|
|---|
| 356 | :param str prog: GRASS module
|
|---|
| 357 | :param str flags: flags to be used (given as a string)
|
|---|
| 358 | :param bool overwrite: True to enable overwriting the output (<tt>--o</tt>)
|
|---|
| 359 | :param bool quiet: True to run quietly (<tt>--q</tt>)
|
|---|
| 360 | :param bool verbose: True to run verbosely (<tt>--v</tt>)
|
|---|
| 361 | :param kwargs: module's parameters
|
|---|
| 362 |
|
|---|
| 363 | :return: Popen object
|
|---|
| 364 | """
|
|---|
| 365 | options = {}
|
|---|
| 366 | popts = {}
|
|---|
| 367 | for opt, val in kwargs.items():
|
|---|
| 368 | if opt in _popen_args:
|
|---|
| 369 | popts[opt] = val
|
|---|
| 370 | else:
|
|---|
| 371 | if isinstance(val, unicode):
|
|---|
| 372 | val = encode(val)
|
|---|
| 373 | options[opt] = val
|
|---|
| 374 |
|
|---|
| 375 | args = make_command(prog, flags, overwrite, quiet, verbose, **options)
|
|---|
| 376 |
|
|---|
| 377 | if debug_level() > 0:
|
|---|
| 378 | sys.stderr.write("D1/%d: %s.start_command(): %s\n" % (debug_level(),
|
|---|
| 379 | __name__,
|
|---|
| 380 | ' '.join(args)))
|
|---|
| 381 | sys.stderr.flush()
|
|---|
| 382 | return Popen(args, **popts)
|
|---|
| 383 |
|
|---|
| 384 |
|
|---|
| 385 | def run_command(*args, **kwargs):
|
|---|
| 386 | """Execute a module synchronously
|
|---|
| 387 |
|
|---|
| 388 | This function passes all arguments to ``start_command()``,
|
|---|
| 389 | then waits for the process to complete. It is similar to
|
|---|
| 390 | ``subprocess.check_call()``, but with the ``make_command()``
|
|---|
| 391 | interface.
|
|---|
| 392 |
|
|---|
| 393 | For backward compatibility, the function returns exit code
|
|---|
| 394 | by default but only if it is equal to zero. An exception is raised
|
|---|
| 395 | in case of an non-zero return code.
|
|---|
| 396 |
|
|---|
| 397 | >>> run_command('g.region', raster='elevation')
|
|---|
| 398 | 0
|
|---|
| 399 |
|
|---|
| 400 | See :func:`start_command()` for details about parameters and usage.
|
|---|
| 401 |
|
|---|
| 402 | ..note::
|
|---|
| 403 | You should ignore the return value of this function unless, you
|
|---|
| 404 | change the default behavior using *errors* parameter.
|
|---|
| 405 |
|
|---|
| 406 | :param *args: unnamed arguments passed to ``start_command()``
|
|---|
| 407 | :param **kwargs: named arguments passed to ``start_command()``
|
|---|
| 408 |
|
|---|
| 409 | :returns: 0 with default parameters for backward compatibility only
|
|---|
| 410 |
|
|---|
| 411 | :raises: ``CalledModuleError`` when module returns non-zero return code
|
|---|
| 412 | """
|
|---|
| 413 | if _capture_stderr and 'stderr' not in kwargs.keys():
|
|---|
| 414 | kwargs['stderr'] = PIPE
|
|---|
| 415 | ps = start_command(*args, **kwargs)
|
|---|
| 416 | if _capture_stderr:
|
|---|
| 417 | stdout, stderr = ps.communicate()
|
|---|
| 418 | returncode = ps.poll()
|
|---|
| 419 | if returncode:
|
|---|
| 420 | sys.stderr.write(stderr)
|
|---|
| 421 | else:
|
|---|
| 422 | returncode = ps.wait()
|
|---|
| 423 | return handle_errors(returncode, returncode, args, kwargs)
|
|---|
| 424 |
|
|---|
| 425 |
|
|---|
| 426 | def pipe_command(*args, **kwargs):
|
|---|
| 427 | """Passes all arguments to start_command(), but also adds
|
|---|
| 428 | "stdout = PIPE". Returns the Popen object.
|
|---|
| 429 |
|
|---|
| 430 | >>> p = pipe_command("g.gisenv")
|
|---|
| 431 | >>> print(p) # doctest: +ELLIPSIS
|
|---|
| 432 | <....Popen object at 0x...>
|
|---|
| 433 | >>> print(p.communicate()[0]) # doctest: +SKIP
|
|---|
| 434 | GISDBASE='/opt/grass-data';
|
|---|
| 435 | LOCATION_NAME='spearfish60';
|
|---|
| 436 | MAPSET='glynn';
|
|---|
| 437 | GUI='text';
|
|---|
| 438 | MONITOR='x0';
|
|---|
| 439 |
|
|---|
| 440 | :param list args: list of unnamed arguments (see start_command() for details)
|
|---|
| 441 | :param list kwargs: list of named arguments (see start_command() for details)
|
|---|
| 442 |
|
|---|
| 443 | :return: Popen object
|
|---|
| 444 | """
|
|---|
| 445 | kwargs['stdout'] = PIPE
|
|---|
| 446 | return start_command(*args, **kwargs)
|
|---|
| 447 |
|
|---|
| 448 |
|
|---|
| 449 | def feed_command(*args, **kwargs):
|
|---|
| 450 | """Passes all arguments to start_command(), but also adds
|
|---|
| 451 | "stdin = PIPE". Returns the Popen object.
|
|---|
| 452 |
|
|---|
| 453 | :param list args: list of unnamed arguments (see start_command() for details)
|
|---|
| 454 | :param list kwargs: list of named arguments (see start_command() for details)
|
|---|
| 455 |
|
|---|
| 456 | :return: Popen object
|
|---|
| 457 | """
|
|---|
| 458 | kwargs['stdin'] = PIPE
|
|---|
| 459 | return start_command(*args, **kwargs)
|
|---|
| 460 |
|
|---|
| 461 |
|
|---|
| 462 | def read_command(*args, **kwargs):
|
|---|
| 463 | """Passes all arguments to pipe_command, then waits for the process to
|
|---|
| 464 | complete, returning its stdout (i.e. similar to shell `backticks`).
|
|---|
| 465 |
|
|---|
| 466 | :param list args: list of unnamed arguments (see start_command() for details)
|
|---|
| 467 | :param list kwargs: list of named arguments (see start_command() for details)
|
|---|
| 468 |
|
|---|
| 469 | :return: stdout
|
|---|
| 470 | """
|
|---|
| 471 | if _capture_stderr and 'stderr' not in kwargs.keys():
|
|---|
| 472 | kwargs['stderr'] = PIPE
|
|---|
| 473 | process = pipe_command(*args, **kwargs)
|
|---|
| 474 | stdout, stderr = process.communicate()
|
|---|
| 475 | returncode = process.poll()
|
|---|
| 476 | if _capture_stderr and returncode:
|
|---|
| 477 | sys.stderr.write(stderr)
|
|---|
| 478 | return handle_errors(returncode, stdout, args, kwargs)
|
|---|
| 479 |
|
|---|
| 480 |
|
|---|
| 481 | def parse_command(*args, **kwargs):
|
|---|
| 482 | """Passes all arguments to read_command, then parses the output
|
|---|
| 483 | by parse_key_val().
|
|---|
| 484 |
|
|---|
| 485 | Parsing function can be optionally given by <em>parse</em> parameter
|
|---|
| 486 | including its arguments, e.g.
|
|---|
| 487 |
|
|---|
| 488 | ::
|
|---|
| 489 |
|
|---|
| 490 | parse_command(..., parse = (grass.parse_key_val, { 'sep' : ':' }))
|
|---|
| 491 |
|
|---|
| 492 | or you can simply define <em>delimiter</em>
|
|---|
| 493 |
|
|---|
| 494 | ::
|
|---|
| 495 |
|
|---|
| 496 | parse_command(..., delimiter = ':')
|
|---|
| 497 |
|
|---|
| 498 | :param args: list of unnamed arguments (see start_command() for details)
|
|---|
| 499 | :param kwargs: list of named arguments (see start_command() for details)
|
|---|
| 500 |
|
|---|
| 501 | :return: parsed module output
|
|---|
| 502 | """
|
|---|
| 503 | parse = None
|
|---|
| 504 | parse_args = {}
|
|---|
| 505 | if 'parse' in kwargs:
|
|---|
| 506 | if isinstance(kwargs['parse'], tuple):
|
|---|
| 507 | parse = kwargs['parse'][0]
|
|---|
| 508 | parse_args = kwargs['parse'][1]
|
|---|
| 509 | del kwargs['parse']
|
|---|
| 510 |
|
|---|
| 511 | if 'delimiter' in kwargs:
|
|---|
| 512 | parse_args = {'sep': kwargs['delimiter']}
|
|---|
| 513 | del kwargs['delimiter']
|
|---|
| 514 |
|
|---|
| 515 | if not parse:
|
|---|
| 516 | parse = parse_key_val # use default fn
|
|---|
| 517 |
|
|---|
| 518 | res = read_command(*args, **kwargs)
|
|---|
| 519 |
|
|---|
| 520 | return parse(res, **parse_args)
|
|---|
| 521 |
|
|---|
| 522 |
|
|---|
| 523 | def write_command(*args, **kwargs):
|
|---|
| 524 | """Execute a module with standard input given by *stdin* parameter.
|
|---|
| 525 |
|
|---|
| 526 | Passes all arguments to ``feed_command()``, with the string specified
|
|---|
| 527 | by the *stdin* argument fed to the process' standard input.
|
|---|
| 528 |
|
|---|
| 529 | >>> gscript.write_command(
|
|---|
| 530 | ... 'v.in.ascii', input='-',
|
|---|
| 531 | ... stdin='%s|%s' % (635818.8, 221342.4),
|
|---|
| 532 | ... output='view_point')
|
|---|
| 533 | 0
|
|---|
| 534 |
|
|---|
| 535 | See ``start_command()`` for details about parameters and usage.
|
|---|
| 536 |
|
|---|
| 537 | :param *args: unnamed arguments passed to ``start_command()``
|
|---|
| 538 | :param **kwargs: named arguments passed to ``start_command()``
|
|---|
| 539 |
|
|---|
| 540 | :returns: 0 with default parameters for backward compatibility only
|
|---|
| 541 |
|
|---|
| 542 | :raises: ``CalledModuleError`` when module returns non-zero return code
|
|---|
| 543 | """
|
|---|
| 544 | # TODO: should we delete it from kwargs?
|
|---|
| 545 | stdin = kwargs['stdin']
|
|---|
| 546 | if _capture_stderr and 'stderr' not in kwargs.keys():
|
|---|
| 547 | kwargs['stderr'] = PIPE
|
|---|
| 548 | process = feed_command(*args, **kwargs)
|
|---|
| 549 | unused, stderr = process.communicate(stdin)
|
|---|
| 550 | returncode = process.poll()
|
|---|
| 551 | if _capture_stderr and returncode:
|
|---|
| 552 | sys.stderr.write(stderr)
|
|---|
| 553 | return handle_errors(returncode, returncode, args, kwargs)
|
|---|
| 554 |
|
|---|
| 555 |
|
|---|
| 556 | def exec_command(prog, flags="", overwrite=False, quiet=False, verbose=False,
|
|---|
| 557 | superquiet=False, env=None, **kwargs):
|
|---|
| 558 | """Interface to os.execvpe(), but with the make_command() interface.
|
|---|
| 559 |
|
|---|
| 560 | :param str prog: GRASS module
|
|---|
| 561 | :param str flags: flags to be used (given as a string)
|
|---|
| 562 | :param bool overwrite: True to enable overwriting the output (<tt>--o</tt>)
|
|---|
| 563 | :param bool quiet: True to run quietly (<tt>--q</tt>)
|
|---|
| 564 | :param bool verbose: True to run verbosely (<tt>--v</tt>)
|
|---|
| 565 | :param env: directory with environmental variables
|
|---|
| 566 | :param list kwargs: module's parameters
|
|---|
| 567 |
|
|---|
| 568 | """
|
|---|
| 569 | args = make_command(prog, flags, overwrite, quiet, verbose, **kwargs)
|
|---|
| 570 |
|
|---|
| 571 | if env is None:
|
|---|
| 572 | env = os.environ
|
|---|
| 573 | os.execvpe(prog, args, env)
|
|---|
| 574 |
|
|---|
| 575 | # interface to g.message
|
|---|
| 576 |
|
|---|
| 577 |
|
|---|
| 578 | def message(msg, flag=None):
|
|---|
| 579 | """Display a message using `g.message`
|
|---|
| 580 |
|
|---|
| 581 | :param str msg: message to be displayed
|
|---|
| 582 | :param str flag: flags (given as string)
|
|---|
| 583 | """
|
|---|
| 584 | run_command("g.message", flags=flag, message=msg, errors='ignore')
|
|---|
| 585 |
|
|---|
| 586 |
|
|---|
| 587 | def debug(msg, debug=1):
|
|---|
| 588 | """Display a debugging message using `g.message -d`
|
|---|
| 589 |
|
|---|
| 590 | :param str msg: debugging message to be displayed
|
|---|
| 591 | :param str debug: debug level (0-5)
|
|---|
| 592 | """
|
|---|
| 593 | if debug_level() >= debug:
|
|---|
| 594 | # TODO: quite a random hack here, do we need it somewhere else too?
|
|---|
| 595 | if sys.platform == "win32":
|
|---|
| 596 | msg = msg.replace('&', '^&')
|
|---|
| 597 |
|
|---|
| 598 | run_command("g.message", flags='d', message=msg, debug=debug)
|
|---|
| 599 |
|
|---|
| 600 | def verbose(msg):
|
|---|
| 601 | """Display a verbose message using `g.message -v`
|
|---|
| 602 |
|
|---|
| 603 | :param str msg: verbose message to be displayed
|
|---|
| 604 | """
|
|---|
| 605 | message(msg, flag='v')
|
|---|
| 606 |
|
|---|
| 607 |
|
|---|
| 608 | def info(msg):
|
|---|
| 609 | """Display an informational message using `g.message -i`
|
|---|
| 610 |
|
|---|
| 611 | :param str msg: informational message to be displayed
|
|---|
| 612 | """
|
|---|
| 613 | message(msg, flag='i')
|
|---|
| 614 |
|
|---|
| 615 |
|
|---|
| 616 | def percent(i, n, s):
|
|---|
| 617 | """Display a progress info message using `g.message -p`
|
|---|
| 618 |
|
|---|
| 619 | ::
|
|---|
| 620 |
|
|---|
| 621 | message(_("Percent complete..."))
|
|---|
| 622 | n = 100
|
|---|
| 623 | for i in range(n):
|
|---|
| 624 | percent(i, n, 1)
|
|---|
| 625 | percent(1, 1, 1)
|
|---|
| 626 |
|
|---|
| 627 | :param int i: current item
|
|---|
| 628 | :param int n: total number of items
|
|---|
| 629 | :param int s: increment size
|
|---|
| 630 | """
|
|---|
| 631 | message("%d %d %d" % (i, n, s), flag='p')
|
|---|
| 632 |
|
|---|
| 633 |
|
|---|
| 634 | def warning(msg):
|
|---|
| 635 | """Display a warning message using `g.message -w`
|
|---|
| 636 |
|
|---|
| 637 | :param str msg: warning message to be displayed
|
|---|
| 638 | """
|
|---|
| 639 | message(msg, flag='w')
|
|---|
| 640 |
|
|---|
| 641 |
|
|---|
| 642 | def error(msg):
|
|---|
| 643 | """Display an error message using `g.message -e`
|
|---|
| 644 |
|
|---|
| 645 | This function does not end the execution of the program.
|
|---|
| 646 | The right action after the error is up to the caller.
|
|---|
| 647 | For error handling using the standard mechanism use :func:`fatal()`.
|
|---|
| 648 |
|
|---|
| 649 | :param str msg: error message to be displayed
|
|---|
| 650 | """
|
|---|
| 651 | message(msg, flag='e')
|
|---|
| 652 |
|
|---|
| 653 |
|
|---|
| 654 | def fatal(msg):
|
|---|
| 655 | """Display an error message using `g.message -e`, then abort or raise
|
|---|
| 656 |
|
|---|
| 657 | Raises exception when module global raise_on_error is 'True', abort
|
|---|
| 658 | (calls exit) otherwise.
|
|---|
| 659 | Use :func:`set_raise_on_error()` to set the behavior.
|
|---|
| 660 |
|
|---|
| 661 | :param str msg: error message to be displayed
|
|---|
| 662 | """
|
|---|
| 663 | global raise_on_error
|
|---|
| 664 | if raise_on_error:
|
|---|
| 665 | raise ScriptError(msg)
|
|---|
| 666 |
|
|---|
| 667 | error(msg)
|
|---|
| 668 | sys.exit(1)
|
|---|
| 669 |
|
|---|
| 670 |
|
|---|
| 671 | def set_raise_on_error(raise_exp=True):
|
|---|
| 672 | """Define behaviour on fatal error (fatal() called)
|
|---|
| 673 |
|
|---|
| 674 | :param bool raise_exp: True to raise ScriptError instead of calling
|
|---|
| 675 | sys.exit(1) in fatal()
|
|---|
| 676 |
|
|---|
| 677 | :return: current status
|
|---|
| 678 | """
|
|---|
| 679 | global raise_on_error
|
|---|
| 680 | tmp_raise = raise_on_error
|
|---|
| 681 | raise_on_error = raise_exp
|
|---|
| 682 | return tmp_raise
|
|---|
| 683 |
|
|---|
| 684 |
|
|---|
| 685 | def get_raise_on_error():
|
|---|
| 686 | """Return True if a ScriptError exception is raised instead of calling
|
|---|
| 687 | sys.exit(1) in case a fatal error was invoked with fatal()
|
|---|
| 688 | """
|
|---|
| 689 | global raise_on_error
|
|---|
| 690 | return raise_on_error
|
|---|
| 691 |
|
|---|
| 692 |
|
|---|
| 693 | # TODO: solve also warnings (not printed now)
|
|---|
| 694 | def set_capture_stderr(capture=True):
|
|---|
| 695 | """Enable capturing standard error output of modules and print it.
|
|---|
| 696 |
|
|---|
| 697 | By default, standard error output (stderr) of child processes shows
|
|---|
| 698 | in the same place as output of the parent process. This may not
|
|---|
| 699 | always be the same place as ``sys.stderr`` is written.
|
|---|
| 700 | After calling this function, functions in the ``grass.script``
|
|---|
| 701 | package will capture the stderr of child processes and pass it
|
|---|
| 702 | to ``sys.stderr`` if there is an error.
|
|---|
| 703 |
|
|---|
| 704 | .. note::
|
|---|
| 705 |
|
|---|
| 706 | This is advantages for interactive shells such as the one in GUI
|
|---|
| 707 | and interactive notebooks such as Jupyer Notebook.
|
|---|
| 708 |
|
|---|
| 709 | The capturing can be applied only in certain cases, for example
|
|---|
| 710 | in case of run_command() it is applied because run_command() nor
|
|---|
| 711 | its callers do not handle the streams, however feed_command()
|
|---|
| 712 | cannot do capturing because its callers handle the streams.
|
|---|
| 713 |
|
|---|
| 714 | The previous state is returned. Passing ``False`` disables the
|
|---|
| 715 | capturing.
|
|---|
| 716 |
|
|---|
| 717 | .. versionadded:: 7.4
|
|---|
| 718 | """
|
|---|
| 719 | global _capture_stderr
|
|---|
| 720 | tmp = _capture_stderr
|
|---|
| 721 | _capture_stderr = capture
|
|---|
| 722 | return tmp
|
|---|
| 723 |
|
|---|
| 724 | def get_capture_stderr():
|
|---|
| 725 | """Return True if stderr is captured, False otherwise.
|
|---|
| 726 |
|
|---|
| 727 | See set_capture_stderr().
|
|---|
| 728 | """
|
|---|
| 729 | global _capture_stderr
|
|---|
| 730 | return _capture_stderr
|
|---|
| 731 |
|
|---|
| 732 | # interface to g.parser
|
|---|
| 733 |
|
|---|
| 734 |
|
|---|
| 735 | def _parse_opts(lines):
|
|---|
| 736 | options = {}
|
|---|
| 737 | flags = {}
|
|---|
| 738 | for line in lines:
|
|---|
| 739 | if not line:
|
|---|
| 740 | break
|
|---|
| 741 | try:
|
|---|
| 742 | [var, val] = line.split(b'=', 1)
|
|---|
| 743 | except:
|
|---|
| 744 | raise SyntaxError("invalid output from g.parser: %s" % line)
|
|---|
| 745 |
|
|---|
| 746 | if var.startswith(b'flag_'):
|
|---|
| 747 | flags[var[5:]] = bool(int(val))
|
|---|
| 748 | elif var.startswith(b'opt_'):
|
|---|
| 749 | options[var[4:]] = val
|
|---|
| 750 | elif var in [b'GRASS_OVERWRITE', b'GRASS_VERBOSE']:
|
|---|
| 751 | os.environ[var] = val
|
|---|
| 752 | else:
|
|---|
| 753 | raise SyntaxError("invalid output from g.parser: %s" % line)
|
|---|
| 754 |
|
|---|
| 755 | return (options, flags)
|
|---|
| 756 |
|
|---|
| 757 |
|
|---|
| 758 | def parser():
|
|---|
| 759 | """Interface to g.parser, intended to be run from the top-level, e.g.:
|
|---|
| 760 |
|
|---|
| 761 | ::
|
|---|
| 762 |
|
|---|
| 763 | if __name__ == "__main__":
|
|---|
| 764 | options, flags = grass.parser()
|
|---|
| 765 | main()
|
|---|
| 766 |
|
|---|
| 767 | Thereafter, the global variables "options" and "flags" will be
|
|---|
| 768 | dictionaries containing option/flag values, keyed by lower-case
|
|---|
| 769 | option/flag names. The values in "options" are strings, those in
|
|---|
| 770 | "flags" are Python booleans.
|
|---|
| 771 |
|
|---|
| 772 | Overview table of parser standard options:
|
|---|
| 773 | https://grass.osgeo.org/grass76/manuals/parser_standard_options.html
|
|---|
| 774 | """
|
|---|
| 775 | if not os.getenv("GISBASE"):
|
|---|
| 776 | print("You must be in GRASS GIS to run this program.", file=sys.stderr)
|
|---|
| 777 | sys.exit(1)
|
|---|
| 778 |
|
|---|
| 779 | cmdline = [basename(encode(sys.argv[0]))]
|
|---|
| 780 | cmdline += [b'"' + encode(arg) + b'"' for arg in sys.argv[1:]]
|
|---|
| 781 | environ[b'CMDLINE'] = b' '.join(cmdline)
|
|---|
| 782 |
|
|---|
| 783 | argv = sys.argv[:]
|
|---|
| 784 | name = argv[0]
|
|---|
| 785 | if not os.path.isabs(name):
|
|---|
| 786 | if os.sep in name or (os.altsep and os.altsep in name):
|
|---|
| 787 | argv[0] = os.path.abspath(name)
|
|---|
| 788 | else:
|
|---|
| 789 | argv[0] = os.path.join(sys.path[0], name)
|
|---|
| 790 |
|
|---|
| 791 | prog = "g.parser.exe" if sys.platform == "win32" else "g.parser"
|
|---|
| 792 | p = subprocess.Popen([prog, '-n'] + argv, stdout=subprocess.PIPE)
|
|---|
| 793 | s = p.communicate()[0]
|
|---|
| 794 | lines = s.split(b'\0')
|
|---|
| 795 |
|
|---|
| 796 | if not lines or lines[0] != b"@ARGS_PARSED@":
|
|---|
| 797 | stdout = os.fdopen(sys.stdout.fileno(), 'wb')
|
|---|
| 798 | stdout.write(s)
|
|---|
| 799 | sys.exit(p.returncode)
|
|---|
| 800 | return _parse_opts(lines[1:])
|
|---|
| 801 |
|
|---|
| 802 | # interface to g.tempfile
|
|---|
| 803 |
|
|---|
| 804 |
|
|---|
| 805 | def tempfile(create=True):
|
|---|
| 806 | """Returns the name of a temporary file, created with g.tempfile.
|
|---|
| 807 |
|
|---|
| 808 | :param bool create: True to create a file
|
|---|
| 809 |
|
|---|
| 810 | :return: path to a tmp file
|
|---|
| 811 | """
|
|---|
| 812 | flags = ''
|
|---|
| 813 | if not create:
|
|---|
| 814 | flags += 'd'
|
|---|
| 815 |
|
|---|
| 816 | return read_command("g.tempfile", flags=flags, pid=os.getpid()).strip()
|
|---|
| 817 |
|
|---|
| 818 |
|
|---|
| 819 | def tempdir():
|
|---|
| 820 | """Returns the name of a temporary dir, created with g.tempfile."""
|
|---|
| 821 | tmp = tempfile(create=False)
|
|---|
| 822 | os.mkdir(tmp)
|
|---|
| 823 |
|
|---|
| 824 | return tmp
|
|---|
| 825 |
|
|---|
| 826 |
|
|---|
| 827 | def tempname(length, lowercase=False):
|
|---|
| 828 | """Generate a GRASS and SQL compliant random name starting with tmp_
|
|---|
| 829 | followed by a random part of length "length"
|
|---|
| 830 |
|
|---|
| 831 | :param int length: length of the random part of the name to generate
|
|---|
| 832 | :param bool lowercase: use only lowercase characters to generate name
|
|---|
| 833 | :returns: String with a random name of length "length" starting with a letter
|
|---|
| 834 | :rtype: str
|
|---|
| 835 |
|
|---|
| 836 | :Example:
|
|---|
| 837 |
|
|---|
| 838 | >>> tempname(12)
|
|---|
| 839 | 'tmp_MxMa1kAS13s9'
|
|---|
| 840 | """
|
|---|
| 841 |
|
|---|
| 842 | chars = string.ascii_lowercase + string.digits
|
|---|
| 843 | if not lowercase:
|
|---|
| 844 | chars += string.ascii_uppercase
|
|---|
| 845 | random_part = ''.join(random.choice(chars) for _ in range(length))
|
|---|
| 846 | randomname = 'tmp_' + random_part
|
|---|
| 847 |
|
|---|
| 848 | return randomname
|
|---|
| 849 |
|
|---|
| 850 |
|
|---|
| 851 | def _compare_projection(dic):
|
|---|
| 852 | """Check if projection has some possibility of duplicate names like
|
|---|
| 853 | Universal Transverse Mercator and Universe Transverse Mercator and
|
|---|
| 854 | unify them
|
|---|
| 855 |
|
|---|
| 856 | :param dic: The dictionary containing information about projection
|
|---|
| 857 |
|
|---|
| 858 | :return: The dictionary with the new values if needed
|
|---|
| 859 |
|
|---|
| 860 | """
|
|---|
| 861 | # the lookup variable is a list of list, each list contains all the
|
|---|
| 862 | # possible name for a projection system
|
|---|
| 863 | lookup = [['Universal Transverse Mercator', 'Universe Transverse Mercator']]
|
|---|
| 864 | for lo in lookup:
|
|---|
| 865 | for n in range(len(dic['name'])):
|
|---|
| 866 | if dic['name'][n] in lo:
|
|---|
| 867 | dic['name'][n] = lo[0]
|
|---|
| 868 | return dic
|
|---|
| 869 |
|
|---|
| 870 |
|
|---|
| 871 | def _compare_units(dic):
|
|---|
| 872 | """Check if units has some possibility of duplicate names like
|
|---|
| 873 | meter and metre and unify them
|
|---|
| 874 |
|
|---|
| 875 | :param dic: The dictionary containing information about units
|
|---|
| 876 |
|
|---|
| 877 | :return: The dictionary with the new values if needed
|
|---|
| 878 |
|
|---|
| 879 | """
|
|---|
| 880 | # the lookup variable is a list of list, each list contains all the
|
|---|
| 881 | # possible name for a units
|
|---|
| 882 | lookup = [['meter', 'metre'], ['meters', 'metres'], ['kilometer',
|
|---|
| 883 | 'kilometre'], ['kilometers', 'kilometres']]
|
|---|
| 884 | for l in lookup:
|
|---|
| 885 | for n in range(len(dic['unit'])):
|
|---|
| 886 | if dic['unit'][n].lower() in l:
|
|---|
| 887 | dic['unit'][n] = l[0]
|
|---|
| 888 | for n in range(len(dic['units'])):
|
|---|
| 889 | if dic['units'][n].lower() in l:
|
|---|
| 890 | dic['units'][n] = l[0]
|
|---|
| 891 | return dic
|
|---|
| 892 |
|
|---|
| 893 |
|
|---|
| 894 | def _text_to_key_value_dict(filename, sep=":", val_sep=",", checkproj=False,
|
|---|
| 895 | checkunits=False):
|
|---|
| 896 | """Convert a key-value text file, where entries are separated by newlines
|
|---|
| 897 | and the key and value are separated by `sep', into a key-value dictionary
|
|---|
| 898 | and discover/use the correct data types (float, int or string) for values.
|
|---|
| 899 |
|
|---|
| 900 | :param str filename: The name or name and path of the text file to convert
|
|---|
| 901 | :param str sep: The character that separates the keys and values, default
|
|---|
| 902 | is ":"
|
|---|
| 903 | :param str val_sep: The character that separates the values of a single
|
|---|
| 904 | key, default is ","
|
|---|
| 905 | :param bool checkproj: True if it has to check some information about
|
|---|
| 906 | projection system
|
|---|
| 907 | :param bool checkproj: True if it has to check some information about units
|
|---|
| 908 |
|
|---|
| 909 | :return: The dictionary
|
|---|
| 910 |
|
|---|
| 911 | A text file with this content:
|
|---|
| 912 | ::
|
|---|
| 913 |
|
|---|
| 914 | a: Hello
|
|---|
| 915 | b: 1.0
|
|---|
| 916 | c: 1,2,3,4,5
|
|---|
| 917 | d : hello,8,0.1
|
|---|
| 918 |
|
|---|
| 919 | Will be represented as this dictionary:
|
|---|
| 920 |
|
|---|
| 921 | ::
|
|---|
| 922 |
|
|---|
| 923 | {'a': ['Hello'], 'c': [1, 2, 3, 4, 5], 'b': [1.0], 'd': ['hello', 8, 0.1]}
|
|---|
| 924 |
|
|---|
| 925 | """
|
|---|
| 926 | text = open(filename, "r").readlines()
|
|---|
| 927 | kvdict = KeyValue()
|
|---|
| 928 |
|
|---|
| 929 | for line in text:
|
|---|
| 930 | if line.find(sep) >= 0:
|
|---|
| 931 | key, value = line.split(sep)
|
|---|
| 932 | key = key.strip()
|
|---|
| 933 | value = value.strip()
|
|---|
| 934 | else:
|
|---|
| 935 | # Jump over empty values
|
|---|
| 936 | continue
|
|---|
| 937 | values = value.split(val_sep)
|
|---|
| 938 | value_list = []
|
|---|
| 939 |
|
|---|
| 940 | for value in values:
|
|---|
| 941 | not_float = False
|
|---|
| 942 | not_int = False
|
|---|
| 943 |
|
|---|
| 944 | # Convert values into correct types
|
|---|
| 945 | # We first try integer then float
|
|---|
| 946 | try:
|
|---|
| 947 | value_converted = int(value)
|
|---|
| 948 | except:
|
|---|
| 949 | not_int = True
|
|---|
| 950 | if not_int:
|
|---|
| 951 | try:
|
|---|
| 952 | value_converted = float(value)
|
|---|
| 953 | except:
|
|---|
| 954 | not_float = True
|
|---|
| 955 |
|
|---|
| 956 | if not_int and not_float:
|
|---|
| 957 | value_converted = value.strip()
|
|---|
| 958 |
|
|---|
| 959 | value_list.append(value_converted)
|
|---|
| 960 |
|
|---|
| 961 | kvdict[key] = value_list
|
|---|
| 962 | if checkproj:
|
|---|
| 963 | kvdict = _compare_projection(kvdict)
|
|---|
| 964 | if checkunits:
|
|---|
| 965 | kvdict = _compare_units(kvdict)
|
|---|
| 966 | return kvdict
|
|---|
| 967 |
|
|---|
| 968 |
|
|---|
| 969 | def compare_key_value_text_files(filename_a, filename_b, sep=":",
|
|---|
| 970 | val_sep=",", precision=0.000001,
|
|---|
| 971 | proj=False, units=False):
|
|---|
| 972 | """Compare two key-value text files
|
|---|
| 973 |
|
|---|
| 974 | This method will print a warning in case keys that are present in the first
|
|---|
| 975 | file are not present in the second one.
|
|---|
| 976 | The comparison method tries to convert the values into their native format
|
|---|
| 977 | (float, int or string) to allow correct comparison.
|
|---|
| 978 |
|
|---|
| 979 | An example key-value text file may have this content:
|
|---|
| 980 |
|
|---|
| 981 | ::
|
|---|
| 982 |
|
|---|
| 983 | a: Hello
|
|---|
| 984 | b: 1.0
|
|---|
| 985 | c: 1,2,3,4,5
|
|---|
| 986 | d : hello,8,0.1
|
|---|
| 987 |
|
|---|
| 988 | :param str filename_a: name of the first key-value text file
|
|---|
| 989 | :param str filenmae_b: name of the second key-value text file
|
|---|
| 990 | :param str sep: character that separates the keys and values, default is ":"
|
|---|
| 991 | :param str val_sep: character that separates the values of a single key, default is ","
|
|---|
| 992 | :param double precision: precision with which the floating point values are compared
|
|---|
| 993 | :param bool proj: True if it has to check some information about projection system
|
|---|
| 994 | :param bool units: True if it has to check some information about units
|
|---|
| 995 |
|
|---|
| 996 | :return: True if full or almost identical, False if different
|
|---|
| 997 | """
|
|---|
| 998 | dict_a = _text_to_key_value_dict(filename_a, sep, checkproj=proj,
|
|---|
| 999 | checkunits=units)
|
|---|
| 1000 | dict_b = _text_to_key_value_dict(filename_b, sep, checkproj=proj,
|
|---|
| 1001 | checkunits=units)
|
|---|
| 1002 |
|
|---|
| 1003 | if sorted(dict_a.keys()) != sorted(dict_b.keys()):
|
|---|
| 1004 | return False
|
|---|
| 1005 |
|
|---|
| 1006 | # We compare matching keys
|
|---|
| 1007 | for key in dict_a.keys():
|
|---|
| 1008 | # Floating point values must be handled separately
|
|---|
| 1009 | if isinstance(dict_a[key], float) and isinstance(dict_b[key], float):
|
|---|
| 1010 | if abs(dict_a[key] - dict_b[key]) > precision:
|
|---|
| 1011 | return False
|
|---|
| 1012 | elif isinstance(dict_a[key], float) or isinstance(dict_b[key], float):
|
|---|
| 1013 | warning(_("Mixing value types. Will try to compare after "
|
|---|
| 1014 | "integer conversion"))
|
|---|
| 1015 | return int(dict_a[key]) == int(dict_b[key])
|
|---|
| 1016 | elif key == "+towgs84":
|
|---|
| 1017 | # We compare the sum of the entries
|
|---|
| 1018 | if abs(sum(dict_a[key]) - sum(dict_b[key])) > precision:
|
|---|
| 1019 | return False
|
|---|
| 1020 | else:
|
|---|
| 1021 | if dict_a[key] != dict_b[key]:
|
|---|
| 1022 | return False
|
|---|
| 1023 | return True
|
|---|
| 1024 |
|
|---|
| 1025 | # interface to g.gisenv
|
|---|
| 1026 |
|
|---|
| 1027 |
|
|---|
| 1028 | def gisenv(env=None):
|
|---|
| 1029 | """Returns the output from running g.gisenv (with no arguments), as a
|
|---|
| 1030 | dictionary. Example:
|
|---|
| 1031 |
|
|---|
| 1032 | >>> env = gisenv()
|
|---|
| 1033 | >>> print(env['GISDBASE']) # doctest: +SKIP
|
|---|
| 1034 | /opt/grass-data
|
|---|
| 1035 |
|
|---|
| 1036 | :param env run with different environment
|
|---|
| 1037 | :return: list of GRASS variables
|
|---|
| 1038 | """
|
|---|
| 1039 | s = read_command("g.gisenv", flags='n', env=env)
|
|---|
| 1040 | return parse_key_val(s)
|
|---|
| 1041 |
|
|---|
| 1042 | # interface to g.region
|
|---|
| 1043 |
|
|---|
| 1044 |
|
|---|
| 1045 | def locn_is_latlong():
|
|---|
| 1046 | """Tests if location is lat/long. Value is obtained
|
|---|
| 1047 | by checking the "g.region -pu" projection code.
|
|---|
| 1048 |
|
|---|
| 1049 | :return: True for a lat/long region, False otherwise
|
|---|
| 1050 | """
|
|---|
| 1051 | s = read_command("g.region", flags='pu')
|
|---|
| 1052 | kv = parse_key_val(s, ':')
|
|---|
| 1053 | if kv['projection'].split(' ')[0] == '3':
|
|---|
| 1054 | return True
|
|---|
| 1055 | else:
|
|---|
| 1056 | return False
|
|---|
| 1057 |
|
|---|
| 1058 |
|
|---|
| 1059 | def region(region3d=False, complete=False, env=None):
|
|---|
| 1060 | """Returns the output from running "g.region -gu", as a
|
|---|
| 1061 | dictionary. Example:
|
|---|
| 1062 |
|
|---|
| 1063 | :param bool region3d: True to get 3D region
|
|---|
| 1064 | :param bool complete:
|
|---|
| 1065 | :param env env
|
|---|
| 1066 |
|
|---|
| 1067 | >>> curent_region = region()
|
|---|
| 1068 | >>> # obtain n, s, e and w values
|
|---|
| 1069 | >>> [curent_region[key] for key in "nsew"] # doctest: +ELLIPSIS
|
|---|
| 1070 | [..., ..., ..., ...]
|
|---|
| 1071 | >>> # obtain ns and ew resulutions
|
|---|
| 1072 | >>> (curent_region['nsres'], curent_region['ewres']) # doctest: +ELLIPSIS
|
|---|
| 1073 | (..., ...)
|
|---|
| 1074 |
|
|---|
| 1075 | :return: dictionary of region values
|
|---|
| 1076 | """
|
|---|
| 1077 | flgs = 'gu'
|
|---|
| 1078 | if region3d:
|
|---|
| 1079 | flgs += '3'
|
|---|
| 1080 | if complete:
|
|---|
| 1081 | flgs += 'cep'
|
|---|
| 1082 |
|
|---|
| 1083 | s = read_command("g.region", flags=flgs, env=env)
|
|---|
| 1084 | reg = parse_key_val(s, val_type=float)
|
|---|
| 1085 | for k in ['projection', 'zone', 'rows', 'cols', 'cells',
|
|---|
| 1086 | 'rows3', 'cols3', 'cells3', 'depths']:
|
|---|
| 1087 | if k not in reg:
|
|---|
| 1088 | continue
|
|---|
| 1089 | reg[k] = int(reg[k])
|
|---|
| 1090 |
|
|---|
| 1091 | return reg
|
|---|
| 1092 |
|
|---|
| 1093 |
|
|---|
| 1094 | def region_env(region3d=False, flags=None, env=None, **kwargs):
|
|---|
| 1095 | """Returns region settings as a string which can used as
|
|---|
| 1096 | GRASS_REGION environmental variable.
|
|---|
| 1097 |
|
|---|
| 1098 | If no 'kwargs' are given then the current region is used. Note
|
|---|
| 1099 | that this function doesn't modify the current region!
|
|---|
| 1100 |
|
|---|
| 1101 | See also :func:`use_temp_region()` for alternative method how to define
|
|---|
| 1102 | temporary region used for raster-based computation.
|
|---|
| 1103 |
|
|---|
| 1104 | :param bool region3d: True to get 3D region
|
|---|
| 1105 | :param string flags: for example 'a'
|
|---|
| 1106 | :param env: different environment than current
|
|---|
| 1107 | :param kwargs: g.region's parameters like 'raster', 'vector' or 'region'
|
|---|
| 1108 |
|
|---|
| 1109 | ::
|
|---|
| 1110 |
|
|---|
| 1111 | os.environ['GRASS_REGION'] = grass.region_env(region='detail')
|
|---|
| 1112 | grass.mapcalc('map=1', overwrite=True)
|
|---|
| 1113 | os.environ.pop('GRASS_REGION')
|
|---|
| 1114 |
|
|---|
| 1115 | :return: string with region values
|
|---|
| 1116 | :return: empty string on error
|
|---|
| 1117 | """
|
|---|
| 1118 | # read proj/zone from WIND file
|
|---|
| 1119 | gis_env = gisenv(env)
|
|---|
| 1120 | windfile = os.path.join(gis_env['GISDBASE'], gis_env['LOCATION_NAME'],
|
|---|
| 1121 | gis_env['MAPSET'], "WIND")
|
|---|
| 1122 | fd = open(windfile, "r")
|
|---|
| 1123 | grass_region = ''
|
|---|
| 1124 | for line in fd.readlines():
|
|---|
| 1125 | key, value = map(lambda x: x.strip(), line.split(":", 1))
|
|---|
| 1126 | if kwargs and key not in ('proj', 'zone'):
|
|---|
| 1127 | continue
|
|---|
| 1128 | if not kwargs and not region3d and \
|
|---|
| 1129 | key in ('top', 'bottom', 'cols3', 'rows3',
|
|---|
| 1130 | 'depths', 'e-w resol3', 'n-s resol3', 't-b resol'):
|
|---|
| 1131 | continue
|
|---|
| 1132 |
|
|---|
| 1133 | grass_region += '%s: %s;' % (key, value)
|
|---|
| 1134 |
|
|---|
| 1135 | if not kwargs: # return current region
|
|---|
| 1136 | return grass_region
|
|---|
| 1137 |
|
|---|
| 1138 | # read other values from `g.region -gu`
|
|---|
| 1139 | flgs = 'ug'
|
|---|
| 1140 | if region3d:
|
|---|
| 1141 | flgs += '3'
|
|---|
| 1142 | if flags:
|
|---|
| 1143 | flgs += flags
|
|---|
| 1144 |
|
|---|
| 1145 | s = read_command('g.region', flags=flgs, env=env, **kwargs)
|
|---|
| 1146 | if not s:
|
|---|
| 1147 | return ''
|
|---|
| 1148 | reg = parse_key_val(s)
|
|---|
| 1149 |
|
|---|
| 1150 | kwdata = [('north', 'n'),
|
|---|
| 1151 | ('south', 's'),
|
|---|
| 1152 | ('east', 'e'),
|
|---|
| 1153 | ('west', 'w'),
|
|---|
| 1154 | ('cols', 'cols'),
|
|---|
| 1155 | ('rows', 'rows'),
|
|---|
| 1156 | ('e-w resol', 'ewres'),
|
|---|
| 1157 | ('n-s resol', 'nsres')]
|
|---|
| 1158 | if region3d:
|
|---|
| 1159 | kwdata += [('top', 't'),
|
|---|
| 1160 | ('bottom', 'b'),
|
|---|
| 1161 | ('cols3', 'cols3'),
|
|---|
| 1162 | ('rows3', 'rows3'),
|
|---|
| 1163 | ('depths', 'depths'),
|
|---|
| 1164 | ('e-w resol3', 'ewres3'),
|
|---|
| 1165 | ('n-s resol3', 'nsres3'),
|
|---|
| 1166 | ('t-b resol', 'tbres')]
|
|---|
| 1167 |
|
|---|
| 1168 | for wkey, rkey in kwdata:
|
|---|
| 1169 | grass_region += '%s: %s;' % (wkey, reg[rkey])
|
|---|
| 1170 |
|
|---|
| 1171 | return grass_region
|
|---|
| 1172 |
|
|---|
| 1173 |
|
|---|
| 1174 | def use_temp_region():
|
|---|
| 1175 | """Copies the current region to a temporary region with "g.region save=",
|
|---|
| 1176 | then sets WIND_OVERRIDE to refer to that region. Installs an atexit
|
|---|
| 1177 | handler to delete the temporary region upon termination.
|
|---|
| 1178 | """
|
|---|
| 1179 | name = "tmp.%s.%d" % (os.path.basename(sys.argv[0]), os.getpid())
|
|---|
| 1180 | run_command("g.region", save=name, overwrite=True)
|
|---|
| 1181 | os.environ['WIND_OVERRIDE'] = name
|
|---|
| 1182 | atexit.register(del_temp_region)
|
|---|
| 1183 |
|
|---|
| 1184 |
|
|---|
| 1185 | def del_temp_region():
|
|---|
| 1186 | """Unsets WIND_OVERRIDE and removes any region named by it."""
|
|---|
| 1187 | try:
|
|---|
| 1188 | name = os.environ.pop('WIND_OVERRIDE')
|
|---|
| 1189 | run_command("g.remove", flags='f', quiet=True, type='region', name=name)
|
|---|
| 1190 | except:
|
|---|
| 1191 | pass
|
|---|
| 1192 |
|
|---|
| 1193 | # interface to g.findfile
|
|---|
| 1194 |
|
|---|
| 1195 |
|
|---|
| 1196 | def find_file(name, element='cell', mapset=None):
|
|---|
| 1197 | """Returns the output from running g.findfile as a
|
|---|
| 1198 | dictionary. Example:
|
|---|
| 1199 |
|
|---|
| 1200 | >>> result = find_file('elevation', element='cell')
|
|---|
| 1201 | >>> print(result['fullname'])
|
|---|
| 1202 | elevation@PERMANENT
|
|---|
| 1203 | >>> print(result['file']) # doctest: +ELLIPSIS
|
|---|
| 1204 | /.../PERMANENT/cell/elevation
|
|---|
| 1205 |
|
|---|
| 1206 |
|
|---|
| 1207 | :param str name: file name
|
|---|
| 1208 | :param str element: element type (default 'cell')
|
|---|
| 1209 | :param str mapset: mapset name (default all mapsets in search path)
|
|---|
| 1210 |
|
|---|
| 1211 | :return: parsed output of g.findfile
|
|---|
| 1212 | """
|
|---|
| 1213 | if element == 'raster' or element == 'rast':
|
|---|
| 1214 | verbose(_('Element type should be "cell" and not "%s"') % element)
|
|---|
| 1215 | element = 'cell'
|
|---|
| 1216 | # g.findfile returns non-zero when file was not found
|
|---|
| 1217 | # se we ignore return code and just focus on stdout
|
|---|
| 1218 | process = start_command('g.findfile', flags='n',
|
|---|
| 1219 | element=element, file=name, mapset=mapset,
|
|---|
| 1220 | stdout=PIPE)
|
|---|
| 1221 | stdout = process.communicate()[0]
|
|---|
| 1222 | return parse_key_val(stdout)
|
|---|
| 1223 |
|
|---|
| 1224 | # interface to g.list
|
|---|
| 1225 |
|
|---|
| 1226 |
|
|---|
| 1227 | def list_strings(type, pattern=None, mapset=None, exclude=None, flag=''):
|
|---|
| 1228 | """List of elements as strings.
|
|---|
| 1229 |
|
|---|
| 1230 | Returns the output from running g.list, as a list of qualified
|
|---|
| 1231 | names.
|
|---|
| 1232 |
|
|---|
| 1233 | :param str type: element type (raster, vector, raster_3d, region, ...)
|
|---|
| 1234 | :param str pattern: pattern string
|
|---|
| 1235 | :param str mapset: mapset name (if not given use search path)
|
|---|
| 1236 | :param str exclude: pattern string to exclude maps from the research
|
|---|
| 1237 | :param str flag: pattern type: 'r' (basic regexp), 'e' (extended regexp),
|
|---|
| 1238 | or '' (glob pattern)
|
|---|
| 1239 |
|
|---|
| 1240 | :return: list of elements
|
|---|
| 1241 | """
|
|---|
| 1242 | if type == 'cell':
|
|---|
| 1243 | verbose(_('Element type should be "raster" and not "%s"') % type)
|
|---|
| 1244 |
|
|---|
| 1245 | result = list()
|
|---|
| 1246 | for line in read_command("g.list",
|
|---|
| 1247 | quiet=True,
|
|---|
| 1248 | flags='m' + flag,
|
|---|
| 1249 | type=type,
|
|---|
| 1250 | pattern=pattern,
|
|---|
| 1251 | exclude=exclude,
|
|---|
| 1252 | mapset=mapset).splitlines():
|
|---|
| 1253 | result.append(line.strip())
|
|---|
| 1254 |
|
|---|
| 1255 | return result
|
|---|
| 1256 |
|
|---|
| 1257 |
|
|---|
| 1258 | def list_pairs(type, pattern=None, mapset=None, exclude=None, flag=''):
|
|---|
| 1259 | """List of elements as pairs
|
|---|
| 1260 |
|
|---|
| 1261 | Returns the output from running g.list, as a list of
|
|---|
| 1262 | (name, mapset) pairs
|
|---|
| 1263 |
|
|---|
| 1264 | :param str type: element type (raster, vector, raster_3d, region, ...)
|
|---|
| 1265 | :param str pattern: pattern string
|
|---|
| 1266 | :param str mapset: mapset name (if not given use search path)
|
|---|
| 1267 | :param str exclude: pattern string to exclude maps from the research
|
|---|
| 1268 | :param str flag: pattern type: 'r' (basic regexp), 'e' (extended regexp),
|
|---|
| 1269 | or '' (glob pattern)
|
|---|
| 1270 |
|
|---|
| 1271 | :return: list of elements
|
|---|
| 1272 | """
|
|---|
| 1273 | return [tuple(map.split('@', 1)) for map in list_strings(type, pattern,
|
|---|
| 1274 | mapset, exclude,
|
|---|
| 1275 | flag)]
|
|---|
| 1276 |
|
|---|
| 1277 |
|
|---|
| 1278 | def list_grouped(type, pattern=None, check_search_path=True, exclude=None,
|
|---|
| 1279 | flag=''):
|
|---|
| 1280 | """List of elements grouped by mapsets.
|
|---|
| 1281 |
|
|---|
| 1282 | Returns the output from running g.list, as a dictionary where the
|
|---|
| 1283 | keys are mapset names and the values are lists of maps in that
|
|---|
| 1284 | mapset. Example:
|
|---|
| 1285 |
|
|---|
| 1286 | >>> list_grouped('vect', pattern='*roads*')['PERMANENT']
|
|---|
| 1287 | ['railroads', 'roadsmajor']
|
|---|
| 1288 |
|
|---|
| 1289 | :param str type: element type (raster, vector, raster_3d, region, ...) or list of elements
|
|---|
| 1290 | :param str pattern: pattern string
|
|---|
| 1291 | :param str check_search_path: True to add mapsets for the search path
|
|---|
| 1292 | with no found elements
|
|---|
| 1293 | :param str exclude: pattern string to exclude maps from the research
|
|---|
| 1294 | :param str flag: pattern type: 'r' (basic regexp), 'e' (extended regexp),
|
|---|
| 1295 | or '' (glob pattern)
|
|---|
| 1296 |
|
|---|
| 1297 | :return: directory of mapsets/elements
|
|---|
| 1298 | """
|
|---|
| 1299 | if isinstance(type, python_types.StringTypes) or len(type) == 1:
|
|---|
| 1300 | types = [type]
|
|---|
| 1301 | store_types = False
|
|---|
| 1302 | else:
|
|---|
| 1303 | types = type
|
|---|
| 1304 | store_types = True
|
|---|
| 1305 | flag += 't'
|
|---|
| 1306 | for i in range(len(types)):
|
|---|
| 1307 | if types[i] == 'cell':
|
|---|
| 1308 | verbose(_('Element type should be "raster" and not "%s"') % types[i])
|
|---|
| 1309 | types[i] = 'raster'
|
|---|
| 1310 | result = {}
|
|---|
| 1311 | if check_search_path:
|
|---|
| 1312 | for mapset in mapsets(search_path=True):
|
|---|
| 1313 | if store_types:
|
|---|
| 1314 | result[mapset] = {}
|
|---|
| 1315 | else:
|
|---|
| 1316 | result[mapset] = []
|
|---|
| 1317 |
|
|---|
| 1318 | mapset = None
|
|---|
| 1319 | for line in read_command("g.list", quiet=True, flags="m" + flag,
|
|---|
| 1320 | type=types, pattern=pattern, exclude=exclude).splitlines():
|
|---|
| 1321 | try:
|
|---|
| 1322 | name, mapset = line.split('@')
|
|---|
| 1323 | except ValueError:
|
|---|
| 1324 | warning(_("Invalid element '%s'") % line)
|
|---|
| 1325 | continue
|
|---|
| 1326 |
|
|---|
| 1327 | if store_types:
|
|---|
| 1328 | type_, name = name.split('/')
|
|---|
| 1329 | if mapset in result:
|
|---|
| 1330 | if type_ in result[mapset]:
|
|---|
| 1331 | result[mapset][type_].append(name)
|
|---|
| 1332 | else:
|
|---|
| 1333 | result[mapset][type_] = [name, ]
|
|---|
| 1334 | else:
|
|---|
| 1335 | result[mapset] = {type_: [name, ]}
|
|---|
| 1336 | else:
|
|---|
| 1337 | if mapset in result:
|
|---|
| 1338 | result[mapset].append(name)
|
|---|
| 1339 | else:
|
|---|
| 1340 | result[mapset] = [name, ]
|
|---|
| 1341 |
|
|---|
| 1342 | return result
|
|---|
| 1343 |
|
|---|
| 1344 | # color parsing
|
|---|
| 1345 |
|
|---|
| 1346 | named_colors = {
|
|---|
| 1347 | "white": (1.00, 1.00, 1.00),
|
|---|
| 1348 | "black": (0.00, 0.00, 0.00),
|
|---|
| 1349 | "red": (1.00, 0.00, 0.00),
|
|---|
| 1350 | "green": (0.00, 1.00, 0.00),
|
|---|
| 1351 | "blue": (0.00, 0.00, 1.00),
|
|---|
| 1352 | "yellow": (1.00, 1.00, 0.00),
|
|---|
| 1353 | "magenta": (1.00, 0.00, 1.00),
|
|---|
| 1354 | "cyan": (0.00, 1.00, 1.00),
|
|---|
| 1355 | "aqua": (0.00, 0.75, 0.75),
|
|---|
| 1356 | "grey": (0.75, 0.75, 0.75),
|
|---|
| 1357 | "gray": (0.75, 0.75, 0.75),
|
|---|
| 1358 | "orange": (1.00, 0.50, 0.00),
|
|---|
| 1359 | "brown": (0.75, 0.50, 0.25),
|
|---|
| 1360 | "purple": (0.50, 0.00, 1.00),
|
|---|
| 1361 | "violet": (0.50, 0.00, 1.00),
|
|---|
| 1362 | "indigo": (0.00, 0.50, 1.00)}
|
|---|
| 1363 |
|
|---|
| 1364 |
|
|---|
| 1365 | def parse_color(val, dflt=None):
|
|---|
| 1366 | """Parses the string "val" as a GRASS colour, which can be either one of
|
|---|
| 1367 | the named colours or an R:G:B tuple e.g. 255:255:255. Returns an
|
|---|
| 1368 | (r,g,b) triple whose components are floating point values between 0
|
|---|
| 1369 | and 1. Example:
|
|---|
| 1370 |
|
|---|
| 1371 | >>> parse_color("red")
|
|---|
| 1372 | (1.0, 0.0, 0.0)
|
|---|
| 1373 | >>> parse_color("255:0:0")
|
|---|
| 1374 | (1.0, 0.0, 0.0)
|
|---|
| 1375 |
|
|---|
| 1376 | :param val: color value
|
|---|
| 1377 | :param dflt: default color value
|
|---|
| 1378 |
|
|---|
| 1379 | :return: tuple RGB
|
|---|
| 1380 | """
|
|---|
| 1381 | if val in named_colors:
|
|---|
| 1382 | return named_colors[val]
|
|---|
| 1383 |
|
|---|
| 1384 | vals = val.split(':')
|
|---|
| 1385 | if len(vals) == 3:
|
|---|
| 1386 | return tuple(float(v) / 255 for v in vals)
|
|---|
| 1387 |
|
|---|
| 1388 | return dflt
|
|---|
| 1389 |
|
|---|
| 1390 | # check GRASS_OVERWRITE
|
|---|
| 1391 |
|
|---|
| 1392 |
|
|---|
| 1393 | def overwrite():
|
|---|
| 1394 | """Return True if existing files may be overwritten"""
|
|---|
| 1395 | owstr = 'GRASS_OVERWRITE'
|
|---|
| 1396 | return owstr in os.environ and os.environ[owstr] != '0'
|
|---|
| 1397 |
|
|---|
| 1398 | # check GRASS_VERBOSE
|
|---|
| 1399 |
|
|---|
| 1400 |
|
|---|
| 1401 | def verbosity():
|
|---|
| 1402 | """Return the verbosity level selected by GRASS_VERBOSE"""
|
|---|
| 1403 | vbstr = os.getenv('GRASS_VERBOSE')
|
|---|
| 1404 | if vbstr:
|
|---|
| 1405 | return int(vbstr)
|
|---|
| 1406 | else:
|
|---|
| 1407 | return 2
|
|---|
| 1408 |
|
|---|
| 1409 | ## various utilities, not specific to GRASS
|
|---|
| 1410 |
|
|---|
| 1411 | def find_program(pgm, *args):
|
|---|
| 1412 | """Attempt to run a program, with optional arguments.
|
|---|
| 1413 |
|
|---|
| 1414 | You must call the program in a way that will return a successful
|
|---|
| 1415 | exit code. For GRASS modules this means you need to pass it some
|
|---|
| 1416 | valid CLI option, like "--help". For other programs a common
|
|---|
| 1417 | valid do-little option is usually "--version".
|
|---|
| 1418 |
|
|---|
| 1419 | Example:
|
|---|
| 1420 |
|
|---|
| 1421 | >>> find_program('r.sun', '--help')
|
|---|
| 1422 | True
|
|---|
| 1423 | >>> find_program('ls', '--version')
|
|---|
| 1424 | True
|
|---|
| 1425 |
|
|---|
| 1426 | :param str pgm: program name
|
|---|
| 1427 | :param args: list of arguments
|
|---|
| 1428 |
|
|---|
| 1429 | :return: False if the attempt failed due to a missing executable
|
|---|
| 1430 | or non-zero return code
|
|---|
| 1431 | :return: True otherwise
|
|---|
| 1432 | """
|
|---|
| 1433 | nuldev = open(os.devnull, 'w+')
|
|---|
| 1434 | try:
|
|---|
| 1435 | # TODO: the doc or impl is not correct, any return code is accepted
|
|---|
| 1436 | call([pgm] + list(args), stdin = nuldev, stdout = nuldev, stderr = nuldev)
|
|---|
| 1437 | found = True
|
|---|
| 1438 | except:
|
|---|
| 1439 | found = False
|
|---|
| 1440 | nuldev.close()
|
|---|
| 1441 |
|
|---|
| 1442 | return found
|
|---|
| 1443 |
|
|---|
| 1444 | # interface to g.mapsets
|
|---|
| 1445 |
|
|---|
| 1446 |
|
|---|
| 1447 | def mapsets(search_path=False):
|
|---|
| 1448 | """List available mapsets
|
|---|
| 1449 |
|
|---|
| 1450 | :param bool search_path: True to list mapsets only in search path
|
|---|
| 1451 |
|
|---|
| 1452 | :return: list of mapsets
|
|---|
| 1453 | """
|
|---|
| 1454 | if search_path:
|
|---|
| 1455 | flags = 'p'
|
|---|
| 1456 | else:
|
|---|
| 1457 | flags = 'l'
|
|---|
| 1458 | mapsets = read_command('g.mapsets',
|
|---|
| 1459 | flags=flags,
|
|---|
| 1460 | sep='newline',
|
|---|
| 1461 | quiet=True)
|
|---|
| 1462 | if not mapsets:
|
|---|
| 1463 | fatal(_("Unable to list mapsets"))
|
|---|
| 1464 |
|
|---|
| 1465 | return mapsets.splitlines()
|
|---|
| 1466 |
|
|---|
| 1467 | # interface to `g.proj -c`
|
|---|
| 1468 |
|
|---|
| 1469 |
|
|---|
| 1470 | def create_location(dbase, location, epsg=None, proj4=None, filename=None,
|
|---|
| 1471 | wkt=None, datum=None, datum_trans=None, desc=None,
|
|---|
| 1472 | overwrite=False):
|
|---|
| 1473 | """Create new location
|
|---|
| 1474 |
|
|---|
| 1475 | Raise ScriptError on error.
|
|---|
| 1476 |
|
|---|
| 1477 | :param str dbase: path to GRASS database
|
|---|
| 1478 | :param str location: location name to create
|
|---|
| 1479 | :param epsg: if given create new location based on EPSG code
|
|---|
| 1480 | :param proj4: if given create new location based on Proj4 definition
|
|---|
| 1481 | :param str filename: if given create new location based on georeferenced file
|
|---|
| 1482 | :param str wkt: if given create new location based on WKT definition
|
|---|
| 1483 | (path to PRJ file)
|
|---|
| 1484 | :param datum: GRASS format datum code
|
|---|
| 1485 | :param datum_trans: datum transformation parameters (used for epsg and proj4)
|
|---|
| 1486 | :param desc: description of the location (creates MYNAME file)
|
|---|
| 1487 | :param bool overwrite: True to overwrite location if exists(WARNING:
|
|---|
| 1488 | ALL DATA from existing location ARE DELETED!)
|
|---|
| 1489 | """
|
|---|
| 1490 | gisdbase = None
|
|---|
| 1491 | if epsg or proj4 or filename or wkt:
|
|---|
| 1492 | # FIXME: changing GISDBASE mid-session is not background-job safe
|
|---|
| 1493 | gisdbase = gisenv()['GISDBASE']
|
|---|
| 1494 | run_command('g.gisenv', set='GISDBASE=%s' % dbase)
|
|---|
| 1495 | # create dbase if not exists
|
|---|
| 1496 | if not os.path.exists(dbase):
|
|---|
| 1497 | os.mkdir(dbase)
|
|---|
| 1498 |
|
|---|
| 1499 | # check if location already exists
|
|---|
| 1500 | if os.path.exists(os.path.join(dbase, location)):
|
|---|
| 1501 | if not overwrite:
|
|---|
| 1502 | warning(_("Location <%s> already exists. Operation canceled.") % location)
|
|---|
| 1503 | return
|
|---|
| 1504 | else:
|
|---|
| 1505 | warning(_("Location <%s> already exists and will be overwritten") % location)
|
|---|
| 1506 | shutil.rmtree(os.path.join(dbase, location))
|
|---|
| 1507 |
|
|---|
| 1508 | kwargs = dict()
|
|---|
| 1509 | if datum:
|
|---|
| 1510 | kwargs['datum'] = datum
|
|---|
| 1511 | if datum_trans:
|
|---|
| 1512 | kwargs['datum_trans'] = datum_trans
|
|---|
| 1513 |
|
|---|
| 1514 | if epsg:
|
|---|
| 1515 | ps = pipe_command('g.proj', quiet=True, flags='t', epsg=epsg,
|
|---|
| 1516 | location=location, stderr=PIPE, **kwargs)
|
|---|
| 1517 | elif proj4:
|
|---|
| 1518 | ps = pipe_command('g.proj', quiet=True, flags='t', proj4=proj4,
|
|---|
| 1519 | location=location, stderr=PIPE, **kwargs)
|
|---|
| 1520 | elif filename:
|
|---|
| 1521 | ps = pipe_command('g.proj', quiet=True, georef=filename,
|
|---|
| 1522 | location=location, stderr=PIPE)
|
|---|
| 1523 | elif wkt:
|
|---|
| 1524 | ps = pipe_command('g.proj', quiet=True, wkt=wkt, location=location,
|
|---|
| 1525 | stderr=PIPE)
|
|---|
| 1526 | else:
|
|---|
| 1527 | _create_location_xy(dbase, location)
|
|---|
| 1528 |
|
|---|
| 1529 | if epsg or proj4 or filename or wkt:
|
|---|
| 1530 | error = ps.communicate()[1]
|
|---|
| 1531 | run_command('g.gisenv', set='GISDBASE=%s' % gisdbase)
|
|---|
| 1532 |
|
|---|
| 1533 | if ps.returncode != 0 and error:
|
|---|
| 1534 | raise ScriptError(repr(error))
|
|---|
| 1535 |
|
|---|
| 1536 | try:
|
|---|
| 1537 | fd = codecs.open(os.path.join(dbase, location, 'PERMANENT', 'MYNAME'),
|
|---|
| 1538 | encoding='utf-8', mode='w')
|
|---|
| 1539 | if desc:
|
|---|
| 1540 | fd.write(desc + os.linesep)
|
|---|
| 1541 | else:
|
|---|
| 1542 | fd.write(os.linesep)
|
|---|
| 1543 | fd.close()
|
|---|
| 1544 | except OSError as e:
|
|---|
| 1545 | raise ScriptError(repr(e))
|
|---|
| 1546 |
|
|---|
| 1547 |
|
|---|
| 1548 | def _create_location_xy(database, location):
|
|---|
| 1549 | """Create unprojected location
|
|---|
| 1550 |
|
|---|
| 1551 | Raise ScriptError on error.
|
|---|
| 1552 |
|
|---|
| 1553 | :param database: GRASS database where to create new location
|
|---|
| 1554 | :param location: location name
|
|---|
| 1555 | """
|
|---|
| 1556 | cur_dir = os.getcwd()
|
|---|
| 1557 | try:
|
|---|
| 1558 | os.chdir(database)
|
|---|
| 1559 | os.mkdir(location)
|
|---|
| 1560 | os.mkdir(os.path.join(location, 'PERMANENT'))
|
|---|
| 1561 |
|
|---|
| 1562 | # create DEFAULT_WIND and WIND files
|
|---|
| 1563 | regioninfo = ['proj: 0',
|
|---|
| 1564 | 'zone: 0',
|
|---|
| 1565 | 'north: 1',
|
|---|
| 1566 | 'south: 0',
|
|---|
| 1567 | 'east: 1',
|
|---|
| 1568 | 'west: 0',
|
|---|
| 1569 | 'cols: 1',
|
|---|
| 1570 | 'rows: 1',
|
|---|
| 1571 | 'e-w resol: 1',
|
|---|
| 1572 | 'n-s resol: 1',
|
|---|
| 1573 | 'top: 1',
|
|---|
| 1574 | 'bottom: 0',
|
|---|
| 1575 | 'cols3: 1',
|
|---|
| 1576 | 'rows3: 1',
|
|---|
| 1577 | 'depths: 1',
|
|---|
| 1578 | 'e-w resol3: 1',
|
|---|
| 1579 | 'n-s resol3: 1',
|
|---|
| 1580 | 't-b resol: 1']
|
|---|
| 1581 |
|
|---|
| 1582 | defwind = open(os.path.join(location,
|
|---|
| 1583 | "PERMANENT", "DEFAULT_WIND"), 'w')
|
|---|
| 1584 | for param in regioninfo:
|
|---|
| 1585 | defwind.write(param + '%s' % os.linesep)
|
|---|
| 1586 | defwind.close()
|
|---|
| 1587 |
|
|---|
| 1588 | shutil.copy(os.path.join(location, "PERMANENT", "DEFAULT_WIND"),
|
|---|
| 1589 | os.path.join(location, "PERMANENT", "WIND"))
|
|---|
| 1590 |
|
|---|
| 1591 | os.chdir(cur_dir)
|
|---|
| 1592 | except OSError as e:
|
|---|
| 1593 | raise ScriptError(repr(e))
|
|---|
| 1594 |
|
|---|
| 1595 | # interface to g.version
|
|---|
| 1596 |
|
|---|
| 1597 |
|
|---|
| 1598 | def version():
|
|---|
| 1599 | """Get GRASS version as dictionary
|
|---|
| 1600 |
|
|---|
| 1601 | ::
|
|---|
| 1602 |
|
|---|
| 1603 | >>> print(version())
|
|---|
| 1604 | {'proj4': '4.8.0', 'geos': '3.3.5', 'libgis_revision': '52468',
|
|---|
| 1605 | 'libgis_date': '2012-07-27 22:53:30 +0200 (Fri, 27 Jul 2012)',
|
|---|
| 1606 | 'version': '7.0.svn', 'date': '2012', 'gdal': '2.0dev',
|
|---|
| 1607 | 'revision': '53670'}
|
|---|
| 1608 |
|
|---|
| 1609 | """
|
|---|
| 1610 | data = parse_command('g.version', flags='rge', errors='ignore')
|
|---|
| 1611 | for k, v in data.items():
|
|---|
| 1612 | data[k.strip()] = v.replace('"', '').strip()
|
|---|
| 1613 |
|
|---|
| 1614 | return data
|
|---|
| 1615 |
|
|---|
| 1616 | # get debug_level
|
|---|
| 1617 | _debug_level = None
|
|---|
| 1618 |
|
|---|
| 1619 |
|
|---|
| 1620 | def debug_level(force=False):
|
|---|
| 1621 | global _debug_level
|
|---|
| 1622 | if not force and _debug_level is not None:
|
|---|
| 1623 | return _debug_level
|
|---|
| 1624 | _debug_level = 0
|
|---|
| 1625 | if find_program('g.gisenv', '--help'):
|
|---|
| 1626 | try:
|
|---|
| 1627 | _debug_level = int(gisenv().get('DEBUG', 0))
|
|---|
| 1628 | if _debug_level < 0 or _debug_level > 5:
|
|---|
| 1629 | raise ValueError(_("Debug level {0}").format(_debug_level))
|
|---|
| 1630 | except ValueError as e:
|
|---|
| 1631 | _debug_level = 0
|
|---|
| 1632 | sys.stderr.write(_("WARNING: Ignoring unsupported debug level (must be >=0 and <=5). {0}\n").format(e))
|
|---|
| 1633 |
|
|---|
| 1634 | return _debug_level
|
|---|
| 1635 |
|
|---|
| 1636 |
|
|---|
| 1637 | def legal_name(s):
|
|---|
| 1638 | """Checks if the string contains only allowed characters.
|
|---|
| 1639 |
|
|---|
| 1640 | This is the Python implementation of :func:`G_legal_filename()` function.
|
|---|
| 1641 |
|
|---|
| 1642 | ..note::
|
|---|
| 1643 |
|
|---|
| 1644 | It is not clear when exactly use this function, but it might be
|
|---|
| 1645 | useful anyway for checking map names and column names.
|
|---|
| 1646 | """
|
|---|
| 1647 | if not s or s[0] == '.':
|
|---|
| 1648 | warning(_("Illegal filename <%s>. Cannot be 'NULL' or start with " \
|
|---|
| 1649 | "'.'.") % s)
|
|---|
| 1650 | return False
|
|---|
| 1651 |
|
|---|
| 1652 | illegal = [c
|
|---|
| 1653 | for c in s
|
|---|
| 1654 | if c in '/"\'@,=*~' or c <= ' ' or c >= '\177']
|
|---|
| 1655 | if illegal:
|
|---|
| 1656 | illegal = ''.join(sorted(set(illegal)))
|
|---|
| 1657 | warning(_("Illegal filename <%(s)s>. <%(il)s> not allowed.\n") % {
|
|---|
| 1658 | 's': s, 'il': illegal})
|
|---|
| 1659 | return False
|
|---|
| 1660 |
|
|---|
| 1661 | return True
|
|---|
| 1662 |
|
|---|
| 1663 |
|
|---|
| 1664 | def create_environment(gisdbase, location, mapset):
|
|---|
| 1665 | """Creates environment to be passed in run_command for example.
|
|---|
| 1666 | Returns tuple with temporary file path and the environment. The user
|
|---|
| 1667 | of this function is responsile for deleting the file."""
|
|---|
| 1668 | tmp_gisrc_file = tempfile()
|
|---|
| 1669 | with open(tmp_gisrc_file, 'w') as f:
|
|---|
| 1670 | f.write('MAPSET: {mapset}\n'.format(mapset=mapset))
|
|---|
| 1671 | f.write('GISDBASE: {g}\n'.format(g=gisdbase))
|
|---|
| 1672 | f.write('LOCATION_NAME: {l}\n'.format(l=location))
|
|---|
| 1673 | f.write('GUI: text\n')
|
|---|
| 1674 | env = os.environ.copy()
|
|---|
| 1675 | env['GISRC'] = tmp_gisrc_file
|
|---|
| 1676 | return tmp_gisrc_file, env
|
|---|
| 1677 |
|
|---|
| 1678 |
|
|---|
| 1679 | if __name__ == '__main__':
|
|---|
| 1680 | import doctest
|
|---|
| 1681 | doctest.testmod()
|
|---|