1
2
3
4 __doc__ = """GNUmed general tools."""
5
6
7 __author__ = "K. Hilbert <Karsten.Hilbert@gmx.net>"
8 __license__ = "GPL v2 or later (details at http://www.gnu.org)"
9
10
11 import sys
12 import os
13 import os.path
14 import csv
15 import tempfile
16 import logging
17 import hashlib
18 import platform
19 import subprocess
20 import decimal
21 import getpass
22 import io
23 import functools
24 import json
25 import shutil
26 import zipfile
27 import datetime as pydt
28 import re as regex
29 import xml.sax.saxutils as xml_tools
30
31 import pickle, zlib
32
33 du_core = None
34
35
36
37 if __name__ == '__main__':
38 sys.path.insert(0, '../../')
39 from Gnumed.pycommon import gmBorg
40
41
42 _log = logging.getLogger('gm.tools')
43
44
45 ( CAPS_NONE,
46 CAPS_FIRST,
47 CAPS_ALLCAPS,
48 CAPS_WORDS,
49 CAPS_NAMES,
50 CAPS_FIRST_ONLY
51 ) = range(6)
52
53
54 u_currency_pound = '\u00A3'
55 u_currency_sign = '\u00A4'
56 u_currency_yen = '\u00A5'
57 u_right_double_angle_quote = '\u00AB'
58 u_registered_trademark = '\u00AE'
59 u_plus_minus = '\u00B1'
60 u_superscript_one = '\u00B9'
61 u_left_double_angle_quote = '\u00BB'
62 u_one_quarter = '\u00BC'
63 u_one_half = '\u00BD'
64 u_three_quarters = '\u00BE'
65 u_multiply = '\u00D7'
66 u_greek_ALPHA = '\u0391'
67 u_greek_alpha = '\u03b1'
68 u_greek_OMEGA = '\u03A9'
69 u_greek_omega = '\u03c9'
70 u_dagger = '\u2020'
71 u_triangular_bullet = '\u2023'
72 u_ellipsis = '\u2026'
73 u_euro = '\u20AC'
74 u_numero = '\u2116'
75 u_down_left_arrow = '\u21B5'
76 u_left_arrow = '\u2190'
77 u_up_arrow = '\u2191'
78 u_arrow2right = '\u2192'
79 u_down_arrow = '\u2193'
80 u_left_arrow_with_tail = '\u21a2'
81 u_arrow2right_from_bar = '\u21a6'
82 u_arrow2right_until_vertical_bar = '\u21e5'
83 u_sum = '\u2211'
84 u_almost_equal_to = '\u2248'
85 u_corresponds_to = '\u2258'
86 u_infinity = '\u221E'
87 u_arrow2right_until_vertical_bar2 = '\u2b72'
88
89 u_diameter = '\u2300'
90 u_checkmark_crossed_out = '\u237B'
91 u_box_horiz_high = '\u23ba'
92 u_box_vert_left = '\u23b8'
93 u_box_vert_right = '\u23b9'
94
95 u_box_horiz_single = '\u2500'
96 u_box_vert_light = '\u2502'
97 u_box_horiz_light_3dashes = '\u2504'
98 u_box_vert_light_4dashes = '\u2506'
99 u_box_horiz_4dashes = '\u2508'
100 u_box_T_right = '\u251c'
101 u_box_T_left = '\u2524'
102 u_box_T_down = '\u252c'
103 u_box_T_up = '\u2534'
104 u_box_plus = '\u253c'
105 u_box_top_double = '\u2550'
106 u_box_top_left_double_single = '\u2552'
107 u_box_top_right_double_single = '\u2555'
108 u_box_top_left_arc = '\u256d'
109 u_box_top_right_arc = '\u256e'
110 u_box_bottom_right_arc = '\u256f'
111 u_box_bottom_left_arc = '\u2570'
112 u_box_horiz_light_heavy = '\u257c'
113 u_box_horiz_heavy_light = '\u257e'
114
115 u_skull_and_crossbones = '\u2620'
116 u_caduceus = '\u2624'
117 u_frowning_face = '\u2639'
118 u_smiling_face = '\u263a'
119 u_black_heart = '\u2665'
120 u_female = '\u2640'
121 u_male = '\u2642'
122 u_male_female = '\u26a5'
123 u_chain = '\u26d3'
124
125 u_checkmark_thin = '\u2713'
126 u_checkmark_thick = '\u2714'
127 u_heavy_greek_cross = '\u271a'
128 u_arrow2right_thick = '\u2794'
129 u_writing_hand = '\u270d'
130 u_pencil_1 = '\u270e'
131 u_pencil_2 = '\u270f'
132 u_pencil_3 = '\u2710'
133 u_latin_cross = '\u271d'
134
135 u_arrow2right_until_black_diamond = '\u291e'
136
137 u_kanji_yen = '\u5186'
138 u_replacement_character = '\ufffd'
139 u_link_symbol = '\u1f517'
140
141 _kB = 1024
142 _MB = 1024 * _kB
143 _GB = 1024 * _MB
144 _TB = 1024 * _GB
145 _PB = 1024 * _TB
146
147 _GM_TITLE_PREFIX = 'GMd'
148
149
151
152 print(".========================================================")
153 print("| Unhandled exception caught !")
154 print("| Type :", t)
155 print("| Value:", v)
156 print("`========================================================")
157 _log.critical('unhandled exception caught', exc_info = (t,v,tb))
158 sys.__excepthook__(t,v,tb)
159
160
161
162
163 -def mkdir(directory=None, mode=None):
164 """Create directory.
165
166 - creates parent dirs if necessary
167 - does not fail if directory exists
168 <mode>: numeric, say 0o0700 for "-rwx------"
169 """
170 if os.path.isdir(directory):
171 if mode is None:
172 return True
173 try:
174 old_umask = os.umask(0)
175
176
177 os.chmod(directory, mode)
178 return True
179 except Exception:
180 _log.exception('cannot os.chmod(%s, %s)', oct(mode), directory)
181 raise
182 finally:
183 os.umask(old_umask)
184 return False
185
186 if mode is None:
187 os.makedirs(directory)
188 return True
189
190 try:
191 old_umask = os.umask(0)
192 os.makedirs(directory, mode)
193 return True
194 except Exception:
195 _log.exception('cannot os.makedirs(%s, %s)', oct(mode), directory)
196 raise
197 finally:
198 os.umask(old_umask)
199 return True
200
201
203
204 def _on_rm_error(func, path, exc):
205 _log.error('error while shutil.rmtree(%s)', path, exc_info=exc)
206 return True
207
208 error_count = 0
209 try:
210 shutil.rmtree(directory, False, _on_rm_error)
211 except Exception:
212 _log.exception('cannot shutil.rmtree(%s)', directory)
213 error_count += 1
214 return error_count
215
216
217 -def rm_dir_content(directory):
218 _log.debug('cleaning out [%s]', directory)
219 try:
220 items = os.listdir(directory)
221 except OSError:
222 return False
223 for item in items:
224
225 full_item = os.path.join(directory, item)
226 try:
227 os.remove(full_item)
228 except OSError:
229 _log.debug('[%s] seems to be a subdirectory', full_item)
230 errors = rmdir(full_item)
231 if errors > 0:
232 return False
233 except Exception:
234 _log.exception('cannot os.remove(%s) [a file or a link]', full_item)
235 return False
236
237 return True
238
239
241 if base_dir is None:
242 base_dir = gmPaths().tmp_dir
243 else:
244 if not os.path.isdir(base_dir):
245 mkdir(base_dir, mode = 0o0700)
246 if prefix is None:
247 prefix = 'sndbx-'
248 return tempfile.mkdtemp(prefix = prefix, suffix = '', dir = base_dir)
249
250
252 return os.path.abspath(os.path.join(directory, '..'))
253
254
256
257
258 return os.path.basename(os.path.normpath(directory))
259
260
262 try:
263 return len(os.listdir(directory)) == 0
264 except OSError as exc:
265 if exc.errno == 2:
266 return None
267 raise
268
269
270 -def copy_tree_content(directory, target_directory):
271 """Copy the *content* of <directory> *into* <target_directory>
272 which is created if need be.
273 """
274 assert (directory is not None), 'source <directory> should not be None'
275 _log.debug('copying content of [%s] into [%s]', directory, target_directory)
276 try:
277 items = os.listdir(directory)
278 except OSError:
279 return None
280
281 for item in items:
282 full_item = os.path.join(directory, item)
283 if os.path.isdir(full_item):
284 target_subdir = os.path.join(target_directory, item)
285 try:
286 shutil.copytree(full_item, target_subdir)
287 except Exception:
288 _log.exception('cannot copy subdir [%s]', full_item)
289 return None
290 else:
291 try:
292 shutil.copy2(full_item, target_directory)
293 except Exception:
294 _log.exception('cannot copy file [%s]', full_item)
295 return None
296
297 return target_directory
298
299
300
302 """This class provides the following paths:
303
304 .home_dir user home
305 .local_base_dir script installation dir
306 .working_dir current dir
307 .user_config_dir
308 .system_config_dir
309 .system_app_data_dir (not writable)
310 .tmp_dir instance-local
311 .user_tmp_dir user-local (NOT per instance)
312 .bytea_cache_dir caches downloaded BYTEA data
313 """
314 - def __init__(self, app_name=None, wx=None):
315 """Setup pathes.
316
317 <app_name> will default to (name of the script - .py)
318 """
319 try:
320 self.already_inited
321 return
322 except AttributeError:
323 pass
324
325 self.init_paths(app_name=app_name, wx=wx)
326 self.already_inited = True
327
328
329
330
332
333 if wx is None:
334 _log.debug('wxPython not available')
335 _log.debug('detecting paths directly')
336
337 if app_name is None:
338 app_name, ext = os.path.splitext(os.path.basename(sys.argv[0]))
339 _log.info('app name detected as [%s]', app_name)
340 else:
341 _log.info('app name passed in as [%s]', app_name)
342
343
344 self.__home_dir = None
345
346
347 if getattr(sys, 'frozen', False):
348 _log.info('frozen app, installed into temporary path')
349
350
351
352
353
354
355
356
357
358 self.local_base_dir = os.path.dirname(sys.executable)
359 else:
360 self.local_base_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
361
362
363 self.working_dir = os.path.abspath(os.curdir)
364
365
366 mkdir(os.path.join(self.home_dir, '.%s' % app_name))
367 self.user_config_dir = os.path.join(self.home_dir, '.%s' % app_name)
368
369
370 try:
371 self.system_config_dir = os.path.join('/etc', app_name)
372 except ValueError:
373
374 self.system_config_dir = self.user_config_dir
375
376
377 try:
378 self.system_app_data_dir = os.path.join(sys.prefix, 'share', app_name)
379 except ValueError:
380 self.system_app_data_dir = self.local_base_dir
381
382
383 try:
384 self.__tmp_dir_already_set
385 _log.debug('temp dir already set')
386 except AttributeError:
387 _log.info('temp file prefix: %s', tempfile.gettempprefix())
388 _log.info('initial (user level) temp dir: %s', tempfile.gettempdir())
389 bytes_free = shutil.disk_usage(tempfile.gettempdir()).free
390 _log.info('free disk space for temp dir: %s (%s bytes)', size2str(size = bytes_free), bytes_free)
391
392 self.user_tmp_dir = os.path.join(tempfile.gettempdir(), app_name + '-' + getpass.getuser())
393 mkdir(self.user_tmp_dir, 0o700)
394 tempfile.tempdir = self.user_tmp_dir
395 _log.info('intermediate (app level) temp dir: %s', tempfile.gettempdir())
396
397 self.tmp_dir = tempfile.mkdtemp(prefix = 'g-')
398 _log.info('final (app instance level) temp dir: %s', tempfile.gettempdir())
399
400
401 cache_dir = os.path.join(self.user_tmp_dir, '.bytea_cache')
402 try:
403 stat = os.stat(cache_dir)
404 _log.warning('reusing BYTEA cache dir: %s', cache_dir)
405 _log.debug(stat)
406 except FileNotFoundError:
407 mkdir(cache_dir, mode = 0o0700)
408 self.bytea_cache_dir = cache_dir
409
410 self.__log_paths()
411 if wx is None:
412 return True
413
414
415 _log.debug('re-detecting paths with wxPython')
416
417 std_paths = wx.StandardPaths.Get()
418 _log.info('wxPython app name is [%s]', wx.GetApp().GetAppName())
419
420
421 mkdir(os.path.join(std_paths.GetUserConfigDir(), '.%s' % app_name))
422 self.user_config_dir = os.path.join(std_paths.GetUserConfigDir(), '.%s' % app_name)
423
424
425 try:
426 tmp = std_paths.GetConfigDir()
427 if not tmp.endswith(app_name):
428 tmp = os.path.join(tmp, app_name)
429 self.system_config_dir = tmp
430 except ValueError:
431
432 pass
433
434
435
436
437 if 'wxMSW' in wx.PlatformInfo:
438 _log.warning('this platform (wxMSW) sometimes returns a broken value for the system-wide application data dir')
439 else:
440 try:
441 self.system_app_data_dir = std_paths.GetDataDir()
442 except ValueError:
443 pass
444
445 self.__log_paths()
446 return True
447
448
450 _log.debug('sys.argv[0]: %s', sys.argv[0])
451 _log.debug('sys.executable: %s', sys.executable)
452 _log.debug('sys._MEIPASS: %s', getattr(sys, '_MEIPASS', '<not found>'))
453 _log.debug('os.environ["_MEIPASS2"]: %s', os.environ.get('_MEIPASS2', '<not found>'))
454 _log.debug('__file__ : %s', __file__)
455 _log.debug('local application base dir: %s', self.local_base_dir)
456 _log.debug('current working dir: %s', self.working_dir)
457 _log.debug('user home dir: %s', self.home_dir)
458 _log.debug('user-specific config dir: %s', self.user_config_dir)
459 _log.debug('system-wide config dir: %s', self.system_config_dir)
460 _log.debug('system-wide application data dir: %s', self.system_app_data_dir)
461 _log.debug('temporary dir (user): %s', self.user_tmp_dir)
462 _log.debug('temporary dir (instance): %s', self.tmp_dir)
463 _log.debug('BYTEA cache dir: %s', self.bytea_cache_dir)
464
465
466
467
469 if not (os.access(path, os.R_OK) and os.access(path, os.X_OK)):
470 msg = '[%s:user_config_dir]: invalid path [%s]' % (self.__class__.__name__, path)
471 _log.error(msg)
472 raise ValueError(msg)
473 self.__user_config_dir = path
474
476 return self.__user_config_dir
477
478 user_config_dir = property(_get_user_config_dir, _set_user_config_dir)
479
481 if not (os.access(path, os.R_OK) and os.access(path, os.X_OK)):
482 msg = '[%s:system_config_dir]: invalid path [%s]' % (self.__class__.__name__, path)
483 _log.error(msg)
484 raise ValueError(msg)
485 self.__system_config_dir = path
486
488 return self.__system_config_dir
489
490 system_config_dir = property(_get_system_config_dir, _set_system_config_dir)
491
493 if not (os.access(path, os.R_OK) and os.access(path, os.X_OK)):
494 msg = '[%s:system_app_data_dir]: invalid path [%s]' % (self.__class__.__name__, path)
495 _log.error(msg)
496 raise ValueError(msg)
497 self.__system_app_data_dir = path
498
500 return self.__system_app_data_dir
501
502 system_app_data_dir = property(_get_system_app_data_dir, _set_system_app_data_dir)
503
505 raise ValueError('invalid to set home dir')
506
508 if self.__home_dir is not None:
509 return self.__home_dir
510
511 tmp = os.path.expanduser('~')
512 if tmp == '~':
513 _log.error('this platform does not expand ~ properly')
514 try:
515 tmp = os.environ['USERPROFILE']
516 except KeyError:
517 _log.error('cannot access $USERPROFILE in environment')
518
519 if not (
520 os.access(tmp, os.R_OK)
521 and
522 os.access(tmp, os.X_OK)
523 and
524 os.access(tmp, os.W_OK)
525 ):
526 msg = '[%s:home_dir]: invalid path [%s]' % (self.__class__.__name__, tmp)
527 _log.error(msg)
528 raise ValueError(msg)
529
530 self.__home_dir = tmp
531 return self.__home_dir
532
533 home_dir = property(_get_home_dir, _set_home_dir)
534
535
537 if not (os.access(path, os.R_OK) and os.access(path, os.X_OK)):
538 msg = '[%s:tmp_dir]: invalid path [%s]' % (self.__class__.__name__, path)
539 _log.error(msg)
540 raise ValueError(msg)
541 _log.debug('previous temp dir: %s', tempfile.gettempdir())
542 self.__tmp_dir = path
543 tempfile.tempdir = self.__tmp_dir
544 _log.debug('new temp dir: %s', tempfile.gettempdir())
545 self.__tmp_dir_already_set = True
546
548 return self.__tmp_dir
549
550 tmp_dir = property(_get_tmp_dir, _set_tmp_dir)
551
552
553
554
555 -def recode_file(source_file=None, target_file=None, source_encoding='utf8', target_encoding=None, base_dir=None, error_mode='replace'):
556 if target_encoding is None:
557 return source_file
558 if target_encoding == source_encoding:
559 return source_file
560 if target_file is None:
561 target_file = get_unique_filename (
562 prefix = '%s-%s_%s-' % (fname_stem(source_file), source_encoding, target_encoding),
563 suffix = fname_extension(source_file, '.txt'),
564 tmp_dir = base_dir
565 )
566
567 _log.debug('[%s] -> [%s] (%s -> %s)', source_encoding, target_encoding, source_file, target_file)
568
569 in_file = io.open(source_file, mode = 'rt', encoding = source_encoding)
570 out_file = io.open(target_file, mode = 'wt', encoding = target_encoding, errors = error_mode)
571 for line in in_file:
572 out_file.write(line)
573 out_file.close()
574 in_file.close()
575
576 return target_file
577
578
579 -def unzip_archive(archive_name, target_dir=None, remove_archive=False):
580 _log.debug('unzipping [%s] -> [%s]', archive_name, target_dir)
581 success = False
582 try:
583 with zipfile.ZipFile(archive_name) as archive:
584 archive.extractall(target_dir)
585 success = True
586 except Exception:
587 _log.exception('cannot unzip')
588 return False
589 if remove_archive:
590 remove_file(archive_name)
591 return success
592
593
594 -def remove_file(filename, log_error=True, force=False):
619
620
621 -def file2md5(filename=None, return_hex=True):
622 blocksize = 2**10 * 128
623 _log.debug('md5(%s): <%s> byte blocks', filename, blocksize)
624
625 f = io.open(filename, mode = 'rb')
626
627 md5 = hashlib.md5()
628 while True:
629 data = f.read(blocksize)
630 if not data:
631 break
632 md5.update(data)
633 f.close()
634
635 _log.debug('md5(%s): %s', filename, md5.hexdigest())
636
637 if return_hex:
638 return md5.hexdigest()
639 return md5.digest()
640
641
643 _log.debug('chunked_md5(%s, chunk_size=%s bytes)', filename, chunk_size)
644 md5_concat = ''
645 f = open(filename, 'rb')
646 while True:
647 md5 = hashlib.md5()
648 data = f.read(chunk_size)
649 if not data:
650 break
651 md5.update(data)
652 md5_concat += md5.hexdigest()
653 f.close()
654 md5 = hashlib.md5()
655 md5.update(md5_concat)
656 hex_digest = md5.hexdigest()
657 _log.debug('md5("%s"): %s', md5_concat, hex_digest)
658 return hex_digest
659
660
661 default_csv_reader_rest_key = 'list_of_values_of_unknown_fields'
662
664 try:
665 is_dict_reader = kwargs['dict']
666 del kwargs['dict']
667 except KeyError:
668 is_dict_reader = False
669
670 if is_dict_reader:
671 kwargs['restkey'] = default_csv_reader_rest_key
672 return csv.DictReader(unicode_csv_data, dialect=dialect, **kwargs)
673 return csv.reader(unicode_csv_data, dialect=dialect, **kwargs)
674
675
676
677
679 for line in unicode_csv_data:
680 yield line.encode(encoding)
681
682
683
684
685
687
688
689 try:
690 is_dict_reader = kwargs['dict']
691 del kwargs['dict']
692 if is_dict_reader is not True:
693 raise KeyError
694 kwargs['restkey'] = default_csv_reader_rest_key
695 csv_reader = csv.DictReader(unicode2charset_encoder(unicode_csv_data), dialect=dialect, **kwargs)
696 except KeyError:
697 is_dict_reader = False
698 csv_reader = csv.reader(unicode2charset_encoder(unicode_csv_data), dialect=dialect, **kwargs)
699
700 for row in csv_reader:
701
702 if is_dict_reader:
703 for key in row.keys():
704 if key == default_csv_reader_rest_key:
705 old_data = row[key]
706 new_data = []
707 for val in old_data:
708 new_data.append(str(val, encoding))
709 row[key] = new_data
710 if default_csv_reader_rest_key not in csv_reader.fieldnames:
711 csv_reader.fieldnames.append(default_csv_reader_rest_key)
712 else:
713 row[key] = str(row[key], encoding)
714 yield row
715 else:
716 yield [ str(cell, encoding) for cell in row ]
717
718
719
721 """Normalizes unicode, removes non-alpha characters, converts spaces to underscores."""
722
723 dir_part, name_part = os.path.split(filename)
724 if name_part == '':
725 return filename
726
727 import unicodedata
728 name_part = unicodedata.normalize('NFKD', name_part)
729
730 name_part = regex.sub (
731 '[^.\w\s[\]()%§+-]',
732 '',
733 name_part,
734 flags = regex.UNICODE
735 ).strip()
736
737 name_part = regex.sub (
738 '\s+',
739 '_',
740 name_part,
741 flags = regex.UNICODE
742 )
743 return os.path.join(dir_part, name_part)
744
745
747 """/home/user/dir/filename.ext -> filename"""
748 return os.path.splitext(os.path.basename(filename))[0]
749
750
752 """/home/user/dir/filename.ext -> /home/user/dir/filename"""
753 return os.path.splitext(filename)[0]
754
755
757 """ /home/user/dir/filename.ext -> .ext
758 '' or '.' -> fallback if any else ''
759 """
760 ext = os.path.splitext(filename)[1]
761 if ext.strip() not in ['.', '']:
762 return ext
763 if fallback is None:
764 return ''
765 return fallback
766
767
769
770 return os.path.split(filename)[0]
771
772
774
775 return os.path.split(filename)[1]
776
777
779 """This function has a race condition between
780 its file.close()
781 and actually
782 using the filename in callers.
783
784 The file will NOT exist after calling this function.
785 """
786 if tmp_dir is not None:
787 if (
788 not os.access(tmp_dir, os.F_OK)
789 or
790 not os.access(tmp_dir, os.X_OK | os.W_OK)
791 ):
792 _log.warning('cannot os.access() temporary dir [%s], using system default', tmp_dir)
793 tmp_dir = None
794
795 if include_timestamp:
796 ts = pydt.datetime.now().strftime('%m%d-%H%M%S-')
797 else:
798 ts = ''
799
800 kwargs = {
801 'dir': tmp_dir,
802
803
804 'delete': True
805 }
806
807 if prefix is None:
808 kwargs['prefix'] = 'gm-%s' % ts
809 else:
810 kwargs['prefix'] = prefix + ts
811
812 if suffix in [None, '']:
813 kwargs['suffix'] = '.tmp'
814 else:
815 if not suffix.startswith('.'):
816 suffix = '.' + suffix
817 kwargs['suffix'] = suffix
818
819 f = tempfile.NamedTemporaryFile(**kwargs)
820 filename = f.name
821 f.close()
822
823 return filename
824
825
827 import ctypes
828
829 kernel32 = ctype.WinDLL('kernel32', use_last_error = True)
830 windows_create_symlink = kernel32.CreateSymbolicLinkW
831 windows_create_symlink.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
832 windows_create_symlink.restype = ctypes.c_ubyte
833 if os.path.isdir(physical_name):
834 flags = 1
835 else:
836 flags = 0
837 ret_code = windows_create_symlink(link_name, physical_name.replace('/', '\\'), flags)
838 _log.debug('ctypes.windll.kernel32.CreateSymbolicLinkW() [%s] exit code: %s', windows_create_symlink, ret_code)
839 if ret_code == 0:
840 raise ctypes.WinError()
841 return ret_code
842
843
844 -def mklink(physical_name, link_name, overwrite=False):
845
846 _log.debug('creating symlink (overwrite = %s):', overwrite)
847 _log.debug('link [%s] =>', link_name)
848 _log.debug('=> physical [%s]', physical_name)
849
850 if os.path.exists(link_name):
851 _log.debug('link exists')
852 if overwrite:
853 return True
854 return False
855
856 try:
857 os.symlink(physical_name, link_name)
858 except (AttributeError, NotImplementedError):
859 _log.debug('this Python does not have os.symlink(), trying via ctypes')
860 __make_symlink_on_windows(physical_name, link_name)
861 except PermissionError:
862 _log.exception('cannot create link')
863 return False
864
865
866 return True
867
868
870 """Import a module from any location."""
871
872 _log.debug('CWD: %s', os.getcwd())
873
874 remove_path = always_remove_path or False
875 if module_path not in sys.path:
876 _log.info('appending to sys.path: [%s]' % module_path)
877 sys.path.append(module_path)
878 remove_path = True
879
880 _log.debug('will remove import path: %s', remove_path)
881
882 if module_name.endswith('.py'):
883 module_name = module_name[:-3]
884
885 try:
886 module = __import__(module_name)
887 except Exception:
888 _log.exception('cannot __import__() module [%s] from [%s]' % (module_name, module_path))
889 while module_path in sys.path:
890 sys.path.remove(module_path)
891 raise
892
893 _log.info('imported module [%s] as [%s]' % (module_name, module))
894 if remove_path:
895 while module_path in sys.path:
896 sys.path.remove(module_path)
897
898 return module
899
900
901
902
904 if size == 1:
905 return template % _('1 Byte')
906 if size < 10 * _kB:
907 return template % _('%s Bytes') % size
908 if size < _MB:
909 return template % '%.1f kB' % (float(size) / _kB)
910 if size < _GB:
911 return template % '%.1f MB' % (float(size) / _MB)
912 if size < _TB:
913 return template % '%.1f GB' % (float(size) / _GB)
914 if size < _PB:
915 return template % '%.1f TB' % (float(size) / _TB)
916 return template % '%.1f PB' % (float(size) / _PB)
917
918
919 -def bool2subst(boolean=None, true_return=True, false_return=False, none_return=None):
920 if boolean is None:
921 return none_return
922 if boolean:
923 return true_return
924 if not boolean:
925 return false_return
926 raise ValueError('bool2subst(): <boolean> arg must be either of True, False, None')
927
928
929 -def bool2str(boolean=None, true_str='True', false_str='False'):
930 return bool2subst (
931 boolean = bool(boolean),
932 true_return = true_str,
933 false_return = false_str
934 )
935
936
937 -def none_if(value=None, none_equivalent=None, strip_string=False):
938 """Modelled after the SQL NULLIF function."""
939 if value is None:
940 return None
941 if strip_string:
942 stripped = value.strip()
943 else:
944 stripped = value
945 if stripped == none_equivalent:
946 return None
947 return value
948
949
950 -def coalesce(value2test=None, return_instead=None, template4value=None, template4instead=None, none_equivalents=None, function4value=None, value2return=None):
951 """Modelled after the SQL coalesce function.
952
953 To be used to simplify constructs like:
954
955 if value2test is None (or in none_equivalents):
956 value = (template4instead % return_instead) or return_instead
957 else:
958 value = (template4value % value2test) or value2test
959 print value
960
961 @param value2test: the value to be tested for <None>
962 @type value2test: any Python type, must have a __str__ method if template4value is not None
963 @param return_instead: the value to be returned if <initial> is None
964 @type return_instead: any Python type, must have a __str__ method if template4instead is not None
965 @param template4value: if <initial> is returned replace the value into this template, must contain one <%s>
966 @type template4value: string or None
967 @param template4instead: if <return_instead> is returned replace the value into this template, must contain one <%s>
968 @type template4instead: string or None
969
970 example:
971 function4value = ('strftime', '%Y-%m-%d')
972
973 Ideas:
974 - list of return_insteads: initial, [return_instead, template], [return_instead, template], [return_instead, template], template4value, ...
975 """
976 if none_equivalents is None:
977 none_equivalents = [None]
978
979 if value2test in none_equivalents:
980 if template4instead is None:
981 return return_instead
982 return template4instead % return_instead
983
984
985
986
987 if value2return is not None:
988 return value2return
989
990 value2return = value2test
991
992 if function4value is not None:
993 funcname, args = function4value
994 func = getattr(value2test, funcname)
995 value2return = func(args)
996
997
998 if template4value is None:
999 return value2return
1000
1001 try:
1002 return template4value % value2return
1003 except TypeError:
1004
1005
1006
1007
1008 if hasattr(_log, 'log_stack_trace'):
1009 _log.log_stack_trace(message = 'deprecated use of <template4value> for <value2return>')
1010 else:
1011 _log.error('deprecated use of <template4value> for <value2return>')
1012 _log.error(locals())
1013 return template4value
1014
1015
1017 val = match_obj.group(0).lower()
1018 if val in ['von', 'van', 'de', 'la', 'l', 'der', 'den']:
1019 return val
1020 buf = list(val)
1021 buf[0] = buf[0].upper()
1022 for part in ['mac', 'mc', 'de', 'la']:
1023 if len(val) > len(part) and val[:len(part)] == part:
1024 buf[len(part)] = buf[len(part)].upper()
1025 return ''.join(buf)
1026
1027
1029 """Capitalize the first character but leave the rest alone.
1030
1031 Note that we must be careful about the locale, this may
1032 have issues ! However, for UTF strings it should just work.
1033 """
1034 if (mode is None) or (mode == CAPS_NONE):
1035 return text
1036
1037 if len(text) == 0:
1038 return text
1039
1040 if mode == CAPS_FIRST:
1041 if len(text) == 1:
1042 return text[0].upper()
1043 return text[0].upper() + text[1:]
1044
1045 if mode == CAPS_ALLCAPS:
1046 return text.upper()
1047
1048 if mode == CAPS_FIRST_ONLY:
1049
1050
1051 return text[0].upper() + text[1:].lower()
1052
1053 if mode == CAPS_WORDS:
1054
1055 return regex.sub(r'(\w)(\w+)', lambda x: x.group(1).upper() + x.group(2).lower(), text)
1056
1057 if mode == CAPS_NAMES:
1058
1059 return capitalize(text=text, mode=CAPS_FIRST)
1060
1061 print("ERROR: invalid capitalization mode: [%s], leaving input as is" % mode)
1062 return text
1063
1064
1086
1087
1113
1114
1115 -def strip_prefix(text, prefix, remove_repeats=False, remove_whitespace=False):
1116 if remove_whitespace:
1117 text = text.lstrip()
1118 if not text.startswith(prefix):
1119 return text
1120
1121 text = text.replace(prefix, '', 1)
1122 if not remove_repeats:
1123 if remove_whitespace:
1124 return text.lstrip()
1125 return text
1126
1127 return strip_prefix(text, prefix, remove_repeats = True, remove_whitespace = remove_whitespace)
1128
1129
1137
1138
1141
1142
1143 -def strip_suffix(text, suffix, remove_repeats=False, remove_whitespace=False):
1144 suffix_len = len(suffix)
1145 if remove_repeats:
1146 if remove_whitespace:
1147 while text.rstrip().endswith(suffix):
1148 text = text.rstrip()[:-suffix_len].rstrip()
1149 return text
1150 while text.endswith(suffix):
1151 text = text[:-suffix_len]
1152 return text
1153 if remove_whitespace:
1154 return text.rstrip()[:-suffix_len].rstrip()
1155 return text[:-suffix_len]
1156
1157
1159 if lines is None:
1160 lines = text.split(eol)
1161
1162 while True:
1163 if lines[0].strip(eol).strip() != '':
1164 break
1165 lines = lines[1:]
1166
1167 if return_list:
1168 return lines
1169
1170 return eol.join(lines)
1171
1172
1174 if lines is None:
1175 lines = text.split(eol)
1176
1177 while True:
1178 if lines[-1].strip(eol).strip() != '':
1179 break
1180 lines = lines[:-1]
1181
1182 if return_list:
1183 return lines
1184
1185 return eol.join(lines)
1186
1187
1195
1196
1197 -def list2text(lines, initial_indent='', subsequent_indent='', eol='\n', strip_leading_empty_lines=True, strip_trailing_empty_lines=True, strip_trailing_whitespace=True, max_line_width=None):
1198
1199 if len(lines) == 0:
1200 return ''
1201
1202 if strip_leading_empty_lines:
1203 lines = strip_leading_empty_lines(lines = lines, eol = eol, return_list = True)
1204
1205 if strip_trailing_empty_lines:
1206 lines = strip_trailing_empty_lines(lines = lines, eol = eol, return_list = True)
1207
1208 if strip_trailing_whitespace:
1209 lines = [ l.rstrip() for l in lines ]
1210
1211 if max_line_width is not None:
1212 wrapped_lines = []
1213 for l in lines:
1214 wrapped_lines.extend(wrap(l, max_line_width).split('\n'))
1215 lines = wrapped_lines
1216
1217 indented_lines = [initial_indent + lines[0]]
1218 indented_lines.extend([ subsequent_indent + l for l in lines[1:] ])
1219
1220 return eol.join(indented_lines)
1221
1222
1223 -def wrap(text=None, width=None, initial_indent='', subsequent_indent='', eol='\n'):
1224 """A word-wrap function that preserves existing line breaks
1225 and most spaces in the text. Expects that existing line
1226 breaks are posix newlines (\n).
1227 """
1228 if width is None:
1229 return text
1230
1231 wrapped = initial_indent + functools.reduce (
1232 lambda line, word, width=width: '%s%s%s' % (
1233 line,
1234 ' \n'[(len(line) - line.rfind('\n') - 1 + len(word.split('\n',1)[0]) >= width)],
1235 word
1236 ),
1237 text.split(' ')
1238 )
1239 if subsequent_indent != '':
1240 wrapped = ('\n%s' % subsequent_indent).join(wrapped.split('\n'))
1241 if eol != '\n':
1242 wrapped = wrapped.replace('\n', eol)
1243 return wrapped
1244
1245
1246 -def unwrap(text=None, max_length=None, strip_whitespace=True, remove_empty_lines=True, line_separator = ' // '):
1247
1248 text = text.replace('\r', '')
1249 lines = text.split('\n')
1250 text = ''
1251 for line in lines:
1252
1253 if strip_whitespace:
1254 line = line.strip().strip('\t').strip()
1255
1256 if remove_empty_lines:
1257 if line == '':
1258 continue
1259
1260 text += ('%s%s' % (line, line_separator))
1261
1262 text = text.rstrip(line_separator)
1263
1264 if max_length is not None:
1265 text = text[:max_length]
1266
1267 text = text.rstrip(line_separator)
1268
1269 return text
1270
1271
1272 -def shorten_text(text=None, max_length=None):
1273
1274 if len(text) <= max_length:
1275 return text
1276
1277 return text[:max_length-1] + u_ellipsis
1278
1279
1281 if text is None:
1282 return None
1283 if max_length is None:
1284 max_length = len(text)
1285 else:
1286 if len(text) <= max_length:
1287 return text
1288 old_words = regex.split('\s+', text, flags = regex.UNICODE)
1289 no_old_words = len(old_words)
1290 max_word_length = max(min_word_length, (max_length // no_old_words))
1291 words = []
1292 for word in old_words:
1293 if len(word) <= max_word_length:
1294 words.append(word)
1295 continue
1296 if ignore_numbers:
1297 tmp = word.replace('-', '').replace('+', '').replace('.', '').replace(',', '').replace('/', '').replace('&', '').replace('*', '')
1298 if tmp.isdigit():
1299 words.append(word)
1300 continue
1301 words.append(word[:max_word_length] + ellipsis)
1302 return ' '.join(words)
1303
1304
1306 """check for special XML characters and transform them"""
1307 return xml_tools.escape(text)
1308
1309
1310 -def tex_escape_string(text=None, replace_known_unicode=True, replace_eol=False, keep_visual_eol=False):
1311 """Check for special TeX characters and transform them.
1312
1313 replace_eol:
1314 replaces "\n" with "\\newline"
1315 keep_visual_eol:
1316 replaces "\n" with "\\newline \n" such that
1317 both LaTeX will know to place a line break
1318 at this point as well as the visual formatting
1319 is preserved in the LaTeX source (think multi-
1320 row table cells)
1321 """
1322 text = text.replace('\\', '\\textbackslash')
1323 text = text.replace('^', '\\textasciicircum')
1324 text = text.replace('~', '\\textasciitilde')
1325
1326 text = text.replace('{', '\\{')
1327 text = text.replace('}', '\\}')
1328 text = text.replace('%', '\\%')
1329 text = text.replace('&', '\\&')
1330 text = text.replace('#', '\\#')
1331 text = text.replace('$', '\\$')
1332 text = text.replace('_', '\\_')
1333 if replace_eol:
1334 if keep_visual_eol:
1335 text = text.replace('\n', '\\newline \n')
1336 else:
1337 text = text.replace('\n', '\\newline ')
1338
1339 if replace_known_unicode:
1340
1341 text = text.replace(u_euro, '\\EUR')
1342 text = text.replace(u_sum, '$\\Sigma$')
1343
1344 return text
1345
1346
1348 global du_core
1349 if du_core is None:
1350 try:
1351 from docutils import core as du_core
1352 except ImportError:
1353 _log.warning('cannot turn ReST into LaTeX: docutils not installed')
1354 return tex_escape_string(text = rst_text)
1355
1356 parts = du_core.publish_parts (
1357 source = rst_text.replace('\\', '\\\\'),
1358 source_path = '<internal>',
1359 writer_name = 'latex',
1360
1361 settings_overrides = {
1362 'input_encoding': 'unicode'
1363 },
1364 enable_exit_status = True
1365 )
1366 return parts['body']
1367
1368
1369 -def rst2html(rst_text, replace_eol=False, keep_visual_eol=False):
1370 global du_core
1371 if du_core is None:
1372 try:
1373 from docutils import core as du_core
1374 except ImportError:
1375 _log.warning('cannot turn ReST into HTML: docutils not installed')
1376 return html_escape_string(text = rst_text, replace_eol=False, keep_visual_eol=False)
1377
1378 parts = du_core.publish_parts (
1379 source = rst_text.replace('\\', '\\\\'),
1380 source_path = '<internal>',
1381 writer_name = 'latex',
1382
1383 settings_overrides = {
1384 'input_encoding': 'unicode'
1385 },
1386 enable_exit_status = True
1387 )
1388 return parts['body']
1389
1390
1395
1396
1397 __html_escape_table = {
1398 "&": "&",
1399 '"': """,
1400 "'": "'",
1401 ">": ">",
1402 "<": "<",
1403 }
1404
1413
1414
1417
1418
1420 if isinstance(obj, pydt.datetime):
1421 return obj.isoformat()
1422 raise TypeError('cannot json_serialize(%s)' % type(obj))
1423
1424
1425
1427 _log.info('comparing dict-likes: %s[%s] vs %s[%s]', coalesce(title1, '', '"%s" '), type(d1), coalesce(title2, '', '"%s" '), type(d2))
1428 try:
1429 d1 = dict(d1)
1430 except TypeError:
1431 pass
1432 try:
1433 d2 = dict(d2)
1434 except TypeError:
1435 pass
1436 keys_d1 = frozenset(d1.keys())
1437 keys_d2 = frozenset(d2.keys())
1438 different = False
1439 if len(keys_d1) != len(keys_d2):
1440 _log.info('different number of keys: %s vs %s', len(keys_d1), len(keys_d2))
1441 different = True
1442 for key in keys_d1:
1443 if key in keys_d2:
1444 if type(d1[key]) != type(d2[key]):
1445 _log.info('%25.25s: type(dict1) = %s = >>>%s<<<' % (key, type(d1[key]), d1[key]))
1446 _log.info('%25.25s type(dict2) = %s = >>>%s<<<' % ('', type(d2[key]), d2[key]))
1447 different = True
1448 continue
1449 if d1[key] == d2[key]:
1450 _log.info('%25.25s: both = >>>%s<<<' % (key, d1[key]))
1451 else:
1452 _log.info('%25.25s: dict1 = >>>%s<<<' % (key, d1[key]))
1453 _log.info('%25.25s dict2 = >>>%s<<<' % ('', d2[key]))
1454 different = True
1455 else:
1456 _log.info('%25.25s: %50.50s | <MISSING>' % (key, '>>>%s<<<' % d1[key]))
1457 different = True
1458 for key in keys_d2:
1459 if key in keys_d1:
1460 continue
1461 _log.info('%25.25s: %50.50s | %.50s' % (key, '<MISSING>', '>>>%s<<<' % d2[key]))
1462 different = True
1463 if different:
1464 _log.info('dict-likes appear to be different from each other')
1465 return False
1466 _log.info('dict-likes appear equal to each other')
1467 return True
1468
1469
1562
1563
1609
1610
1611 -def dicts2table(dict_list, left_margin=0, eol='\n', keys2ignore=None, headers=None, show_only_changes=False, equality_value='<=>', date_format=None):
1612
1613 keys2show = []
1614 dict_max_size = {}
1615 max_row_label_size = 0
1616 if keys2ignore is None:
1617 keys2ignore = []
1618
1619
1620 for d in dict_list:
1621 dict_max_size[id(d)] = 0
1622 for key in d.keys():
1623 if key in keys2ignore:
1624 continue
1625 dict_max_size[id(d)] = max(dict_max_size[id(d)], len('%s' % d[key]))
1626 if key in keys2show:
1627 continue
1628 keys2show.append(key)
1629 max_row_label_size = max(max_row_label_size, len('%s' % key))
1630
1631
1632 lines = { k: [] for k in keys2show }
1633 prev_vals = {}
1634 for d in dict_list:
1635 if show_only_changes:
1636 max_size = max(dict_max_size[id(d)], len(equality_value))
1637 else:
1638 max_size = dict_max_size[id(d)]
1639 max_len_str = '%s.%s' % (max_size, max_size)
1640 field_template = ' %' + max_len_str + 's'
1641 for key in keys2show:
1642 try:
1643 val = d[key]
1644 except KeyError:
1645 lines[key].append(field_template % _('<missing>'))
1646 continue
1647 if isinstance(val, pydt.datetime):
1648 if date_format is not None:
1649 val = val.strftime(date_format)
1650 lines[key].append(field_template % val)
1651 if show_only_changes:
1652 if key not in prev_vals:
1653 prev_vals[key] = '%s' % lines[key][-1]
1654 continue
1655 if lines[key][-1] != prev_vals[key]:
1656 prev_vals[key] = '%s' % lines[key][-1]
1657 continue
1658 lines[key][-1] = field_template % equality_value
1659
1660
1661 table_lines = []
1662 max_len_str = '%s.%s' % (max_row_label_size, max_row_label_size)
1663 row_label_template = '%' + max_len_str + 's'
1664 for key in lines:
1665 line = (' ' * left_margin) + row_label_template % key + '|'
1666 line += '|'.join(lines[key])
1667 table_lines.append(line)
1668
1669
1670 if headers is not None:
1671 header_line1 = (' ' * left_margin) + row_label_template % ''
1672 header_line2 = (' ' * left_margin) + u_box_horiz_single * (max_row_label_size)
1673 header_sizes = [ max(dict_max_size[dict_id], len(equality_value)) for dict_id in dict_max_size ]
1674 for idx in range(len(headers)):
1675 max_len_str = '%s.%s' % (header_sizes[idx], header_sizes[idx])
1676 header_template = '%' + max_len_str + 's'
1677 header_line1 += '| '
1678 header_line1 += header_template % headers[idx]
1679 header_line2 += '%s%s' % (u_box_plus, u_box_horiz_single)
1680 header_line2 += u_box_horiz_single * header_sizes[idx]
1681 table_lines.insert(0, header_line2)
1682 table_lines.insert(0, header_line1)
1683
1684 if eol is None:
1685 return table_lines
1686
1687 return ('|' + eol).join(table_lines) + '|' + eol
1688
1689
1691 for key in required_keys:
1692 try:
1693 d[key]
1694 except KeyError:
1695 if missing_key_template is None:
1696 d[key] = None
1697 else:
1698 d[key] = missing_key_template % {'key': key}
1699 return d
1700
1701
1703 try:
1704 import pyudev
1705 import psutil
1706 except ImportError:
1707 _log.error('pyudev and/or psutil not installed')
1708 return {}
1709
1710 removable_partitions = {}
1711 ctxt = pyudev.Context()
1712 removable_devices = [ dev for dev in ctxt.list_devices(subsystem='block', DEVTYPE='disk') if dev.attributes.get('removable') == b'1' ]
1713 all_mounted_partitions = { part.device: part for part in psutil.disk_partitions() }
1714 for device in removable_devices:
1715 _log.debug('removable device: %s', device.properties['ID_MODEL'])
1716 partitions_on_removable_device = {
1717 part.device_node: {
1718 'type': device.properties['ID_TYPE'],
1719 'bus': device.properties['ID_BUS'],
1720 'device': device.properties['DEVNAME'],
1721 'partition': part.properties['DEVNAME'],
1722 'vendor': part.properties['ID_VENDOR'],
1723 'model': part.properties['ID_MODEL'],
1724 'fs_label': part.properties['ID_FS_LABEL'],
1725 'is_mounted': False,
1726 'mountpoint': None,
1727 'fs_type': None,
1728 'size_in_bytes': -1,
1729 'bytes_free': 0
1730 } for part in ctxt.list_devices(subsystem='block', DEVTYPE='partition', parent=device)
1731 }
1732 for part in partitions_on_removable_device:
1733 try:
1734 partitions_on_removable_device[part]['mountpoint'] = all_mounted_partitions[part].mountpoint
1735 partitions_on_removable_device[part]['is_mounted'] = True
1736 partitions_on_removable_device[part]['fs_type'] = all_mounted_partitions[part].fstype
1737 du = shutil.disk_usage(all_mounted_partitions[part].mountpoint)
1738 partitions_on_removable_device[part]['size_in_bytes'] = du.total
1739 partitions_on_removable_device[part]['bytes_free'] = du.free
1740 except KeyError:
1741 pass
1742 removable_partitions.update(partitions_on_removable_device)
1743 return removable_partitions
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1756 try:
1757 import pyudev
1758 except ImportError:
1759 _log.error('pyudev not installed')
1760 return []
1761
1762 optical_writers = []
1763 ctxt = pyudev.Context()
1764 for dev in [ dev for dev in ctxt.list_devices(subsystem='block', DEVTYPE='disk') if dev.properties.get('ID_CDROM_CD_RW', None) == '1' ]:
1765 optical_writers.append ({
1766 'type': dev.properties['ID_TYPE'],
1767 'bus': dev.properties['ID_BUS'],
1768 'device': dev.properties['DEVNAME'],
1769 'model': dev.properties['ID_MODEL']
1770 })
1771 return optical_writers
1772
1773
1774
1801
1802
1803
1804
1805
1806 __icon_serpent = \
1807 """x\xdae\x8f\xb1\x0e\x83 \x10\x86w\x9f\xe2\x92\x1blb\xf2\x07\x96\xeaH:0\xd6\
1808 \xc1\x85\xd5\x98N5\xa5\xef?\xf5N\xd0\x8a\xdcA\xc2\xf7qw\x84\xdb\xfa\xb5\xcd\
1809 \xd4\xda;\xc9\x1a\xc8\xb6\xcd<\xb5\xa0\x85\x1e\xeb\xbc\xbc7b!\xf6\xdeHl\x1c\
1810 \x94\x073\xec<*\xf7\xbe\xf7\x99\x9d\xb21~\xe7.\xf5\x1f\x1c\xd3\xbdVlL\xc2\
1811 \xcf\xf8ye\xd0\x00\x90\x0etH \x84\x80B\xaa\x8a\x88\x85\xc4(U\x9d$\xfeR;\xc5J\
1812 \xa6\x01\xbbt9\xceR\xc8\x81e_$\x98\xb9\x9c\xa9\x8d,y\xa9t\xc8\xcf\x152\xe0x\
1813 \xe9$\xf5\x07\x95\x0cD\x95t:\xb1\x92\xae\x9cI\xa8~\x84\x1f\xe0\xa3ec"""
1814
1816
1817 paths = gmPaths(app_name = 'gnumed', wx = wx)
1818
1819 candidates = [
1820 os.path.join(paths.system_app_data_dir, 'bitmaps', 'gm_icon-serpent_and_gnu.png'),
1821 os.path.join(paths.local_base_dir, 'bitmaps', 'gm_icon-serpent_and_gnu.png'),
1822 os.path.join(paths.system_app_data_dir, 'bitmaps', 'serpent.png'),
1823 os.path.join(paths.local_base_dir, 'bitmaps', 'serpent.png')
1824 ]
1825
1826 found_as = None
1827 for candidate in candidates:
1828 try:
1829 open(candidate, 'r').close()
1830 found_as = candidate
1831 break
1832 except IOError:
1833 _log.debug('icon not found in [%s]', candidate)
1834
1835 if found_as is None:
1836 _log.warning('no icon file found, falling back to builtin (ugly) icon')
1837 icon_bmp_data = wx.BitmapFromXPMData(pickle.loads(zlib.decompress(__icon_serpent)))
1838 icon.CopyFromBitmap(icon_bmp_data)
1839 else:
1840 _log.debug('icon found in [%s]', found_as)
1841 icon = wx.Icon()
1842 try:
1843 icon.LoadFile(found_as, wx.BITMAP_TYPE_ANY)
1844 except AttributeError:
1845 _log.exception("this platform doesn't support wx.Icon().LoadFile()")
1846
1847 return icon
1848
1849
1850 -def create_qrcode(text=None, filename=None, qr_filename=None, verbose=False):
1851 assert (not ((text is None) and (filename is None))), 'either <text> or <filename> must be specified'
1852
1853 try:
1854 import pyqrcode
1855 except ImportError:
1856 _log.exception('cannot import <pyqrcode>')
1857 return None
1858 if text is None:
1859 with io.open(filename, mode = 'rt', encoding = 'utf8') as input_file:
1860 text = input_file.read()
1861 if qr_filename is None:
1862 if filename is None:
1863 qr_filename = get_unique_filename(prefix = 'gm-qr-', suffix = '.png')
1864 else:
1865 qr_filename = get_unique_filename (
1866 prefix = fname_stem(filename) + '-',
1867 suffix = fname_extension(filename) + '.png'
1868 )
1869 _log.debug('[%s] -> [%s]', filename, qr_filename)
1870 qr = pyqrcode.create(text, encoding = 'utf8')
1871 if verbose:
1872 print('input file:', filename)
1873 print('output file:', qr_filename)
1874 print('text to encode:', text)
1875 print(qr.terminal())
1876 qr.png(qr_filename, quiet_zone = 1)
1877 return qr_filename
1878
1879
1880
1881
1882 if __name__ == '__main__':
1883
1884 if len(sys.argv) < 2:
1885 sys.exit()
1886
1887 if sys.argv[1] != 'test':
1888 sys.exit()
1889
1890
1891 logging.basicConfig(level = logging.DEBUG)
1892 from Gnumed.pycommon import gmI18N
1893 gmI18N.activate_locale()
1894 gmI18N.install_domain()
1895
1896
1954
1959
1961
1962 val = None
1963 print(val, coalesce(val, 'is None', 'is not None'))
1964 val = 1
1965 print(val, coalesce(val, 'is None', 'is not None'))
1966 return
1967
1968 import datetime as dt
1969 print(coalesce(value2test = dt.datetime.now(), template4value = '-- %s --', function4value = ('strftime', '%Y-%m-%d')))
1970
1971 print('testing coalesce()')
1972 print("------------------")
1973 tests = [
1974 [None, 'something other than <None>', None, None, 'something other than <None>'],
1975 ['Captain', 'Mr.', '%s.'[:4], 'Mr.', 'Capt.'],
1976 ['value to test', 'test 3 failed', 'template with "%s" included', None, 'template with "value to test" included'],
1977 ['value to test', 'test 4 failed', 'template with value not included', None, 'template with value not included'],
1978 [None, 'initial value was None', 'template4value: %s', None, 'initial value was None'],
1979 [None, 'initial value was None', 'template4value: %%(abc)s', None, 'initial value was None']
1980 ]
1981 passed = True
1982 for test in tests:
1983 result = coalesce (
1984 value2test = test[0],
1985 return_instead = test[1],
1986 template4value = test[2],
1987 template4instead = test[3]
1988 )
1989 if result != test[4]:
1990 print("ERROR")
1991 print("coalesce: (%s, %s, %s, %s)" % (test[0], test[1], test[2], test[3]))
1992 print("expected:", test[4])
1993 print("received:", result)
1994 passed = False
1995
1996 if passed:
1997 print("passed")
1998 else:
1999 print("failed")
2000 return passed
2001
2003 print('testing capitalize() ...')
2004 success = True
2005 pairs = [
2006
2007 ['Boot', 'Boot', CAPS_FIRST_ONLY],
2008 ['boot', 'Boot', CAPS_FIRST_ONLY],
2009 ['booT', 'Boot', CAPS_FIRST_ONLY],
2010 ['BoOt', 'Boot', CAPS_FIRST_ONLY],
2011 ['boots-Schau', 'Boots-Schau', CAPS_WORDS],
2012 ['boots-sChau', 'Boots-Schau', CAPS_WORDS],
2013 ['boot camp', 'Boot Camp', CAPS_WORDS],
2014 ['fahrner-Kampe', 'Fahrner-Kampe', CAPS_NAMES],
2015 ['häkkönen', 'Häkkönen', CAPS_NAMES],
2016 ['McBurney', 'McBurney', CAPS_NAMES],
2017 ['mcBurney', 'McBurney', CAPS_NAMES],
2018 ['blumberg', 'Blumberg', CAPS_NAMES],
2019 ['roVsing', 'RoVsing', CAPS_NAMES],
2020 ['Özdemir', 'Özdemir', CAPS_NAMES],
2021 ['özdemir', 'Özdemir', CAPS_NAMES],
2022 ]
2023 for pair in pairs:
2024 result = capitalize(pair[0], pair[2])
2025 if result != pair[1]:
2026 success = False
2027 print('ERROR (caps mode %s): "%s" -> "%s", expected "%s"' % (pair[2], pair[0], result, pair[1]))
2028
2029 if success:
2030 print("... SUCCESS")
2031
2032 return success
2033
2035 print("testing import_module_from_directory()")
2036 path = sys.argv[1]
2037 name = sys.argv[2]
2038 try:
2039 mod = import_module_from_directory(module_path = path, module_name = name)
2040 except:
2041 print("module import failed, see log")
2042 return False
2043
2044 print("module import succeeded", mod)
2045 print(dir(mod))
2046 return True
2047
2049 print("testing mkdir(%s)" % sys.argv[2])
2050 mkdir(sys.argv[2], 0o0700)
2051
2062
2064 print("testing none_if()")
2065 print("-----------------")
2066 tests = [
2067 [None, None, None],
2068 ['a', 'a', None],
2069 ['a', 'b', 'a'],
2070 ['a', None, 'a'],
2071 [None, 'a', None],
2072 [1, 1, None],
2073 [1, 2, 1],
2074 [1, None, 1],
2075 [None, 1, None]
2076 ]
2077
2078 for test in tests:
2079 if none_if(value = test[0], none_equivalent = test[1]) != test[2]:
2080 print('ERROR: none_if(%s) returned [%s], expected [%s]' % (test[0], none_if(test[0], test[1]), test[2]))
2081
2082 return True
2083
2085 tests = [
2086 [True, 'Yes', 'Yes', 'Yes'],
2087 [False, 'OK', 'not OK', 'not OK']
2088 ]
2089 for test in tests:
2090 if bool2str(test[0], test[1], test[2]) != test[3]:
2091 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]))
2092
2093 return True
2094
2096
2097 print(bool2subst(True, 'True', 'False', 'is None'))
2098 print(bool2subst(False, 'True', 'False', 'is None'))
2099 print(bool2subst(None, 'True', 'False', 'is None'))
2100
2107
2109 print("testing size2str()")
2110 print("------------------")
2111 tests = [0, 1, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000, 100000000000, 1000000000000, 10000000000000]
2112 for test in tests:
2113 print(size2str(test))
2114
2116
2117 test = """
2118 second line\n
2119 3rd starts with tab \n
2120 4th with a space \n
2121
2122 6th
2123
2124 """
2125 print(unwrap(text = test, max_length = 25))
2126
2128 test = 'line 1\nline 2\nline 3'
2129
2130 print("wrap 5-6-7 initial 0, subsequent 0")
2131 print(wrap(test, 5))
2132 print()
2133 print(wrap(test, 6))
2134 print()
2135 print(wrap(test, 7))
2136 print("-------")
2137 input()
2138 print("wrap 5 initial 1-1-3, subsequent 1-3-1")
2139 print(wrap(test, 5, ' ', ' '))
2140 print()
2141 print(wrap(test, 5, ' ', ' '))
2142 print()
2143 print(wrap(test, 5, ' ', ' '))
2144 print("-------")
2145 input()
2146 print("wrap 6 initial 1-1-3, subsequent 1-3-1")
2147 print(wrap(test, 6, ' ', ' '))
2148 print()
2149 print(wrap(test, 6, ' ', ' '))
2150 print()
2151 print(wrap(test, 6, ' ', ' '))
2152 print("-------")
2153 input()
2154 print("wrap 7 initial 1-1-3, subsequent 1-3-1")
2155 print(wrap(test, 7, ' ', ' '))
2156 print()
2157 print(wrap(test, 7, ' ', ' '))
2158 print()
2159 print(wrap(test, 7, ' ', ' '))
2160
2162 print('md5 %s: %s' % (sys.argv[2], file2md5(sys.argv[2])))
2163 print('chunked md5 %s: %s' % (sys.argv[2], file2chunked_md5(sys.argv[2])))
2164
2167
2172
2174 tests = ['\\', '^', '~', '{', '}', '%', '&', '#', '$', '_', u_euro, 'abc\ndef\n\n1234']
2175 tests.append(' '.join(tests))
2176 for test in tests:
2177 print('%s:' % test, tex_escape_string(test))
2178
2179
2181 tests = ['\\', '^', '~', '{', '}', '%', '&', '#', '$', '_', u_euro, 'abc\ndef\n\n1234']
2182 tests.append(' '.join(tests))
2183 tests.append('C:\Windows\Programme\System 32\lala.txt')
2184 tests.extend([
2185 'should be identical',
2186 'text *some text* text',
2187 """A List
2188 ======
2189
2190 1. 1
2191 2. 2
2192
2193 3. ist-list
2194 1. more
2195 2. noch was ü
2196 #. nummer x"""
2197 ])
2198 for test in tests:
2199 print('==================================================')
2200 print('raw:')
2201 print(test)
2202 print('---------')
2203 print('ReST 2 LaTeX:')
2204 latex = rst2latex_snippet(test)
2205 print(latex)
2206 if latex.strip() == test.strip():
2207 print('=> identical')
2208 print('---------')
2209 print('tex_escape_string:')
2210 print(tex_escape_string(test))
2211 input()
2212
2213
2215 tests = [
2216 'one line, no embedded line breaks ',
2217 'one line\nwith embedded\nline\nbreaks\n '
2218 ]
2219 for test in tests:
2220 print('as list:')
2221 print(strip_trailing_empty_lines(text = test, eol='\n', return_list = True))
2222 print('as string:')
2223 print('>>>%s<<<' % strip_trailing_empty_lines(text = test, eol='\n', return_list = False))
2224 tests = [
2225 ['list', 'without', 'empty', 'trailing', 'lines'],
2226 ['list', 'with', 'empty', 'trailing', 'lines', '', ' ', '']
2227 ]
2228 for test in tests:
2229 print('as list:')
2230 print(strip_trailing_empty_lines(lines = test, eol = '\n', return_list = True))
2231 print('as string:')
2232 print(strip_trailing_empty_lines(lines = test, eol = '\n', return_list = False))
2233
2235 tests = [
2236 r'abc.exe',
2237 r'\abc.exe',
2238 r'c:\abc.exe',
2239 r'c:\d\abc.exe',
2240 r'/home/ncq/tmp.txt',
2241 r'~/tmp.txt',
2242 r'./tmp.txt',
2243 r'./.././tmp.txt',
2244 r'tmp.txt'
2245 ]
2246 for t in tests:
2247 print("[%s] -> [%s]" % (t, fname_stem(t)))
2248
2250 print(sys.argv[2], 'empty:', dir_is_empty(sys.argv[2]))
2251
2252
2254 d1 = {}
2255 d2 = {}
2256 d1[1] = 1
2257 d1[2] = 2
2258 d1[3] = 3
2259
2260 d1[5] = 5
2261
2262 d2[1] = 1
2263 d2[2] = None
2264
2265 d2[4] = 4
2266
2267
2268
2269 d1 = {1: 1, 2: 2}
2270 d2 = {1: 1, 2: 2}
2271
2272
2273 print(format_dict_like(d1, tabular = False))
2274 print(format_dict_like(d1, tabular = True))
2275
2276
2277
2298
2299
2301 rmdir('cx:\windows\system3__2xxxxxxxxxxxxx')
2302
2303
2305
2306 print(rm_dir_content('/tmp/user/1000/tmp'))
2307
2308
2310 tests = [
2311 ('', '', ''),
2312 ('a', 'a', ''),
2313 ('GMd: a window title', _GM_TITLE_PREFIX + ':', 'a window title'),
2314 ('\.br\MICROCYTES+1\.br\SPHEROCYTES present\.br\POLYCHROMASIAmoderate\.br\\', '\.br\\', 'MICROCYTES+1\.br\SPHEROCYTES present\.br\POLYCHROMASIAmoderate\.br\\')
2315 ]
2316 for test in tests:
2317 text, prefix, expect = test
2318 result = strip_prefix(text, prefix, remove_whitespace = True)
2319 if result == expect:
2320 continue
2321 print('test failed:', test)
2322 print('result:', result)
2323
2324
2326 tst = [
2327 ('123', 1),
2328 ('123', 2),
2329 ('123', 3),
2330 ('123', 4),
2331 ('', 1),
2332 ('1', 1),
2333 ('12', 1),
2334 ('', 2),
2335 ('1', 2),
2336 ('12', 2),
2337 ('123', 2)
2338 ]
2339 for txt, lng in tst:
2340 print('max', lng, 'of', txt, '=', shorten_text(txt, lng))
2341
2343 tests = [
2344 '/tmp/test.txt',
2345 '/tmp/ test.txt',
2346 '/tmp/ tes\\t.txt',
2347 'test'
2348 ]
2349 for test in tests:
2350 print (test, fname_sanitize(test))
2351
2352
2355
2356
2358 parts = enumerate_removable_partitions()
2359 for part_name in parts:
2360 part = parts[part_name]
2361 print(part['device'])
2362 print(part['partition'])
2363 if part['is_mounted']:
2364 print('%s@%s: %s on %s by %s @ %s (FS=%s: %s free of %s total)' % (
2365 part['type'],
2366 part['bus'],
2367 part['fs_label'],
2368 part['model'],
2369 part['vendor'],
2370 part['mountpoint'],
2371 part['fs_type'],
2372 part['bytes_free'],
2373 part['size_in_bytes']
2374 ))
2375 else:
2376 print('%s@%s: %s on %s by %s (not mounted)' % (
2377 part['type'],
2378 part['bus'],
2379 part['fs_label'],
2380 part['model'],
2381 part['vendor']
2382 ))
2383
2384
2386 for writer in enumerate_optical_writers():
2387 print('%s@%s: %s @ %s' % (
2388 writer['type'],
2389 writer['bus'],
2390 writer['model'],
2391 writer['device']
2392 ))
2393
2394
2396 print(sys.argv[2], '->', sys.argv[3])
2397 print(copy_tree_content(sys.argv[2], sys.argv[3]))
2398
2399
2402
2403
2405 dicts = [
2406 {'pkey': 1, 'value': 'a1'},
2407 {'pkey': 2, 'value': 'b2'},
2408 {'pkey': 3, 'value': 'c3'},
2409 {'pkey': 4, 'value': 'd4'},
2410 {'pkey': 5, 'value': 'd4'},
2411 {'pkey': 5, 'value': 'c5'},
2412 ]
2413 with open('x.txt', 'w', encoding = 'utf8') as f:
2414 f.write(dicts2table(dicts, left_margin=2, eol='\n', keys2ignore=None, show_only_changes=True, headers = ['d1', 'd2', 'd3', 'd4', 'd5', 'd6']))
2415
2416
2417
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458 test_decorate_window_title()
2459
2460
2461