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