File indexing completed on 2024-04-21 16:29:17

0001 # -*- coding: UTF-8 -*-
0002 
0003 """
0004 Report info, warning and error messages.
0005 
0006 Functions for Pology tools to issue reports to the user at runtime.
0007 May colorize some output.
0008 
0009 @author: Chusslove Illich (Часлав Илић) <caslav.ilic@gmx.net>
0010 @license: GPLv3
0011 """
0012 
0013 import os
0014 import sys
0015 import locale
0016 import time
0017 
0018 from pology import _, n_, t_, TextTrans
0019 from pology.colors import ColorString
0020 
0021 
0022 _prev_text_cr = [None, None]
0023 
0024 def encwrite(file, text):
0025     """
0026     Write unicode text to file using best encoding guess.
0027 
0028     If the file has been opened with explicit encoding, that encoding is used.
0029     Otherwise a guess is made based on the environment locale.
0030 
0031     @param file: file to write to
0032     @type file: C{file}
0033     @param text: text to write
0034     @type text: string or unicode
0035     """
0036 
0037     if hasattr(file, "buffer"):
0038         file = file.buffer
0039     enc = getattr(file, "encoding", None) or locale.getpreferredencoding()
0040     text = text.encode(enc, "replace")
0041 
0042     # If last output was returning to line start with CR, clean up the line.
0043     if _prev_text_cr[0] is not None and not _prev_text_cr[1].closed:
0044         cstr = b"\r%s\r" % (b" " * len(_prev_text_cr[0]))
0045         _prev_text_cr[0] = None
0046         _prev_text_cr[1].write(cstr)
0047         _prev_text_cr[1] = None
0048 
0049     # If current output is returning to line start with CR, record it.
0050     if text.endswith(b"\r"):
0051         cstr = text
0052         if b"\n" in cstr:
0053             cstr = cstr[cstr.rfind(b"\n") + 1:]
0054         _prev_text_cr[0] = cstr
0055         _prev_text_cr[1] = file
0056 
0057     file.write(text)
0058 
0059 
0060 def report (text, showcmd=False, subsrc=None, file=sys.stdout, newline=True):
0061     """
0062     Generic report.
0063 
0064     Text is output to the file descriptor,
0065     with one newline appended by default.
0066 
0067     @param text: text to report
0068     @type text: string
0069     @param showcmd: whether to show the command name
0070     @type showcmd: bool
0071     @param subsrc: more detailed source of the text
0072     @type subsrc: C{None} or string
0073     @param file: send output to this file descriptor
0074     @type file: C{file}
0075     @param newline: whether to append newline to output
0076     @type newline: bool
0077     """
0078 
0079     if not isinstance(text, ColorString):
0080          text = ColorString("%s") % text
0081     text = text.resolve(dest=file)
0082 
0083     cmdname = None
0084     if showcmd:
0085         cmdname = os.path.basename(sys.argv[0])
0086 
0087     lines = text.split("\n")
0088     for i in range(len(lines)):
0089         if i == 0:
0090             if cmdname and subsrc:
0091                 head = "%s (%s): " % (cmdname, subsrc)
0092             elif cmdname:
0093                 head = "%s: " % cmdname
0094             elif subsrc:
0095                 head = "(%s): " % subsrc
0096             else:
0097                 head = ""
0098             lhead = len(head)
0099         else:
0100             if lhead:
0101                 head = "... "
0102             else:
0103                 head = ""
0104         lines[i] = head + lines[i]
0105 
0106     if newline:
0107         lines.append("")
0108 
0109     text = "\n".join(lines)
0110 
0111     encwrite(file, text)
0112 
0113 
0114 def warning (text, showcmd=True, subsrc=None, file=sys.stderr):
0115     """
0116     Generic warning.
0117 
0118     @param text: text to report
0119     @type text: string
0120     @param showcmd: whether to show the command name
0121     @type showcmd: bool
0122     @param subsrc: more detailed source of the text
0123     @type subsrc: C{None} or string
0124     @param file: send output to this file descriptor
0125     @type file: C{file}
0126     """
0127 
0128     rtext = _("@info",
0129               "<bold>[warning]</bold> <orange>%(msg)s</orange>",
0130               msg=text)
0131     report(rtext, showcmd=showcmd, subsrc=subsrc, file=file)
0132 
0133 
0134 def error (text, code=1, showcmd=True, subsrc=None, file=sys.stderr):
0135     """
0136     Generic error (aborts the execution).
0137 
0138     Exits with the given code.
0139 
0140     @param text: text to report
0141     @type text: string
0142     @param code: the exit code
0143     @type code: int
0144     @param showcmd: whether to show the command name
0145     @type showcmd: bool
0146     @param file: send output to this file descriptor
0147     @type file: C{file}
0148     """
0149 
0150     rtext = _("@info",
0151               "<bold>[error]</bold> <red>%(msg)s</red>",
0152               msg=text)
0153     report(rtext, showcmd=showcmd, subsrc=subsrc, file=file)
0154     sys.exit(code)
0155 
0156 
0157 def init_file_progress (fpaths, timeint=1.0, stream=sys.stderr, addfmt=None):
0158     """
0159     Create a function to output progress bar while processing files.
0160 
0161     When a collection of files is about to be processed,
0162     this function can be used to construct a progress update function,
0163     which shows and updates the progress bar in the terminal.
0164     The progress update function can be called as frequently as desired
0165     during processing of a particular file, with file path as argument.
0166     For example::
0167 
0168         update_progress == init_file_progress(file_paths)
0169         for file_path in file_paths:
0170             for line in open(file_path).readlines():
0171                 update_progress(file_path)
0172                 # ...
0173                 # Processing.
0174                 # ...
0175         update_progress() # clears last progress line
0176 
0177     Parameter C{timeint} determines the frequency of update, in seconds.
0178     It should be chosen such that the progress updates themselves
0179     (formatting, writing out to shell) are only a small fraction
0180     of total processing time.
0181 
0182     The output stream for the progress bar can be specified
0183     by the C{stream} parameter.
0184 
0185     Additional formatting for the progress bar may be supplied
0186     by the C{addfmt} parameter. It can be one of: a function taking one
0187     string parameter (the basic progress bar) and returning a string,
0188     a delayed translation (L{TextTrans}) with single named formatting
0189     directive C{%(file)s}, or a plain string with same formatting directive.
0190 
0191     @param fpaths: collection of file paths
0192     @type fpaths: list of strings
0193     @param timeint: update interval in seconds
0194     @type timeint: float
0195     @param stream: the stream to output progress to
0196     @type stream: file
0197     @param addfmt: additional format for the progress line
0198     @type addfmt: (text) -> text or L{TextTrans} or string
0199 
0200     @returns: progress updating function
0201     @rtype: (file_path, last_time, time_interval) -> new_last_time
0202     """
0203 
0204     if not fpaths or not stream.isatty():
0205         return lambda x=None: x
0206 
0207     try:
0208         import curses
0209         curses.setupterm()
0210     except:
0211         return lambda x=None: x
0212 
0213     def postfmt (pstr):
0214         if callable(addfmt):
0215             pstr = addfmt(pstr)
0216         elif isinstance(addfmt, TextTrans):
0217             pstr = addfmt.with_args(file=pstr).to_string()
0218         elif addfmt:
0219             pstr = addfmt % dict(file=pstr)
0220         if isinstance(pstr, ColorString):
0221             pstr = pstr.resolve(dest=stream)
0222         return pstr
0223 
0224     pfmt = ("%%1s %%%dd/%d %%s" % (len(str(len(fpaths))), len(fpaths)))
0225     pspins = ["–", "\\", "|", "/"]
0226     i_spin = [0]
0227     i_file = [0]
0228     seen_fpaths = set()
0229     otime = [-timeint]
0230     enc = getattr(stream, "encoding", None) or locale.getpreferredencoding()
0231     minenclen = len(postfmt(pfmt % (pspins[0], 0, "")).encode(enc, "replace"))
0232 
0233     def update_progress (fpath=None):
0234 
0235         ntime = time.time()
0236         if ntime - otime[0] >= timeint:
0237             otime[0] = ntime
0238         elif fpath in seen_fpaths:
0239             return
0240 
0241         if fpath:
0242             i_spin[0] = (i_spin[0] + 1) % len(pspins)
0243             if fpath not in seen_fpaths:
0244                 seen_fpaths.add(fpath)
0245                 i_file[0] += 1
0246 
0247             # Squeeze file path to fit into the terminal width.
0248             curses.setupterm()
0249             acolfp = curses.tigetnum("cols") - minenclen - 2 # 2 for \r\r
0250             rfpath = fpath
0251             infix = "..."
0252             lenred = 1
0253             while len(rfpath.encode(enc, "replace")) > acolfp:
0254                 hlfp = (len(fpath) - len(infix)) // 2 - lenred
0255                 lenred += 1
0256                 rfpath = fpath[:hlfp] + infix + fpath[-hlfp:]
0257 
0258             pstr = postfmt(pfmt % (pspins[i_spin[0]], i_file[0], rfpath))
0259             encwrite(stream, "\r%s\r" % pstr)
0260         else:
0261             encwrite(stream, "")
0262         stream.flush()
0263 
0264     return update_progress
0265 
0266 
0267 def list_options (optparser, short=False, both=False):
0268     """
0269     Simple list of all option names found in the option parser.
0270 
0271     The list is composed of option names delimited by newlines.
0272 
0273     If an option is having both short and long name, the behavior
0274     is determined by parameters C{short} and C{both}.
0275     If neither is C{True}, only the long name is added to list.
0276     If only C{short} is C{True}, only the short name is added to list.
0277     If C{both} is C{True} both names are added to the list, in the order
0278     determined by C{short} -- if C{True}, short name is listed first.
0279 
0280     The list is sorted by long option names where available,
0281     with short name listed before or after the long name,
0282     depending on C{short} (C{True} for before).
0283 
0284     @param optparser: option parser
0285     @type optparser: OptionParser
0286     @param short: whether to prefer short names
0287     @type short: bool
0288     @param both: whether to show both long and short name of an option
0289     @type both: bool
0290 
0291     @returns: formated list of option names
0292     @rtype: string
0293     """
0294 
0295     optnames = []
0296     for opt in optparser.option_list:
0297         if str(opt) != opt.get_opt_string():
0298             sname, lname = str(opt).split("/")
0299             if both:
0300                 onames = [sname, lname] if short else [lname, sname]
0301             else:
0302                 onames = [sname] if short else [lname]
0303         else:
0304             onames = [opt.get_opt_string()]
0305         optnames.append(onames)
0306 
0307     elind = -1 if short else 0
0308     optnames.sort(key=lambda x: x[elind].lstrip("-"))
0309     fmtlist = "\n".join(sum(optnames, []))
0310 
0311     return fmtlist
0312 
0313 
0314 def format_item_list (items, incmp=False, quoted=False):
0315     """
0316     Format inline item list, for insertion into text.
0317 
0318     @param items: items to list
0319     @type items: sequence of elements convertible to string by unicode()
0320     @param incmp: whether some items are omitted from the list
0321     @type incmp: bool
0322     @param quoted: whether each item should be quoted
0323     @type quoted: bool
0324     @returns: inline formatted list of items
0325     @rtype: string
0326     """
0327 
0328     sep = _("@item:intext general separator for inline lists of items, "
0329             "e.g. \", \" in \"apples, bananas, cherries, and plums\"",
0330             ", ")
0331     sep_last = _("@item:intext last separator for inline lists of items, "
0332                  "e.g. \", and \" in \"apples, bananas, cherries, and plums\"",
0333                  ", and ")
0334     sep_two = _("@item:intext separator for inline list of exactly two items, "
0335                  "e.g. \" and \" in \"apples and bananas\"",
0336                  " and ")
0337     ellipsis = _("@item:intext trailing string for incomplete lists, "
0338                  "e.g. \"...\" in \"apples, bananas, cherries...\"",
0339                  "...")
0340     quoting = t_("@item:intext quotes around each element in the list",
0341                  "'%(el)s'")
0342     itemstrs = list(map(str, items))
0343     if quoted:
0344         itemstrs = [quoting.with_args(el=x).to_string() for x in items]
0345     if not incmp:
0346         if len(itemstrs) == 0:
0347             return ""
0348         elif len(itemstrs) == 1:
0349             return itemstrs[0]
0350         elif len(itemstrs) == 2:
0351             return sep_two.join(itemstrs)
0352         else:
0353             return sep.join(itemstrs[:-1]) + sep_last + itemstrs[-1]
0354     else:
0355         return sep.join(itemstrs) + ellipsis
0356