File indexing completed on 2024-10-27 05:14:08
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