File indexing completed on 2024-12-01 13:47:51

0001 # -*- coding: UTF-8 -*-
0002 
0003 """
0004 Message entries in PO catalogs.
0005 
0006 Classes from this module define the entries proper,
0007 while the header entry is handled by L{pology.header}.
0008 
0009 @see: L{pology.header}
0010 
0011 @author: Chusslove Illich (Часлав Илић) <caslav.ilic@gmx.net>
0012 @license: GPLv3
0013 """
0014 
0015 from pology.colors import ColorString, cjoin
0016 from pology.escape import escape_c
0017 from pology.wrap import wrap_field, wrap_comment, wrap_comment_unwrap
0018 from pology.monitored import Monitored, Monlist, Monset, Monpair
0019 
0020 
0021 _Message_spec = {
0022     "manual_comment" : {"type" : Monlist,
0023                         "spec" : {"*" : {"type" : str}}},
0024     "auto_comment" : {"type" : Monlist,
0025                       "spec" : {"*" : {"type" : str}}},
0026     "source" : {"type" : Monlist,
0027                 "spec" : {"*" : {"type" : Monpair,
0028                                  "spec" : {"first" : {"type" : str},
0029                                            "second" : {"type" : int}}}}},
0030     "flag" : {"type" : Monset,
0031               "spec" : {"*" : {"type" : str}}},
0032 
0033     "obsolete" : {"type" : bool},
0034 
0035     "msgctxt_previous" : {"type" : (str, type(None))},
0036     "msgid_previous" : {"type" : (str, type(None))},
0037     "msgid_plural_previous" : {"type" : (str, type(None))},
0038 
0039     "msgctxt" : {"type" : (str, type(None))},
0040     "msgid" : {"type" : str},
0041     "msgid_plural" : {"type" : (str, type(None))},
0042     "msgstr" : {"type" : Monlist,
0043                 "spec" : {"*" : {"type" : str}}},
0044 
0045     "key" : {"type" : str, "derived" : True},
0046     "fmt" : {"type" : str, "derived" : True},
0047     "inv" : {"type" : str, "derived" : True},
0048     "trn" : {"type" : str, "derived" : True},
0049     "fuzzy" : {"type" : bool},
0050     "untranslated" : {"type" : bool, "derived" : True},
0051     "translated" : {"type" : bool, "derived" : True},
0052     "active" : {"type" : bool, "derived" : True},
0053     "format" : {"type" : str, "derived" : True},
0054 
0055     "refline" : {"type" : int},
0056     "refentry" : {"type" : int},
0057 }
0058 
0059 # Exclusive groupings.
0060 _Message_single_fields = (
0061     "msgctxt_previous", "msgid_previous", "msgid_plural_previous",
0062     "msgctxt", "msgid", "msgid_plural",
0063 )
0064 _Message_list_fields = (
0065     "manual_comment", "auto_comment",
0066     "msgstr",
0067 )
0068 _Message_list2_fields = (
0069     "source",
0070 )
0071 _Message_set_fields = (
0072     "flag",
0073 )
0074 _Message_state_fields = (
0075     "fuzzy", "obsolete",
0076 )
0077 
0078 # Convenience groupings.
0079 _Message_all_fields = (()
0080     + _Message_single_fields
0081     + _Message_list_fields
0082     + _Message_list2_fields
0083     + _Message_set_fields
0084     + _Message_state_fields
0085 )
0086 _Message_sequence_fields = (()
0087     + _Message_list_fields
0088     + _Message_list2_fields
0089     + _Message_set_fields
0090 )
0091 _Message_key_fields = (
0092     "msgctxt", "msgid",
0093 )
0094 _Message_mandatory_fields = (
0095     "msgid", "msgstr",
0096 )
0097 _Message_currprev_fields = (
0098     ("msgctxt", "msgctxt_previous"),
0099     ("msgid", "msgid_previous"),
0100     ("msgid_plural", "msgid_plural_previous"),
0101 )
0102 _Message_fmt_fields = (
0103     "msgctxt",
0104     "msgid",
0105     "msgid_plural",
0106     "msgstr",
0107     "obsolete",
0108     "fuzzy",
0109 )
0110 _Message_inv_fields = (
0111     "obsolete",
0112     "fuzzy",
0113     "manual_comment",
0114     "msgctxt_previous",
0115     "msgid_previous",
0116     "msgid_plural_previous",
0117     "msgctxt",
0118     "msgid",
0119     "msgid_plural",
0120     "msgstr",
0121 )
0122 
0123 
0124 def _escape (text):
0125 
0126     text = escape_c(text)
0127     if isinstance(text, ColorString):
0128         text = text.replace("&quot;", "\\&quot;")
0129     return text
0130 
0131 
0132 class Message_base (object):
0133     """
0134     Abstract base class for entries in PO catalogs.
0135 
0136     Elements of the message are accessed through instance attributes.
0137     Some of them are read-only, typically those that are derived from the
0138     normal read-write attributes and cannot be set independently.
0139 
0140     The precise type of each attribute depends on the subclass through which
0141     it is accessed, but has a general behavior of one of the standard types.
0142     E.g. when the behavior is that of a list, the type is stated as C{list*}.
0143     All strings are assumed unicode, except where noted otherwise.
0144 
0145     Regardless of the exact composition of the message, each message object
0146     will have all the instance attributes listed. In case the message actually
0147     does not have an element corresponding to an instance attribute,
0148     that attribute will have an appropriate null value.
0149 
0150     Only the read-only attributes are provided by this base class,
0151     while the read-write attributes are to be provided by its subclasses.
0152     All are listed here, however, as the interface that all subclasses
0153     should implement.
0154 
0155     @ivar manual_comment: manual (translator) comments (C{# ...})
0156     @type manual_comment: list* of strings
0157 
0158     @ivar auto_comment: automatic (extracted) comments (C{#. ...})
0159     @type auto_comment: list* of strings
0160 
0161     @ivar source: source references, as filepath:lineno pairs (C{#: ...})
0162     @type source: list* of pairs*
0163 
0164     @ivar flag: message flags (C{#, ...})
0165     @type flag: set* of strings
0166 
0167     @ivar obsolete: whether entry is obsolete (C{#~ ...})
0168     @type obsolete: bool
0169 
0170     @ivar msgctxt_previous: previous context field (C{#| msgctxt "..."})
0171     @type msgctxt_previous: string or None
0172 
0173     @ivar msgid_previous: previous message field (C{#| msgid "..."})
0174     @type msgid_previous: string or None
0175 
0176     @ivar msgid_plural_previous:
0177         previous plural field (C{#| msgid_plural "..."})
0178     @type msgid_plural_previous: string or None
0179 
0180     @ivar msgctxt: context field (C{msgctxt "..."})
0181     @type msgctxt: string or None
0182 
0183     @ivar msgid: message field (C{msgid "..."})
0184     @type msgid: string
0185 
0186     @ivar msgid_plural: plural field (C{msgid_plural "..."})
0187     @type msgid_plural: string or None
0188 
0189     @ivar msgstr: translation fields (C{msgstr "..."}, C{msgstr[n] "..."})
0190     @type msgstr: list* of strings
0191 
0192     @ivar key: (read-only) key composition
0193 
0194         Message key is formed by the parts of the message which define
0195         unique entry in a catalog.
0196 
0197         The value is an undefined serialization of C{msgctxt} and C{msgid}.
0198     @type key: string
0199 
0200     @ivar fmt: (read-only) format composition
0201 
0202         Format composition consists of all message parts which determine
0203         contents of compiled message in the MO file, including whether
0204         it is compiled at all.
0205 
0206         The value is an undefined serialization of: C{msgctxt}, C{msgid},
0207         C{msgid_plural}, C{msgstr}, C{fuzzy}, C{obsolete}.
0208     @type fmt: string
0209 
0210     @ivar inv: (read-only) extraction-invariant composition
0211 
0212         Extraction-invariant parts of the message are those that are not
0213         dependent on the placement and comments to the message in the code.
0214         In effect, these are the parts which are not eliminated when
0215         the message is obsoleted after merging.
0216 
0217         The value is an undefined serialization of: C{msgctxt}, C{msgid},
0218         C{msgid_plural}, C{msgstr}, C{fuzzy}, C{obsolete}, C{manual_comment},
0219         C{msgctxt_previous}, C{msgid_previous}, C{msgid_plural_previous}.
0220     @type inv: string
0221 
0222     @ivar trn: (read-only) translator-controlled composition
0223 
0224         Translator-controlled parts of the message are those that are
0225         normally modified by a translator when working on a PO file.
0226 
0227         The value is an undefined serialization of: C{msgstr}, C{fuzzy},
0228         C{manual_comment}.
0229     @type trn: string
0230 
0231     @ivar fuzzy:
0232         whether the message is fuzzy
0233 
0234         The state of fuzziness can be also checked and set by looking for
0235         and adding/removing the C{fuzzy} flag from the set of flags,
0236         but this is needed frequently enough to deserve a standalone attribute.
0237         Note: To "thoroughly" unfuzzy the message, see method L{unfuzzy}.
0238     @type fuzzy: bool
0239 
0240     @ivar untranslated: (read-only)
0241         whether the message is untranslated (False for fuzzy messages)
0242     @type untranslated: bool
0243 
0244     @ivar translated: (read-only)
0245         whether the message is translated (False for fuzzy messages)
0246     @type translated: bool
0247 
0248     @ivar active: (read-only)
0249         whether the translation of the message is used at destination
0250         (C{False} for untranslated, fuzzy and obsolete messages)
0251     @type active: bool
0252 
0253     @ivar format: (read-only)
0254         the format flag of the message (e.g. C{c-format}) or empty string
0255     @type format: string
0256 
0257     @ivar refline:
0258         referent line number of the message inside the catalog
0259 
0260         Valid only if there were no modifications to the catalog, otherwise
0261         undefined (made valid again after syncing the catalog).
0262         Normally this is the line number of C{msgid} keyword,
0263         but not guaranteed to be so.
0264     @type refline: int
0265 
0266     @ivar refentry:
0267         referent entry number of the message inside the catalog
0268 
0269         Valid only if there were no additions/removals of messages from the
0270         catalog, otherwise undefined (made valid again after syncing the
0271         catalog).
0272     @type refentry: int
0273 
0274     @ivar key_previous: (read-only) previous key composition
0275 
0276         Like L{key}, except this is for previous fields.
0277         If there are no previous fields, this is C{None}.
0278 
0279         The value is an undefined serialization of C{msgctxt_previous}
0280         and C{msgid_previous}.
0281     @type key: string or C{None}
0282 
0283     @see: L{Message}
0284     @see: L{MessageUnsafe}
0285     """
0286 
0287     def __init__ (self, getsetattr):
0288         """
0289         Internal constructor for subclasses' usage.
0290 
0291         @param getsetattr:
0292             the object with C{__getattr__} and C{__setattr__} methods,
0293             as handler for unhandled instance attributes
0294         """
0295 
0296         self.__dict__["^getsetattr"] = getsetattr
0297         self._colorize_prev = 0
0298 
0299 
0300     def __getattr__ (self, att):
0301         """
0302         Attribute getter.
0303 
0304         Processes read-only attributes, and sends others to the getter
0305         given by the constructor.
0306 
0307         @param att: name of the attribute to get
0308         @returns: attribute value
0309         """
0310 
0311         if 0: pass
0312 
0313         elif att == "translated":
0314             if self.fuzzy:
0315                 return False
0316             # Consider message translated if at least one msgstr is translated:
0317             # that's how gettext tools do, but then they report an error for
0318             # missing argument in non-translated msgstrs.
0319             for val in self.msgstr:
0320                 if val:
0321                     return True
0322             return False
0323 
0324         elif att == "untranslated":
0325             if self.fuzzy:
0326                 return False
0327             for val in self.msgstr:
0328                 if val:
0329                     return False
0330             return True
0331 
0332         elif att == "active":
0333             return self.translated and not self.obsolete
0334 
0335         elif att == "key":
0336             return self._compose(["msgctxt", "msgid"])
0337 
0338         elif att == "fmt":
0339             return self._compose(["msgctxt", "msgid",
0340                                   "msgid_plural", "msgstr",
0341                                   "fuzzy", "obsolete"])
0342 
0343         elif att == "inv":
0344             return self._compose(["msgctxt", "msgid",
0345                                   "msgid_plural", "msgstr",
0346                                   "fuzzy", "obsolete",
0347                                   "manual_comment", "msgctxt_previous",
0348                                   "msgid_previous", "msgid_plural_previous"])
0349 
0350         elif att == "trn":
0351             return self._compose(["msgstr", "fuzzy", "manual_comment"])
0352 
0353         elif att == "format":
0354             format_flag = ""
0355             for flag in self.flag:
0356                 if flag.find("-format") >= 0:
0357                     format_flag = flag
0358                     break
0359             return format_flag
0360 
0361         elif att == "fuzzy":
0362             return "fuzzy" in self.flag
0363 
0364         elif att == "key_previous":
0365             if self.msgid_previous is not None:
0366                 return self._compose(["msgctxt_previous", "msgid_previous"])
0367             else:
0368                 return None
0369 
0370         else:
0371             return self.__dict__["^getsetattr"].__getattr__(self, att)
0372 
0373 
0374     def _compose (self, fields):
0375 
0376         fmtvals = []
0377         for field in fields:
0378             val = self.get(field)
0379             if field in _Message_state_fields:
0380                 fval = val and "1" or "0"
0381             elif field in _Message_list_fields:
0382                 fval = "\x02".join(["%s" % x for x in val])
0383             elif field in _Message_list2_fields:
0384                 fval = "\x02".join(["%s:%s" % tuple(x) for x in val])
0385             elif field in _Message_set_fields:
0386                 vlst = ["%s" % x for x in val]
0387                 vlst.sort()
0388                 fval = "\x02".join(vlst)
0389             else:
0390                 fval = val is None and "\x00" or "%s" % val
0391             fmtvals.append(fval)
0392         return "\x04".join(fmtvals)
0393 
0394 
0395     def get (self, att, default=None):
0396         """
0397         Get attribute value.
0398 
0399         Allows accessing the message like a dictionary.
0400 
0401         @param att: name of the attribute to get
0402         @type att: string
0403         @param default: value to return if attribute does not exist
0404 
0405         @returns: value of the attribute or the default value
0406         """
0407 
0408         if hasattr(self, att):
0409             return getattr(self, att)
0410         else:
0411             return default
0412 
0413 
0414     def __setattr__ (self, att, val):
0415         """
0416         Attribute setter.
0417 
0418         May act upon some attributes (e.g. checks), but finally passes
0419         all of them to the setter given by the constructor.
0420 
0421         @param att: name of the attribute to set
0422         @param val: value to set the attribute to
0423         """
0424 
0425         if 0: pass
0426 
0427         elif att == "fuzzy":
0428             if val == True:
0429                 self.flag.add("fuzzy")
0430             elif "fuzzy" in self.flag:
0431                 self.flag.remove("fuzzy")
0432 
0433         else:
0434             self.__dict__["^getsetattr"].__setattr__(self, att, val)
0435 
0436 
0437     def __eq__ (self, omsg):
0438         """
0439         Reports whether messages are equal in all apparent parts.
0440 
0441         "Apparent" parts include all those which are visible in the PO file.
0442         I.e. the check will ignore internal states, like line caches, etc.
0443 
0444         @returns: C{True} if messages are equal in apparent parts
0445         @rtype: bool
0446         """
0447 
0448         # Make messages the same type.
0449         # NOTE: All this instead of just omsg = type(self)(omsg)
0450         # for the sake of performance.
0451         if not isinstance(omsg, Message_base):
0452             omsg = MessageUnsafe(omsg)
0453         msg = self
0454         if isinstance(self, Message) and isinstance(omsg, MessageUnsafe):
0455             msg = MessageUnsafe(msg)
0456         elif isinstance(self, MessageUnsafe) and isinstance(omsg, Message):
0457             omsg = MessageUnsafe(omsg)
0458 
0459         for field in _Message_all_fields:
0460             if msg.get(field) != omsg.get(field):
0461                 return False
0462 
0463         return True
0464 
0465 
0466     def __ne__ (self, omsg):
0467         """
0468         Reports whether messages are not equal in some apparent parts.
0469 
0470         Equivalent to C{not (self == omsg)}.
0471 
0472         @returns: C{False} if messages are equal in all apparent parts
0473         @rtype: bool
0474         """
0475 
0476         return not self.__eq__(omsg)
0477 
0478     def __hash__ (self):
0479         return id(self)//16
0480 
0481     def _renew_lines_bymod (self, mod, wrapf=wrap_field, force=False,
0482                             colorize=0):
0483 
0484         prefix = {}
0485         if self.obsolete:
0486             prefix["curr"] = "#~ "
0487             prefix["prev"] = "#~| "
0488         else:
0489             prefix["curr"] = ""
0490             prefix["prev"] = "#| "
0491 
0492         if force or mod["manual_comment"] or not self._lines_manual_comment:
0493             self._lines_manual_comment = []
0494             for manc in self.manual_comment:
0495                 ls = wrap_comment_unwrap("", manc)
0496                 if colorize >= 2:
0497                     ls = [ColorString("<grey>%s</grey>") % x for x in ls]
0498                 self._lines_manual_comment.extend(ls)
0499 
0500         if force or mod["auto_comment"] or not self._lines_auto_comment:
0501             self._lines_auto_comment = []
0502             for autoc in self.auto_comment:
0503                 ls = wrap_comment_unwrap(".", autoc)
0504                 if colorize >= 2:
0505                     ls = [ColorString("<blue>%s</blue>") % x for x in ls]
0506                 self._lines_auto_comment.extend(ls)
0507 
0508         if force or mod["source"] or not self._lines_source:
0509             self._lines_source = []
0510             srcrefs = []
0511             for src in self.source:
0512                 if src[1] > 0:
0513                     srcrefs.append(src[0] + ":" + str(src[1]))
0514                 else:
0515                     srcrefs.append(src[0])
0516             if srcrefs:
0517                 ls = wrap_comment(":", cjoin(srcrefs, " "))
0518                 if colorize >= 2:
0519                     ls = [ColorString("<blue>%s</blue>") % x for x in ls]
0520                 self._lines_source = ls
0521 
0522         if force or mod["flag"] or not self._lines_flag:
0523             self._lines_flag = []
0524             # Rearange so that fuzzy is first, if present.
0525             flst = []
0526             for fl in self.flag:
0527                 if fl == "fuzzy":
0528                     if colorize >= 1:
0529                         fl = ColorString("<underline>%s</underline>") % fl
0530                     flst.insert(0, fl)
0531                 else:
0532                     flst.append(fl)
0533             if flst:
0534                 ls = wrap_comment(",", cjoin(flst, ", "))
0535                 if colorize >= 2:
0536                     ls = [ColorString("<blue>%s</blue>") % x for x in ls]
0537                 self._lines_flag = ls
0538 
0539         for att in _Message_single_fields:
0540             att_lins = "_lines_" + att
0541             if force or mod[att] or not self.__dict__[att_lins]:
0542                 # modcount of this string > 0 or lines not cached or forced
0543                 self.__dict__[att_lins] = []
0544                 msgsth = getattr(self, att)
0545                 if msgsth is not None or att in _Message_mandatory_fields:
0546                     if msgsth is None:
0547                         msgsth = ""
0548                     if att.endswith("_previous"):
0549                         fname = att[:-len("_previous")]
0550                         pstat = "prev"
0551                     else:
0552                         fname = att
0553                         pstat = "curr"
0554                         if colorize >= 1:
0555                             fname = ColorString("<bold>%s</bold>") % fname
0556                     self.__dict__[att_lins] = wrapf(fname, _escape(msgsth),
0557                                                     prefix[pstat])
0558 
0559         # msgstr must be renewed if the plurality of the message changed.
0560         new_plurality = (    getattr(self, "_lines_msgstr", [])
0561                          and (   (    self.msgid_plural is None
0562                                   and "msgstr[" in self._lines_msgstr[0])
0563                               or (    self.msgid_plural is not None
0564                                   and "msgstr[" not in self._lines_msgstr[0])))
0565 
0566         if force or mod["msgstr"] or not self._lines_msgstr or new_plurality:
0567             self._lines_msgstr = []
0568             msgstr = self.msgstr or [""]
0569             if self.msgid_plural is None:
0570                 fname = "msgstr"
0571                 if colorize >= 1:
0572                     fname = ColorString("<bold>%s</bold>") % fname
0573                 self._lines_msgstr.extend(wrapf(fname,
0574                                           _escape(msgstr[0]),
0575                                           prefix["curr"]))
0576             else:
0577                 for i in range(len(msgstr)):
0578                     fname = "msgstr[%d]" % i
0579                     if colorize >= 1:
0580                         fname = ColorString("<bold>%s</bold>") % fname
0581                     self._lines_msgstr.extend(wrapf(fname,
0582                                                     _escape(msgstr[i]),
0583                                                     prefix["curr"]))
0584 
0585         # Marshal the lines into proper order.
0586         self._lines_all = []
0587         lins = self._lines_all
0588 
0589         lins.extend(self._lines_manual_comment)
0590         lins.extend(self._lines_auto_comment)
0591         if not self.obsolete: # no source for an obsolete message
0592             lins.extend(self._lines_source)
0593         lins.extend(self._lines_flag)
0594 
0595         # Actually, it might make sense regardless...
0596         ## Old originals makes sense only for a message with a fuzzy flag.
0597         #if self.fuzzy:
0598         lins.extend(self._lines_msgctxt_previous)
0599         lins.extend(self._lines_msgid_previous)
0600         lins.extend(self._lines_msgid_plural_previous)
0601 
0602         lins.extend(self._lines_msgctxt)
0603         lins.extend(self._lines_msgid)
0604         lins.extend(self._lines_msgid_plural)
0605         lins.extend(self._lines_msgstr)
0606 
0607         if self._lines_all[-1] != "\n":
0608             lins.extend("\n")
0609 
0610 
0611     def to_lines (self, wrapf=wrap_field, force=False, colorize=0):
0612         """
0613         The line-representation of the message.
0614 
0615         Lines are returned with newlines included.
0616 
0617         @param wrapf:
0618             the function used for wrapping message fields (msgctxt, msgid, ...)
0619             As arguments the function should accept the field name,
0620             the field text, and the prefix to all lines,
0621             and return the list of wrapped lines (with newlines included).
0622         @type wrapf: string, string, string -> list of strings
0623 
0624         @param force:
0625             whether to force reformatting of all elements.
0626             Subclasses may keep a track of lines exactly as read from the
0627             PO file, and allow reformatting of only the modified elements of
0628             the message.
0629         @type force: bool
0630 
0631         @param colorize: whether and how much to colorize the message.
0632             Typically useful when the message is output to terminal,
0633             HTML file, etc. as accompanying information to a user.
0634             If the value is 0, no colorization is applied;
0635             1 gives conservative colorization, 2 and more full colorization.
0636         @type colorize: int
0637 
0638         @returns: formatted lines
0639         @rtype: list of strings
0640 
0641         @see: L{pology.wrap}
0642         """
0643 
0644         # Renew lines if one of: forced, no lines formed yet, no modcounter,
0645         # different colorization.
0646         if colorize != self._colorize_prev:
0647             force = True
0648         if force or getattr(self, "modcount", True) or not self._lines_all:
0649             self._renew_lines(wrapf, force, colorize)
0650             self._colorize_prev = colorize
0651 
0652         return self._lines_all
0653 
0654 
0655     def to_string (self, wrapf=wrap_field, force=False, colorize=0):
0656         """
0657         The string-representation of the message.
0658 
0659         Passes the arguments to L{to_lines} and joins the resulting list.
0660 
0661         @see: L{to_lines}
0662         """
0663 
0664         return cjoin(self.to_lines(wrapf, force, colorize))
0665 
0666 
0667     def _append_to_list (self, other, att):
0668 
0669         self_list = getattr(self, att)
0670         other_list = getattr(other, att)
0671         for el in other_list:
0672             self_list.append(el)
0673 
0674 
0675     def _overwrite_list (self, other, att):
0676 
0677         # Overwrites self list by element-assignment/append/pop,
0678         # so that modification history is tracked.
0679         self_list = getattr(self, att)
0680         other_list = getattr(other, att)
0681         self_len = len(self_list)
0682         other_len = len(other_list)
0683         if self_len <= other_len:
0684             for i in range(self_len):
0685                 self_list[i] = other_list[i]
0686             for i in range(self_len, other_len):
0687                 self_list.append(other_list[i])
0688         else:
0689             for i in range(other_len):
0690                 self_list[i] = other_list[i]
0691             for i in range(other_len, self_len):
0692                 self_list.pop()
0693 
0694 
0695     def unfuzzy (self):
0696         """
0697         Thoroughly unfuzzy the message.
0698 
0699         Strictly speaking, a message is fuzzy if it has the C{fuzzy} flag set.
0700         Thus a message can be unfuzzied by removing this flag, either
0701         manually from the C{flag} set, or through attribute C{fuzzy}.
0702         But if there were previous fields (e.g. C{msgid_previous})
0703         added to the message when it was made fuzzy on merge, they will
0704         remain in the message after it has been unfuzzied in this way.
0705         This is normally not wanted, and in such cases this method may
0706         be used to I{thouroughly} unfuzzy the message: remove C{fuzzy} flag,
0707         set C{fuzzy} attribute to C{False}, and all C{*_previous}
0708         attributes to C{None}.
0709 
0710         If the message is not strictly fuzzy upon this call,
0711         it is undefined whether any present previous fields will be
0712         left untouched, or removed nontheless.
0713 
0714         @returns: True if the message was unfuzzied, false otherwise
0715         """
0716 
0717         if not self.fuzzy:
0718             return False
0719 
0720         self.fuzzy = False # also removes fuzzy flag
0721         self.msgctxt_previous = None
0722         self.msgid_previous = None
0723         self.msgid_plural_previous = None
0724 
0725         return True
0726 
0727 
0728     def clear (self, keepmanc=False, msgstrlen=None):
0729         """
0730         Revert message to pristine untranslated state.
0731 
0732         Reverting to untranslated state removes manual comments (by default),
0733         C{fuzzy} flag, and previous fields, and clears C{msgstr} fields.
0734 
0735         @param keepmanc: do not remove manual comments
0736         @type keepmanc: bool
0737         @param msgstrlen: the number of empty msgstr fields;
0738             if C{None}, the existing number of fields is preserved
0739         @type msgstrlen: int
0740         """
0741 
0742         if not keepmanc:
0743             self.manual_comment = type(self.manual_comment)()
0744         self.fuzzy = False # also removes fuzzy flag
0745         self.msgctxt_previous = None
0746         self.msgid_previous = None
0747         self.msgid_plural_previous = None
0748         if msgstrlen is None:
0749             msgstrlen = len(self.msgstr)
0750         self.msgstr = type(self.msgstr)([""] * msgstrlen)
0751 
0752 
0753     def state (self):
0754         """
0755         Coded description of the translation state of the message.
0756 
0757         Code string can be one of:
0758         "T" (translated), "F" (fuzzy), "U" (untranslated),
0759         "OT" (obsolete translated), "OF" (obsolete fuzzy),
0760         "OU" (obsolete untranslated).
0761 
0762         @returns: coded translation state
0763         @rtype: string
0764         """
0765 
0766         if not self.obsolete:
0767             if self.fuzzy:
0768                 return "F"
0769             elif self.translated:
0770                 return "T"
0771             else:
0772                 return "U"
0773         else:
0774             if self.fuzzy:
0775                 return "OF"
0776             elif self.translated:
0777                 return "OT"
0778             else:
0779                 return "OU"
0780 
0781 
0782     def set (self, omsg):
0783         """
0784         Copy all parts from the other message.
0785 
0786         All mutable parts are deeply copied.
0787 
0788         @param omsg: the message from which to copy the parts
0789         @type omsg: instance of L{Message_base}
0790 
0791         @returns: self
0792         """
0793 
0794         return self._set_parts(omsg, _Message_all_fields)
0795 
0796 
0797     def set_key (self, omsg):
0798         """
0799         Copy all key parts from the other message.
0800 
0801         See L{key} attribute for the description and list of key parts.
0802 
0803         All mutable parts are deeply copied.
0804 
0805         @param omsg: the message from which to copy the parts
0806         @type omsg: instance of L{Message_base}
0807 
0808         @returns: self
0809         """
0810 
0811         return self._set_parts(omsg, _Message_key_fields)
0812 
0813 
0814     def set_fmt (self, omsg):
0815         """
0816         Copy all format parts from the other message.
0817 
0818         See L{fmt} attribute for the description and list of format parts.
0819 
0820         All mutable parts are deeply copied.
0821 
0822         @param omsg: the message from which to copy the parts
0823         @type omsg: instance of L{Message_base}
0824 
0825         @returns: self
0826         """
0827 
0828         return self._set_parts(omsg, _Message_fmt_fields)
0829 
0830 
0831     def set_inv (self, omsg):
0832         """
0833         Copy extraction-invariant parts from the other message.
0834 
0835         See L{inv} attribute for the description and list of
0836         extraction-invariant parts.
0837 
0838         All mutable parts are deeply copied.
0839 
0840         @param omsg: the message from which to copy the parts
0841         @type omsg: instance of L{Message_base}
0842 
0843         @returns: self
0844         """
0845 
0846         return self._set_parts(omsg, _Message_inv_fields)
0847 
0848 
0849     def _set_parts (self, omsg, parts):
0850         """
0851         Worker for set* methods.
0852         """
0853 
0854         for part in parts:
0855             oval = omsg.get(part)
0856             val = self.get(part)
0857             if oval is not None:
0858                 if part in _Message_list2_fields:
0859                     oval = type(val)([type(x)(x) for x in oval])
0860                 elif part in _Message_sequence_fields:
0861                     oval = type(val)(oval)
0862                 elif val is not None:
0863                     oval = type(val)(oval)
0864             setattr(self, part, oval)
0865 
0866         return self
0867 
0868 
0869 class Message (Message_base, Monitored): # order important for get/setattr
0870     """
0871     The default class for catalog entries.
0872 
0873     The interface is inherited from L{Message_base}, but when used through
0874     this class it behaves in a special way: the modifications are I{monitored},
0875     such that no new attributes can be created by assignment
0876     and all assignments are checked for value types.
0877     If you don't need to modify the messages after creation, consider using
0878     the faster L{MessageUnsafe} class.
0879 
0880     The loosely defined types in the base class (those with a star)
0881     are resolved into one of C{Mon*} types: L{Monlist}, L{Monset}, L{Monpair}.
0882     They implement some, but not all, of the functionality of their standard
0883     counterparts.
0884 
0885     @see: L{Message_base}
0886     @see: L{MessageUnsafe}
0887     @see: L{pology.monitored}
0888     """
0889 
0890     def __init__ (self, init={}):
0891         """
0892         Initializes the message elements by the values in the dictionary.
0893 
0894         The dictionary keys are like the names of attributes in the
0895         interface, and not all must be supplied. Those left out will be
0896         initialized to appropriate null values.
0897 
0898         The monitored sequences should be supplied as their ordinary
0899         counterparts (e.g. a C{list} in place of L{Monlist}),
0900 
0901         @param init: dictionary of initial values
0902         @type init: dict
0903         """
0904 
0905         # NOTE: Make sure all sequences are shallow copied.
0906 
0907         Message_base.__init__(self, Monitored)
0908 
0909         self._manual_comment = Monlist(init.get("manual_comment", [])[:])
0910         self._auto_comment = Monlist(init.get("auto_comment", [])[:])
0911         self._source = Monlist(list(map(Monpair, init.get("source", [])[:])))
0912         self._flag = Monset(init.get("flag", []))
0913 
0914         self._obsolete = init.get("obsolete", False)
0915 
0916         self._msgctxt_previous = init.get("msgctxt_previous", None)
0917         self._msgid_previous = init.get("msgid_previous", None)
0918         self._msgid_plural_previous = init.get("msgid_plural_previous", None)
0919 
0920         self._msgctxt = init.get("msgctxt", None)
0921         self._msgid = init.get("msgid", "")
0922         self._msgid_plural = init.get("msgid_plural", None)
0923         self._msgstr = Monlist(init.get("msgstr", [])[:])
0924 
0925         self._fuzzy = ("fuzzy" in self._flag and not self._obsolete)
0926 
0927         self._refline = init.get("refline", -1)
0928         self._refentry = init.get("refentry", -1)
0929 
0930         self.assert_spec_init(_Message_spec)
0931 
0932         # Line caches.
0933         self._lines_all = init.get("_lines_all", [])[:]
0934         self._lines_manual_comment = init.get("_lines_manual_comment", [])[:]
0935         self._lines_auto_comment = init.get("_lines_auto_comment", [])[:]
0936         self._lines_source = init.get("_lines_source", [])[:]
0937         self._lines_flag = init.get("_lines_flag", [])[:]
0938         self._lines_msgctxt_previous = init.get("_lines_msgctxt_previous", [])[:]
0939         self._lines_msgid_previous = init.get("_lines_msgid_previous", [])[:]
0940         self._lines_msgid_plural_previous = init.get("_lines_msgid_plural_previous", [])[:]
0941         self._lines_msgctxt = init.get("_lines_msgctxt", [])[:]
0942         self._lines_msgid = init.get("_lines_msgid", [])[:]
0943         self._lines_msgid_plural = init.get("_lines_msgid_plural", [])[:]
0944         self._lines_msgstr = init.get("_lines_msgstr", [])[:]
0945 
0946     def _renew_lines (self, wrapf=wrap_field, force=False, colorize=0):
0947 
0948         if not self.obsolete_modcount:
0949             mod = {}
0950             mod["manual_comment"] = (   self.manual_comment_modcount
0951                                      or self.manual_comment.modcount)
0952             mod["auto_comment"] = (   self.auto_comment_modcount
0953                                    or self.auto_comment.modcount)
0954             mod["source"] = self.source_modcount or self.source.modcount
0955             mod["flag"] = self.flag_modcount or self.flag.modcount
0956             for att in _Message_single_fields:
0957                 mod[att] = getattr(self, att + "_modcount") > 0
0958             mod["msgstr"] = self.msgstr_modcount or self.msgstr.modcount
0959         else:
0960             # Must recompute all lines if the message has been modified
0961             # by changing the obsolete status.
0962             mod = None
0963             force = True
0964 
0965         return self._renew_lines_bymod(mod, wrapf, force, colorize)
0966 
0967 
0968 class MessageUnsafe (Message_base):
0969     """
0970     The lightweight class for catalog entries, for read-only applications.
0971 
0972     Unlike the L{Message}, this class does nothing special with attributes.
0973     The interface attributes are implemented as in L{Message_base},
0974     where the starred lists are standard lists, starred sets
0975     standard sets, etc. There is no assignment and type checking, nor
0976     modification monitoring. You should use this class when messages are not
0977     expected to be modified, for the performance benefit.
0978 
0979     The top modification counter still exists, but only as an ordinary
0980     inactive attribute, which the client code can manually increase
0981     to signal that the message has changed. This may be necessary for some
0982     client code, which relies on top counter to function properly.
0983 
0984     @see: L{Message_base}
0985     """
0986 
0987     def __init__ (self, init={}):
0988         """
0989         Initializes the message elements by the values in the dictionary.
0990 
0991         The dictionary keys are like the names of attributes in the
0992         interface, and not all must be supplied. Those left out will be
0993         initialized to appropriate null values.
0994 
0995         @param init: dictionary of initial values
0996         @type init: dict
0997         """
0998 
0999         # NOTE: Make sure all sequences are shallow copied.
1000 
1001         Message_base.__init__(self, object)
1002 
1003         self.manual_comment = list(init.get("manual_comment", []))
1004         self.auto_comment = list(init.get("auto_comment", []))
1005         self.source = [tuple(x) for x in init.get("source", [])]
1006         self.flag = set(init.get("flag", []))
1007 
1008         self.obsolete = init.get("obsolete", False)
1009 
1010         self.msgctxt_previous = init.get("msgctxt_previous", None)
1011         self.msgid_previous = init.get("msgid_previous", None)
1012         self.msgid_plural_previous = init.get("msgid_plural_previous", None)
1013 
1014         self.msgctxt = init.get("msgctxt", None)
1015         self.msgid = init.get("msgid", "")
1016         self.msgid_plural = init.get("msgid_plural", None)
1017         self.msgstr = list(init.get("msgstr", [""]))
1018 
1019         self.refline = init.get("refline", -1)
1020         self.refentry = init.get("refentry", -1)
1021 
1022         # No need to look for line caches, as lines must always be reformatted.
1023 
1024 
1025     def _renew_lines (self, wrapf=wrap_field, force=False, colorize=0):
1026 
1027         # No monitoring, content must always be reformatted.
1028         return self._renew_lines_bymod(None, wrapf, True, colorize)
1029