Package Gnumed :: Package pycommon :: Module gmTools
[frames] | no frames]

Source Code for Module Gnumed.pycommon.gmTools

   1  # -*- coding: utf-8 -*- 
   2   
   3   
   4   
   5  __doc__ = """GNUmed general tools.""" 
   6   
   7  #=========================================================================== 
   8  __author__ = "K. Hilbert <Karsten.Hilbert@gmx.net>" 
   9  __license__ = "GPL v2 or later (details at http://www.gnu.org)" 
  10   
  11  # std libs 
  12  import sys 
  13  import os 
  14  import os.path 
  15  import csv 
  16  import tempfile 
  17  import logging 
  18  import hashlib 
  19  import platform 
  20  import subprocess 
  21  import decimal 
  22  import getpass 
  23  import io 
  24  import functools 
  25  import json 
  26  import shutil 
  27  import zipfile 
  28  import datetime as pydt 
  29  import re as regex 
  30  import xml.sax.saxutils as xml_tools 
  31  # old: 
  32  import pickle, zlib 
  33   
  34   
  35  # GNUmed libs 
  36  if __name__ == '__main__': 
  37          sys.path.insert(0, '../../') 
  38   
  39   
  40  from Gnumed.pycommon import gmBorg 
  41   
  42   
  43  _log = logging.getLogger('gm.tools') 
  44   
  45  # CAPitalization modes: 
  46  (       CAPS_NONE,                                      # don't touch it 
  47          CAPS_FIRST,                                     # CAP first char, leave rest as is 
  48          CAPS_ALLCAPS,                           # CAP all chars 
  49          CAPS_WORDS,                                     # CAP first char of every word 
  50          CAPS_NAMES,                                     # CAP in a way suitable for names (tries to be smart) 
  51          CAPS_FIRST_ONLY                         # CAP first char, lowercase the rest 
  52  ) = range(6) 
  53   
  54   
  55  u_currency_pound = '\u00A3'                             # Pound sign 
  56  u_currency_sign = '\u00A4'                                      # generic currency sign 
  57  u_currency_yen = '\u00A5'                                       # Yen sign 
  58  u_right_double_angle_quote = '\u00AB'           # << 
  59  u_registered_trademark = '\u00AE' 
  60  u_plus_minus = '\u00B1' 
  61  u_superscript_one = '\u00B9'                            # ^1 
  62  u_left_double_angle_quote = '\u00BB'            # >> 
  63  u_one_quarter = '\u00BC' 
  64  u_one_half = '\u00BD' 
  65  u_three_quarters = '\u00BE' 
  66  u_multiply = '\u00D7'                                           # x 
  67  u_greek_ALPHA = '\u0391' 
  68  u_greek_alpha = '\u03b1' 
  69  u_greek_OMEGA = '\u03A9' 
  70  u_greek_omega = '\u03c9' 
  71  u_dagger = '\u2020' 
  72  u_triangular_bullet = '\u2023'                                  # triangular bullet  (>) 
  73  u_ellipsis = '\u2026'                                                   # ... 
  74  u_euro = '\u20AC'                                                               # EURO sign 
  75  u_numero = '\u2116'                                                             # No. / # sign 
  76  u_down_left_arrow = '\u21B5'                                    # <-' 
  77  u_left_arrow = '\u2190'                                                 # <-- 
  78  u_up_arrow = '\u2191' 
  79  u_arrow2right = '\u2192'                                                # --> 
  80  u_down_arrow = '\u2193' 
  81  u_left_arrow_with_tail = '\u21a2'                               # <--< 
  82  u_arrow2right_from_bar = '\u21a6'                               # |-> 
  83  u_arrow2right_until_vertical_bar = '\u21e5'             # -->| 
  84  u_sum = '\u2211'                                                                # sigma 
  85  u_almost_equal_to = '\u2248'                                    # approximately / nearly / roughly 
  86  u_corresponds_to = '\u2258' 
  87  u_infinity = '\u221E' 
  88  u_arrow2right_until_vertical_bar2 = '\u2b72'    # -->| 
  89  u_diameter = '\u2300' 
  90  u_checkmark_crossed_out = '\u237B' 
  91  u_box_vert_left = '\u23b8' 
  92  u_box_vert_right = '\u23b9' 
  93  u_box_horiz_single = '\u2500'                           # - 
  94  u_box_vert_light = '\u2502' 
  95  u_box_horiz_light_3dashes = '\u2504'            # ... 
  96  u_box_vert_light_4dashes = '\u2506' 
  97  u_box_horiz_4dashes = '\u2508'                          # .... 
  98  u_box_T_right = '\u251c'                                        # |- 
  99  u_box_T_left = '\u2524'                                         # -| 
 100  u_box_T_down = '\u252c' 
 101  u_box_T_up = '\u2534' 
 102  u_box_plus = '\u253c' 
 103  u_box_top_double = '\u2550' 
 104  u_box_top_left_double_single = '\u2552' 
 105  u_box_top_right_double_single = '\u2555' 
 106  u_box_top_left_arc = '\u256d' 
 107  u_box_top_right_arc = '\u256e' 
 108  u_box_bottom_right_arc = '\u256f' 
 109  u_box_bottom_left_arc = '\u2570' 
 110  u_box_horiz_light_heavy = '\u257c' 
 111  u_box_horiz_heavy_light = '\u257e' 
 112  u_skull_and_crossbones = '\u2620' 
 113  u_caduceus = '\u2624' 
 114  u_frowning_face = '\u2639' 
 115  u_smiling_face = '\u263a' 
 116  u_black_heart = '\u2665' 
 117  u_female = '\u2640' 
 118  u_male = '\u2642' 
 119  u_male_female = '\u26a5' 
 120  u_checkmark_thin = '\u2713' 
 121  u_checkmark_thick = '\u2714' 
 122  u_heavy_greek_cross = '\u271a' 
 123  u_arrow2right_thick = '\u2794' 
 124  u_writing_hand = '\u270d' 
 125  u_pencil_1 = '\u270e' 
 126  u_pencil_2 = '\u270f' 
 127  u_pencil_3 = '\u2710' 
 128  u_latin_cross = '\u271d' 
 129  u_arrow2right_until_black_diamond = '\u291e'    # ->* 
 130  u_kanji_yen = '\u5186'                                                  # Yen kanji 
 131  u_replacement_character = '\ufffd' 
 132  u_link_symbol = '\u1f517' 
 133   
 134  _kB = 1024 
 135  _MB = 1024 * _kB 
 136  _GB = 1024 * _MB 
 137  _TB = 1024 * _GB 
 138  _PB = 1024 * _TB 
 139   
 140  #=========================================================================== 
141 -def handle_uncaught_exception_console(t, v, tb):
142 143 print(".========================================================") 144 print("| Unhandled exception caught !") 145 print("| Type :", t) 146 print("| Value:", v) 147 print("`========================================================") 148 _log.critical('unhandled exception caught', exc_info = (t,v,tb)) 149 sys.__excepthook__(t,v,tb)
150 151 #=========================================================================== 152 # path level operations 153 #---------------------------------------------------------------------------
154 -def mkdir(directory=None, mode=None):
155 try: 156 if mode is None: 157 os.makedirs(directory) 158 else: 159 old_umask = os.umask(0) 160 os.makedirs(directory, mode) 161 os.umask(old_umask) 162 except OSError as e: 163 if (e.errno == 17) and not os.path.isdir(directory): 164 raise 165 return True
166 167 #---------------------------------------------------------------------------
168 -def rmdir(directory):
169 #------------------------------- 170 def _on_rm_error(func, path, exc): 171 _log.error('error while shutil.rmtree(%s)', path, exc_info=exc) 172 return True
173 #------------------------------- 174 error_count = 0 175 try: 176 shutil.rmtree(directory, False, _on_rm_error) 177 except Exception: 178 _log.exception('cannot shutil.rmtree(%s)', directory) 179 error_count += 1 180 return error_count 181 182 #---------------------------------------------------------------------------
183 -def rm_dir_content(directory):
184 _log.debug('cleaning out [%s]', directory) 185 try: 186 items = os.listdir(directory) 187 except OSError: 188 return False 189 for item in items: 190 # attempt file/link removal and ignore (but log) errors 191 full_item = os.path.join(directory, item) 192 try: 193 os.remove(full_item) 194 except OSError: # as per the docs, this is a directory 195 _log.debug('[%s] seems to be a subdirectory', full_item) 196 errors = rmdir(full_item) 197 if errors > 0: 198 return False 199 except Exception: 200 _log.exception('cannot os.remove(%s) [a file or a link]', full_item) 201 return False 202 203 return True
204 205 #---------------------------------------------------------------------------
206 -def mk_sandbox_dir(prefix=None, base_dir=None):
207 if prefix is None: 208 if base_dir is None: 209 prefix = 'sandbox-' 210 else: 211 prefix = 'gm_sandbox-' 212 return tempfile.mkdtemp ( 213 prefix = prefix, 214 suffix = '', 215 dir = base_dir 216 )
217 218 #---------------------------------------------------------------------------
219 -def parent_dir(directory):
220 return os.path.abspath(os.path.join(directory, '..'))
221 222 #---------------------------------------------------------------------------
223 -def dirname_stem(directory):
224 # /home/user/dir/ -> dir 225 # /home/user/dir -> dir 226 return os.path.basename(os.path.normpath(directory)) # normpath removes trailing slashes if any
227 228 #---------------------------------------------------------------------------
229 -def dir_is_empty(directory=None):
230 try: 231 return len(os.listdir(directory)) == 0 232 except OSError as exc: 233 if exc.errno == 2: 234 return None 235 raise
236 237 #---------------------------------------------------------------------------
238 -class gmPaths(gmBorg.cBorg):
239 """This class provides the following paths: 240 241 .home_dir user home 242 .local_base_dir script installation dir 243 .working_dir current dir 244 .user_config_dir 245 .system_config_dir 246 .system_app_data_dir (not writable) 247 .tmp_dir instance-local 248 .user_tmp_dir user-local (NOT per instance) 249 """
250 - def __init__(self, app_name=None, wx=None):
251 """Setup pathes. 252 253 <app_name> will default to (name of the script - .py) 254 """ 255 try: 256 self.already_inited 257 return 258 except AttributeError: 259 pass 260 261 self.init_paths(app_name=app_name, wx=wx) 262 self.already_inited = True
263 #-------------------------------------- 264 # public API 265 #--------------------------------------
266 - def init_paths(self, app_name=None, wx=None):
267 268 if wx is None: 269 _log.debug('wxPython not available') 270 _log.debug('detecting paths directly') 271 272 if app_name is None: 273 app_name, ext = os.path.splitext(os.path.basename(sys.argv[0])) 274 _log.info('app name detected as [%s]', app_name) 275 else: 276 _log.info('app name passed in as [%s]', app_name) 277 278 # the user home, doesn't work in Wine so work around that 279 self.__home_dir = None 280 281 # where the main script (the "binary") is installed 282 if getattr(sys, 'frozen', False): 283 _log.info('frozen app, installed into temporary path') 284 # this would find the path of *THIS* file 285 #self.local_base_dir = os.path.dirname(__file__) 286 # while this is documented on the web, the ${_MEIPASS2} does not exist 287 #self.local_base_dir = os.environ.get('_MEIPASS2') 288 # this is what Martin Zibricky <mzibr.public@gmail.com> told us to use 289 # when asking about this on pyinstaller@googlegroups.com 290 #self.local_base_dir = sys._MEIPASS 291 # however, we are --onedir, so we should look at sys.executable 292 # as per the pyinstaller manual 293 self.local_base_dir = os.path.dirname(sys.executable) 294 else: 295 self.local_base_dir = os.path.abspath(os.path.dirname(sys.argv[0])) 296 297 # the current working dir at the OS 298 self.working_dir = os.path.abspath(os.curdir) 299 300 # user-specific config dir, usually below the home dir 301 mkdir(os.path.join(self.home_dir, '.%s' % app_name)) 302 self.user_config_dir = os.path.join(self.home_dir, '.%s' % app_name) 303 304 # system-wide config dir, usually below /etc/ under UN*X 305 try: 306 self.system_config_dir = os.path.join('/etc', app_name) 307 except ValueError: 308 #self.system_config_dir = self.local_base_dir 309 self.system_config_dir = self.user_config_dir 310 311 # system-wide application data dir 312 try: 313 self.system_app_data_dir = os.path.join(sys.prefix, 'share', app_name) 314 except ValueError: 315 self.system_app_data_dir = self.local_base_dir 316 317 # temporary directory 318 try: 319 self.__tmp_dir_already_set 320 _log.debug('temp dir already set') 321 except AttributeError: 322 _log.info('initial (user level) temp dir: %s', tempfile.gettempdir()) 323 # $TMP/gnumed-$USER/ 324 self.user_tmp_dir = os.path.join(tempfile.gettempdir(), app_name + r'-' + getpass.getuser()) 325 mkdir(self.user_tmp_dir, 0o700) 326 tempfile.tempdir = self.user_tmp_dir 327 _log.info('intermediate (app level) temp dir: %s', tempfile.gettempdir()) 328 # $TMP/gnumed-$USER/g$UNIQUE/ 329 self.tmp_dir = tempfile.mkdtemp(prefix = r'g') 330 _log.info('final (app instance level) temp dir: %s', tempfile.gettempdir()) 331 332 self.__log_paths() 333 if wx is None: 334 return True 335 336 # retry with wxPython 337 _log.debug('re-detecting paths with wxPython') 338 339 std_paths = wx.StandardPaths.Get() 340 _log.info('wxPython app name is [%s]', wx.GetApp().GetAppName()) 341 342 # user-specific config dir, usually below the home dir 343 mkdir(os.path.join(std_paths.GetUserConfigDir(), '.%s' % app_name)) 344 self.user_config_dir = os.path.join(std_paths.GetUserConfigDir(), '.%s' % app_name) 345 346 # system-wide config dir, usually below /etc/ under UN*X 347 try: 348 tmp = std_paths.GetConfigDir() 349 if not tmp.endswith(app_name): 350 tmp = os.path.join(tmp, app_name) 351 self.system_config_dir = tmp 352 except ValueError: 353 # leave it at what it was from direct detection 354 pass 355 356 # system-wide application data dir 357 # Robin attests that the following doesn't always 358 # give sane values on Windows, so IFDEF it 359 if 'wxMSW' in wx.PlatformInfo: 360 _log.warning('this platform (wxMSW) sometimes returns a broken value for the system-wide application data dir') 361 else: 362 try: 363 self.system_app_data_dir = std_paths.GetDataDir() 364 except ValueError: 365 pass 366 367 self.__log_paths() 368 return True
369 #--------------------------------------
370 - def __log_paths(self):
371 _log.debug('sys.argv[0]: %s', sys.argv[0]) 372 _log.debug('sys.executable: %s', sys.executable) 373 _log.debug('sys._MEIPASS: %s', getattr(sys, '_MEIPASS', '<not found>')) 374 _log.debug('os.environ["_MEIPASS2"]: %s', os.environ.get('_MEIPASS2', '<not found>')) 375 _log.debug('__file__ : %s', __file__) 376 _log.debug('local application base dir: %s', self.local_base_dir) 377 _log.debug('current working dir: %s', self.working_dir) 378 _log.debug('user home dir: %s', self.home_dir) 379 _log.debug('user-specific config dir: %s', self.user_config_dir) 380 _log.debug('system-wide config dir: %s', self.system_config_dir) 381 _log.debug('system-wide application data dir: %s', self.system_app_data_dir) 382 _log.debug('temporary dir (user): %s', self.user_tmp_dir) 383 _log.debug('temporary dir (instance): %s', self.tmp_dir)
384 385 #-------------------------------------- 386 # properties 387 #--------------------------------------
388 - def _set_user_config_dir(self, path):
389 if not (os.access(path, os.R_OK) and os.access(path, os.X_OK)): 390 msg = '[%s:user_config_dir]: invalid path [%s]' % (self.__class__.__name__, path) 391 _log.error(msg) 392 raise ValueError(msg) 393 self.__user_config_dir = path
394
395 - def _get_user_config_dir(self):
396 return self.__user_config_dir
397 398 user_config_dir = property(_get_user_config_dir, _set_user_config_dir) 399 #--------------------------------------
400 - def _set_system_config_dir(self, path):
401 if not (os.access(path, os.R_OK) and os.access(path, os.X_OK)): 402 msg = '[%s:system_config_dir]: invalid path [%s]' % (self.__class__.__name__, path) 403 _log.error(msg) 404 raise ValueError(msg) 405 self.__system_config_dir = path
406
407 - def _get_system_config_dir(self):
408 return self.__system_config_dir
409 410 system_config_dir = property(_get_system_config_dir, _set_system_config_dir) 411 #--------------------------------------
412 - def _set_system_app_data_dir(self, path):
413 if not (os.access(path, os.R_OK) and os.access(path, os.X_OK)): 414 msg = '[%s:system_app_data_dir]: invalid path [%s]' % (self.__class__.__name__, path) 415 _log.error(msg) 416 raise ValueError(msg) 417 self.__system_app_data_dir = path
418
419 - def _get_system_app_data_dir(self):
420 return self.__system_app_data_dir
421 422 system_app_data_dir = property(_get_system_app_data_dir, _set_system_app_data_dir) 423 #--------------------------------------
424 - def _set_home_dir(self, path):
425 raise ValueError('invalid to set home dir')
426
427 - def _get_home_dir(self):
428 if self.__home_dir is not None: 429 return self.__home_dir 430 431 tmp = os.path.expanduser('~') 432 if tmp == '~': 433 _log.error('this platform does not expand ~ properly') 434 try: 435 tmp = os.environ['USERPROFILE'] 436 except KeyError: 437 _log.error('cannot access $USERPROFILE in environment') 438 439 if not ( 440 os.access(tmp, os.R_OK) 441 and 442 os.access(tmp, os.X_OK) 443 and 444 os.access(tmp, os.W_OK) 445 ): 446 msg = '[%s:home_dir]: invalid path [%s]' % (self.__class__.__name__, tmp) 447 _log.error(msg) 448 raise ValueError(msg) 449 450 self.__home_dir = tmp 451 return self.__home_dir
452 453 home_dir = property(_get_home_dir, _set_home_dir) 454 #--------------------------------------
455 - def _set_tmp_dir(self, path):
456 if not (os.access(path, os.R_OK) and os.access(path, os.X_OK)): 457 msg = '[%s:tmp_dir]: invalid path [%s]' % (self.__class__.__name__, path) 458 _log.error(msg) 459 raise ValueError(msg) 460 _log.debug('previous temp dir: %s', tempfile.gettempdir()) 461 self.__tmp_dir = path 462 tempfile.tempdir = self.__tmp_dir 463 _log.debug('new temp dir: %s', tempfile.gettempdir()) 464 self.__tmp_dir_already_set = True
465
466 - def _get_tmp_dir(self):
467 return self.__tmp_dir
468 469 tmp_dir = property(_get_tmp_dir, _set_tmp_dir)
470 471 #=========================================================================== 472 # file related tools 473 #---------------------------------------------------------------------------
474 -def recode_file(source_file=None, target_file=None, source_encoding='utf8', target_encoding=None, base_dir=None, error_mode='replace'):
475 if target_encoding is None: 476 return source_file 477 if target_encoding == source_encoding: 478 return source_file 479 if target_file is None: 480 target_file = get_unique_filename ( 481 prefix = '%s-%s_%s-' % (fname_stem(source_file), source_encoding, target_encoding), 482 suffix = fname_extension(source_file, '.txt'), 483 tmp_dir = base_dir 484 ) 485 486 _log.debug('[%s] -> [%s] (%s -> %s)', source_encoding, target_encoding, source_file, target_file) 487 488 in_file = io.open(source_file, mode = 'rt', encoding = source_encoding) 489 out_file = io.open(target_file, mode = 'wt', encoding = target_encoding, errors = error_mode) 490 for line in in_file: 491 out_file.write(line) 492 out_file.close() 493 in_file.close() 494 495 return target_file
496 497 #---------------------------------------------------------------------------
498 -def unzip_archive(archive_name, target_dir=None, remove_archive=False):
499 _log.debug('unzipping [%s] -> [%s]', archive_name, target_dir) 500 success = False 501 try: 502 with zipfile.ZipFile(archive_name) as archive: 503 archive.extractall(target_dir) 504 success = True 505 except Exception: 506 _log.exception('cannot unzip') 507 return False 508 if remove_archive: 509 remove_file(archive_name) 510 return success
511 512 #---------------------------------------------------------------------------
513 -def remove_file(filename, log_error=True):
514 # attempt file remove and ignore (but log) errors 515 try: 516 os.remove(filename) 517 except Exception: 518 if log_error: 519 _log.exception('cannot os.remove(%s)', filename) 520 return False 521 522 return True
523 524 #---------------------------------------------------------------------------
525 -def gpg_decrypt_file(filename=None, passphrase=None):
526 527 if platform.system() == 'Windows': 528 exec_name = 'gpg.exe' 529 else: 530 exec_name = 'gpg' 531 532 tmp, fname = os.path.split(filename) 533 basename, tmp = os.path.splitext(fname) 534 filename_decrypted = get_unique_filename(prefix = '%s-decrypted-' % basename) 535 536 args = [exec_name, '--verbose', '--batch', '--yes', '--passphrase-fd', '0', '--output', filename_decrypted, '--decrypt', filename] 537 _log.debug('GnuPG args: %s' % str(args)) 538 539 try: 540 gpg = subprocess.Popen ( 541 args = args, 542 stdin = subprocess.PIPE, 543 stdout = subprocess.PIPE, 544 stderr = subprocess.PIPE, 545 close_fds = False 546 ) 547 except (OSError, ValueError, subprocess.CalledProcessError): 548 _log.exception('there was a problem executing gpg') 549 gmDispatcher.send(signal = 'statustext', msg = _('Error running GnuPG. Cannot decrypt data.'), beep = True) 550 return 551 552 out, error = gpg.communicate(passphrase) 553 _log.debug('gpg returned [%s]', gpg.returncode) 554 if gpg.returncode != 0: 555 _log.debug('GnuPG STDOUT:\n%s', out) 556 _log.debug('GnuPG STDERR:\n%s', error) 557 return None 558 559 return filename_decrypted
560 561 #---------------------------------------------------------------------------
562 -def file2md5(filename=None, return_hex=True):
563 blocksize = 2**10 * 128 # 128k, since md5 uses 128 byte blocks 564 _log.debug('md5(%s): <%s> byte blocks', filename, blocksize) 565 566 f = io.open(filename, mode = 'rb') 567 568 md5 = hashlib.md5() 569 while True: 570 data = f.read(blocksize) 571 if not data: 572 break 573 md5.update(data) 574 f.close() 575 576 _log.debug('md5(%s): %s', filename, md5.hexdigest()) 577 578 if return_hex: 579 return md5.hexdigest() 580 return md5.digest()
581 582 #---------------------------------------------------------------------------
583 -def file2chunked_md5(filename=None, chunk_size=500*_MB):
584 _log.debug('chunked_md5(%s, chunk_size=%s bytes)', filename, chunk_size) 585 md5_concat = '' 586 f = open(filename, 'rb') 587 while True: 588 md5 = hashlib.md5() 589 data = f.read(chunk_size) 590 if not data: 591 break 592 md5.update(data) 593 md5_concat += md5.hexdigest() 594 f.close() 595 md5 = hashlib.md5() 596 md5.update(md5_concat) 597 hex_digest = md5.hexdigest() 598 _log.debug('md5("%s"): %s', md5_concat, hex_digest) 599 return hex_digest
600 601 #---------------------------------------------------------------------------
602 -def unicode2charset_encoder(unicode_csv_data, encoding='utf-8'):
603 for line in unicode_csv_data: 604 yield line.encode(encoding)
605 606 #def utf_8_encoder(unicode_csv_data): 607 # for line in unicode_csv_data: 608 # yield line.encode('utf-8') 609 610 default_csv_reader_rest_key = 'list_of_values_of_unknown_fields' 611
612 -def unicode_csv_reader(unicode_csv_data, dialect=csv.excel, encoding='utf-8', **kwargs):
613 614 # csv.py doesn't do Unicode; encode temporarily as UTF-8: 615 try: 616 is_dict_reader = kwargs['dict'] 617 del kwargs['dict'] 618 if is_dict_reader is not True: 619 raise KeyError 620 kwargs['restkey'] = default_csv_reader_rest_key 621 csv_reader = csv.DictReader(unicode2charset_encoder(unicode_csv_data), dialect=dialect, **kwargs) 622 except KeyError: 623 is_dict_reader = False 624 csv_reader = csv.reader(unicode2charset_encoder(unicode_csv_data), dialect=dialect, **kwargs) 625 626 for row in csv_reader: 627 # decode ENCODING back to Unicode, cell by cell: 628 if is_dict_reader: 629 for key in row.keys(): 630 if key == default_csv_reader_rest_key: 631 old_data = row[key] 632 new_data = [] 633 for val in old_data: 634 new_data.append(str(val, encoding)) 635 row[key] = new_data 636 if default_csv_reader_rest_key not in csv_reader.fieldnames: 637 csv_reader.fieldnames.append(default_csv_reader_rest_key) 638 else: 639 row[key] = str(row[key], encoding) 640 yield row 641 else: 642 yield [ str(cell, encoding) for cell in row ]
643 #yield [str(cell, 'utf-8') for cell in row] 644 645 #---------------------------------------------------------------------------
646 -def fname_sanitize(filename):
647 """Normalizes unicode, removes non-alpha characters, converts spaces to underscores.""" 648 649 dir_part, name_part = os.path.split(filename) 650 if name_part == '': 651 return filename 652 653 import unicodedata 654 name_part = unicodedata.normalize('NFKD', name_part) 655 # remove everything not in group [] 656 name_part = regex.sub ( 657 '[^.\w\s[\]()%§+-]', 658 '', 659 name_part, 660 flags = regex.UNICODE 661 ).strip() 662 # translate whitespace to underscore 663 name_part = regex.sub ( 664 '\s+', 665 '_', 666 name_part, 667 flags = regex.UNICODE 668 ) 669 return os.path.join(dir_part, name_part)
670 671 #---------------------------------------------------------------------------
672 -def fname_stem(filename):
673 """/home/user/dir/filename.ext -> filename""" 674 return os.path.splitext(os.path.basename(filename))[0]
675 676 #---------------------------------------------------------------------------
677 -def fname_stem_with_path(filename):
678 """/home/user/dir/filename.ext -> /home/user/dir/filename""" 679 return os.path.splitext(filename)[0]
680 681 #---------------------------------------------------------------------------
682 -def fname_extension(filename=None, fallback=None):
683 """ /home/user/dir/filename.ext -> .ext 684 '' or '.' -> fallback if any else '' 685 """ 686 ext = os.path.splitext(filename)[1] 687 if ext.strip() not in ['.', '']: 688 return ext 689 if fallback is None: 690 return '' 691 return fallback
692 693 #---------------------------------------------------------------------------
694 -def fname_dir(filename):
695 # /home/user/dir/filename.ext -> /home/user/dir 696 return os.path.split(filename)[0]
697 698 #---------------------------------------------------------------------------
699 -def fname_from_path(filename):
700 # /home/user/dir/filename.ext -> filename.ext 701 return os.path.split(filename)[1]
702 703 #---------------------------------------------------------------------------
704 -def get_unique_filename(prefix=None, suffix=None, tmp_dir=None, include_timestamp=False):
705 """This introduces a race condition between the file.close() and 706 actually using the filename. 707 708 The file will NOT exist after calling this function. 709 """ 710 if tmp_dir is not None: 711 if ( 712 not os.access(tmp_dir, os.F_OK) 713 or 714 not os.access(tmp_dir, os.X_OK | os.W_OK) 715 ): 716 _log.warning('cannot os.access() temporary dir [%s], using system default', tmp_dir) 717 tmp_dir = None 718 719 if include_timestamp: 720 ts = pydt.datetime.now().strftime('%m%d-%H%M%S-') 721 else: 722 ts = '' 723 724 kwargs = { 725 'dir': tmp_dir, 726 # make sure file gets deleted as soon as 727 # .close()d so we can "safely" open it again 728 'delete': True 729 } 730 731 if prefix is None: 732 kwargs['prefix'] = 'gmd-%s' % ts 733 else: 734 kwargs['prefix'] = prefix + ts 735 736 if suffix in [None, '']: 737 kwargs['suffix'] = '.tmp' 738 else: 739 if not suffix.startswith('.'): 740 suffix = '.' + suffix 741 kwargs['suffix'] = suffix 742 743 f = tempfile.NamedTemporaryFile(**kwargs) 744 filename = f.name 745 f.close() 746 747 return filename
748 749 #--------------------------------------------------------------------------- 766 767 #--------------------------------------------------------------------------- 789 790 #===========================================================================
791 -def import_module_from_directory(module_path=None, module_name=None, always_remove_path=False):
792 """Import a module from any location.""" 793 794 _log.debug('CWD: %s', os.getcwd()) 795 796 remove_path = always_remove_path or False 797 if module_path not in sys.path: 798 _log.info('appending to sys.path: [%s]' % module_path) 799 sys.path.append(module_path) 800 remove_path = True 801 802 _log.debug('will remove import path: %s', remove_path) 803 804 if module_name.endswith('.py'): 805 module_name = module_name[:-3] 806 807 try: 808 module = __import__(module_name) 809 except Exception: 810 _log.exception('cannot __import__() module [%s] from [%s]' % (module_name, module_path)) 811 while module_path in sys.path: 812 sys.path.remove(module_path) 813 raise 814 815 _log.info('imported module [%s] as [%s]' % (module_name, module)) 816 if remove_path: 817 while module_path in sys.path: 818 sys.path.remove(module_path) 819 820 return module
821 822 #=========================================================================== 823 # text related tools 824 #---------------------------------------------------------------------------
825 -def size2str(size=0, template='%s'):
826 if size == 1: 827 return template % _('1 Byte') 828 if size < 10 * _kB: 829 return template % _('%s Bytes') % size 830 if size < _MB: 831 return template % '%.1f kB' % (float(size) / _kB) 832 if size < _GB: 833 return template % '%.1f MB' % (float(size) / _MB) 834 if size < _TB: 835 return template % '%.1f GB' % (float(size) / _GB) 836 if size < _PB: 837 return template % '%.1f TB' % (float(size) / _TB) 838 return template % '%.1f PB' % (float(size) / _PB)
839 840 #---------------------------------------------------------------------------
841 -def bool2subst(boolean=None, true_return=True, false_return=False, none_return=None):
842 if boolean is None: 843 return none_return 844 if boolean: 845 return true_return 846 if not boolean: 847 return false_return 848 raise ValueError('bool2subst(): <boolean> arg must be either of True, False, None')
849 850 #---------------------------------------------------------------------------
851 -def bool2str(boolean=None, true_str='True', false_str='False'):
852 return bool2subst ( 853 boolean = bool(boolean), 854 true_return = true_str, 855 false_return = false_str 856 )
857 858 #---------------------------------------------------------------------------
859 -def none_if(value=None, none_equivalent=None, strip_string=False):
860 """Modelled after the SQL NULLIF function.""" 861 if value is None: 862 return None 863 if strip_string: 864 stripped = value.strip() 865 else: 866 stripped = value 867 if stripped == none_equivalent: 868 return None 869 return value
870 871 #---------------------------------------------------------------------------
872 -def coalesce(initial=None, instead=None, template_initial=None, template_instead=None, none_equivalents=None, function_initial=None):
873 """Modelled after the SQL coalesce function. 874 875 To be used to simplify constructs like: 876 877 if initial is None (or in none_equivalents): 878 real_value = (template_instead % instead) or instead 879 else: 880 real_value = (template_initial % initial) or initial 881 print real_value 882 883 @param initial: the value to be tested for <None> 884 @type initial: any Python type, must have a __str__ method if template_initial is not None 885 @param instead: the value to be returned if <initial> is None 886 @type instead: any Python type, must have a __str__ method if template_instead is not None 887 @param template_initial: if <initial> is returned replace the value into this template, must contain one <%s> 888 @type template_initial: string or None 889 @param template_instead: if <instead> is returned replace the value into this template, must contain one <%s> 890 @type template_instead: string or None 891 892 example: 893 function_initial = ('strftime', '%Y-%m-%d') 894 895 Ideas: 896 - list of insteads: initial, [instead, template], [instead, template], [instead, template], template_initial, ... 897 """ 898 if none_equivalents is None: 899 none_equivalents = [None] 900 901 if initial in none_equivalents: 902 903 if template_instead is None: 904 return instead 905 906 return template_instead % instead 907 908 if function_initial is not None: 909 funcname, args = function_initial 910 func = getattr(initial, funcname) 911 initial = func(args) 912 913 if template_initial is None: 914 return initial 915 916 try: 917 return template_initial % initial 918 except TypeError: 919 return template_initial
920 921 #---------------------------------------------------------------------------
922 -def __cap_name(match_obj=None):
923 val = match_obj.group(0).lower() 924 if val in ['von', 'van', 'de', 'la', 'l', 'der', 'den']: # FIXME: this needs to expand, configurable ? 925 return val 926 buf = list(val) 927 buf[0] = buf[0].upper() 928 for part in ['mac', 'mc', 'de', 'la']: 929 if len(val) > len(part) and val[:len(part)] == part: 930 buf[len(part)] = buf[len(part)].upper() 931 return ''.join(buf)
932 933 #---------------------------------------------------------------------------
934 -def capitalize(text=None, mode=CAPS_NAMES):
935 """Capitalize the first character but leave the rest alone. 936 937 Note that we must be careful about the locale, this may 938 have issues ! However, for UTF strings it should just work. 939 """ 940 if (mode is None) or (mode == CAPS_NONE): 941 return text 942 943 if len(text) == 0: 944 return text 945 946 if mode == CAPS_FIRST: 947 if len(text) == 1: 948 return text[0].upper() 949 return text[0].upper() + text[1:] 950 951 if mode == CAPS_ALLCAPS: 952 return text.upper() 953 954 if mode == CAPS_FIRST_ONLY: 955 # if len(text) == 1: 956 # return text[0].upper() 957 return text[0].upper() + text[1:].lower() 958 959 if mode == CAPS_WORDS: 960 #return regex.sub(ur'(\w)(\w+)', lambda x: x.group(1).upper() + x.group(2).lower(), text) 961 return regex.sub(r'(\w)(\w+)', lambda x: x.group(1).upper() + x.group(2).lower(), text) 962 963 if mode == CAPS_NAMES: 964 #return regex.sub(r'\w+', __cap_name, text) 965 return capitalize(text=text, mode=CAPS_FIRST) # until fixed 966 967 print("ERROR: invalid capitalization mode: [%s], leaving input as is" % mode) 968 return text
969 970 #---------------------------------------------------------------------------
971 -def input2decimal(initial=None):
972 973 if isinstance(initial, decimal.Decimal): 974 return True, initial 975 976 val = initial 977 978 # float ? -> to string first 979 if type(val) == type(float(1.4)): 980 val = str(val) 981 982 # string ? -> "," to "." 983 if isinstance(val, str): 984 val = val.replace(',', '.', 1) 985 val = val.strip() 986 987 try: 988 d = decimal.Decimal(val) 989 return True, d 990 except (TypeError, decimal.InvalidOperation): 991 return False, val
992 993 #---------------------------------------------------------------------------
994 -def input2int(initial=None, minval=None, maxval=None):
995 996 val = initial 997 998 # string ? -> "," to "." 999 if isinstance(val, str): 1000 val = val.replace(',', '.', 1) 1001 val = val.strip() 1002 1003 try: 1004 int_val = int(val) 1005 except (TypeError, ValueError): 1006 _log.exception('int(%s) failed', val) 1007 return False, initial 1008 1009 if minval is not None: 1010 if int_val < minval: 1011 _log.debug('%s < min (%s)', val, minval) 1012 return False, initial 1013 if maxval is not None: 1014 if int_val > maxval: 1015 _log.debug('%s > max (%s)', val, maxval) 1016 return False, initial 1017 1018 return True, int_val
1019 1020 #---------------------------------------------------------------------------
1021 -def strip_prefix(text, prefix, remove_repeats=False, remove_whitespace=False):
1022 if remove_repeats: 1023 if remove_whitespace: 1024 while text.lstrip().startswith(prefix): 1025 text = text.lstrip().replace(prefix, '', 1).lstrip() 1026 return text 1027 while text.startswith(prefix): 1028 text = text.replace(prefix, '', 1) 1029 return text 1030 if remove_whitespace: 1031 return text.lstrip().replace(prefix, '', 1).lstrip() 1032 return text.replace(prefix, '', 1)
1033 1034 #---------------------------------------------------------------------------
1035 -def strip_suffix(text, suffix, remove_repeats=False, remove_whitespace=False):
1036 suffix_len = len(suffix) 1037 if remove_repeats: 1038 if remove_whitespace: 1039 while text.rstrip().endswith(suffix): 1040 text = text.rstrip()[:-suffix_len].rstrip() 1041 return text 1042 while text.endswith(suffix): 1043 text = text[:-suffix_len] 1044 return text 1045 if remove_whitespace: 1046 return text.rstrip()[:-suffix_len].rstrip() 1047 return text[:-suffix_len]
1048 1049 #---------------------------------------------------------------------------
1050 -def strip_leading_empty_lines(lines=None, text=None, eol='\n', return_list=True):
1051 if lines is None: 1052 lines = text.split(eol) 1053 1054 while True: 1055 if lines[0].strip(eol).strip() != '': 1056 break 1057 lines = lines[1:] 1058 1059 if return_list: 1060 return lines 1061 1062 return eol.join(lines)
1063 1064 #---------------------------------------------------------------------------
1065 -def strip_trailing_empty_lines(lines=None, text=None, eol='\n', return_list=True):
1066 if lines is None: 1067 lines = text.split(eol) 1068 1069 while True: 1070 if lines[-1].strip(eol).strip() != '': 1071 break 1072 lines = lines[:-1] 1073 1074 if return_list: 1075 return lines 1076 1077 return eol.join(lines)
1078 1079 #---------------------------------------------------------------------------
1080 -def strip_empty_lines(lines=None, text=None, eol='\n', return_list=True):
1081 return strip_trailing_empty_lines ( 1082 lines = strip_leading_empty_lines(lines = lines, text = text, eol = eol, return_list = True), 1083 text = None, 1084 eol = eol, 1085 return_list = return_list 1086 )
1087 1088 #---------------------------------------------------------------------------
1089 -def list2text(lines, initial_indent='', subsequent_indent='', eol='\n', strip_leading_empty_lines=True, strip_trailing_empty_lines=True, strip_trailing_whitespace=True):
1090 1091 if len(lines) == 0: 1092 return '' 1093 1094 if strip_leading_empty_lines: 1095 lines = strip_leading_empty_lines(lines = lines, eol = eol, return_list = True) 1096 1097 if strip_trailing_empty_lines: 1098 lines = strip_trailing_empty_lines(lines = lines, eol = eol, return_list = True) 1099 1100 if strip_trailing_whitespace: 1101 lines = [ l.rstrip() for l in lines ] 1102 1103 indented_lines = [initial_indent + lines[0]] 1104 indented_lines.extend([ subsequent_indent + l for l in lines[1:] ]) 1105 1106 return eol.join(indented_lines)
1107 1108 #---------------------------------------------------------------------------
1109 -def wrap(text=None, width=None, initial_indent='', subsequent_indent='', eol='\n'):
1110 """A word-wrap function that preserves existing line breaks 1111 and most spaces in the text. Expects that existing line 1112 breaks are posix newlines (\n). 1113 """ 1114 if width is None: 1115 return text 1116 wrapped = initial_indent + functools.reduce ( 1117 lambda line, word, width=width: '%s%s%s' % ( 1118 line, 1119 ' \n'[(len(line) - line.rfind('\n') - 1 + len(word.split('\n',1)[0]) >= width)], 1120 word 1121 ), 1122 text.split(' ') 1123 ) 1124 1125 if subsequent_indent != '': 1126 wrapped = ('\n%s' % subsequent_indent).join(wrapped.split('\n')) 1127 1128 if eol != '\n': 1129 wrapped = wrapped.replace('\n', eol) 1130 1131 return wrapped
1132 1133 #---------------------------------------------------------------------------
1134 -def unwrap(text=None, max_length=None, strip_whitespace=True, remove_empty_lines=True, line_separator = ' // '):
1135 1136 text = text.replace('\r', '') 1137 lines = text.split('\n') 1138 text = '' 1139 for line in lines: 1140 1141 if strip_whitespace: 1142 line = line.strip().strip('\t').strip() 1143 1144 if remove_empty_lines: 1145 if line == '': 1146 continue 1147 1148 text += ('%s%s' % (line, line_separator)) 1149 1150 text = text.rstrip(line_separator) 1151 1152 if max_length is not None: 1153 text = text[:max_length] 1154 1155 text = text.rstrip(line_separator) 1156 1157 return text
1158 1159 #---------------------------------------------------------------------------
1160 -def shorten_text(text=None, max_length=None):
1161 1162 if len(text) <= max_length: 1163 return text 1164 1165 return text[:max_length-1] + u_ellipsis
1166 1167 #---------------------------------------------------------------------------
1168 -def shorten_words_in_line(text=None, max_length=None, min_word_length=None, ignore_numbers=True, ellipsis=u_ellipsis):
1169 if text is None: 1170 return None 1171 if max_length is None: 1172 max_length = len(text) 1173 else: 1174 if len(text) <= max_length: 1175 return text 1176 old_words = regex.split('\s+', text, flags = regex.UNICODE) 1177 no_old_words = len(old_words) 1178 max_word_length = max(min_word_length, (max_length // no_old_words)) 1179 words = [] 1180 for word in old_words: 1181 if len(word) <= max_word_length: 1182 words.append(word) 1183 continue 1184 if ignore_numbers: 1185 tmp = word.replace('-', '').replace('+', '').replace('.', '').replace(',', '').replace('/', '').replace('&', '').replace('*', '') 1186 if tmp.isdigit(): 1187 words.append(word) 1188 continue 1189 words.append(word[:max_word_length] + ellipsis) 1190 return ' '.join(words)
1191 1192 #---------------------------------------------------------------------------
1193 -def xml_escape_string(text=None):
1194 """check for special XML characters and transform them""" 1195 return xml_tools.escape(text)
1196 1197 #---------------------------------------------------------------------------
1198 -def tex_escape_string(text=None, replace_known_unicode=True, replace_eol=False, keep_visual_eol=False):
1199 """Check for special TeX characters and transform them. 1200 1201 replace_eol: 1202 replaces "\n" with "\\newline" 1203 keep_visual_eol: 1204 replaces "\n" with "\\newline \n" such that 1205 both LaTeX will know to place a line break 1206 at this point as well as the visual formatting 1207 is preserved in the LaTeX source (think multi- 1208 row table cells) 1209 """ 1210 text = text.replace('\\', '\\textbackslash') # requires \usepackage{textcomp} in LaTeX source 1211 text = text.replace('^', '\\textasciicircum') # requires \usepackage{textcomp} in LaTeX source 1212 text = text.replace('~', '\\textasciitilde') # requires \usepackage{textcomp} in LaTeX source 1213 1214 text = text.replace('{', '\\{') 1215 text = text.replace('}', '\\}') 1216 text = text.replace('%', '\\%') 1217 text = text.replace('&', '\\&') 1218 text = text.replace('#', '\\#') 1219 text = text.replace('$', '\\$') 1220 text = text.replace('_', '\\_') 1221 if replace_eol: 1222 if keep_visual_eol: 1223 text = text.replace('\n', '\\newline \n') 1224 else: 1225 text = text.replace('\n', '\\newline ') 1226 1227 if replace_known_unicode: 1228 # this should NOT be replaced for Xe(La)Tex 1229 text = text.replace(u_euro, '\\EUR') 1230 1231 return text
1232 1233 #---------------------------------------------------------------------------
1234 -def xetex_escape_string(text=None):
1235 # a web search did not reveal anything else for Xe(La)Tex 1236 # as opposed to LaTeX, except true unicode chars 1237 return tex_escape_string(text = text, replace_known_unicode = False)
1238 1239 #--------------------------------------------------------------------------- 1240 __html_escape_table = { 1241 "&": "&amp;", 1242 '"': "&quot;", 1243 "'": "&apos;", 1244 ">": "&gt;", 1245 "<": "&lt;", 1246 } 1247
1248 -def html_escape_string(text=None, replace_eol=False, keep_visual_eol=False):
1249 text = ''.join(__html_escape_table.get(char, char) for char in text) 1250 if replace_eol: 1251 if keep_visual_eol: 1252 text = text.replace('\n', '<br>\n') 1253 else: 1254 text = text.replace('\n', '<br>') 1255 return text
1256 1257 #---------------------------------------------------------------------------
1258 -def dict2json(obj):
1259 return json.dumps(obj, default = json_serialize)
1260 1261 #---------------------------------------------------------------------------
1262 -def json_serialize(obj):
1263 if isinstance(obj, pydt.datetime): 1264 return obj.isoformat() 1265 raise TypeError('cannot json_serialize(%s)' % type(obj))
1266 1267 #--------------------------------------------------------------------------- 1268 #---------------------------------------------------------------------------
1269 -def compare_dict_likes(d1, d2, title1=None, title2=None):
1270 _log.info('comparing dict-likes: %s[%s] vs %s[%s]', coalesce(title1, '', '"%s" '), type(d1), coalesce(title2, '', '"%s" '), type(d2)) 1271 try: 1272 d1 = dict(d1) 1273 except TypeError: 1274 pass 1275 try: 1276 d2 = dict(d2) 1277 except TypeError: 1278 pass 1279 keys_d1 = frozenset(d1.keys()) 1280 keys_d2 = frozenset(d2.keys()) 1281 different = False 1282 if len(keys_d1) != len(keys_d2): 1283 _log.info('different number of keys: %s vs %s', len(keys_d1), len(keys_d2)) 1284 different = True 1285 for key in keys_d1: 1286 if key in keys_d2: 1287 if type(d1[key]) != type(d2[key]): 1288 _log.info('%25.25s: type(dict1) = %s = >>>%s<<<' % (key, type(d1[key]), d1[key])) 1289 _log.info('%25.25s type(dict2) = %s = >>>%s<<<' % ('', type(d2[key]), d2[key])) 1290 different = True 1291 continue 1292 if d1[key] == d2[key]: 1293 _log.info('%25.25s: both = >>>%s<<<' % (key, d1[key])) 1294 else: 1295 _log.info('%25.25s: dict1 = >>>%s<<<' % (key, d1[key])) 1296 _log.info('%25.25s dict2 = >>>%s<<<' % ('', d2[key])) 1297 different = True 1298 else: 1299 _log.info('%25.25s: %50.50s | <MISSING>' % (key, '>>>%s<<<' % d1[key])) 1300 different = True 1301 for key in keys_d2: 1302 if key in keys_d1: 1303 continue 1304 _log.info('%25.25s: %50.50s | %.50s' % (key, '<MISSING>', '>>>%s<<<' % d2[key])) 1305 different = True 1306 if different: 1307 _log.info('dict-likes appear to be different from each other') 1308 return False 1309 _log.info('dict-likes appear equal to each other') 1310 return True
1311 1312 #---------------------------------------------------------------------------
1313 -def format_dict_likes_comparison(d1, d2, title_left=None, title_right=None, left_margin=0, key_delim=' || ', data_delim=' | ', missing_string='=/=', difference_indicator='! ', ignore_diff_in_keys=None):
1314 1315 _log.info('comparing dict-likes: %s[%s] vs %s[%s]', coalesce(title_left, '', '"%s" '), type(d1), coalesce(title_right, '', '"%s" '), type(d2)) 1316 append_type = False 1317 if None not in [title_left, title_right]: 1318 append_type = True 1319 type_left = type(d1) 1320 type_right = type(d2) 1321 if title_left is None: 1322 title_left = '%s' % type_left 1323 if title_right is None: 1324 title_right = '%s' % type_right 1325 1326 try: d1 = dict(d1) 1327 except TypeError: pass 1328 try: d2 = dict(d2) 1329 except TypeError: pass 1330 keys_d1 = d1.keys() 1331 keys_d2 = d2.keys() 1332 data = {} 1333 for key in keys_d1: 1334 data[key] = [d1[key], ' '] 1335 if key in d2: 1336 data[key][1] = d2[key] 1337 for key in keys_d2: 1338 if key in keys_d1: 1339 continue 1340 data[key] = [' ', d2[key]] 1341 max1 = max([ len('%s' % k) for k in keys_d1 ]) 1342 max2 = max([ len('%s' % k) for k in keys_d2 ]) 1343 max_len = max(max1, max2, len(_('<type>'))) 1344 max_key_len_str = '%' + '%s.%s' % (max_len, max_len) + 's' 1345 max1 = max([ len('%s' % d1[k]) for k in keys_d1 ]) 1346 max2 = max([ len('%s' % d2[k]) for k in keys_d2 ]) 1347 max_data_len = min(max(max1, max2), 100) 1348 max_data_len_str = '%' + '%s.%s' % (max_data_len, max_data_len) + 's' 1349 diff_indicator_len_str = '%' + '%s.%s' % (len(difference_indicator), len(difference_indicator)) + 's' 1350 line_template = (' ' * left_margin) + diff_indicator_len_str + max_key_len_str + key_delim + max_data_len_str + data_delim + '%s' 1351 1352 lines = [] 1353 # debugging: 1354 #lines.append(u' (40 regular spaces)') 1355 #lines.append((u' ' * 40) + u"(u' ' * 40)") 1356 #lines.append((u'%40.40s' % u'') + u"(u'%40.40s' % u'')") 1357 #lines.append((u'%40.40s' % u' ') + u"(u'%40.40s' % u' ')") 1358 #lines.append((u'%40.40s' % u'.') + u"(u'%40.40s' % u'.')") 1359 #lines.append(line_template) 1360 lines.append(line_template % ('', '', title_left, title_right)) 1361 if append_type: 1362 lines.append(line_template % ('', _('<type>'), type_left, type_right)) 1363 1364 if ignore_diff_in_keys is None: 1365 ignore_diff_in_keys = [] 1366 1367 for key in keys_d1: 1368 append_type = False 1369 txt_left_col = '%s' % d1[key] 1370 try: 1371 txt_right_col = '%s' % d2[key] 1372 if type(d1[key]) != type(d2[key]): 1373 append_type = True 1374 except KeyError: 1375 txt_right_col = missing_string 1376 lines.append(line_template % ( 1377 bool2subst ( 1378 ((txt_left_col == txt_right_col) or (key in ignore_diff_in_keys)), 1379 '', 1380 difference_indicator 1381 ), 1382 key, 1383 shorten_text(txt_left_col, max_data_len), 1384 shorten_text(txt_right_col, max_data_len) 1385 )) 1386 if append_type: 1387 lines.append(line_template % ( 1388 '', 1389 _('<type>'), 1390 shorten_text('%s' % type(d1[key]), max_data_len), 1391 shorten_text('%s' % type(d2[key]), max_data_len) 1392 )) 1393 1394 for key in keys_d2: 1395 if key in keys_d1: 1396 continue 1397 lines.append(line_template % ( 1398 bool2subst((key in ignore_diff_in_keys), '', difference_indicator), 1399 key, 1400 shorten_text(missing_string, max_data_len), 1401 shorten_text('%s' % d2[key], max_data_len) 1402 )) 1403 1404 return lines
1405 1406 #---------------------------------------------------------------------------
1407 -def format_dict_like(d, relevant_keys=None, template=None, missing_key_template='<[%(key)s] MISSING>', left_margin=0, tabular=False, value_delimiters=('>>>', '<<<'), eol='\n'):
1408 if template is not None: 1409 # all keys in template better exist in d 1410 try: 1411 return template % d 1412 except KeyError: 1413 # or else 1414 _log.exception('template contains %%()s key(s) which do not exist in dict') 1415 # try to extend dict <d> to contain all required keys, 1416 # for that to work <relevant_keys> better list all 1417 # keys used in <template> 1418 if relevant_keys is not None: 1419 for key in relevant_keys: 1420 try: 1421 d[key] 1422 except KeyError: 1423 d[key] = missing_key_template % {'key': key} 1424 return template % d 1425 1426 if relevant_keys is None: 1427 relevant_keys = d.keys() 1428 lines = [] 1429 if value_delimiters is None: 1430 delim_left = '' 1431 delim_right = '' 1432 else: 1433 delim_left, delim_right = value_delimiters 1434 if tabular: 1435 max_len = max([ len('%s' % k) for k in relevant_keys ]) 1436 max_len_str = '%s.%s' % (max_len, max_len) 1437 line_template = (' ' * left_margin) + '%' + max_len_str + ('s: %s%%s%s' % (delim_left, delim_right)) 1438 else: 1439 line_template = (' ' * left_margin) + '%%s: %s%%s%s' % (delim_left, delim_right) 1440 for key in relevant_keys: 1441 try: 1442 lines.append(line_template % (key, d[key])) 1443 except KeyError: 1444 pass 1445 if eol is None: 1446 return lines 1447 return eol.join(lines)
1448 1449 #---------------------------------------------------------------------------
1450 -def normalize_dict_like(d, required_keys, missing_key_template='<[%(key)s] MISSING>'):
1451 for key in required_keys: 1452 try: 1453 d[key] 1454 except KeyError: 1455 if missing_key_template is None: 1456 d[key] = None 1457 else: 1458 d[key] = missing_key_template % {'key': key} 1459 return d
1460 1461 #--------------------------------------------------------------------------- 1462 #---------------------------------------------------------------------------
1463 -def prompted_input(prompt=None, default=None):
1464 """Obtains entry from standard input. 1465 1466 prompt: Prompt text to display in standard output 1467 default: Default value (for user to press enter only) 1468 CTRL-C: aborts and returns None 1469 """ 1470 if prompt is None: 1471 msg = '(CTRL-C aborts)' 1472 else: 1473 msg = '%s (CTRL-C aborts)' % prompt 1474 1475 if default is None: 1476 msg = msg + ': ' 1477 else: 1478 msg = '%s [%s]: ' % (msg, default) 1479 1480 try: 1481 usr_input = input(msg) 1482 except KeyboardInterrupt: 1483 return None 1484 1485 if usr_input == '': 1486 return default 1487 1488 return usr_input
1489 1490 #=========================================================================== 1491 # image handling tools 1492 #--------------------------------------------------------------------------- 1493 # builtin (ugly but tried and true) fallback icon 1494 __icon_serpent = \ 1495 """x\xdae\x8f\xb1\x0e\x83 \x10\x86w\x9f\xe2\x92\x1blb\xf2\x07\x96\xeaH:0\xd6\ 1496 \xc1\x85\xd5\x98N5\xa5\xef?\xf5N\xd0\x8a\xdcA\xc2\xf7qw\x84\xdb\xfa\xb5\xcd\ 1497 \xd4\xda;\xc9\x1a\xc8\xb6\xcd<\xb5\xa0\x85\x1e\xeb\xbc\xbc7b!\xf6\xdeHl\x1c\ 1498 \x94\x073\xec<*\xf7\xbe\xf7\x99\x9d\xb21~\xe7.\xf5\x1f\x1c\xd3\xbdVlL\xc2\ 1499 \xcf\xf8ye\xd0\x00\x90\x0etH \x84\x80B\xaa\x8a\x88\x85\xc4(U\x9d$\xfeR;\xc5J\ 1500 \xa6\x01\xbbt9\xceR\xc8\x81e_$\x98\xb9\x9c\xa9\x8d,y\xa9t\xc8\xcf\x152\xe0x\ 1501 \xe9$\xf5\x07\x95\x0cD\x95t:\xb1\x92\xae\x9cI\xa8~\x84\x1f\xe0\xa3ec""" 1502
1503 -def get_icon(wx=None):
1504 1505 paths = gmPaths(app_name = 'gnumed', wx = wx) 1506 1507 candidates = [ 1508 os.path.join(paths.system_app_data_dir, 'bitmaps', 'gm_icon-serpent_and_gnu.png'), 1509 os.path.join(paths.local_base_dir, 'bitmaps', 'gm_icon-serpent_and_gnu.png'), 1510 os.path.join(paths.system_app_data_dir, 'bitmaps', 'serpent.png'), 1511 os.path.join(paths.local_base_dir, 'bitmaps', 'serpent.png') 1512 ] 1513 1514 found_as = None 1515 for candidate in candidates: 1516 try: 1517 open(candidate, 'r').close() 1518 found_as = candidate 1519 break 1520 except IOError: 1521 _log.debug('icon not found in [%s]', candidate) 1522 1523 if found_as is None: 1524 _log.warning('no icon file found, falling back to builtin (ugly) icon') 1525 icon_bmp_data = wx.BitmapFromXPMData(pickle.loads(zlib.decompress(__icon_serpent))) 1526 icon.CopyFromBitmap(icon_bmp_data) 1527 else: 1528 _log.debug('icon found in [%s]', found_as) 1529 icon = wx.Icon() 1530 try: 1531 icon.LoadFile(found_as, wx.BITMAP_TYPE_ANY) #_PNG 1532 except AttributeError: 1533 _log.exception("this platform doesn't support wx.Icon().LoadFile()") 1534 1535 return icon
1536 1537 #=========================================================================== 1538 # main 1539 #--------------------------------------------------------------------------- 1540 if __name__ == '__main__': 1541 1542 if len(sys.argv) < 2: 1543 sys.exit() 1544 1545 if sys.argv[1] != 'test': 1546 sys.exit() 1547 1548 # for testing: 1549 logging.basicConfig(level = logging.DEBUG) 1550 from Gnumed.pycommon import gmI18N 1551 gmI18N.activate_locale() 1552 gmI18N.install_domain() 1553 1554 #-----------------------------------------------------------------------
1555 - def test_input2decimal():
1556 1557 tests = [ 1558 [None, False], 1559 1560 ['', False], 1561 [' 0 ', True, 0], 1562 1563 [0, True, 0], 1564 [0.0, True, 0], 1565 [.0, True, 0], 1566 ['0', True, 0], 1567 ['0.0', True, 0], 1568 ['0,0', True, 0], 1569 ['00.0', True, 0], 1570 ['.0', True, 0], 1571 [',0', True, 0], 1572 1573 [0.1, True, decimal.Decimal('0.1')], 1574 [.01, True, decimal.Decimal('0.01')], 1575 ['0.1', True, decimal.Decimal('0.1')], 1576 ['0,1', True, decimal.Decimal('0.1')], 1577 ['00.1', True, decimal.Decimal('0.1')], 1578 ['.1', True, decimal.Decimal('0.1')], 1579 [',1', True, decimal.Decimal('0.1')], 1580 1581 [1, True, 1], 1582 [1.0, True, 1], 1583 ['1', True, 1], 1584 ['1.', True, 1], 1585 ['1,', True, 1], 1586 ['1.0', True, 1], 1587 ['1,0', True, 1], 1588 ['01.0', True, 1], 1589 ['01,0', True, 1], 1590 [' 01, ', True, 1], 1591 1592 [decimal.Decimal('1.1'), True, decimal.Decimal('1.1')] 1593 ] 1594 for test in tests: 1595 conversion_worked, result = input2decimal(initial = test[0]) 1596 1597 expected2work = test[1] 1598 1599 if conversion_worked: 1600 if expected2work: 1601 if result == test[2]: 1602 continue 1603 else: 1604 print("ERROR (conversion result wrong): >%s<, expected >%s<, got >%s<" % (test[0], test[2], result)) 1605 else: 1606 print("ERROR (conversion worked but was expected to fail): >%s<, got >%s<" % (test[0], result)) 1607 else: 1608 if not expected2work: 1609 continue 1610 else: 1611 print("ERROR (conversion failed but was expected to work): >%s<, expected >%s<" % (test[0], test[2]))
1612 #-----------------------------------------------------------------------
1613 - def test_input2int():
1614 print(input2int(0)) 1615 print(input2int('0')) 1616 print(input2int('0', 0, 0))
1617 #-----------------------------------------------------------------------
1618 - def test_coalesce():
1619 1620 val = None 1621 print(val, coalesce(val, 'is None', 'is not None')) 1622 val = 1 1623 print(val, coalesce(val, 'is None', 'is not None')) 1624 return 1625 1626 import datetime as dt 1627 print(coalesce(initial = dt.datetime.now(), template_initial = '-- %s --', function_initial = ('strftime', '%Y-%m-%d'))) 1628 1629 print('testing coalesce()') 1630 print("------------------") 1631 tests = [ 1632 [None, 'something other than <None>', None, None, 'something other than <None>'], 1633 ['Captain', 'Mr.', '%s.'[:4], 'Mr.', 'Capt.'], 1634 ['value to test', 'test 3 failed', 'template with "%s" included', None, 'template with "value to test" included'], 1635 ['value to test', 'test 4 failed', 'template with value not included', None, 'template with value not included'], 1636 [None, 'initial value was None', 'template_initial: %s', None, 'initial value was None'], 1637 [None, 'initial value was None', 'template_initial: %%(abc)s', None, 'initial value was None'] 1638 ] 1639 passed = True 1640 for test in tests: 1641 result = coalesce ( 1642 initial = test[0], 1643 instead = test[1], 1644 template_initial = test[2], 1645 template_instead = test[3] 1646 ) 1647 if result != test[4]: 1648 print("ERROR") 1649 print("coalesce: (%s, %s, %s, %s)" % (test[0], test[1], test[2], test[3])) 1650 print("expected:", test[4]) 1651 print("received:", result) 1652 passed = False 1653 1654 if passed: 1655 print("passed") 1656 else: 1657 print("failed") 1658 return passed
1659 #-----------------------------------------------------------------------
1660 - def test_capitalize():
1661 print('testing capitalize() ...') 1662 success = True 1663 pairs = [ 1664 # [original, expected result, CAPS mode] 1665 ['Boot', 'Boot', CAPS_FIRST_ONLY], 1666 ['boot', 'Boot', CAPS_FIRST_ONLY], 1667 ['booT', 'Boot', CAPS_FIRST_ONLY], 1668 ['BoOt', 'Boot', CAPS_FIRST_ONLY], 1669 ['boots-Schau', 'Boots-Schau', CAPS_WORDS], 1670 ['boots-sChau', 'Boots-Schau', CAPS_WORDS], 1671 ['boot camp', 'Boot Camp', CAPS_WORDS], 1672 ['fahrner-Kampe', 'Fahrner-Kampe', CAPS_NAMES], 1673 ['häkkönen', 'Häkkönen', CAPS_NAMES], 1674 ['McBurney', 'McBurney', CAPS_NAMES], 1675 ['mcBurney', 'McBurney', CAPS_NAMES], 1676 ['blumberg', 'Blumberg', CAPS_NAMES], 1677 ['roVsing', 'RoVsing', CAPS_NAMES], 1678 ['Özdemir', 'Özdemir', CAPS_NAMES], 1679 ['özdemir', 'Özdemir', CAPS_NAMES], 1680 ] 1681 for pair in pairs: 1682 result = capitalize(pair[0], pair[2]) 1683 if result != pair[1]: 1684 success = False 1685 print('ERROR (caps mode %s): "%s" -> "%s", expected "%s"' % (pair[2], pair[0], result, pair[1])) 1686 1687 if success: 1688 print("... SUCCESS") 1689 1690 return success
1691 #-----------------------------------------------------------------------
1692 - def test_import_module():
1693 print("testing import_module_from_directory()") 1694 path = sys.argv[1] 1695 name = sys.argv[2] 1696 try: 1697 mod = import_module_from_directory(module_path = path, module_name = name) 1698 except: 1699 print("module import failed, see log") 1700 return False 1701 1702 print("module import succeeded", mod) 1703 print(dir(mod)) 1704 return True
1705 #-----------------------------------------------------------------------
1706 - def test_mkdir():
1707 print("testing mkdir(%s)" % sys.argv[2]) 1708 mkdir(sys.argv[2])
1709 #-----------------------------------------------------------------------
1710 - def test_gmPaths():
1711 print("testing gmPaths()") 1712 print("-----------------") 1713 paths = gmPaths(wx=None, app_name='gnumed') 1714 print("user config dir:", paths.user_config_dir) 1715 print("system config dir:", paths.system_config_dir) 1716 print("local base dir:", paths.local_base_dir) 1717 print("system app data dir:", paths.system_app_data_dir) 1718 print("working directory :", paths.working_dir) 1719 print("temp directory :", paths.tmp_dir)
1720 #-----------------------------------------------------------------------
1721 - def test_none_if():
1722 print("testing none_if()") 1723 print("-----------------") 1724 tests = [ 1725 [None, None, None], 1726 ['a', 'a', None], 1727 ['a', 'b', 'a'], 1728 ['a', None, 'a'], 1729 [None, 'a', None], 1730 [1, 1, None], 1731 [1, 2, 1], 1732 [1, None, 1], 1733 [None, 1, None] 1734 ] 1735 1736 for test in tests: 1737 if none_if(value = test[0], none_equivalent = test[1]) != test[2]: 1738 print('ERROR: none_if(%s) returned [%s], expected [%s]' % (test[0], none_if(test[0], test[1]), test[2])) 1739 1740 return True
1741 #-----------------------------------------------------------------------
1742 - def test_bool2str():
1743 tests = [ 1744 [True, 'Yes', 'Yes', 'Yes'], 1745 [False, 'OK', 'not OK', 'not OK'] 1746 ] 1747 for test in tests: 1748 if bool2str(test[0], test[1], test[2]) != test[3]: 1749 print('ERROR: bool2str(%s, %s, %s) returned [%s], expected [%s]' % (test[0], test[1], test[2], bool2str(test[0], test[1], test[2]), test[3])) 1750 1751 return True
1752 #-----------------------------------------------------------------------
1753 - def test_bool2subst():
1754 1755 print(bool2subst(True, 'True', 'False', 'is None')) 1756 print(bool2subst(False, 'True', 'False', 'is None')) 1757 print(bool2subst(None, 'True', 'False', 'is None'))
1758 #-----------------------------------------------------------------------
1759 - def test_get_unique_filename():
1760 print(get_unique_filename()) 1761 print(get_unique_filename(prefix='test-')) 1762 print(get_unique_filename(suffix='tst')) 1763 print(get_unique_filename(prefix='test-', suffix='tst')) 1764 print(get_unique_filename(tmp_dir='/home/ncq/Archiv/'))
1765 #-----------------------------------------------------------------------
1766 - def test_size2str():
1767 print("testing size2str()") 1768 print("------------------") 1769 tests = [0, 1, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000, 100000000000, 1000000000000, 10000000000000] 1770 for test in tests: 1771 print(size2str(test))
1772 #-----------------------------------------------------------------------
1773 - def test_unwrap():
1774 1775 test = """ 1776 second line\n 1777 3rd starts with tab \n 1778 4th with a space \n 1779 1780 6th 1781 1782 """ 1783 print(unwrap(text = test, max_length = 25))
1784 #-----------------------------------------------------------------------
1785 - def test_wrap():
1786 test = 'line 1\nline 2\nline 3' 1787 1788 print("wrap 5-6-7 initial 0, subsequent 0") 1789 print(wrap(test, 5)) 1790 print() 1791 print(wrap(test, 6)) 1792 print() 1793 print(wrap(test, 7)) 1794 print("-------") 1795 input() 1796 print("wrap 5 initial 1-1-3, subsequent 1-3-1") 1797 print(wrap(test, 5, ' ', ' ')) 1798 print() 1799 print(wrap(test, 5, ' ', ' ')) 1800 print() 1801 print(wrap(test, 5, ' ', ' ')) 1802 print("-------") 1803 input() 1804 print("wrap 6 initial 1-1-3, subsequent 1-3-1") 1805 print(wrap(test, 6, ' ', ' ')) 1806 print() 1807 print(wrap(test, 6, ' ', ' ')) 1808 print() 1809 print(wrap(test, 6, ' ', ' ')) 1810 print("-------") 1811 input() 1812 print("wrap 7 initial 1-1-3, subsequent 1-3-1") 1813 print(wrap(test, 7, ' ', ' ')) 1814 print() 1815 print(wrap(test, 7, ' ', ' ')) 1816 print() 1817 print(wrap(test, 7, ' ', ' '))
1818 #-----------------------------------------------------------------------
1819 - def test_md5():
1820 print('md5 %s: %s' % (sys.argv[2], file2md5(sys.argv[2]))) 1821 print('chunked md5 %s: %s' % (sys.argv[2], file2chunked_md5(sys.argv[2])))
1822 #-----------------------------------------------------------------------
1823 - def test_unicode():
1824 print(u_link_symbol * 10)
1825 #-----------------------------------------------------------------------
1826 - def test_xml_escape():
1827 print(xml_escape_string('<')) 1828 print(xml_escape_string('>')) 1829 print(xml_escape_string('&'))
1830 #-----------------------------------------------------------------------
1831 - def test_tex_escape():
1832 tests = ['\\', '^', '~', '{', '}', '%', '&', '#', '$', '_', u_euro, 'abc\ndef\n\n1234'] 1833 tests.append(' '.join(tests)) 1834 for test in tests: 1835 print('%s:' % test, tex_escape_string(test))
1836 #-----------------------------------------------------------------------
1837 - def test_gpg_decrypt():
1838 fname = gpg_decrypt_file(filename = sys.argv[2], passphrase = sys.argv[3]) 1839 if fname is not None: 1840 print("successfully decrypted:", fname)
1841 #-----------------------------------------------------------------------
1842 - def test_strip_trailing_empty_lines():
1843 tests = [ 1844 'one line, no embedded line breaks ', 1845 'one line\nwith embedded\nline\nbreaks\n ' 1846 ] 1847 for test in tests: 1848 print('as list:') 1849 print(strip_trailing_empty_lines(text = test, eol='\n', return_list = True)) 1850 print('as string:') 1851 print('>>>%s<<<' % strip_trailing_empty_lines(text = test, eol='\n', return_list = False)) 1852 tests = [ 1853 ['list', 'without', 'empty', 'trailing', 'lines'], 1854 ['list', 'with', 'empty', 'trailing', 'lines', '', ' ', ''] 1855 ] 1856 for test in tests: 1857 print('as list:') 1858 print(strip_trailing_empty_lines(lines = test, eol = '\n', return_list = True)) 1859 print('as string:') 1860 print(strip_trailing_empty_lines(lines = test, eol = '\n', return_list = False))
1861 #-----------------------------------------------------------------------
1862 - def test_fname_stem():
1863 tests = [ 1864 r'abc.exe', 1865 r'\abc.exe', 1866 r'c:\abc.exe', 1867 r'c:\d\abc.exe', 1868 r'/home/ncq/tmp.txt', 1869 r'~/tmp.txt', 1870 r'./tmp.txt', 1871 r'./.././tmp.txt', 1872 r'tmp.txt' 1873 ] 1874 for t in tests: 1875 print("[%s] -> [%s]" % (t, fname_stem(t)))
1876 #-----------------------------------------------------------------------
1877 - def test_dir_is_empty():
1878 print(sys.argv[2], 'empty:', dir_is_empty(sys.argv[2]))
1879 1880 #-----------------------------------------------------------------------
1881 - def test_compare_dicts():
1882 d1 = {} 1883 d2 = {} 1884 d1[1] = 1 1885 d1[2] = 2 1886 d1[3] = 3 1887 # 4 1888 d1[5] = 5 1889 1890 d2[1] = 1 1891 d2[2] = None 1892 # 3 1893 d2[4] = 4 1894 1895 #compare_dict_likes(d1, d2) 1896 1897 d1 = {1: 1, 2: 2} 1898 d2 = {1: 1, 2: 2} 1899 1900 #compare_dict_likes(d1, d2, 'same1', 'same2') 1901 print(format_dict_like(d1, tabular = False)) 1902 print(format_dict_like(d1, tabular = True))
1903 #print(format_dict_like(d2)) 1904 1905 #-----------------------------------------------------------------------
1906 - def test_format_compare_dicts():
1907 d1 = {} 1908 d2 = {} 1909 d1[1] = 1 1910 d1[2] = 2 1911 d1[3] = 3 1912 # 4 1913 d1[5] = 5 1914 1915 d2[1] = 1 1916 d2[2] = None 1917 # 3 1918 d2[4] = 4 1919 1920 print('\n'.join(format_dict_likes_comparison(d1, d2, 'd1', 'd2'))) 1921 1922 d1 = {1: 1, 2: 2} 1923 d2 = {1: 1, 2: 2} 1924 1925 print('\n'.join(format_dict_likes_comparison(d1, d2, 'd1', 'd2')))
1926 1927 #-----------------------------------------------------------------------
1928 - def test_rm_dir():
1929 rmdir('cx:\windows\system3__2xxxxxxxxxxxxx')
1930 1931 #-----------------------------------------------------------------------
1932 - def test_rm_dir_content():
1933 #print(rm_dir_content('cx:\windows\system3__2xxxxxxxxxxxxx')) 1934 print(rm_dir_content('/tmp/user/1000/tmp'))
1935 1936 #-----------------------------------------------------------------------
1937 - def test_strip_prefix():
1938 tests = [ 1939 ('', '', ''), 1940 ('a', 'a', ''), 1941 ('\.br\MICROCYTES+1\.br\SPHEROCYTES present\.br\POLYCHROMASIAmoderate\.br\\', '\.br\\', 'MICROCYTES+1\.br\SPHEROCYTES present\.br\POLYCHROMASIAmoderate\.br\\') 1942 ] 1943 for test in tests: 1944 text, prefix, expect = test 1945 result = strip_prefix(text, prefix) 1946 if result == expect: 1947 continue 1948 print('test failed:', test) 1949 print('result:', result)
1950 #-----------------------------------------------------------------------
1951 - def test_shorten_text():
1952 tst = [ 1953 ('123', 1), 1954 ('123', 2), 1955 ('123', 3), 1956 ('123', 4), 1957 ('', 1), 1958 ('1', 1), 1959 ('12', 1), 1960 ('', 2), 1961 ('1', 2), 1962 ('12', 2), 1963 ('123', 2) 1964 ] 1965 for txt, lng in tst: 1966 print('max', lng, 'of', txt, '=', shorten_text(txt, lng))
1967 #-----------------------------------------------------------------------
1968 - def test_fname_sanitize():
1969 tests = [ 1970 '/tmp/test.txt', 1971 '/tmp/ test.txt', 1972 '/tmp/ tes\\t.txt', 1973 'test' 1974 ] 1975 for test in tests: 1976 print (test, fname_sanitize(test))
1977 1978 #----------------------------------------------------------------------- 1979 #test_coalesce() 1980 #test_capitalize() 1981 #test_import_module() 1982 test_mkdir() 1983 #test_gmPaths() 1984 #test_none_if() 1985 #test_bool2str() 1986 #test_bool2subst() 1987 #test_get_unique_filename() 1988 #test_size2str() 1989 #test_wrap() 1990 #test_input2decimal() 1991 #test_input2int() 1992 #test_unwrap() 1993 #test_md5() 1994 #test_unicode() 1995 #test_xml_escape() 1996 #test_gpg_decrypt() 1997 #test_strip_trailing_empty_lines() 1998 #test_fname_stem() 1999 #test_tex_escape() 2000 #test_dir_is_empty() 2001 #test_compare_dicts() 2002 #test_rm_dir() 2003 #test_rm_dir_content() 2004 #test_strip_prefix() 2005 #test_shorten_text() 2006 #test_format_compare_dicts() 2007 #test_fname_sanitize() 2008 2009 #=========================================================================== 2010