1
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
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
32 import pickle, zlib
33
34
35
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
46 ( CAPS_NONE,
47 CAPS_FIRST,
48 CAPS_ALLCAPS,
49 CAPS_WORDS,
50 CAPS_NAMES,
51 CAPS_FIRST_ONLY
52 ) = range(6)
53
54
55 u_currency_pound = '\u00A3'
56 u_currency_sign = '\u00A4'
57 u_currency_yen = '\u00A5'
58 u_right_double_angle_quote = '\u00AB'
59 u_registered_trademark = '\u00AE'
60 u_plus_minus = '\u00B1'
61 u_superscript_one = '\u00B9'
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'
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'
73 u_ellipsis = '\u2026'
74 u_euro = '\u20AC'
75 u_numero = '\u2116'
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'
85 u_almost_equal_to = '\u2248'
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'
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
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
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
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
191 full_item = os.path.join(directory, item)
192 try:
193 os.remove(full_item)
194 except OSError:
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
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
220 return os.path.abspath(os.path.join(directory, '..'))
221
222
224
225
226 return os.path.basename(os.path.normpath(directory))
227
228
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
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
265
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
279 self.__home_dir = None
280
281
282 if getattr(sys, 'frozen', False):
283 _log.info('frozen app, installed into temporary path')
284
285
286
287
288
289
290
291
292
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
298 self.working_dir = os.path.abspath(os.curdir)
299
300
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
305 try:
306 self.system_config_dir = os.path.join('/etc', app_name)
307 except ValueError:
308
309 self.system_config_dir = self.user_config_dir
310
311
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
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
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
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
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
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
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
354 pass
355
356
357
358
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
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
387
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
396 return self.__user_config_dir
397
398 user_config_dir = property(_get_user_config_dir, _set_user_config_dir)
399
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
408 return self.__system_config_dir
409
410 system_config_dir = property(_get_system_config_dir, _set_system_config_dir)
411
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
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
425 raise ValueError('invalid to set home dir')
426
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
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
467 return self.__tmp_dir
468
469 tmp_dir = property(_get_tmp_dir, _set_tmp_dir)
470
471
472
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
514
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
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
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
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
603 for line in unicode_csv_data:
604 yield line.encode(encoding)
605
606
607
608
609
610 default_csv_reader_rest_key = 'list_of_values_of_unknown_fields'
611
613
614
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
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
644
645
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
656 name_part = regex.sub (
657 '[^.\w\s[\]()%§+-]',
658 '',
659 name_part,
660 flags = regex.UNICODE
661 ).strip()
662
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
673 """/home/user/dir/filename.ext -> filename"""
674 return os.path.splitext(os.path.basename(filename))[0]
675
676
678 """/home/user/dir/filename.ext -> /home/user/dir/filename"""
679 return os.path.splitext(filename)[0]
680
681
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
695
696 return os.path.split(filename)[0]
697
698
700
701 return os.path.split(filename)[1]
702
703
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
727
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
751 import ctypes
752
753 kernel32 = ctype.WinDLL('kernel32', use_last_error = True)
754 windows_create_symlink = kernel32.CreateSymbolicLinkW
755 windows_create_symlink.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
756 windows_create_symlink.restype = ctypes.c_ubyte
757 if os.path.isdir(physical_name):
758 flags = 1
759 else:
760 flags = 0
761 ret_code = windows_create_symlink(link_name, physical_name.replace('/', '\\'), flags)
762 _log.debug('ctypes.windll.kernel32.CreateSymbolicLinkW() [%s] exit code: %s', windows_create_symlink, ret_code)
763 if ret_code == 0:
764 raise ctypes.WinError()
765 return ret_code
766
767
768 -def mklink(physical_name, link_name, overwrite=False):
769
770 _log.debug('creating symlink (overwrite = %s):', overwrite)
771 _log.debug('link [%s] =>', link_name)
772 _log.debug('=> physical [%s]', physical_name)
773
774 if os.path.exists(link_name):
775 _log.debug('link exists')
776 if overwrite:
777 return True
778 return False
779
780 try:
781 os.symlink(physical_name, link_name)
782 except (AttributeError, NotImplementedError):
783 _log.debug('this Python does not have os.symlink(), resorting to ctypes')
784 __make_symlink_on_windows(physical_name, link_name)
785
786
787
788 return True
789
790
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
824
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
923 val = match_obj.group(0).lower()
924 if val in ['von', 'van', 'de', 'la', 'l', 'der', 'den']:
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
969
970
992
993
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
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
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
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
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
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')
1211 text = text.replace('^', '\\textasciicircum')
1212 text = text.replace('~', '\\textasciitilde')
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
1229 text = text.replace(u_euro, '\\EUR')
1230
1231 return text
1232
1233
1238
1239
1240 __html_escape_table = {
1241 "&": "&",
1242 '"': """,
1243 "'": "'",
1244 ">": ">",
1245 "<": "<",
1246 }
1247
1256
1257
1260
1261
1263 if isinstance(obj, pydt.datetime):
1264 return obj.isoformat()
1265 raise TypeError('cannot json_serialize(%s)' % type(obj))
1266
1267
1268
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
1405
1406
1448
1449
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
1489
1490
1491
1492
1493
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
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)
1532 except AttributeError:
1533 _log.exception("this platform doesn't support wx.Icon().LoadFile()")
1534
1535 return icon
1536
1537
1538
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
1549 logging.basicConfig(level = logging.DEBUG)
1550 from Gnumed.pycommon import gmI18N
1551 gmI18N.activate_locale()
1552 gmI18N.install_domain()
1553
1554
1612
1617
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
1661 print('testing capitalize() ...')
1662 success = True
1663 pairs = [
1664
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
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
1707 print("testing mkdir(%s)" % sys.argv[2])
1708 mkdir(sys.argv[2])
1709
1720
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
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
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
1765
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
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
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
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
1825
1830
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
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
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
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
1878 print(sys.argv[2], 'empty:', dir_is_empty(sys.argv[2]))
1879
1880
1882 d1 = {}
1883 d2 = {}
1884 d1[1] = 1
1885 d1[2] = 2
1886 d1[3] = 3
1887
1888 d1[5] = 5
1889
1890 d2[1] = 1
1891 d2[2] = None
1892
1893 d2[4] = 4
1894
1895
1896
1897 d1 = {1: 1, 2: 2}
1898 d2 = {1: 1, 2: 2}
1899
1900
1901 print(format_dict_like(d1, tabular = False))
1902 print(format_dict_like(d1, tabular = True))
1903
1904
1905
1926
1927
1929 rmdir('cx:\windows\system3__2xxxxxxxxxxxxx')
1930
1931
1933
1934 print(rm_dir_content('/tmp/user/1000/tmp'))
1935
1936
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
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
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
1980
1981
1982 test_mkdir()
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010