common.py
Go to the documentation of this file.
1 # -*- coding: utf-8 -*-
2 """
3 Common utilities.
4 
5 ------------------------------------------------------------------------------
6 This file is part of grepros - grep for ROS1 bag files and live topics.
7 Released under the BSD License.
8 
9 @author Erki Suurjaak
10 @created 23.10.2021
11 @modified 28.12.2023
12 ------------------------------------------------------------------------------
13 """
14 
15 from __future__ import print_function
16 import argparse
17 import copy
18 import datetime
19 import functools
20 import glob
21 import importlib
22 import inspect
23 import io
24 import itertools
25 import logging
26 import math
27 import os
28 import re
29 import shutil
30 import sys
31 import threading
32 import time
33 try: import curses
34 except ImportError: curses = None
35 
36 import six
37 try: import zstandard
38 except ImportError: zstandard = None
39 
40 
41 
42 PATH_TYPES = (six.binary_type, six.text_type)
43 if six.PY34: PATH_TYPES += (importlib.import_module("pathlib").Path, )
44 
45 STRING_TYPES = (six.binary_type, six.text_type)
46 
47 TEXT_TYPES = (six.binary_type, six.text_type) if six.PY2 else (six.text_type, )
48 
49 
50 class MatchMarkers(object):
51  """Highlight markers for matches in message values."""
52 
53 
54  ID = "matching"
55 
56  START = "<%s>" % ID
57 
58  END = "</%s>" % ID
59 
60  EMPTY = START + END
61 
62  EMPTY_REPL = "%s''%s" % (START, END)
63 
64  @classmethod
65  def populate(cls, value):
66  """Populates highlight markers with specified value."""
67  cls.ID = str(value)
68  cls.START = "<%s>" % cls.ID
69  cls.END = "</%s>" % cls.ID
70  cls.EMPTY = cls.START + cls.END
71  cls.EMPTY_REPL = "%s''%s" % (cls.START, cls.END)
72 
73 
74 
75 class ConsolePrinter(object):
76  """
77  Prints to console, supports color output.
78 
79  If configured with `apimode=True`, logs debugs and warnings to logger and raises errors.
80  """
81 
82  STYLE_RESET = "\x1b(B\x1b[m" # Default color+weight
83  STYLE_HIGHLIGHT = "\x1b[31m" # Red
84  STYLE_LOWLIGHT = "\x1b[38;2;105;105;105m" # Dim gray
85  STYLE_SPECIAL = "\x1b[35m" # Purple
86  STYLE_SPECIAL2 = "\x1b[36m" # Cyan
87  STYLE_WARN = "\x1b[33m" # Yellow
88  STYLE_ERROR = "\x1b[31m\x1b[2m" # Dim red
89 
90  DEBUG_START, DEBUG_END = STYLE_LOWLIGHT, STYLE_RESET # Metainfo wrappers
91  WARN_START, WARN_END = STYLE_WARN, STYLE_RESET # Warning message wrappers
92  ERROR_START, ERROR_END = STYLE_ERROR, STYLE_RESET # Error message wrappers
93 
94 
95  COLOR = None
96 
97 
98  WIDTH = 80
99 
100 
101  PRINTS = {}
102 
103 
104  APIMODE = False
105 
106  _COLORFLAG = None
107 
108  _LINEOPEN = False
109 
110  _UNIQUES = set()
111 
112  @classmethod
113  def configure(cls, color=True, apimode=False):
114  """
115  Initializes printer, for terminal output or library mode.
116 
117  For terminal output, initializes terminal colors, or disables colors if unsupported.
118 
119  @param color True / False / None for auto-detect from TTY support;
120  will be disabled if terminal does not support colors
121  @param apimode whether to log debugs and warnings to logger and raise errors,
122  instead of printing
123  """
124  cls.APIMODE = bool(apimode)
125  cls._COLORFLAG = color
126  if apimode:
127  cls.DEBUG_START, cls.DEBUG_END = "", ""
128  cls.WARN_START, cls.WARN_END = "", ""
129  cls.ERROR_START, cls.ERROR_END = "", ""
130  else: cls.init_terminal()
131 
132 
133  @classmethod
134  def init_terminal(cls):
135  """Initializes terminal for color output, or disables color output if unsupported."""
136  if cls.COLOR is not None: return
137 
138  try: cls.WIDTH = shutil.get_terminal_size().columns # Py3
139  except Exception: pass # Py2
140  cls.COLOR = (cls._COLORFLAG is not False)
141  try:
142  curses.setupterm()
143  if cls.COLOR and not sys.stdout.isatty():
144  raise Exception()
145  except Exception:
146  cls.COLOR = bool(cls._COLORFLAG)
147  try:
148  if sys.stdout.isatty() or cls.COLOR:
149  cls.WIDTH = curses.initscr().getmaxyx()[1]
150  curses.endwin()
151  except Exception: pass
152 
153  if cls.COLOR:
154  cls.DEBUG_START, cls.DEBUG_END = cls.STYLE_LOWLIGHT, cls.STYLE_RESET
155  cls.WARN_START, cls.WARN_END = cls.STYLE_WARN, cls.STYLE_RESET
156  cls.ERROR_START, cls.ERROR_END = cls.STYLE_ERROR, cls.STYLE_RESET
157  else:
158  cls.DEBUG_START, cls.DEBUG_END = "", ""
159  cls.WARN_START, cls.WARN_END = "", ""
160  cls.ERROR_START, cls.ERROR_END = "", ""
161 
162 
163  @classmethod
164  def print(cls, text="", *args, **kwargs):
165  """
166  Prints text, formatted with args and kwargs.
167 
168  @param __file file object to print to if not sys.stdout
169  @param __end line end to use if not linefeed "\n"
170  @param __once whether text should be printed only once
171  and discarded on any further calls (applies to unformatted text)
172  """
173  text = str(text)
174  if kwargs.pop("__once", False):
175  if text in cls._UNIQUES: return
176  cls._UNIQUES.add(text)
177  fileobj, end = kwargs.pop("__file", sys.stdout), kwargs.pop("__end", "\n")
178  pref, suff = kwargs.pop("__prefix", ""), kwargs.pop("__suffix", "")
179  if cls._LINEOPEN and "\n" in end: pref = "\n" + pref # Add linefeed to end open line
180  text = cls._format(text, *args, **kwargs)
181 
182  cls.PRINTS[fileobj] = cls.PRINTS.get(fileobj, 0) + 1
183  cls._LINEOPEN = "\n" not in end
184  cls.init_terminal()
185  print(pref + text + suff, end=end, file=fileobj)
186  not fileobj.isatty() and fileobj.flush()
187 
188 
189  @classmethod
190  def error(cls, text="", *args, **kwargs):
191  """
192  Prints error to stderr, formatted with args and kwargs, in error colors if supported.
193 
194  Raises exception instead if APIMODE.
195  """
196  if cls.APIMODE:
197  raise Exception(cls._format(text, *args, __once=False, **kwargs))
198  KWS = dict(__file=sys.stderr, __prefix=cls.ERROR_START, __suffix=cls.ERROR_END)
199  cls.print(text, *args, **dict(kwargs, **KWS))
200 
201 
202  @classmethod
203  def warn(cls, text="", *args, **kwargs):
204  """
205  Prints warning to stderr, or logs to logger if APIMODE.
206 
207  Text is formatted with args and kwargs, in warning colors if supported.
208  """
209  if cls.APIMODE:
210  text = cls._format(text, *args, **kwargs)
211  if text: logging.getLogger(__name__).warning(text)
212  return
213  KWS = dict(__file=sys.stderr, __prefix=cls.WARN_START, __suffix=cls.WARN_END)
214  cls.print(text, *args, **dict(kwargs, **KWS))
215 
216 
217  @classmethod
218  def debug(cls, text="", *args, **kwargs):
219  """
220  Prints debug text to stderr, or logs to logger if APIMODE.
221 
222  Text is formatted with args and kwargs, in warning colors if supported.
223  """
224  if cls.APIMODE:
225  text = cls._format(text, *args, **kwargs)
226  if text: logging.getLogger(__name__).debug(text)
227  return
228  KWS = dict(__file=sys.stderr, __prefix=cls.DEBUG_START, __suffix=cls.DEBUG_END)
229  cls.print(text, *args, **dict(kwargs, **KWS))
230 
231 
232  @classmethod
233  def log(cls, level, text="", *args, **kwargs):
234  """
235  Prints text to stderr, or logs to logger if APIMODE.
236 
237  Text is formatted with args and kwargs, in level colors if supported.
238 
239  @param level logging level like `logging.ERROR` or "ERROR"
240  """
241  if cls.APIMODE:
242  text = cls._format(text, *args, **kwargs)
243  if text: logging.getLogger(__name__).log(level, text)
244  return
245  level = logging.getLevelName(level)
246  if not isinstance(level, TEXT_TYPES): level = logging.getLevelName(level)
247  func = {"DEBUG": cls.debug, "WARNING": cls.warn, "ERROR": cls.error}.get(level, cls.print)
248  func(text, *args, **dict(kwargs, __file=sys.stderr))
249 
250 
251  @classmethod
252  def flush(cls):
253  """Ends current open line, if any."""
254  if cls._LINEOPEN: print()
255  cls._LINEOPEN = False
256 
257 
258  @classmethod
259  def _format(cls, text="", *args, **kwargs):
260  """
261  Returns text formatted with printf-style or format() arguments.
262 
263  @param __once registers text, returns "" if text not unique
264  """
265  text, fmted = str(text), False
266  if kwargs.get("__once"):
267  if text in cls._UNIQUES: return ""
268  cls._UNIQUES.add(text)
269  for k in ("__file", "__end", "__once", "__prefix", "__suffix"): kwargs.pop(k, None)
270  try: text, fmted = (text % args if args else text), bool(args)
271  except Exception: pass
272  try: text, fmted = (text % kwargs if kwargs else text), fmted or bool(kwargs)
273  except Exception: pass
274  try: text = text.format(*args, **kwargs) if not fmted and (args or kwargs) else text
275  except Exception: pass
276  return text
277 
278 
279 class Decompressor(object):
280  """Decompresses zstandard archives."""
281 
282 
283  EXTENSIONS = (".zst", ".zstd")
284 
285 
286  ZSTD_MAGIC = b"\x28\xb5\x2f\xfd"
287 
288 
289  @classmethod
290  def decompress(cls, path, progress=False):
291  """
292  Decompresses file to same directory, showing optional progress bar.
293 
294  @return uncompressed file path
295  """
296  cls.validate()
297  path2, bar, size, processed = os.path.splitext(path)[0], None, os.path.getsize(path), 0
298  fmt = lambda s: format_bytes(s, strip=False)
299  if progress:
300  tpl = " Decompressing %s (%s): {afterword}" % (os.path.basename(path), fmt(size))
301  bar = ProgressBar(pulse=True, aftertemplate=tpl)
302 
303  ConsolePrinter.warn("Compressed file %s (%s), decompressing to %s.", path, fmt(size), path2)
304  bar and bar.update(0).start() # Start progress pulse
305  try:
306  with open(path, "rb") as f, open(path2, "wb") as g:
307  reader = zstandard.ZstdDecompressor().stream_reader(f)
308  while True:
309  chunk = reader.read(1048576)
310  if not chunk: break # while
311 
312  g.write(chunk)
313  processed += len(chunk)
314  bar and (setattr(bar, "afterword", fmt(processed)), bar.update(processed))
315  reader.close()
316  except Exception:
317  os.remove(path2)
318  raise
319  finally: bar and (setattr(bar, "pulse", False), bar.update(processed).stop())
320  return path2
321 
322 
323  @classmethod
324  def is_compressed(cls, path):
325  """Returns whether file is a recognized archive."""
326  result = os.path.isfile(path)
327  if result:
328  result = any(str(path).lower().endswith(x) for x in cls.EXTENSIONS)
329  if result:
330  with open(path, "rb") as f:
331  result = (f.read(len(cls.ZSTD_MAGIC)) == cls.ZSTD_MAGIC)
332  return result
333 
334 
335  @classmethod
336  def make_decompressed_name(cls, path):
337  """Returns the path without archive extension, if any."""
338  return os.path.splitext(path)[0] if cls.is_compressed(path) else path
339 
340 
341  @classmethod
342  def validate(cls):
343  """Raises error if decompression library not available."""
344  if not zstandard: raise Exception("zstandard not installed, cannot decompress")
345 
346 
347 
348 class ProgressBar(threading.Thread):
349  """
350  A simple ASCII progress bar with a ticker thread
351 
352  Drawn like
353  '[---------/ 36% ] Progressing text..'.
354  or for pulse mode
355  '[ ---- ] Progressing text..'.
356  """
357 
358  def __init__(self, max=100, value=0, min=0, width=30, forechar="-",
359  backchar=" ", foreword="", afterword="", interval=1,
360  pulse=False, aftertemplate=" {afterword}"):
361  """
362  Creates a new progress bar, without drawing it yet.
363 
364  @param max progress bar maximum value, 100%
365  @param value progress bar initial value
366  @param min progress bar minimum value, for 0%
367  @param width progress bar width (in characters)
368  @param forechar character used for filling the progress bar
369  @param backchar character used for filling the background
370  @param foreword text in front of progress bar
371  @param afterword text after progress bar
372  @param interval ticker thread interval, in seconds
373  @param pulse ignore value-min-max, use constant pulse instead
374  @param aftertemplate afterword format() template, populated with vars(self)
375  """
376  threading.Thread.__init__(self)
377  for k, v in locals().items(): setattr(self, k, v) if "self" != k else 0
378  afterword = aftertemplate.format(**vars(self))
379  self.daemon = True # Daemon threads do not keep application running
380  self.percent = None # Current progress ratio in per cent
381  self.value = None # Current progress bar value
382  self.pause = False # Whether drawing is currently paused
383  self.pulse_pos = 0 # Current pulse position
384  self.bar = "%s[%s%s]%s" % (foreword,
385  backchar if pulse else forechar,
386  backchar * (width - 3),
387  afterword)
388  self.printbar = self.bar # Printable text, with padding to clear previous
389  self.progresschar = itertools.cycle("-\\|/")
390  self.is_running = False
391 
392 
393  def update(self, value=None, draw=True, flush=False):
394  """Updates the progress bar value, and refreshes by default; returns self."""
395  if value is not None: self.value = min(self.max, max(self.min, value))
396  afterword = self.aftertemplate.format(**vars(self))
397  w_full = self.width - 2
398  if self.pulse:
399  if self.pulse_pos is None:
400  bartext = "%s[%s]%s" % (self.foreword,
401  self.forechar * (self.width - 2),
402  afterword)
403  else:
404  dash = self.forechar * max(1, int((self.width - 2) / 7))
405  pos = self.pulse_pos
406  if pos < len(dash):
407  dash = dash[:pos]
408  elif pos >= self.width - 1:
409  dash = dash[:-(pos - self.width - 2)]
410 
411  bar = "[%s]" % (self.backchar * w_full)
412  # Write pulse dash into the middle of the bar
413  pos1 = min(self.width - 1, pos + 1)
414  bar = bar[:pos1 - len(dash)] + dash + bar[pos1:]
415  bartext = "%s%s%s" % (self.foreword, bar, afterword)
416  self.pulse_pos = (self.pulse_pos + 1) % (self.width + 2)
417  else:
418  percent = int(round(100.0 * self.value / (self.max or 1)))
419  percent = 99 if percent == 100 and self.value < self.max else percent
420  w_done = max(1, int(round((percent / 100.0) * w_full)))
421  # Build bar outline, animate by cycling last char from progress chars
422  char_last = self.forechar
423  if draw and w_done < w_full: char_last = next(self.progresschar)
424  bartext = "%s[%s%s%s]%s" % (
425  self.foreword, self.forechar * (w_done - 1), char_last,
426  self.backchar * (w_full - w_done), afterword)
427  # Write percentage into the middle of the bar
428  centertxt = " %2d%% " % percent
429  pos = len(self.foreword) + int(self.width / 2 - len(centertxt) / 2)
430  bartext = bartext[:pos] + centertxt + bartext[pos + len(centertxt):]
431  self.percent = percent
432  self.printbar = bartext + " " * max(0, len(self.bar) - len(bartext))
433  self.bar, prevbar = bartext, self.bar
434  if draw and (flush or prevbar != self.bar): self.draw(flush)
435  return self
436 
437 
438  def draw(self, flush=False):
439  """
440  Prints the progress bar, from the beginning of the current line.
441 
442  @param flush add linefeed to end, forcing a new line for any next print
443  """
444  ConsolePrinter.print("\r" + self.printbar, __end=" ")
445  if len(self.printbar) != len(self.bar): # Draw twice to position caret at true content end
446  self.printbar = self.bar
447  ConsolePrinter.print("\r" + self.printbar, __end=" ")
448  if flush: ConsolePrinter.flush()
449 
450 
451  def run(self):
452  self.is_running = True
453  while self.is_running:
454  if not self.pause: self.update(self.value)
455  time.sleep(self.interval)
456 
457 
458  def stop(self):
459  self.is_running = False
460 
461 
462 
463 class LenIterable(object):
464  """Wrapper for iterable value with specified fixed length."""
465 
466  def __init__(self, iterable, count):
467  """
468  @param iterable any iterable value
469  @param count value to return for len(self), or callable to return value from
470  """
471  self._iterer = iter(iterable)
472  self._count = count
473 
474  def __iter__(self): return self
475 
476  def __next__(self): return next(self._iterer)
477 
478  def __len__(self): return self._count() if callable(self._count) else self._count
479 
480 
481 
482 class TextWrapper(object):
483  """
484  TextWrapper that supports custom substring widths in line width calculation.
485 
486  Intended for wrapping text containing ANSI control codes.
487  Heavily refactored from Python standard library textwrap.TextWrapper.
488  """
489 
490 
491  SPACE_RGX = re.compile(r"([%s]+)" % re.escape("\t\n\x0b\x0c\r "))
492 
493 
494  LENCACHEMAX = 10000
495 
496 
497  def __init__(self, width=80, subsequent_indent=" ", break_long_words=True,
498  drop_whitespace=False, max_lines=None, placeholder=" ...", custom_widths=None):
499  """
500  @param width default maximum width to wrap at, 0 disables
501  @param subsequent_indent string prepended to all consecutive lines
502  @param break_long_words break words longer than width
503  @param drop_whitespace drop leading and trailing whitespace from lines
504  @param max_lines count to truncate lines from
505  @param placeholder appended to last retained line when truncating
506  @param custom_widths {substring: len} to use in line width calculation
507  """
508  self.width = width
509  self.subsequent_indent = subsequent_indent
510  self.break_long_words = break_long_words
511  self.drop_whitespace = drop_whitespace
512  self.max_lines = max_lines
513  self.placeholder = placeholder
514 
515  self.lencache = {}
516  self.customs = {s: l for s, l in (custom_widths or {}).items() if s}
517  self.custom_lens = [(s, len(s) - l) for s, l in self.customs.items()]
518  self.custom_rgx = re.compile("(%s)" % "|".join(re.escape(s) for s in self.customs))
519  self.disabled = not self.width
520  self.minwidth = 1 + self.strlen(self.subsequent_indent) \
521  + self.strlen(self.placeholder if self.max_lines else "")
522  self.width = max(self.width, self.minwidth)
523  self.realwidth = self.width
524 
525 
526  def wrap(self, text):
527  """Returns a list of wrapped text lines, without linebreaks."""
528  if self.disabled: return [text]
529  result = []
530  for i, line in enumerate(text.splitlines()):
531  chunks = [c for c in self.SPACE_RGX.split(line) if c]
532  lines = self._wrap_chunks(chunks)
533  if i and lines and self.subsequent_indent:
534  lines[0] = self.subsequent_indent + lines[0]
535  result.extend(lines)
536  if self.max_lines and result and len(result) >= self.max_lines:
537  break # for i, line
538  if self.max_lines and result and (len(result) > self.max_lines
539  or len(result) == self.max_lines and not text.endswith(result[-1].strip())):
540  result = result[:self.max_lines]
541  if not result[-1].endswith(self.placeholder.lstrip()):
542  result[-1] += self.placeholder
543  if len(self.lencache) > self.LENCACHEMAX: self.lencache.clear()
544  return result
545 
546 
547  def reserve_width(self, reserved=""):
548  """Decreases the configured width by given amount (number or string)."""
549  reserved = self.strlen(reserved) if isinstance(reserved, TEXT_TYPES) else reserved
550  self.width = max(self.minwidth, self.realwidth - reserved)
551 
552 
553  def strlen(self, v):
554  """Returns length of string, using custom substring widths."""
555  if v not in self.lencache:
556  self.lencache[v] = len(v) - sum(v.count(s) * ld for s, ld in self.custom_lens)
557  return self.lencache[v]
558 
559 
560  def strip(self, v):
561  """Returns string with custom substrings and whitespace stripped."""
562  return self.custom_rgx.sub("", v).strip()
563 
564 
565  def _wrap_chunks(self, chunks):
566  """Returns a list of lines joined from text chunks, wrapped to width."""
567  lines = []
568  chunks.reverse() # Reverse for efficient popping
569 
570  placeholder_len = self.strlen(self.placeholder)
571  while chunks:
572  cur_line, cur_len = [], 0 # [chunk, ], sum(map(len, cur_line))
573  indent = self.subsequent_indent if lines else ""
574  width = self.width - self.strlen(indent)
575 
576  if self.drop_whitespace and lines and not self.strip(chunks[-1]):
577  del chunks[-1] # Drop initial whitespace on subsequent lines
578 
579  while chunks:
580  l = self.strlen(chunks[-1])
581  if cur_len + l <= width:
582  cur_line.append(chunks.pop())
583  cur_len += l
584  else: # Line full
585  break # while chunks (inner-while)
586 
587  if chunks and self.strlen(chunks[-1]) > width:
588  # Current line is full, and next chunk is too big to fit on any line
589  self._handle_long_word(chunks, cur_line, cur_len, width)
590  cur_len = sum(map(self.strlen, cur_line))
591 
592  if self.drop_whitespace and cur_line and not self.strip(cur_line[-1]):
593  cur_len -= len(cur_line[-1]) # Drop line last whitespace chunk
594  del cur_line[-1]
595 
596  if cur_line:
597  if (self.max_lines is None or len(lines) + 1 < self.max_lines
598  or (not chunks or self.drop_whitespace
599  and len(chunks) == 1 and not self.strip(chunks[0])) \
600  and cur_len <= width): # Current line ok
601  lines.append(indent + "".join(cur_line))
602  continue # while chunks
603  else:
604  continue # while chunks
605 
606  while cur_line: # Truncate for max_lines
607  if self.strip(cur_line[-1]):
608  if cur_len + placeholder_len <= width:
609  lines.append(indent + "".join(cur_line))
610  break # while cur_line
611  if len(cur_line) == 1:
612  lines.append(indent + cur_line[-1])
613  cur_len -= self.strlen(cur_line[-1])
614  del cur_line[-1]
615  else:
616  if not lines or self.strlen(lines[-1]) + placeholder_len > self.width:
617  lines.append(indent + self.placeholder.lstrip())
618  break # while chunks
619 
620  return lines
621 
622 
623  def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
624  """
625  Breaks last chunk if not only containing a custom-width string,
626  else adds last chunk to current line if line still empty.
627  """
628  text = reversed_chunks[-1]
629  break_pos = 1 if width < 1 else width - cur_len
630  breakable = self.break_long_words and text not in self.customs
631  if breakable:
632  unbreakable_spans = [m.span() for m in self.custom_rgx.finditer(text)]
633  text_in_spans = [x for x in unbreakable_spans if x[0] <= break_pos < x[1]]
634  last_span = text_in_spans and sorted(text_in_spans, key=lambda x: -x[1])[0]
635  break_pos = last_span[1] if last_span else break_pos
636  breakable = 0 < break_pos < len(text)
637 
638  if breakable:
639  cur_line.append(text[:break_pos])
640  reversed_chunks[-1] = text[break_pos:]
641  elif not cur_line:
642  cur_line.append(reversed_chunks.pop())
643 
644 
645 
646 def drop_zeros(v, replace=""):
647  """Drops or replaces trailing zeros and empty decimal separator, if any."""
648  return re.sub(r"\.?0+$", lambda x: len(x.group()) * replace, str(v))
649 
650 
651 def ellipsize(text, limit, ellipsis=".."):
652  """Returns text ellipsized if beyond limit."""
653  if limit <= 0 or len(text) < limit:
654  return text
655  return text[:max(0, limit - len(ellipsis))] + ellipsis
656 
657 
658 def ensure_namespace(val, defaults=None, dashify=("WRITE_OPTIONS", ), **kwargs):
659  """
660  Returns a copy of value as `argparse.Namespace`, with all keys uppercase.
661 
662  Arguments with list/tuple values in defaults are ensured to have list/tuple values.
663 
664  @param val `argparse.Namespace` or dictionary or `None`
665  @param defaults additional arguments to set to namespace if missing
666  @param dashify names of dictionary arguments where to replace
667  the first underscore in string keys with a dash
668  @param kwargs any and all argument overrides as keyword overrides
669  """
670  if val is None or isinstance(val, dict): val = argparse.Namespace(**val or {})
671  else: val = structcopy(val)
672  for k, v in vars(val).items():
673  if not k.isupper():
674  delattr(val, k)
675  setattr(val, k.upper(), v)
676  for k, v in ((k.upper(), v) for k, v in (defaults.items() if defaults else ())):
677  if not hasattr(val, k): setattr(val, k, structcopy(v))
678  for k, v in ((k.upper(), v) for k, v in kwargs.items()): setattr(val, k, v)
679  for k, v in ((k.upper(), v) for k, v in (defaults.items() if defaults else ())):
680  if isinstance(v, (tuple, list)) and not isinstance(getattr(val, k), (tuple, list)):
681  setattr(val, k, [getattr(val, k)])
682  for arg in (getattr(val, n, None) for n in dashify or ()):
683  for k in (list(arg) if isinstance(arg, dict) else []):
684  if isinstance(k, six.text_type) and "_" in k and 0 < k.index("_") < len(k) - 1:
685  arg[k.replace("_", "-", 1)] = arg.pop(k)
686  return val
687 
688 
689 def filter_dict(dct, keys=(), values=(), reverse=False):
690  """
691  Filters string dictionary by keys and values, supporting * wildcards.
692  Dictionary values may be additional lists; keys with emptied lists are dropped.
693 
694  Retains only entries that find a match (supports * wildcards);
695  if reverse, retains only entries that do not find a match.
696  """
697  result = type(dct)()
698  kpatterns = [wildcard_to_regex(x, end=True) for x in keys]
699  vpatterns = [wildcard_to_regex(x, end=True) for x in values]
700  for k, vv in dct.items() if not reverse else ():
701  is_array = isinstance(vv, (list, tuple))
702  for v in (vv if is_array else [vv]):
703  if (not keys or k in keys or any(p.match(k) for p in kpatterns)) \
704  and (not values or v in values or any(p.match(v) for p in vpatterns)):
705  result.setdefault(k, []).append(v) if is_array else result.update({k: v})
706  for k, vv in dct.items() if reverse else ():
707  is_array = isinstance(vv, (list, tuple))
708  for v in (vv if is_array else [vv]):
709  if (k not in keys and not any(p.match(k) for p in kpatterns)) \
710  and (v not in values and not any(p.match(v) for p in vpatterns)):
711  result.setdefault(k, []).append(v) if is_array else result.update({k: v})
712  return result
713 
714 
715 def find_files(names=(), paths=(), extensions=(), skip_extensions=(), recurse=False):
716  """
717  Yields filenames from current directory or given paths.
718 
719  Seeks only files with given extensions if names not given.
720  Logs errors for names and paths not found.
721 
722  @param names list of specific files to return (supports * wildcards)
723  @param paths list of paths to look under, if not using current directory
724  @param extensions list of extensions to select if not using names, as (".ext1", ..)
725  @param skip_extensions list of extensions to skip if not using names, as (".ext1", ..)
726  @param recurse whether to recurse into subdirectories
727  """
728  namesfound, pathsfound = set(), set()
729  def iter_files(directory):
730  """Yields matching filenames from path."""
731  if os.path.isfile(directory):
732  ConsolePrinter.log(logging.ERROR, "%s: Is a file", directory)
733  return
734  for path in sorted(glob.glob(directory)): # Expand * wildcards, if any
735  pathsfound.add(directory)
736  for n in names:
737  p = n if not paths or os.path.isabs(n) else os.path.join(path, n)
738  for f in (f for f in glob.glob(p) if "*" not in n
739  or not any(map(f.endswith, skip_extensions))):
740  if os.path.isdir(f):
741  ConsolePrinter.log(logging.ERROR, "%s: Is a directory", f)
742  continue # for n
743  namesfound.add(n)
744  yield f
745  for root, _, files in os.walk(path) if not names else ():
746  for f in (os.path.join(root, f) for f in sorted(files)
747  if (not extensions or any(map(f.endswith, extensions)))
748  and not any(map(f.endswith, skip_extensions))):
749  yield f
750  if not recurse:
751  break # for root
752 
753  processed = set()
754  for f in (f for p in paths or ["."] for f in iter_files(p)):
755  if os.path.abspath(f) not in processed:
756  processed.add(os.path.abspath(f))
757  if not paths and f == os.path.join(".", os.path.basename(f)):
758  f = os.path.basename(f) # Strip leading "./"
759  yield f
760 
761  for path in (p for p in paths if p not in pathsfound):
762  ConsolePrinter.log(logging.ERROR, "%s: No such directory", path)
763  for name in (n for n in names if n not in namesfound):
764  ConsolePrinter.log(logging.ERROR, "%s: No such file", name)
765 
766 
767 def format_timedelta(delta):
768  """Formats the datetime.timedelta as "3d 40h 23min 23.1sec"."""
769  dd, rem = divmod(delta.total_seconds(), 24*3600)
770  hh, rem = divmod(rem, 3600)
771  mm, ss = divmod(rem, 60)
772  items = []
773  for c, n in (dd, "d"), (hh, "h"), (mm, "min"), (ss, "sec"):
774  f = "%d" % c if "sec" != n else drop_zeros(round(c, 9))
775  if f != "0": items += [f + n]
776  return " ".join(items or ["0sec"])
777 
778 
779 def format_bytes(size, precision=2, inter=" ", strip=True):
780  """Returns a formatted byte size (like 421.40 MB), trailing zeros optionally removed."""
781  result = "0 bytes"
782  if size:
783  UNITS = [("bytes", "byte")[1 == size]] + [x + "B" for x in "KMGTPEZY"]
784  exponent = min(int(math.log(size, 1024)), len(UNITS) - 1)
785  result = "%.*f" % (precision, size / (1024. ** exponent))
786  result += "" if precision > 0 else "." # Do not strip integer zeroes
787  result = (drop_zeros(result) if strip else result) + inter + UNITS[exponent]
788  return result
789 
790 
791 def format_stamp(stamp):
792  """Returns ISO datetime from UNIX timestamp."""
793  return datetime.datetime.fromtimestamp(stamp).isoformat(sep=" ")
794 
795 
796 def get_name(obj):
797  """
798  Returns the fully namespaced name for a Python module, class, function or object.
799 
800  E.g. "my.thing" or "my.module.MyCls" or "my.module.MyCls.my_method"
801  or "my.module.MyCls<0x1234abcd>" or "my.module.MyCls<0x1234abcd>.my_method".
802  """
803  namer = lambda x: getattr(x, "__qualname__", getattr(x, "__name__", ""))
804  if inspect.ismodule(obj): return namer(obj)
805  if inspect.isclass(obj): return ".".join((obj.__module__, namer(obj)))
806  if inspect.isroutine(obj):
807  parts, self = [], six.get_method_self(obj)
808  if self is not None: parts.extend((get_name(self), obj.__name__))
809  elif hasattr(obj, "im_class"): parts.extend((get_name(obj.im_class), namer(obj))) # Py2
810  else: parts.extend((obj.__module__, namer(obj))) # Py3
811  return ".".join(parts)
812  cls = type(obj)
813  return "%s.%s<0x%x>" % (cls.__module__, namer(cls), id(obj))
814 
815 
816 def has_arg(func, name):
817  """Returns whether function supports taking specified argument by name."""
818  spec = getattr(inspect, "getfullargspec", getattr(inspect, "getargspec", None))(func) # Py3/Py2
819  return name in spec.args or name in getattr(spec, "kwonlyargs", ()) or \
820  getattr(spec, "varkw", None) or getattr(spec, "keywords", None)
821 
822 
823 def import_item(name):
824  """
825  Returns imported module, or identifier from imported namespace; raises on error.
826 
827  @param name Python module name like "my.module"
828  or module namespace identifier like "my.module.Class"
829  """
830  result, parts = None, name.split(".")
831  for i, item in enumerate(parts):
832  path, success = ".".join(parts[:i + 1]), False
833  try: result, success = importlib.import_module(path), True
834  except ImportError: pass
835  if not success and i:
836  try: result, success = getattr(result, item), True
837  except AttributeError: pass
838  if not success:
839  raise ImportError("No module or identifier named %r" % path)
840  return result
841 
842 
843 def is_iterable(value):
844  """Returns whether value is iterable."""
845  try: iter(value)
846  except Exception: return False
847  return True
848 
849 
850 def is_stream(value):
851  """Returns whether value is a file-like object."""
852  try: return isinstance(value, (file, io.IOBase)) # Py2
853  except NameError: return isinstance(value, io.IOBase) # Py3
854 
855 
856 def makedirs(path):
857  """Creates directory structure for path if not already existing."""
858  parts, accum = list(filter(bool, os.path.realpath(path).split(os.sep))), []
859  while parts:
860  accum.append(parts.pop(0))
861  curpath = os.path.join(os.sep, accum[0] + os.sep, *accum[1:]) # Windows drive letter thing
862  if not os.path.exists(curpath):
863  os.mkdir(curpath)
864 
865 
866 def structcopy(value):
867  """
868  Returns a deep copy of a standard data structure (dict, list, set, tuple),
869  other object types reused instead of copied.
870  """
871  COLLECTIONS = (dict, list, set, tuple)
872  memo = {}
873  def collect(x): # Walk structure and collect objects to skip copying
874  if isinstance(x, argparse.Namespace): x = vars(x)
875  if not isinstance(x, COLLECTIONS): return memo.update([(id(x), x)])
876  for y in sum(map(list, x.items()), []) if isinstance(x, dict) else x: collect(y)
877  collect(value)
878  return copy.deepcopy(value, memo)
879 
880 
881 def memoize(func):
882  """Returns a results-caching wrapper for the function, cache used if arguments hashable."""
883  cache = {}
884  def inner(*args, **kwargs):
885  key = args + sum(kwargs.items(), ())
886  try: hash(key)
887  except Exception: return func(*args, **kwargs)
888  if key not in cache:
889  cache[key] = func(*args, **kwargs)
890  return cache[key]
891  return functools.update_wrapper(inner, func)
892 
893 
894 def merge_dicts(d1, d2):
895  """Merges d2 into d1, recursively for nested dicts."""
896  for k, v in d2.items():
897  if k in d1 and isinstance(v, dict) and isinstance(d1[k], dict):
898  merge_dicts(d1[k], v)
899  else:
900  d1[k] = v
901 
902 
903 def merge_spans(spans, join_blanks=False):
904  """
905  Returns a sorted list of (start, end) spans with overlapping spans merged.
906 
907  @param join_blanks whether to merge consecutive zero-length spans,
908  e.g. [(0, 0), (1, 1)] -> [(0, 1)]
909  """
910  result = sorted(spans)
911  if result and join_blanks:
912  blanks = [(a, b) for a, b in result if a == b]
913  others = [(a, b) for a, b in result if a != b]
914  others.extend(blanks[:1])
915  for span in blanks[1:]:
916  if span[0] == others[-1][1] + 1:
917  others[-1] = (others[-1][0], span[1])
918  else:
919  others.append(span)
920  result = sorted(others)
921  result, rest = result[:1], result[1:]
922  for span in rest:
923  if span[0] <= result[-1][1]:
924  result[-1] = (result[-1][0], max(span[1], result[-1][1]))
925  else:
926  result.append(span)
927  return result
928 
929 
930 def parse_datetime(text):
931  """Returns datetime object from ISO datetime string (may be partial). Raises if invalid."""
932  BASE = re.sub(r"\D", "", datetime.datetime.min.isoformat()) # "00010101000000"
933  text = re.sub(r"\D", "", text)
934  text += BASE[len(text):] if text else ""
935  dt = datetime.datetime.strptime(text[:len(BASE)], "%Y%m%d%H%M%S")
936  return dt + datetime.timedelta(microseconds=int(text[len(BASE):] or "0"))
937 
938 
939 def parse_number(value, suffixes=None):
940  """
941  Returns an integer parsed from text, raises on error.
942 
943  @param value text or binary string to parse, may contain abbrevations like "12K"
944  @param suffixes a dictionary of multipliers like {"K": 1024}, case-insensitive
945  """
946  value, suffix = value.decode() if isinstance(value, six.binary_type) else value, None
947  if suffixes:
948  suffix = next((k for k, v in suffixes.items() if value.lower().endswith(k.lower())), None)
949  value = value[:-len(suffix)] if suffix else value
950  return int(float(value) * (suffixes[suffix] if suffix else 1))
951 
952 
953 def plural(word, items=None, numbers=True, single="1", sep=",", pref="", suf=""):
954  """
955  Returns the word as 'count words', or '1 word' if count is 1,
956  or 'words' if count omitted.
957 
958  @param items item collection or count,
959  or None to get just the plural of the word
960  @param numbers if False, count is omitted from final result
961  @param single prefix to use for word if count is 1, e.g. "a"
962  @param sep thousand-separator to use for count
963  @param pref prefix to prepend to count, e.g. "~150"
964  @param suf suffix to append to count, e.g. "150+"
965  """
966  count = len(items) if hasattr(items, "__len__") else items or 0
967  isupper = word[-1:].isupper()
968  suffix = "es" if word and word[-1:].lower() in "xyz" \
969  and not word[-2:].lower().endswith("ay") \
970  else "s" if word else ""
971  if isupper: suffix = suffix.upper()
972  if count != 1 and "es" == suffix and "y" == word[-1:].lower():
973  word = word[:-1] + ("I" if isupper else "i")
974  result = word + ("" if 1 == count else suffix)
975  if numbers and items is not None:
976  if 1 == count: fmtcount = single
977  elif not count: fmtcount = "0"
978  elif sep: fmtcount = "".join([
979  x + (sep if i and not i % 3 else "") for i, x in enumerate(str(count)[::-1])
980  ][::-1])
981  else: fmtcount = str(count)
982 
983  fmtcount = pref + fmtcount + suf
984  result = "%s %s" % (single if 1 == count else fmtcount, result)
985  return result.strip()
986 
987 
988 def unique_path(pathname, empty_ok=False):
989  """
990  Returns a unique version of the path.
991 
992  If a file or directory with the same name already exists, returns a unique
993  version (e.g. "/tmp/my.2.file" if ""/tmp/my.file" already exists).
994 
995  @param empty_ok whether to ignore existence if file is empty
996  """
997  result = pathname
998  if "linux2" == sys.platform and six.PY2 and isinstance(result, six.text_type) \
999  and "utf-8" != sys.getfilesystemencoding():
1000  result = result.encode("utf-8") # Linux has trouble if locale not UTF-8
1001  if os.path.isfile(result) and empty_ok and not os.path.getsize(result):
1002  return result if isinstance(result, STRING_TYPES) else str(result)
1003  path, name = os.path.split(result)
1004  base, ext = os.path.splitext(name)
1005  if len(name) > 255: # Filesystem limitation
1006  name = base[:255 - len(ext) - 2] + ".." + ext
1007  result = os.path.join(path, name)
1008  counter = 2
1009  while os.path.exists(result):
1010  suffix = ".%s%s" % (counter, ext)
1011  name = base + suffix
1012  if len(name) > 255:
1013  name = base[:255 - len(suffix) - 2] + ".." + suffix
1014  result = os.path.join(path, name)
1015  counter += 1
1016  return result
1017 
1018 
1019 def verify_io(f, mode):
1020  """
1021  Returns whether stream or file path can be read from and/or written to as binary.
1022 
1023  Prints or raises error if not.
1024 
1025  Tries to open file in append mode if verifying path writability,
1026  auto-creating missing directories if any, will delete any file or directory created.
1027 
1028  @param f file path, or stream
1029  @param mode "r" for readable, "w" for writable, "a" for readable and writable
1030  """
1031  result, op = True, ""
1032  if is_stream(f):
1033  try:
1034  pos = f.tell()
1035  if mode in ("r", "a"):
1036  op = " reading from"
1037  result = isinstance(f.read(1), bytes)
1038  if result and mode in ("w", "a"):
1039  op = " writing to"
1040  result, _ = True, f.write(b"")
1041  f.seek(pos)
1042  return result
1043  except Exception as e:
1044  ConsolePrinter.log(logging.ERROR, "Error%s %s: %s", op, type(f).__name__, e)
1045  return False
1046 
1047  present, paths_created = os.path.exists(f), []
1048  try:
1049  if not present and mode in ("w", "a"):
1050  op = " writing to"
1051  path = os.path.realpath(os.path.dirname(f))
1052  parts, accum = [x for x in path.split(os.sep) if x], []
1053  while parts:
1054  accum.append(parts.pop(0))
1055  curpath = os.path.join(os.sep, accum[0] + os.sep, *accum[1:]) # Windows drive letter thing
1056  if not os.path.exists(curpath):
1057  os.mkdir(curpath)
1058  paths_created.append(curpath)
1059  elif not present and "r" == mode:
1060  return False
1061  with open(f, {"r": "rb", "w": "ab", "a": "ab+"}[mode]) as g:
1062  if mode in ("r", "a"):
1063  op = " reading from"
1064  result = isinstance(g.read(1), bytes)
1065  if result and mode in ("w", "a"):
1066  op = " writing to"
1067  result, _ = True, g.write(b"")
1068  return result
1069  except Exception as e:
1070  ConsolePrinter.log(logging.ERROR, "Error%s %s: %s", f, e)
1071  return False
1072  finally:
1073  if not present:
1074  try: os.remove(f)
1075  except Exception: pass
1076  for path in paths_created[::-1]:
1077  try: os.rmdir(path)
1078  except Exception: pass
1079 
1080 
1081 def wildcard_to_regex(text, end=False):
1082  """
1083  Returns plain wildcard like "foo*bar" as re.Pattern("foo.*bar", re.I).
1084 
1085  @param end whether pattern should match until end (adds $)
1086  """
1087  suff = "$" if end else ""
1088  return re.compile(".*".join(map(re.escape, text.split("*"))) + suff, re.I)
1089 
1090 
1091 __all__ = [
1092  "PATH_TYPES", "ConsolePrinter", "Decompressor", "MatchMarkers", "ProgressBar", "TextWrapper",
1093  "drop_zeros", "ellipsize", "ensure_namespace", "filter_dict", "find_files",
1094  "format_bytes", "format_stamp", "format_timedelta", "get_name", "has_arg", "import_item",
1095  "is_iterable", "is_stream", "makedirs", "memoize", "merge_dicts", "merge_spans",
1096  "parse_datetime", "parse_number", "plural", "unique_path", "verify_io", "wildcard_to_regex",
1097 ]
grepros.common.get_name
def get_name(obj)
Definition: common.py:796
grepros.common.ProgressBar.progresschar
progresschar
Definition: common.py:387
grepros.common.ConsolePrinter.warn
def warn(cls, text="", *args, **kwargs)
Definition: common.py:203
grepros.common.ProgressBar.is_running
is_running
Definition: common.py:388
grepros.common.MatchMarkers.populate
def populate(cls, value)
Definition: common.py:65
grepros.common.TextWrapper.width
width
Definition: common.py:507
grepros.common.TextWrapper.custom_rgx
custom_rgx
Definition: common.py:517
grepros.common.TextWrapper._handle_long_word
def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width)
Definition: common.py:623
grepros.common.Decompressor.decompress
def decompress(cls, path, progress=False)
Definition: common.py:290
grepros.common.LenIterable
Definition: common.py:463
grepros.common.TextWrapper.customs
customs
Definition: common.py:515
grepros.common.ConsolePrinter.DEBUG_START
DEBUG_START
Definition: common.py:90
grepros.common.ConsolePrinter.ERROR_END
ERROR_END
Definition: common.py:92
grepros.common.ProgressBar.pause
pause
Definition: common.py:380
grepros.common.TextWrapper.strip
def strip(self, v)
Definition: common.py:560
grepros.common.ConsolePrinter.WARN_START
WARN_START
Definition: common.py:91
grepros.common.drop_zeros
def drop_zeros(v, replace="")
Definition: common.py:646
grepros.common.ConsolePrinter._LINEOPEN
bool _LINEOPEN
Definition: common.py:108
grepros.common.ConsolePrinter._COLORFLAG
_COLORFLAG
Definition: common.py:106
grepros.common.verify_io
def verify_io(f, mode)
Definition: common.py:1019
grepros.common.format_bytes
def format_bytes(size, precision=2, inter=" ", strip=True)
Definition: common.py:779
grepros.common.LenIterable.__len__
def __len__(self)
Definition: common.py:478
grepros.common.TextWrapper.reserve_width
def reserve_width(self, reserved="")
Definition: common.py:547
grepros.common.ConsolePrinter._UNIQUES
_UNIQUES
Definition: common.py:110
grepros.common.ConsolePrinter._format
def _format(cls, text="", *args, **kwargs)
Definition: common.py:259
grepros.common.wildcard_to_regex
def wildcard_to_regex(text, end=False)
Definition: common.py:1081
grepros.common.TextWrapper.lencache
lencache
Definition: common.py:514
grepros.common.is_iterable
def is_iterable(value)
Definition: common.py:843
grepros.common.is_stream
def is_stream(value)
Definition: common.py:850
grepros.common.format_stamp
def format_stamp(stamp)
Definition: common.py:791
grepros.common.merge_spans
def merge_spans(spans, join_blanks=False)
Definition: common.py:903
grepros.common.structcopy
def structcopy(value)
Definition: common.py:866
grepros.common.TextWrapper.wrap
def wrap(self, text)
Definition: common.py:526
grepros.common.TextWrapper.placeholder
placeholder
Definition: common.py:512
grepros.common.ellipsize
def ellipsize(text, limit, ellipsis="..")
Definition: common.py:651
grepros.common.ProgressBar.stop
def stop(self)
Definition: common.py:458
grepros.common.ConsolePrinter.STYLE_WARN
string STYLE_WARN
Definition: common.py:87
grepros.common.ProgressBar.value
value
Definition: common.py:379
grepros.common.MatchMarkers.EMPTY_REPL
string EMPTY_REPL
Replacement for empty string match.
Definition: common.py:62
grepros.common.Decompressor.is_compressed
def is_compressed(cls, path)
Definition: common.py:324
grepros.common.TextWrapper._wrap_chunks
def _wrap_chunks(self, chunks)
Definition: common.py:565
grepros.common.ConsolePrinter.print
def print(cls, text="", *args, **kwargs)
Definition: common.py:164
grepros.common.MatchMarkers
Definition: common.py:50
grepros.common.ConsolePrinter
Definition: common.py:75
grepros.common.TextWrapper.disabled
disabled
Definition: common.py:518
grepros.common.ConsolePrinter.APIMODE
bool APIMODE
Whether logging debugs and warnings and raising errors, instead of printing.
Definition: common.py:104
grepros.common.ConsolePrinter.STYLE_LOWLIGHT
string STYLE_LOWLIGHT
Definition: common.py:84
grepros.common.ConsolePrinter.init_terminal
def init_terminal(cls)
Definition: common.py:134
grepros.common.ProgressBar.__init__
def __init__(self, max=100, value=0, min=0, width=30, forechar="-", backchar=" ", foreword="", afterword="", interval=1, pulse=False, aftertemplate=" {afterword}")
Definition: common.py:358
grepros.common.ProgressBar.pulse_pos
pulse_pos
Definition: common.py:381
grepros.common.ConsolePrinter.DEBUG_END
DEBUG_END
Definition: common.py:90
grepros.common.TextWrapper.custom_lens
custom_lens
Definition: common.py:516
grepros.common.ensure_namespace
def ensure_namespace(val, defaults=None, dashify=("WRITE_OPTIONS",), **kwargs)
Definition: common.py:658
grepros.common.ProgressBar.update
def update(self, value=None, draw=True, flush=False)
Definition: common.py:393
grepros.common.find_files
def find_files(names=(), paths=(), extensions=(), skip_extensions=(), recurse=False)
Definition: common.py:715
grepros.common.ConsolePrinter.configure
def configure(cls, color=True, apimode=False)
Definition: common.py:113
grepros.common.LenIterable._iterer
_iterer
Definition: common.py:471
grepros.common.unique_path
def unique_path(pathname, empty_ok=False)
Definition: common.py:988
grepros.common.TextWrapper.minwidth
minwidth
Definition: common.py:519
grepros.common.Decompressor.validate
def validate(cls)
Definition: common.py:342
grepros.common.ConsolePrinter.debug
def debug(cls, text="", *args, **kwargs)
Definition: common.py:218
grepros.common.memoize
def memoize(func)
Definition: common.py:881
grepros.common.ProgressBar.draw
def draw(self, flush=False)
Definition: common.py:438
grepros.common.parse_datetime
def parse_datetime(text)
Definition: common.py:930
grepros.common.Decompressor.make_decompressed_name
def make_decompressed_name(cls, path)
Definition: common.py:336
grepros.common.LenIterable.__iter__
def __iter__(self)
Definition: common.py:474
grepros.common.plural
def plural(word, items=None, numbers=True, single="1", sep=",", pref="", suf="")
Definition: common.py:953
grepros.common.parse_number
def parse_number(value, suffixes=None)
Definition: common.py:939
grepros.common.TextWrapper.max_lines
max_lines
Definition: common.py:511
grepros.common.MatchMarkers.START
string START
Placeholder in front of match.
Definition: common.py:56
grepros.common.ConsolePrinter.STYLE_RESET
string STYLE_RESET
Definition: common.py:82
grepros.common.makedirs
def makedirs(path)
Definition: common.py:856
grepros.common.filter_dict
def filter_dict(dct, keys=(), values=(), reverse=False)
Definition: common.py:689
grepros.common.Decompressor.EXTENSIONS
tuple EXTENSIONS
Supported archive extensions.
Definition: common.py:283
grepros.common.ProgressBar.run
def run(self)
Definition: common.py:451
grepros.common.format_timedelta
def format_timedelta(delta)
Definition: common.py:767
grepros.common.MatchMarkers.EMPTY
string EMPTY
Placeholder for empty string match.
Definition: common.py:60
grepros.common.ConsolePrinter.ERROR_START
ERROR_START
Definition: common.py:92
grepros.common.ProgressBar.bar
bar
Definition: common.py:382
grepros.common.ProgressBar.printbar
printbar
Definition: common.py:386
grepros.common.TextWrapper.strlen
def strlen(self, v)
Definition: common.py:553
grepros.common.MatchMarkers.ID
string ID
Unique marker for match highlight replacements.
Definition: common.py:54
grepros.common.TextWrapper.LENCACHEMAX
int LENCACHEMAX
Max length of strlen cache.
Definition: common.py:494
grepros.common.ConsolePrinter.COLOR
COLOR
Whether using colors in output.
Definition: common.py:95
grepros.common.ConsolePrinter.PRINTS
dictionary PRINTS
{sys.stdout: number of texts printed, sys.stderr: ..}
Definition: common.py:101
grepros.common.TextWrapper.realwidth
realwidth
Definition: common.py:522
grepros.common.ProgressBar
Definition: common.py:348
grepros.common.ProgressBar.percent
percent
Definition: common.py:378
grepros.common.ConsolePrinter.WIDTH
int WIDTH
Console width in characters, updated from shutil and curses.
Definition: common.py:98
grepros.common.Decompressor.ZSTD_MAGIC
string ZSTD_MAGIC
zstd file header magic start bytes
Definition: common.py:286
grepros.common.TextWrapper.break_long_words
break_long_words
Definition: common.py:509
grepros.common.ProgressBar.daemon
daemon
Definition: common.py:377
grepros.common.has_arg
def has_arg(func, name)
Definition: common.py:816
grepros.common.import_item
def import_item(name)
Definition: common.py:823
grepros.common.ConsolePrinter.WARN_END
WARN_END
Definition: common.py:91
grepros.common.ConsolePrinter.STYLE_ERROR
string STYLE_ERROR
Definition: common.py:88
grepros.common.TextWrapper.__init__
def __init__(self, width=80, subsequent_indent=" ", break_long_words=True, drop_whitespace=False, max_lines=None, placeholder=" ...", custom_widths=None)
Definition: common.py:497
grepros.common.TextWrapper
Definition: common.py:482
grepros.common.LenIterable.__init__
def __init__(self, iterable, count)
Definition: common.py:466
grepros.common.MatchMarkers.END
string END
Placeholder at end of match.
Definition: common.py:58
grepros.common.Decompressor
Definition: common.py:279
grepros.common.LenIterable._count
_count
Definition: common.py:472
grepros.common.TextWrapper.subsequent_indent
subsequent_indent
Definition: common.py:508
grepros.common.merge_dicts
def merge_dicts(d1, d2)
Definition: common.py:894
grepros.common.TextWrapper.SPACE_RGX
SPACE_RGX
Regex for breaking text at whitespace.
Definition: common.py:491
grepros.common.ConsolePrinter.error
def error(cls, text="", *args, **kwargs)
Definition: common.py:190
grepros.common.ConsolePrinter.flush
def flush(cls)
Definition: common.py:252
grepros.common.ConsolePrinter.log
def log(cls, level, text="", *args, **kwargs)
Definition: common.py:233
grepros.common.TextWrapper.drop_whitespace
drop_whitespace
Definition: common.py:510


grepros
Author(s): Erki Suurjaak
autogenerated on Sat Jan 6 2024 03:11:29