File indexing completed on 2024-12-01 13:47:54
0001 # -*- coding: UTF-8 -*- 0002 0003 """ 0004 Assemble a property map from entries in manual comments. 0005 0006 Documented in C{doc/user/sieving.docbook}. 0007 0008 @author: Chusslove Illich (Часлав Илић) <caslav.ilic@gmx.net> 0009 @license: GPLv3 0010 """ 0011 0012 import os 0013 import re 0014 0015 from pology import _, n_ 0016 from pology.colors import cjoin 0017 from pology.fsops import str_to_unicode 0018 from pology.msgreport import warning_on_msg 0019 from pology.report import report, format_item_list 0020 from pology.sieve import SieveError 0021 from pology.synder import Synder 0022 0023 0024 def setup_sieve (p): 0025 0026 p.set_desc(_("@info sieve discription", 0027 "Assemble a property map from entries in manual comments." 0028 )) 0029 0030 p.add_param("outfile", str, 0031 metavar=_("@info sieve parameter value placeholder", "FILE"), 0032 desc=_("@info sieve parameter discription", 0033 "File to output the property map into. " 0034 "If not given, nothing is output (useful for validation runs)." 0035 )) 0036 p.add_param("propcons", str, 0037 metavar=_("@info sieve parameter value placeholder", "FILE"), 0038 desc=_("@info sieve parameter discription", 0039 "File defining the constraints on property keys and values." 0040 )) 0041 p.add_param("extrakeys", bool, defval=False, 0042 desc=_("@info sieve parameter discription", 0043 "Allow defining additional entry keys." 0044 )) 0045 p.add_param("derivs", str, 0046 metavar=_("@info sieve parameter value placeholder", "FILE"), 0047 desc=_("@info sieve parameter discription", 0048 "File defining the derivators used in derived entries." 0049 )) 0050 p.add_param("pmhead", str, defval="pmap:", 0051 metavar=_("@info sieve parameter value placeholder", "STRING"), 0052 desc=_("@info sieve parameter discription", 0053 "Prefix which starts property map entries in comments." 0054 )) 0055 p.add_param("sdhead", str, defval="synder:", 0056 metavar=_("@info sieve parameter value placeholder", "STRING"), 0057 desc=_("@info sieve parameter discription", 0058 "Prefix which starts syntagma derivator entries in comments." 0059 )) 0060 0061 0062 class Sieve (object): 0063 0064 def __init__ (self, params): 0065 0066 self.caller_sync = False 0067 self.caller_monitored = False 0068 0069 self.propcons = None 0070 if params.propcons: 0071 self.propcons = self._read_propcons(params.propcons) 0072 0073 self.p = params 0074 0075 if not params.pmhead: 0076 raise SieveError(_("@info", 0077 "Prefix which starts property map entries " 0078 "in comments cannot be empty.")) 0079 if not params.sdhead: 0080 raise SieveError(_("@info", 0081 "Prefix which starts syntagma derivator entries " 0082 "in comments cannot be empty.")) 0083 0084 # Collected entries. 0085 # Each element is a tuple of the form: 0086 # (ekeys, props, psep, kvsep, msg, cat) 0087 self.entries = [] 0088 0089 # Syntagma derivator, for synder entries. 0090 self.synder = Synder() 0091 self.sdord = 0 0092 0093 0094 def process (self, msg, cat): 0095 0096 if not msg.translated or msg.obsolete: 0097 return 0098 if msg.msgid_plural is not None: 0099 return 0100 0101 # Parse property map entries from the message. 0102 psep, kvsep = None, None 0103 ekeys = set() 0104 props = {} 0105 for i in range(len(msg.manual_comment)): 0106 ind = i + 1 0107 manc = (msg.manual_comment[i]).strip() 0108 if manc.startswith(self.p.pmhead): 0109 # Parse and check consistency of separators. 0110 espec = manc[len(self.p.pmhead):].lstrip() 0111 lkvsep, lpsep = espec[:2] 0112 if lkvsep.isalnum() or lpsep.isalnum(): 0113 warning_on_msg(_("@info", 0114 "An alphanumeric separator is used for " 0115 "property map entry in comment " 0116 "no. %(ord)d.", ord=ind), 0117 msg, cat) 0118 return 0119 if not psep: 0120 psep, kvsep = lpsep, lkvsep 0121 elif (psep, kvsep) != (lpsep, lkvsep): 0122 warning_on_msg(_("@info", 0123 "Inconsistent separators for " 0124 "continued property map entry in comment " 0125 "no. %(ord)d.", ord=ind), 0126 msg, cat) 0127 return 0128 # Remove leading and trailing separators. 0129 respec = espec[2:] 0130 if respec.endswith(psep + psep): 0131 respec = respec[:-2] 0132 elif respec.endswith(psep): 0133 respec = respec[:-1] 0134 else: 0135 warning_on_msg(_("@info", 0136 "Missing terminating separator for " 0137 "property map entry in comment " 0138 "no. %(ord)d.", ord=ind), 0139 msg, cat) 0140 return 0141 # Parse entry keys and key-value pairs. 0142 for elspec in respec.split(psep): 0143 if kvsep in elspec: 0144 pkey, pval = elspec.split(kvsep, 1) 0145 props[pkey] = pval 0146 else: 0147 ekey = elspec 0148 if not self.p.extrakeys: 0149 warning_on_msg(_("@info", 0150 "Additional entry key '%(key)s' " 0151 "is defined but not allowed for " 0152 "property map entry in comment " 0153 "no. %(ord)d.", key=ekey, ord=ind), 0154 msg, cat) 0155 return 0156 ekeys.add(ekey) 0157 0158 elif manc.startswith(self.p.sdhead): 0159 sddef = manc[len(self.p.sdhead):].lstrip() 0160 sdkey = str(self.sdord) 0161 sdexpr = sdkey + ":" + sddef 0162 if self.p.derivs: 0163 sdexpr = ">" + self.p.derivs + "\n" + sdexpr 0164 try: 0165 self.synder.import_string(sdexpr) 0166 cprops = self.synder.props(sdkey) 0167 except Exception as e: 0168 errmsg = str_to_unicode(str(e)) 0169 warning_on_msg(_("@info", 0170 "Invalid derivation '%(deriv)s':\n" 0171 "%(msg)s", deriv=sddef, msg=errmsg), 0172 msg, cat) 0173 return 0174 0175 jumble = "".join(["".join(x) for x in list(cprops.items())]) 0176 if not psep: 0177 psep = self._pick_sep(jumble, "/|¦") 0178 kvsep = self._pick_sep(jumble, "=:→") 0179 if not psep or not kvsep: 0180 warning_on_msg(_("@info", 0181 "No known separator are applicable " 0182 "to keys and values derived from " 0183 "'%(deriv)s'.", deriv=sddef), 0184 msg, cat) 0185 return 0186 else: 0187 if psep in jumble or kvsep in jumble: 0188 warning_on_msg(_("@info", 0189 "Previously selected separators " 0190 "are not applicable to " 0191 "keys and values derived from " 0192 "'%(deriv)s'.", deriv=sddef), 0193 msg, cat) 0194 return 0195 0196 props.update(cprops) 0197 0198 if not props: 0199 if ekeys: 0200 warning_on_msg(_("@info", 0201 "Some additional entry keys " 0202 "are defined for property map entry, " 0203 "but there are no properties."), 0204 msg, cat) 0205 return 0206 props = sorted(props.items()) # no need for dictionary any more 0207 0208 # Add default keys. 0209 ekeys.add(msg.msgid) 0210 ekeys.add(msg.msgstr[0]) 0211 0212 # Validate entry if requested. 0213 if self.propcons: 0214 errs = self._validate_props(props, msg, cat, self.propcons) 0215 if errs: 0216 problems = cjoin([" " + x for x in errs], "\n") 0217 warning_on_msg(_("@info", 0218 "Property map entry fails validation:\n" 0219 "%(msgs)s", msgs=problems), 0220 msg, cat) 0221 return 0222 0223 # Entry parsed. 0224 ekeys = sorted(ekeys) 0225 props = sorted(props) 0226 self.entries.append((ekeys, props, psep, kvsep, msg, cat)) 0227 0228 0229 def finalize (self): 0230 0231 # Check cross-entry validity, select valid. 0232 msgs_by_seen_msgstr = {} 0233 unique_entries = [] 0234 for entry in self.entries: 0235 d1, props, d3, d4, msg, cat = entry 0236 msgstr = msg.msgstr[0] 0237 if msgstr not in msgs_by_seen_msgstr: 0238 msgs_by_seen_msgstr[msgstr] = [] 0239 else: 0240 for d1, d2, oprops in msgs_by_seen_msgstr[msgstr]: 0241 if props == oprops: 0242 props = None 0243 break 0244 if props: 0245 unique_entries.append(entry) 0246 msgs_by_seen_msgstr[msgstr].append((msg, cat, props)) 0247 good_entries = [] 0248 for ekeys, props, psep, kvsep, msg, cat in unique_entries: 0249 eq_msgstr_set = msgs_by_seen_msgstr.get(msg.msgstr[0]) 0250 if eq_msgstr_set is not None: 0251 if len(eq_msgstr_set) > 1: 0252 cmsgcats = msgs_by_seen_msgstr.pop(msg.msgstr[0]) 0253 msg0, cat0, d3 = cmsgcats[0] 0254 warning_on_msg(_("@info split to link below", 0255 "Property map entries removed due " 0256 "to translation conflict with..."), 0257 msg0, cat0) 0258 for msg, cat, d3 in cmsgcats[1:]: 0259 warning_on_msg(_("@info continuation from above", 0260 "...this message."), 0261 msg, cat) 0262 else: 0263 good_entries.append((ekeys, props, psep, kvsep)) 0264 0265 # If output file has not been given, only validation was expected. 0266 if not self.p.outfile: 0267 return 0268 0269 # Serialize entries. 0270 good_entries.sort(key=lambda x: x[0]) 0271 lines = [] 0272 for ekeys, props, psep, kvsep in good_entries: 0273 # Do Unicode, locale-unaware sorting, 0274 # for equal results over different systems; 0275 # they are not to be read by humans anyway. 0276 propstr = psep.join([kvsep.join(x) for x in sorted(props)]) 0277 ekeystr = psep.join(sorted(ekeys)) 0278 estr = kvsep + psep + ekeystr + psep + propstr + psep + psep 0279 lines.append(estr) 0280 0281 # Write out the property map. 0282 lines.append("") 0283 fstr = "\n".join(lines) 0284 fstr = fstr.encode("UTF-8") 0285 fh = open(self.p.outfile, "w") 0286 fh.write(fstr) 0287 fh.close() 0288 0289 msg = n_("@info:progress", 0290 "Collected %(num)d entry for the property map.", 0291 "Collected %(num)d entries for the property map.", 0292 num=len(good_entries)) 0293 report("===== " + msg) 0294 0295 0296 def _pick_sep (self, teststr, seps): 0297 0298 good = False 0299 for sep in seps: 0300 if sep not in teststr: 0301 good = True 0302 break 0303 return sep if good else None 0304 0305 0306 def _read_propcons (self, fpath): 0307 0308 if not os.path.isfile(fpath): 0309 raise SieveError(_("@info", 0310 "Property constraint file '%(file)s' " 0311 "does not exist.", 0312 file=fpath)) 0313 lines = open(fpath).read().decode("UTF-8").split("\n") 0314 if not lines[-1]: 0315 lines.pop() 0316 0317 cmrx = re.compile(r"#.*") 0318 # Constraints collected as list of tuples: 0319 # (compiled key regex, string key regex, 0320 # compiled value regex, string value regex, 0321 # string of flags) 0322 propcons = [] 0323 lno = 0 0324 def mkerr (problem): 0325 return _("@info", 0326 "Invalid property map constraint " 0327 "at %(file)s:%(line)d: %(snippet)s.", 0328 file=fpath, line=lno, snippet=problem) 0329 known_flags = set(("i", "I", "t", "r")) 0330 for line in lines: 0331 lno += 1 0332 line = cmrx.sub("", line).strip() 0333 if not line: 0334 continue 0335 0336 sep = line[0] 0337 if sep.isalnum(): 0338 raise SieveError(mkerr(_("@item:intext", 0339 "alphanumeric separators " 0340 "not allowed"))) 0341 lst = line.split(sep) 0342 if len(lst) < 4: 0343 raise SieveError(mkerr(_("@item:intext", 0344 "too few separators"))) 0345 elif len(lst) > 4: 0346 raise SieveError(mkerr(_("@item:intext", 0347 "too many separators"))) 0348 0349 d1, keyrxstr, valrxstr, flags = lst 0350 0351 unknown_flags = set(flags).difference(known_flags) 0352 if unknown_flags: 0353 fmtflags = format_item_list(sorted(unknown_flags), quoted=True) 0354 raise SieveError(mkerr(_("@item:intext", 0355 "unknown flags %(flaglist)s", 0356 flaglist=fmtflags))) 0357 0358 rxs = [] 0359 for rxstr, iflag in ((keyrxstr, "I"), (valrxstr, "i")): 0360 rxfls = re.U 0361 if iflag in flags: 0362 rxfls |= re.I 0363 wrxstr = r"^(?:%s)$" % rxstr 0364 try: 0365 rx = re.compile(wrxstr, rxfls) 0366 except: 0367 raise SieveError(mkerr(_("@item:intext", 0368 "invalid regular expression " 0369 "'%(regex)s'", 0370 regex=rxstr))) 0371 rxs.append(rx) 0372 keyrx, valrx = rxs 0373 0374 propcons.append((keyrx, keyrxstr, valrx, valrxstr, flags)) 0375 0376 return propcons 0377 0378 0379 def _validate_props (self, props, msg, cat, propcons): 0380 0381 matched_cons = set() 0382 errs = [] 0383 adderr = lambda err: errs.append(err) 0384 for prop, ip in zip(props, list(range(len(props)))): 0385 key, val = prop 0386 key_matched = False 0387 for propcon, ic in zip(propcons, list(range(len(propcons)))): 0388 keyrx, keyrxstr, valrx, valrxstr, flags = propcon 0389 if keyrx.search(key): 0390 key_matched = True 0391 matched_cons.add(ic) 0392 if not valrx.search(val): 0393 pattern = valrx 0394 adderr(_("@info", 0395 "Value '%(val)s' to key '%(key)s' " 0396 "does not match '%(pattern)s'.", 0397 val=val, key=key, pattern=pattern)) 0398 if "t" in flags: 0399 if "i" in flags: 0400 eq = (val.lower() == msg.msgstr[0].lower()) 0401 else: 0402 eq = (val == msg.msgstr[0]) 0403 if not eq: 0404 adderr(_("@info", 0405 "Value '%(val)s' to key '%(key)s' " 0406 "does not match translation " 0407 "of the message.", 0408 val=val, key=key)) 0409 if not key_matched: 0410 adderr(_("@info", 0411 "Key '%(key)s' does not match any constraint.", 0412 key=key)) 0413 0414 for propcon, ic in zip(propcons, list(range(len(propcons)))): 0415 pattern, rlags = propcon[1], propcon[-1] 0416 if "r" in flags and ic not in matched_cons: 0417 adderr(_("@info", 0418 "No key matched required constraint '%(pattern)s'.", 0419 pattern=pattern)) 0420 0421 return errs 0422