| 1 | """
|
|---|
| 2 | Useful functions to be used in Python scripts.
|
|---|
| 3 |
|
|---|
| 4 | Usage:
|
|---|
| 5 |
|
|---|
| 6 | ::
|
|---|
| 7 |
|
|---|
| 8 | from grass.script import utils as gutils
|
|---|
| 9 |
|
|---|
| 10 | (C) 2014-2016 by the GRASS Development Team
|
|---|
| 11 | This program is free software under the GNU General Public
|
|---|
| 12 | License (>=v2). Read the file COPYING that comes with GRASS
|
|---|
| 13 | for details.
|
|---|
| 14 |
|
|---|
| 15 | .. sectionauthor:: Glynn Clements
|
|---|
| 16 | .. sectionauthor:: Martin Landa <landa.martin gmail.com>
|
|---|
| 17 | .. sectionauthor:: Anna Petrasova <kratochanna gmail.com>
|
|---|
| 18 | """
|
|---|
| 19 |
|
|---|
| 20 | import os
|
|---|
| 21 | import sys
|
|---|
| 22 | import shutil
|
|---|
| 23 | import locale
|
|---|
| 24 | import shlex
|
|---|
| 25 | import re
|
|---|
| 26 |
|
|---|
| 27 | def float_or_dms(s):
|
|---|
| 28 | """Convert DMS to float.
|
|---|
| 29 |
|
|---|
| 30 | >>> round(float_or_dms('26:45:30'), 5)
|
|---|
| 31 | 26.75833
|
|---|
| 32 | >>> round(float_or_dms('26:0:0.1'), 5)
|
|---|
| 33 | 26.00003
|
|---|
| 34 |
|
|---|
| 35 | :param s: DMS value
|
|---|
| 36 |
|
|---|
| 37 | :return: float value
|
|---|
| 38 | """
|
|---|
| 39 | return sum(float(x) / 60 ** n for (n, x) in enumerate(s.split(':')))
|
|---|
| 40 |
|
|---|
| 41 |
|
|---|
| 42 | def separator(sep):
|
|---|
| 43 | """Returns separator from G_OPT_F_SEP appropriately converted
|
|---|
| 44 | to character.
|
|---|
| 45 |
|
|---|
| 46 | >>> separator('pipe')
|
|---|
| 47 | '|'
|
|---|
| 48 | >>> separator('comma')
|
|---|
| 49 | ','
|
|---|
| 50 |
|
|---|
| 51 | If the string does not match any of the separator keywords,
|
|---|
| 52 | it is returned as is:
|
|---|
| 53 |
|
|---|
| 54 | >>> separator(', ')
|
|---|
| 55 | ', '
|
|---|
| 56 |
|
|---|
| 57 | :param str separator: character or separator keyword
|
|---|
| 58 |
|
|---|
| 59 | :return: separator character
|
|---|
| 60 | """
|
|---|
| 61 | if sep == "pipe":
|
|---|
| 62 | return "|"
|
|---|
| 63 | elif sep == "comma":
|
|---|
| 64 | return ","
|
|---|
| 65 | elif sep == "space":
|
|---|
| 66 | return " "
|
|---|
| 67 | elif sep == "tab" or sep == "\\t":
|
|---|
| 68 | return "\t"
|
|---|
| 69 | elif sep == "newline" or sep == "\\n":
|
|---|
| 70 | return "\n"
|
|---|
| 71 | return sep
|
|---|
| 72 |
|
|---|
| 73 |
|
|---|
| 74 | def diff_files(filename_a, filename_b):
|
|---|
| 75 | """Diffs two text files and returns difference.
|
|---|
| 76 |
|
|---|
| 77 | :param str filename_a: first file path
|
|---|
| 78 | :param str filename_b: second file path
|
|---|
| 79 |
|
|---|
| 80 | :return: list of strings
|
|---|
| 81 | """
|
|---|
| 82 | import difflib
|
|---|
| 83 | differ = difflib.Differ()
|
|---|
| 84 | fh_a = open(filename_a, 'r')
|
|---|
| 85 | fh_b = open(filename_b, 'r')
|
|---|
| 86 | result = list(differ.compare(fh_a.readlines(),
|
|---|
| 87 | fh_b.readlines()))
|
|---|
| 88 | return result
|
|---|
| 89 |
|
|---|
| 90 |
|
|---|
| 91 | def try_remove(path):
|
|---|
| 92 | """Attempt to remove a file; no exception is generated if the
|
|---|
| 93 | attempt fails.
|
|---|
| 94 |
|
|---|
| 95 | :param str path: path to file to remove
|
|---|
| 96 | """
|
|---|
| 97 | try:
|
|---|
| 98 | os.remove(path)
|
|---|
| 99 | except:
|
|---|
| 100 | pass
|
|---|
| 101 |
|
|---|
| 102 |
|
|---|
| 103 | def try_rmdir(path):
|
|---|
| 104 | """Attempt to remove a directory; no exception is generated if the
|
|---|
| 105 | attempt fails.
|
|---|
| 106 |
|
|---|
| 107 | :param str path: path to directory to remove
|
|---|
| 108 | """
|
|---|
| 109 | try:
|
|---|
| 110 | os.rmdir(path)
|
|---|
| 111 | except:
|
|---|
| 112 | shutil.rmtree(path, ignore_errors=True)
|
|---|
| 113 |
|
|---|
| 114 |
|
|---|
| 115 | def basename(path, ext=None):
|
|---|
| 116 | """Remove leading directory components and an optional extension
|
|---|
| 117 | from the specified path
|
|---|
| 118 |
|
|---|
| 119 | :param str path: path
|
|---|
| 120 | :param str ext: extension
|
|---|
| 121 | """
|
|---|
| 122 | name = os.path.basename(path)
|
|---|
| 123 | if not ext:
|
|---|
| 124 | return name
|
|---|
| 125 | fs = name.rsplit('.', 1)
|
|---|
| 126 | if len(fs) > 1 and fs[1].lower() == ext:
|
|---|
| 127 | name = fs[0]
|
|---|
| 128 | return name
|
|---|
| 129 |
|
|---|
| 130 |
|
|---|
| 131 | class KeyValue(dict):
|
|---|
| 132 | """A general-purpose key-value store.
|
|---|
| 133 |
|
|---|
| 134 | KeyValue is a subclass of dict, but also allows entries to be read and
|
|---|
| 135 | written using attribute syntax. Example:
|
|---|
| 136 |
|
|---|
| 137 | >>> reg = KeyValue()
|
|---|
| 138 | >>> reg['north'] = 489
|
|---|
| 139 | >>> reg.north
|
|---|
| 140 | 489
|
|---|
| 141 | >>> reg.south = 205
|
|---|
| 142 | >>> reg['south']
|
|---|
| 143 | 205
|
|---|
| 144 | """
|
|---|
| 145 |
|
|---|
| 146 | def __getattr__(self, key):
|
|---|
| 147 | return self[key]
|
|---|
| 148 |
|
|---|
| 149 | def __setattr__(self, key, value):
|
|---|
| 150 | self[key] = value
|
|---|
| 151 |
|
|---|
| 152 |
|
|---|
| 153 | def decode(bytes_):
|
|---|
| 154 | """Decode bytes with default locale and return (unicode) string
|
|---|
| 155 |
|
|---|
| 156 | No-op if parameter is not bytes (assumed unicode string).
|
|---|
| 157 |
|
|---|
| 158 | :param bytes bytes_: the bytes to decode
|
|---|
| 159 | """
|
|---|
| 160 | if isinstance(bytes_, bytes):
|
|---|
| 161 | enc = locale.getdefaultlocale()[1]
|
|---|
| 162 | return bytes_.decode(enc) if enc else bytes_.decode()
|
|---|
| 163 | return bytes_
|
|---|
| 164 |
|
|---|
| 165 |
|
|---|
| 166 | def encode(string):
|
|---|
| 167 | """Encode string with default locale and return bytes with that encoding
|
|---|
| 168 |
|
|---|
| 169 | No-op if parameter is bytes (assumed already encoded).
|
|---|
| 170 | This ensures garbage in, garbage out.
|
|---|
| 171 |
|
|---|
| 172 | :param str string: the string to encode
|
|---|
| 173 | """
|
|---|
| 174 | if isinstance(string, bytes):
|
|---|
| 175 | return string
|
|---|
| 176 | enc = locale.getdefaultlocale()[1]
|
|---|
| 177 | return string.encode(enc) if enc else string.encode()
|
|---|
| 178 |
|
|---|
| 179 |
|
|---|
| 180 | def parse_key_val(s, sep='=', dflt=None, val_type=None, vsep=None):
|
|---|
| 181 | """Parse a string into a dictionary, where entries are separated
|
|---|
| 182 | by newlines and the key and value are separated by `sep` (default: `=`)
|
|---|
| 183 |
|
|---|
| 184 | >>> parse_key_val('min=20\\nmax=50') == {'min': '20', 'max': '50'}
|
|---|
| 185 | True
|
|---|
| 186 | >>> parse_key_val('min=20\\nmax=50',
|
|---|
| 187 | ... val_type=float) == {'min': 20, 'max': 50}
|
|---|
| 188 | True
|
|---|
| 189 |
|
|---|
| 190 | :param str s: string to be parsed
|
|---|
| 191 | :param str sep: key/value separator
|
|---|
| 192 | :param dflt: default value to be used
|
|---|
| 193 | :param val_type: value type (None for no cast)
|
|---|
| 194 | :param vsep: vertical separator (default is Python 'universal newlines' approach)
|
|---|
| 195 |
|
|---|
| 196 | :return: parsed input (dictionary of keys/values)
|
|---|
| 197 | """
|
|---|
| 198 | result = KeyValue()
|
|---|
| 199 |
|
|---|
| 200 | if not s:
|
|---|
| 201 | return result
|
|---|
| 202 |
|
|---|
| 203 | if isinstance(s, bytes):
|
|---|
| 204 | sep = encode(sep)
|
|---|
| 205 | vsep = encode(vsep) if vsep else vsep
|
|---|
| 206 |
|
|---|
| 207 | if vsep:
|
|---|
| 208 | lines = s.split(vsep)
|
|---|
| 209 | try:
|
|---|
| 210 | lines.remove('\n')
|
|---|
| 211 | except ValueError:
|
|---|
| 212 | pass
|
|---|
| 213 | else:
|
|---|
| 214 | lines = s.splitlines()
|
|---|
| 215 |
|
|---|
| 216 | for line in lines:
|
|---|
| 217 | kv = line.split(sep, 1)
|
|---|
| 218 | k = decode(kv[0].strip())
|
|---|
| 219 | if len(kv) > 1:
|
|---|
| 220 | v = decode(kv[1].strip())
|
|---|
| 221 | else:
|
|---|
| 222 | v = dflt
|
|---|
| 223 |
|
|---|
| 224 | if val_type:
|
|---|
| 225 | result[k] = val_type(v)
|
|---|
| 226 | else:
|
|---|
| 227 | result[k] = v
|
|---|
| 228 |
|
|---|
| 229 | return result
|
|---|
| 230 |
|
|---|
| 231 |
|
|---|
| 232 | def get_num_suffix(number, max_number):
|
|---|
| 233 | """Returns formatted number with number of padding zeros
|
|---|
| 234 | depending on maximum number, used for creating suffix for data series.
|
|---|
| 235 | Does not include the suffix separator.
|
|---|
| 236 |
|
|---|
| 237 | :param number: number to be formatted as map suffix
|
|---|
| 238 | :param max_number: maximum number of the series to get number of digits
|
|---|
| 239 |
|
|---|
| 240 | >>> get_num_suffix(10, 1000)
|
|---|
| 241 | '0010'
|
|---|
| 242 | >>> get_num_suffix(10, 10)
|
|---|
| 243 | '10'
|
|---|
| 244 | """
|
|---|
| 245 | return '{number:0{width}d}'.format(width=len(str(max_number)),
|
|---|
| 246 | number=number)
|
|---|
| 247 |
|
|---|
| 248 | def split(s):
|
|---|
| 249 | """!Platform specific shlex.split"""
|
|---|
| 250 | if sys.version_info >= (2, 6):
|
|---|
| 251 | return shlex.split(s, posix = (sys.platform != "win32"))
|
|---|
| 252 | elif sys.platform == "win32":
|
|---|
| 253 | return shlex.split(s.replace('\\', r'\\'))
|
|---|
| 254 | else:
|
|---|
| 255 | return shlex.split(s)
|
|---|
| 256 |
|
|---|
| 257 |
|
|---|
| 258 | # source:
|
|---|
| 259 | # http://stackoverflow.com/questions/4836710/
|
|---|
| 260 | # does-python-have-a-built-in-function-for-string-natural-sort/4836734#4836734
|
|---|
| 261 | def natural_sort(l):
|
|---|
| 262 | """Returns sorted strings using natural sort
|
|---|
| 263 | """
|
|---|
| 264 | convert = lambda text: int(text) if text.isdigit() else text.lower()
|
|---|
| 265 | alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
|
|---|
| 266 | return sorted(l, key=alphanum_key)
|
|---|
| 267 |
|
|---|
| 268 |
|
|---|
| 269 | def get_lib_path(modname, libname=None):
|
|---|
| 270 | """Return the path of the libname contained in the module.
|
|---|
| 271 | """
|
|---|
| 272 | from os.path import isdir, join, sep
|
|---|
| 273 | from os import getenv
|
|---|
| 274 |
|
|---|
| 275 | if isdir(join(getenv('GISBASE'), 'etc', modname)):
|
|---|
| 276 | path = join(os.getenv('GISBASE'), 'etc', modname)
|
|---|
| 277 | elif getenv('GRASS_ADDON_BASE') and libname and \
|
|---|
| 278 | isdir(join(getenv('GRASS_ADDON_BASE'), 'etc', modname, libname)):
|
|---|
| 279 | path = join(getenv('GRASS_ADDON_BASE'), 'etc', modname)
|
|---|
| 280 | elif getenv('GRASS_ADDON_BASE') and \
|
|---|
| 281 | isdir(join(getenv('GRASS_ADDON_BASE'), 'etc', modname)):
|
|---|
| 282 | path = join(getenv('GRASS_ADDON_BASE'), 'etc', modname)
|
|---|
| 283 | elif getenv('GRASS_ADDON_BASE') and \
|
|---|
| 284 | isdir(join(getenv('GRASS_ADDON_BASE'), modname, modname)):
|
|---|
| 285 | path = join(os.getenv('GRASS_ADDON_BASE'), modname, modname)
|
|---|
| 286 | else:
|
|---|
| 287 | # used by g.extension compilation process
|
|---|
| 288 | cwd = os.getcwd()
|
|---|
| 289 | idx = cwd.find(modname)
|
|---|
| 290 | if idx < 0:
|
|---|
| 291 | return None
|
|---|
| 292 | path = '{cwd}{sep}etc{sep}{modname}'.format(cwd=cwd[:idx+len(modname)],
|
|---|
| 293 | sep=sep,
|
|---|
| 294 | modname=modname)
|
|---|
| 295 | if libname:
|
|---|
| 296 | path += '{pathsep}{cwd}{sep}etc{sep}{modname}{sep}{libname}'.format(
|
|---|
| 297 | cwd=cwd[:idx+len(modname)],
|
|---|
| 298 | sep=sep,
|
|---|
| 299 | modname=modname, libname=libname,
|
|---|
| 300 | pathsep=os.pathsep
|
|---|
| 301 | )
|
|---|
| 302 |
|
|---|
| 303 | return path
|
|---|
| 304 |
|
|---|
| 305 |
|
|---|
| 306 | def set_path(modulename, dirname=None, path='.'):
|
|---|
| 307 | """Set sys.path looking in the the local directory GRASS directories.
|
|---|
| 308 |
|
|---|
| 309 | :param modulename: string with the name of the GRASS module
|
|---|
| 310 | :param dirname: string with the directory name containing the python
|
|---|
| 311 | libraries, default None
|
|---|
| 312 | :param path: string with the path to reach the dirname locally.
|
|---|
| 313 |
|
|---|
| 314 | Example
|
|---|
| 315 | --------
|
|---|
| 316 |
|
|---|
| 317 | "set_path" example working locally with the source code of a module
|
|---|
| 318 | (r.green) calling the function with all the parameters. Below it is
|
|---|
| 319 | reported the directory structure on the r.green module.
|
|---|
| 320 |
|
|---|
| 321 | ::
|
|---|
| 322 |
|
|---|
| 323 | grass_prompt> pwd
|
|---|
| 324 | ~/Download/r.green/r.green.hydro/r.green.hydro.financial
|
|---|
| 325 |
|
|---|
| 326 | grass_prompt> tree ../../../r.green
|
|---|
| 327 | ../../../r.green
|
|---|
| 328 | |-- ...
|
|---|
| 329 | |-- libgreen
|
|---|
| 330 | | |-- pyfile1.py
|
|---|
| 331 | | +-- pyfile2.py
|
|---|
| 332 | +-- r.green.hydro
|
|---|
| 333 | |-- Makefile
|
|---|
| 334 | |-- libhydro
|
|---|
| 335 | | |-- pyfile1.py
|
|---|
| 336 | | +-- pyfile2.py
|
|---|
| 337 | |-- r.green.hydro.*
|
|---|
| 338 | +-- r.green.hydro.financial
|
|---|
| 339 | |-- Makefile
|
|---|
| 340 | |-- ...
|
|---|
| 341 | +-- r.green.hydro.financial.py
|
|---|
| 342 |
|
|---|
| 343 | 21 directories, 125 files
|
|---|
| 344 |
|
|---|
| 345 | in the source code the function is called with the following parameters: ::
|
|---|
| 346 |
|
|---|
| 347 | set_path('r.green', 'libhydro', '..')
|
|---|
| 348 | set_path('r.green', 'libgreen', os.path.join('..', '..'))
|
|---|
| 349 |
|
|---|
| 350 | when we are executing the module: r.green.hydro.financial locally from
|
|---|
| 351 | the command line: ::
|
|---|
| 352 |
|
|---|
| 353 | grass_prompt> python r.green.hydro.financial.py --ui
|
|---|
| 354 |
|
|---|
| 355 | In this way we are executing the local code even if the module was already
|
|---|
| 356 | installed as grass-addons and it is available in GRASS standards path.
|
|---|
| 357 |
|
|---|
| 358 | The function is cheching if the dirname is provided and if the
|
|---|
| 359 | directory exists and it is available using the path
|
|---|
| 360 | provided as third parameter, if yes add the path to sys.path to be
|
|---|
| 361 | importable, otherwise it will check on GRASS GIS standard paths.
|
|---|
| 362 |
|
|---|
| 363 | """
|
|---|
| 364 | import sys
|
|---|
| 365 | # TODO: why dirname is checked first - the logic should be revised
|
|---|
| 366 | pathlib = None
|
|---|
| 367 | if dirname:
|
|---|
| 368 | pathlib = os.path.join(path, dirname)
|
|---|
| 369 | if pathlib and os.path.exists(pathlib):
|
|---|
| 370 | # we are running the script from the script directory, therefore
|
|---|
| 371 | # we add the path to sys.path to reach the directory (dirname)
|
|---|
| 372 | sys.path.append(os.path.abspath(path))
|
|---|
| 373 | else:
|
|---|
| 374 | # running from GRASS GIS session
|
|---|
| 375 | path = get_lib_path(modulename, dirname)
|
|---|
| 376 | if path is None:
|
|---|
| 377 | pathname = os.path.join(modulename, dirname) if dirname else modulename
|
|---|
| 378 | raise ImportError("Not able to find the path '%s' directory "
|
|---|
| 379 | "(current dir '%s')." % (pathname, os.getcwd()))
|
|---|
| 380 |
|
|---|
| 381 | sys.path.insert(0, path)
|
|---|