Debug Tools
utilities.py
1 #!/usr/bin/env python3
2 # -*- coding: UTF-8 -*-
3 
4 
38 
39 # To run this file, run on the Sublime Text console:
40 # import imp; import debugtools.all.debug_tools.utilities; imp.reload( debugtools.all.debug_tools.utilities )
41 
42 import os
43 import io
44 import re
45 import sys
46 
47 import time
48 import random
49 
50 import threading
51 import traceback
52 import textwrap
53 
54 from collections import OrderedDict
55 
56 try:
57  from natsort import natsorted
58 
59 except( ImportError, ValueError ):
60 
61  def natsorted(*args, **kwargs):
62  raise RuntimeError( "The library natsort is required to run this function.\nYou can install it with `pip install natsort`" )
63 
64 try:
65  import diff_match_patch
66 
67 except( ImportError, ValueError ):
68  diffmatchpatch = None
69  diff_match_patch = None
70 
71 
72 is_python2 = False
73 
74 if sys.version_info[0] < 3:
75  is_python2 = True
76  Event = threading._Event
77 
78 else:
79  Event = threading.Event
80 
81 class SleepEvent(Event):
82 
83  def __init__(self):
84  super( SleepEvent, self ).__init__()
85 
86  def sleep(self, timeout=None):
87  """
88  If blockOnSleepCall() was called before, this will sleep the current thread for ever if
89  not arguments are passed. Otherwise, it accepts a positive floating point number for
90  seconds in which this thread will sleep.
91 
92  If disableSleepCall() is called before the timeout has passed, it will immediately get
93  out of sleep and this thread will wake up.
94  """
95  self.wait( timeout )
96 
97  def blockOnSleepCall(self):
98  self.clear()
99 
100  def disableSleepCall(self):
101  self.set()
102 
103 
104 if is_python2:
105  # Based on Python 3's textwrap.indent
106  def textwrap_indent(text, prefix, predicate=None):
107  """Adds 'prefix' to the beginning of selected lines in 'text'.
108  If 'predicate' is provided, 'prefix' will only be added to the lines
109  where 'predicate(line)' is True. If 'predicate' is not provided,
110  it will default to adding 'prefix' to all non-empty lines that do not
111  consist solely of whitespace characters.
112  """
113  if predicate is None:
114  def predicate(line):
115  return line.strip()
116 
117  def prefixed_lines():
118  for line in text.splitlines(True):
119  yield (prefix + line if predicate(line) else line)
120  return u"".join(prefixed_lines())
121 
122 else:
123  textwrap_indent = textwrap.indent
124 
125 
126 if diff_match_patch:
127 
128  if is_python2:
129  # Bail out at 65535 because unichr(65536) throws.
130  _g_maximum_lines = 40000
131  _g_char_limit = 65535
132  else:
133  unichr = chr
134 
135  # Bail out at 1114111 because unichr(1114112) throws.
136  _g_maximum_lines = 666666
137  _g_char_limit = 1114111
138 
139  class diffmatchpatch(diff_match_patch.diff_match_patch):
140 
141  def diff_prettyText(self, diffs):
142  """Convert a diff array into a pretty Text report.
143  Args:
144  diffs: Array of diff tuples.
145  Returns:
146  Text representation.
147  """
148  last_text = "\n"
149  last_op_type = self.DIFF_EQUAL
150 
151  results_diff = []
152  cut_next_new_line = [False]
153 
154  # print('\ndiffs:\n%s\n' % diffs)
155  operations = (self.DIFF_INSERT, self.DIFF_DELETE)
156 
157  def parse(sign):
158  # print('new1:', text.encode( 'ascii' ))
159 
160  if text:
161  new = text
162 
163  else:
164  return ''
165 
166  new = textwrap_indent( "%s" % new, sign, lambda line: True )
167 
168  # force the diff change to show up on a new line for highlighting
169  if len(results_diff) > 0:
170  new = '\n' + new
171 
172  if new[-1] == '\n':
173 
174  if op == self.DIFF_INSERT and next_text and new[-1] == '\n' and next_text[0] == '\n':
175  cut_next_new_line[0] = True;
176 
177  # Avoids a double plus sign showing up when the diff has the element (1, '\n')
178  if len(text) > 1: new = new + '%s\n' % sign
179 
180  elif next_op not in operations and next_text and next_text[0] != '\n':
181  new = new + '\n'
182 
183  # print('new2:', new.encode( 'ascii' ))
184  return new
185 
186  for index in range(len(diffs)):
187  op, text = diffs[index]
188  if index < len(diffs) - 1:
189  next_op, next_text = diffs[index+1]
190  else:
191  next_op, next_text = (0, "")
192 
193  if op == self.DIFF_INSERT:
194  results_diff.append( parse( "+ " ) )
195 
196  elif op == self.DIFF_DELETE:
197  results_diff.append( parse( "- " ) )
198 
199  elif op == self.DIFF_EQUAL:
200  # print('new3:', text.encode( 'ascii' ))
201  # if last_op_type != op or last_text and last_text[-1] == '\n': text = textwrap_indent(text, " ")
202  text = textwrap_indent(text, " ")
203 
204  if cut_next_new_line[0]:
205  cut_next_new_line[0] = False
206  text = text[1:]
207 
208  results_diff.append(text)
209  # print('new4:', text.encode( 'ascii' ))
210 
211  last_text = text
212  last_op_type = op
213 
214  return "".join(results_diff)
215 
216  def diff_linesToWords(self, text1, text2, delimiter=re.compile('\n')):
217  """
218  Split two texts into an array of strings. Reduce the texts to a string
219  of hashes where each Unicode character represents one line.
220 
221  95% of this function code is copied from `diff_linesToChars` on:
222  https://github.com/google/diff-match-patch/blob/895a9512bbcee0ac5a8ffcee36062c8a79f5dcda/python3/diff_match_patch.py#L381
223 
224  Copyright 2018 The diff-match-patch Authors.
225  https://github.com/google/diff-match-patch
226  Licensed under the Apache License, Version 2.0 (the "License");
227  you may not use this file except in compliance with the License.
228  You may obtain a copy of the License at
229  http://www.apache.org/licenses/LICENSE-2.0
230 
231  Args:
232  text1: First string.
233  text2: Second string.
234  delimiter: a re.compile() expression for the word delimiter type
235 
236  Returns:
237  Three element tuple, containing the encoded text1, the encoded text2 and
238  the array of unique strings. The zeroth element of the array of unique
239  strings is intentionally blank.
240  """
241  lineArray = [] # e.g. lineArray[4] == "Hello\n"
242  lineHash = {} # e.g. lineHash["Hello\n"] == 4
243 
244  # "\x00" is a valid character, but various debuggers don't like it.
245  # So we'll insert a junk entry to avoid generating a null character.
246  lineArray.append('')
247 
248  def diff_linesToCharsMunge(text):
249  """Split a text into an array of strings. Reduce the texts to a string
250  of hashes where each Unicode character represents one line.
251  Modifies linearray and linehash through being a closure.
252  Args:
253  text: String to encode.
254  Returns:
255  Encoded string.
256  """
257  chars = []
258  # Walk the text, pulling out a substring for each line.
259  # text.split('\n') would would temporarily double our memory footprint.
260  # Modifying text would create many large strings to garbage collect.
261  lineStart = 0
262  lineEnd = -1
263  while lineEnd < len(text) - 1:
264  lineEnd = delimiter.search(text, lineStart)
265 
266  if lineEnd:
267  lineEnd = lineEnd.start()
268 
269  else:
270  lineEnd = len(text) - 1
271 
272  line = text[lineStart:lineEnd + 1]
273 
274  if line in lineHash:
275  chars.append(unichr(lineHash[line]))
276  else:
277  if len(lineArray) == maxLines:
278  # Bail out at maxLines because unichr(maxLines+1) throws.
279  line = text[lineStart:]
280  lineEnd = len(text)
281  lineArray.append(line)
282  lineHash[line] = len(lineArray) - 1
283  chars.append(unichr(len(lineArray) - 1))
284  lineStart = lineEnd + 1
285  return "".join(chars)
286 
287  # Allocate 2/3rds of the space for text1, the rest for text2.
288  maxLines = _g_maximum_lines
289  chars1 = diff_linesToCharsMunge(text1)
290  maxLines = _g_char_limit
291  chars2 = diff_linesToCharsMunge(text2)
292  return (chars1, chars2, lineArray)
293 
294 
295 # An unique identifier for any created object
296 initial_hash = random.getrandbits( 32 )
297 
298 def get_unique_hash():
299  """
300  Generates an unique identifier which can be used to uniquely identify distinct object
301  instances.
302  """
303  global initial_hash
304 
305  initial_hash += 1
306  return initial_hash
307 
308 
309 def pop_dict_last_item(dictionary):
310  """
311  Until python 3.5 the popitem() has has a bug where it does not accepts the last=False argument
312  https://bugs.python.org/issue24394 TypeError: popitem() takes no keyword arguments, then,
313  automatically detect which one we have here:
314  https://docs.python.org/3/library/collections.html#collections.OrderedDict.popitem
315  """
316  dictionary.popitem(last=True)
317 
318 try:
319  {1: 'a'}.popitem(last=True)
320 
321 except TypeError:
322 
323  def pop_dict_last_item(dictionary):
324  dictionary.popitem()
325 
326 
327 def move_to_dict_beginning(dictionary, key):
328  """
329  Move a OrderedDict item to its beginning, or add it to its beginning.
330 
331  Compatible with Python 2.7
332  https://stackoverflow.com/questions/16664874/how-to-add-an-element-to-the-beginning-of-an-ordereddict
333  """
334 
335  if is_python2:
336  value = dictionary[key]
337  del dictionary[key]
338  root = dictionary._OrderedDict__root
339 
340  first = root[1]
341  root[1] = first[0] = dictionary._OrderedDict__map[key] = [root, first, key]
342  dict.__setitem__(dictionary, key, value)
343 
344  else:
345  dictionary.move_to_end( key, last=False )
346 
347 
348 def get_relative_path(relative_path, script_file):
349  """
350  Computes a relative path for a file on the same folder as this class file declaration.
351  https://stackoverflow.com/questions/4381569/python-os-module-open-file-above-current-directory-with-relative-path
352  """
353  basepath = os.path.dirname( script_file )
354  filepath = os.path.abspath( os.path.join( basepath, relative_path ) )
355  return filepath
356 
357 
358 def join_path(*args):
359  """ Call join path and then abspath on the result. """
360  return os.path.abspath( os.path.join( *args ) )
361 
362 
363 def get_duplicated_elements(elements_list):
364  """
365  Given an `elements_list` with duplicated elements, return a set only with the duplicated
366  elements in the list. If there are not duplicated elements, an empty set is returned.
367 
368  How do I find the duplicates in a list and create another list with them?
369  https://stackoverflow.com/questions/9835762/how-do-i-find-the-duplicates-in-a-list-and-create-another-list-with-them
370  """
371  visited_elements = set()
372  visited_and_duplicated = set()
373 
374  add_item_to_visited_elements = visited_elements.add
375  add_item_to_visited_and_duplicated = visited_and_duplicated.add
376 
377  for item in elements_list:
378 
379  if item in visited_elements:
380  add_item_to_visited_and_duplicated(item)
381 
382  else:
383  add_item_to_visited_elements(item)
384 
385  return visited_and_duplicated
386 
387 
388 def emquote_string(string):
389  """
390  Return a string escape into single or double quotes accordingly to its contents.
391  """
392  string = str( string )
393  is_single = "'" in string
394  is_double = '"' in string
395 
396  if is_single and is_double:
397  return '"{}"'.format( string.replace( "'", "\\'" ) )
398 
399  if is_single:
400  return '"{}"'.format( string )
401 
402  return "'{}'".format( string )
403 
404 
405 def sort_dictionary_lists(dictionary):
406  """
407  Give a dictionary, call `sorted` on all its elements.
408  """
409 
410  for key, value in dictionary.items():
411  dictionary[key] = sorted( value )
412 
413  return dictionary
414 
415 
416 def sort_alphabetically_and_by_length(iterable):
417  """
418  Give an `iterable`, sort its elements accordingly to the following criteria:
419  1. Sorts normally by alphabetical order
420  2. Sorts by descending length
421 
422  How to sort by length of string followed by alphabetical order?
423  https://stackoverflow.com/questions/4659524/how-to-sort-by-length-of-string-followed-by-alphabetical-order
424  """
425  return sorted( sorted( natsorted( iterable, key=lambda item: str( item ).lower() ),
426  key=lambda item: str( item ).istitle() ),
427  key=lambda item: len( str( item ) ) )
428 
429 
430 def sort_correctly(iterable):
431  """
432  Sort the given iterable in the way that humans expect.
433 
434  How to sort alpha numeric set in python
435  https://stackoverflow.com/questions/2669059/how-to-sort-alpha-numeric-set-in-python
436  """
437  convert = lambda text: int( text ) if text.isdigit() else text
438  alphanum_key = lambda key: [convert( characters ) for characters in re.split( '([0-9]+)', str( key ).lower() )]
439  return sorted( sorted( iterable, key=alphanum_key ), key=lambda item: str( item ).istitle() )
440 
441 
442 def sort_dictionary(dictionary):
443  return OrderedDict( sorted( dictionary.items() ) )
444 
445 
446 def sort_dictionaries_on_list(list_of_dictionaries):
447  sorted_dictionaries = []
448 
449  for dictionary in list_of_dictionaries:
450  sorted_dictionaries.append( sort_dictionary( dictionary ) )
451 
452  return sorted_dictionaries
453 
454 
455 def sort_list_of_dictionaries(list_of_dictionaries):
456  """
457  How do I sort a list of dictionaries by values of the dictionary in Python?
458  https://stackoverflow.com/questions/72899/how-do-i-sort-a-list-of-dictionaries-by-values-of-the-dictionary-in-python
459 
460  case-insensitive list sorting, without lowercasing the result?
461  https://stackoverflow.com/questions/10269701/case-insensitive-list-sorting-without-lowercasing-the-result
462  """
463  sorted_dictionaries = sort_dictionaries_on_list( list_of_dictionaries )
464  return sorted( sorted_dictionaries, key=lambda k: k['name'].lower() )
465 
466 
467 def get_largest_item_size(iterable):
468  """
469  Given a iterable, get the size/length of its largest key value.
470  """
471  largest_key = 0
472 
473  for key in iterable:
474 
475  if len( key ) > largest_key:
476  largest_key = len( key )
477 
478  return largest_key
479 
480 
481 def dictionary_to_string(dictionary):
482  """
483  Given a dictionary with a list for each string key, call `sort_dictionary_lists()` and
484  return a string representation by line of its entries.
485  """
486 
487  if not len( dictionary ):
488  return " No elements found."
489 
490  strings = []
491  elements_strings = []
492 
493  dictionary = sort_dictionary_lists( dictionary )
494  largest_key = get_largest_item_size( dictionary.keys() ) + 1
495 
496  for key, values in dictionary.items():
497  elements_strings.clear()
498 
499  for item in values:
500  elements_strings.append( "{}".format( str( item ) ) )
501 
502  strings.append( "{:>{largest_key}}: {}".format( str( key ), " ".join( elements_strings ),
503  largest_key=largest_key ) )
504 
505  return "\n".join( strings )
506 
507 
508 def convert_to_text_lines(iterable, use_repr=True, new_line=True, sort=None):
509  """
510  Given a dictionary with a list for each string key, call `sort_dictionary_lists()` and
511  return a string representation by line of its entries.
512  """
513 
514  if isinstance( iterable, dict):
515  return dictionary_to_string( iterable )
516 
517  if not iterable:
518  return " No elements found."
519 
520  strings = []
521 
522  if sort:
523  iterable = sort( iterable )
524 
525  else:
526  iterable = sort_alphabetically_and_by_length( iterable )
527 
528  for item in iterable:
529  strings.append( " {}".format( repr( item ) ) )
530 
531  return ( "\n" if new_line else "" ).join( strings )
532 
533 
534 def getCleanSpaces(inputText, minimumLength=0, lineCutTrigger="", keepSpaceSepators=False):
535  """
536  Removes spaces and comments from the input expression.
537 
538  `minimumLength` of a line to not be ignored
539  `lineCutTrigger` all lines after a line starting with this string will be ignored
540  `keepSpaceSepators` if True, it will keep at a single space between sentences as `S S`, given `S S`
541  """
542 
543  if keepSpaceSepators:
544  removeNewSpaces = ' '.join( inputText.split( ' ' ) )
545  lineCutTriggerNew = ' '.join( lineCutTrigger.split( ' ' ) ).strip( ' ' )
546 
547  else:
548  removeNewSpaces = re.sub( r"\t| ", "", inputText )
549  lineCutTriggerNew = re.sub( r"\t| ", "", lineCutTrigger )
550 
551  # print( "%s" % ( inputText ) )
552  lines = removeNewSpaces.split( "\n" )
553  clean_lines = []
554 
555  for line in lines:
556 
557  if keepSpaceSepators:
558  line = line.strip( ' ' )
559 
560  if minimumLength:
561 
562  if len( line ) < minimumLength:
563  continue
564 
565  if lineCutTrigger:
566 
567  if line.startswith( lineCutTriggerNew ):
568  break
569 
570  if line.startswith( "#" ):
571  continue
572 
573  clean_lines.append( line )
574 
575  return clean_lines
576 
577 
578 def wrap_text(text, wrap=0, trim_tabs=None, trim_spaces=None, trim_lines=None,
579  trim_plus='+', indent="", initial="", single_lines=False):
580  """
581  1. Remove input text leading common indentation, trailing white spaces
582  2. If `wrap`, wraps big lists on 80 characters
583  3. If `trim_tabs`, replace all tabs this value
584  4. If `trim_spaces`, remove this leading symbols
585  5. If `trim_lines`, replace all new line characters by this value
586  5. If `indent`, the subsequent indent to use
587  6. If `trim_plus`, remove this leading symbols
588  7. If `single_lines`, remove single new lines but keep consecutive new lines
589  """
590  clean_lines = []
591 
592  if not isinstance( text, str ):
593  text = str( text )
594 
595  if trim_tabs is not None:
596  text = text.replace( '\t', trim_tabs )
597 
598  dedent_lines = textwrap.dedent( text ).strip( '\n' )
599 
600  if trim_spaces and trim_plus:
601 
602  for line in dedent_lines.split( '\n' ):
603  line = line.rstrip( trim_spaces ).lstrip( trim_plus )
604  clean_lines.append( line )
605 
606  elif trim_spaces:
607 
608  for line in dedent_lines.split( '\n' ):
609  line = line.rstrip( trim_spaces )
610  clean_lines.append( line )
611 
612  elif trim_plus:
613 
614  for line in dedent_lines.split( '\n' ):
615  line = line.lstrip( trim_plus )
616  clean_lines.append( line )
617 
618  if trim_spaces is not None or trim_plus is not None:
619  dedent_lines = textwrap.dedent( "\n".join( clean_lines ) )
620 
621  if wrap:
622  clean_lines.clear()
623 
624  for line in dedent_lines.split( '\n' ):
625  line = textwrap.fill( line, width=wrap, initial_indent=initial, subsequent_indent=indent )
626  clean_lines.append( line )
627 
628  dedent_lines = "\n".join( clean_lines )
629 
630  if trim_lines is not None:
631  dedent_lines = trim_lines.join( dedent_lines.split( '\n' ) )
632 
633  if single_lines:
634  dedent_lines = re.sub( r'(?<!\n)\n(?!\n)', ' ', dedent_lines)
635 
636  return dedent_lines
637 
638 
639 def recursive_get_representation(*args, **kwargs):
640  """ It attempt to detect the `get_representation` function was called recursively
641  It can happen when one attribute contains the other and vice-versa.
642 
643  kwargs `recursive_depth=2` how many recursions levels to dive in
644  """
645  # https://stackoverflow.com/questions/22435992/python-trying-to-place-keyword-arguments-after-args
646  recursive_depth = kwargs.pop( "recursive_depth", 2 )
647 
648  # https://stackoverflow.com/questions/7900345/can-a-python-method-check-if-it-has-been-called-from-within-itself
649  is_recursive = len(
650  [ stack[-3]
651  for stack in traceback.extract_stack()
652  if stack[2] == 'recursive_get_representation'
653  ]
654  ) > recursive_depth
655 
656  kwargs['is_recursive'] = is_recursive
657  return get_representation(*args, **kwargs)
658 
659 
660 def get_representation(objectname, ignore=[], emquote=False, repr=repr, is_recursive=False):
661  """ Iterating through all its public attributes and return then as a string representation
662  `ignore` a list of attributes to be ignored
663  `emquote` if True, puts the attributes values inside single or double quotes accordingly.
664  `repr` is the callback to call recursively on nested objects, can be either `repr` or `str`.
665  """
666  clean_attributes = []
667 
668  if emquote:
669 
670  def pack_attribute(string):
671  return emquote_string( string )
672 
673  else:
674 
675  def pack_attribute(string):
676  return string
677 
678  if hasattr( objectname, '__dict__' ):
679  valid_attributes = objectname.__dict__.keys()
680 
681  if is_recursive:
682  clean_attributes.append( "{}".format( '<recursive>' ) )
683 
684  else:
685  for attribute in valid_attributes:
686 
687  if not attribute.startswith( '_' ) and attribute not in ignore:
688  clean_attributes.append( "{}: {}".format( attribute, pack_attribute( objectname.__dict__[attribute] ) ) )
689 
690  return "%s %s;" % ( objectname.__class__.__name__, ", ".join( clean_attributes ) )
691 
692  else:
693  for attribute in objectname:
694  if attribute not in ignore:
695  clean_attributes.append( "{}".format( pack_attribute( attribute ) ) )
696 
697  return "%s" % ", ".join( clean_attributes )
698 
699 
700 def _create_stdout_handler():
701  """
702  Call this method to create a copy of this stream model as `stdout_replacement.py`
703  using the `sys.stdout` instead of `sys.stderr`.
704  """
705  model_relative_path = get_relative_path( 'stderr_replacement.py', __file__ )
706  destine_relative_path = get_relative_path( 'stdout_replacement.py', __file__ )
707 
708  warning_message = wrap_text(
709  """
710  \"\"\"
711  This file is generated automatically based `stderr_replacement.py` while `debug_tools`
712  library/package is being developed.
713 
714  If you developing the `debug_tools` library, please do not edit this file, but the file
715  `stderr_replacement.py` and run `logger.py` function `create_stdout_handler()` by
716  uncommenting it on `all/debug_tools/logger.py` file.
717  \"\"\"
718  """ )
719 
720  sys.stderr.write( '\nCreating the `stdout_replacement.py` file!\n' )
721  sys.stderr.write( 'model_relative_path %s\n' % model_relative_path )
722  sys.stderr.write( 'destine_relative_path %s\n' % destine_relative_path )
723 
724  # https://stackoverflow.com/questions/29151181/writing-an-ascii-string-as-binary-in-python
725  # https://stackoverflow.com/questions/33054527/python-3-5-typeerror-a-bytes-like-object-is-required-not-str-when-writing-t
726  with io.open(model_relative_path, 'rb') as model_file:
727  model_text = model_file.read().decode('utf-8')
728 
729  with io.open(destine_relative_path, 'wb') as destine_file:
730  model_text = model_text.replace( 'stderr', 'stdout' )
731  model_text = model_text.replace( '# Warning message here', warning_message )
732  destine_file.write( model_text.encode() )
733 
def sleep(self, timeout=None)
Definition: utilities.py:86
def diff_linesToWords(self, text1, text2, delimiter=re.compile('\n'))
Definition: utilities.py:216