Module pylupdate
[hide private]
[frames] | no frames]

Source Code for Module pylupdate

  1  #!/usr/bin/env python3 
  2  """ 
  3  pylupdate parses python files to search for calls to pytson.tr resp 
  4  pytson.Translatable._tr to generate a Qt linguist translation file 
  5   
  6  These are the two valid usecases from module pytson: 
  7   
  8  class Translatable(object): 
  9      @classmethod 
 10      def _tr(cls, sourcetext, *, disambiguation="", n=-1, context=None): 
 11          pass 
 12   
 13  def tr(context, sourcetext, *, disambiguation="", n=-1): 
 14          pass 
 15  """ 
 16  import sys 
 17  import os 
 18   
 19  import ast 
 20   
 21  from argparse import ArgumentParser 
 22   
 23  import logging 
 24   
 25  from lxml import etree 
 26   
 27  from io import StringIO 
 28   
 29   
 30  """ 
 31  there is no valid dtd for Qt's ts files (because of the "wildcard" elemenents 
 32  extra-*), so drop that feature for validation 
 33  """ 
 34   
 35  DTD = r"""<!-- 
 36   ! 
 37   ! Some notes to the DTD: 
 38   ! 
 39   ! The location element is set as optional since it was introduced first in 
 40   ! Qt 4.2. 
 41   ! The userdata element is set as optional since it was introduced first in 
 42   ! Qt 4.4. 
 43   ! The vanished message type was introduced first in Qt 5.2. 
 44   ! 
 45    --> 
 46  <!-- 
 47   ! Macro used in order to escape byte entities not allowed in an xml document 
 48   ! for instance, only #x9, #xA and #xD are allowed characters below #x20. 
 49   --> 
 50  <!ENTITY % evilstring '(#PCDATA | byte)*' > 
 51  <!ELEMENT byte EMPTY> 
 52  <!-- value contains decimal (e.g. 1000) or hex (e.g. x3e8) unicode encoding of 
 53   ! one char 
 54    --> 
 55  <!ATTLIST byte 
 56            value CDATA #REQUIRED> 
 57  <!-- 
 58   ! This element wildcard is no valid DTD. No better solution available. 
 59   ! extra elements may appear in TS and message elements. Each element may 
 60   ! appear 
 61   ! only once within each scope. The contents are preserved verbatim; any 
 62   ! attributes are dropped. Currently recognized extra tags include: 
 63   !   extra-po-msgid_plural, extra-po-old_msgid_plural 
 64   !   extra-po-flags (comma-space separated list) 
 65   !   extra-loc-layout_id 
 66   !   extra-loc-feature 
 67   !   extra-loc-blank 
 68   <!ELEMENT extra-* %evilstring; > 
 69   <!ELEMENT TS (defaultcodec?, extra-**, dependencies?, (context|message)+) > 
 70    --> 
 71  <!ELEMENT TS (defaultcodec?, dependencies?, (context|message)+) > 
 72  <!ATTLIST TS 
 73            version CDATA #IMPLIED 
 74            sourcelanguage CDATA #IMPLIED 
 75            language CDATA #IMPLIED> 
 76  <!-- The encoding to use in the QM file by default. Default is ISO-8859-1. --> 
 77  <!ELEMENT defaultcodec (#PCDATA) > 
 78  <!ELEMENT context (name, comment?, (context|message)+) > 
 79  <!ATTLIST context 
 80            encoding CDATA #IMPLIED> 
 81  <!ELEMENT dependencies (dependency+) > 
 82  <!ATTLIST dependency 
 83            catalog CDATA #IMPLIED> 
 84  <!ELEMENT name %evilstring; > 
 85  <!-- This is "disambiguation" in the (new) API, or "msgctxt" in gettext speak 
 86    --> 
 87  <!ELEMENT comment %evilstring; > 
 88  <!-- Previous content of comment (result of merge) --> 
 89  <!ELEMENT oldcomment %evilstring; > 
 90  <!-- The real comment (added by developer/designer) --> 
 91  <!ELEMENT extracomment %evilstring; > 
 92  <!-- Comment added by translator --> 
 93  <!ELEMENT translatorcomment %evilstring; > 
 94  <!-- <!ELEMENT message (location*, source?, oldsource?, comment?, oldcomment?, 
 95   ! extracomment?, translatorcomment?, translation?, userdata?, extra-**) > 
 96    --> 
 97  <!ELEMENT message (location*, source?, oldsource?, comment?, oldcomment?, 
 98  extracomment?, translatorcomment?, translation?, userdata?) > 
 99  <!-- 
100   ! If utf8 is "true", the defaultcodec is overridden and the message is encoded 
101   ! in UTF-8 in the QM file. If it is "both", both source encodings are stored 
102   ! in the QM file. 
103    --> 
104  <!ATTLIST message 
105            id CDATA #IMPLIED 
106            utf8 (true|false|both) "false" 
107            numerus (yes|no) "no"> 
108  <!ELEMENT location EMPTY> 
109  <!-- 
110   ! If the line is omitted, the location specifies only a file. 
111   ! 
112   ! location supports relative specifications as well. Line numbers are 
113   ! relative (explicitly positive or negative) to the last reference to a 
114   ! given filename; each file starts with current line 0. If the filename 
115   ! is omitted, the "current" one is used. For the 1st location in a message, 
116   ! "current" is the filename used for the 1st location of the previous message. 
117   ! For subsequent locations, it is the filename used for the previous location. 
118   ! A single TS file has either all absolute or all relative locations. 
119    --> 
120  <!ATTLIST location 
121            filename CDATA #IMPLIED 
122            line CDATA #IMPLIED> 
123  <!ELEMENT source %evilstring;> 
124  <!-- Previous content of source (result of merge) --> 
125  <!ELEMENT oldsource %evilstring;> 
126  <!-- 
127   ! The following should really say one evilstring macro or several 
128   ! numerusform or lengthvariant elements, but the DTD can't express this. 
129    --> 
130  <!ELEMENT translation (#PCDATA|byte|numerusform|lengthvariant)* > 
131  <!-- 
132   ! If no type is set, the message is "finished". 
133   ! Length variants must be ordered by falling display length. 
134   ! variants may not be yes if the message has numerus yes. 
135    --> 
136  <!ATTLIST translation 
137            type (unfinished|vanished|obsolete) #IMPLIED 
138            variants (yes|no) "no"> 
139  <!-- Deprecated. Use extra-* --> 
140  <!ELEMENT userdata (#PCDATA)* > 
141  <!-- 
142   ! The following should really say one evilstring macro or several 
143   ! lengthvariant elements, but the DTD can't express this. 
144   ! Length variants must be ordered by falling display length. 
145    --> 
146  <!ELEMENT numerusform (#PCDATA|byte|lengthvariant)* > 
147  <!ATTLIST numerusform 
148            variants (yes|no) "no"> 
149  <!ELEMENT lengthvariant %evilstring; > 
150  """ 
151 152 153 -class Message(object):
154 """ 155 Wrapper for a translated message 156 """ 157
158 - def __init__(self, sourcetext, disambiguation, nused, finished=False):
159 """ 160 Instantiates a new Message object 161 @param sourcetext: sourcetext 162 @type sourcetext: str 163 @param disambiguation: string to distinguish between two equal 164 sourcetexts 165 @type disambiguation: str or None 166 @param nused: if True, numerous translations will be used 167 @type nused: bool 168 @param finished: defines, whether the translation is finished. 169 defaults to False 170 @type finished: bool 171 """ 172 self.sourcetext = sourcetext 173 self.disambiguation = disambiguation 174 self.isNumerous = nused 175 self.isFinished = finished 176 self.translations = [""]
177 178 @property
179 - def sourcetext(self):
180 return self._sourcetext
181 182 @sourcetext.setter
183 - def sourcetext(self, val):
184 self._sourcetext = val
185 186 @property
187 - def disambiguation(self):
188 return self._disambiguation
189 190 @disambiguation.setter
191 - def disambiguation(self, val):
192 self._disambiguation = val
193 194 @property
195 - def isNumerous(self):
196 return self._numerous
197 198 @isNumerous.setter
199 - def isNumerous(self, val):
200 self._numerous = val
201 202 @property
203 - def isFinished(self):
204 return self._finished
205 206 @isFinished.setter
207 - def isFinished(self, val):
208 self._finished = val
209
210 - def __iter__(self):
211 """ 212 Yields the translations of the Message 213 @return: the translated string(s) 214 @rtype: str 215 """ 216 for t in self.translations: 217 yield t
218
219 - def __repr__(self):
220 return "Message(%s, %s)" % (self._sourcetext, self._disambiguation)
221
222 - def setTranslation(self, trans, numeroustrans=None):
223 """ 224 Sets the translation of the Message 225 @param trans: the translation 226 @type trans: str 227 @param numeroustrans: if not None, the numerous translation 228 @type numeroustrans: str or None 229 """ 230 self.translations = [trans] 231 if numeroustrans: 232 self.translations.append(numeroustrans) 233 self._numerous = True 234 else: 235 self._numerous = False
236
237 - def update(self, msg):
238 """ 239 Updates the properties with the ones from another message 240 @param msg: the other message 241 @type msg: Message 242 """ 243 self.translations = msg.translations 244 245 if msg.isNumerous: 246 self.isNumerous = True 247 248 if msg.isFinished: 249 self.isFinished = True
250 251 @staticmethod
252 - def fromXml(elem):
253 """ 254 Parses the xml element message to a Message object 255 @param elem: the xml element 256 @type elem: ElementTree.Element 257 @return: a new created message object 258 @rtype: Message 259 """ 260 nused = "numerus" in elem.attrib and elem.attrib["numerus"] == "yes" 261 sourcetext = elem.find("source").text 262 disambiguation = None 263 disel = elem.find("comment") 264 if disel: 265 disambiguation = disel.text 266 267 transel = elem.find("translation") 268 269 msg = Message(sourcetext, disambiguation, nused, 270 "type" not in transel.attrib) 271 272 if not nused: 273 msg.setTranslation(transel.text or "") 274 else: 275 msg.setTranslation(*[x.text or "" for x in 276 transel.findall("numerusform")]) 277 278 return msg
279
280 - def toXml(self):
281 """ 282 Creates the xml elements of the message 283 @return: the xml element 284 @rtype: ElementTree.Element 285 """ 286 if self.isNumerous: 287 elem = etree.Element("message", attrib={"numerus": "yes"}) 288 else: 289 elem = etree.Element("message") 290 291 etree.SubElement(elem, "source").text = self.sourcetext 292 293 if self.disambiguation: 294 etree.SubElement(elem, "comment").text = self.disambiguation 295 296 if self.isFinished: 297 trans = etree.SubElement(elem, "translation") 298 else: 299 trans = etree.SubElement(elem, "translation", 300 attrib={"type": "unfinished"}) 301 302 if self.isNumerous: 303 etree.SubElement(trans, "numerusform").text = self.translations[0] 304 nfel = etree.SubElement(trans, "numerusform") 305 if len(self.translations) > 1: 306 nfel.text = self.translations[1] 307 else: 308 trans.text = self.translations[0] 309 310 return elem
311
312 313 -class Context(object):
314 """ 315 Wrapper for a translation context 316 """ 317
318 - def __init__(self):
319 """ 320 Instantiates a new Context object 321 """ 322 self.msgs = {}
323
324 - def __iter__(self):
325 """ 326 Yields each Message object in the context 327 @return: the message objects 328 @rtype: Message 329 """ 330 for msg in self.msgs.values(): 331 yield msg
332
333 - def __repr__(self):
334 return "Context(%s)" % ", ".join(str(x) for x in self.msgs.values())
335
336 - def addMessage(self, msg):
337 """ 338 Adds a message to the context. If already exists, updates it. 339 @param msg: the message 340 @type msg: Message 341 """ 342 if (msg.sourcetext, msg.disambiguation) in self.msgs: 343 self.msgs[(msg.sourcetext, msg.disambiguation)].update(msg) 344 else: 345 self.msgs[(msg.sourcetext, msg.disambiguation)] = msg
346
347 - def update(self, ctx):
348 """ 349 Updates all current messages with data from another 350 context (if contained). 351 @param ctx: the other context 352 @type ctx: Context 353 """ 354 for msg in ctx: 355 if (msg.sourcetext, msg.disambiguation) in self.msgs: 356 self.msgs[(msg.sourcetext, msg.disambiguation)].update(msg)
357 358 @staticmethod
359 - def fromXml(elem):
360 """ 361 Creates a context object from the xml element 362 @param elem: the xml element 363 @type elem: ElementTree.Element 364 @return: a tuple containing the name and the new context object 365 @rtype: tuple(str, Context) 366 """ 367 name = elem.find("name").text 368 369 ctx = Context() 370 for subelem in elem.findall("message"): 371 ctx.addMessage(Message.fromXml(subelem)) 372 373 return (name, ctx)
374
375 - def toXml(self, name):
376 """ 377 Creates the xml elements of the context 378 @param name: the name of the context 379 @type name: str 380 @return: the xml element 381 @rtype: ElementTree.Element 382 """ 383 elem = etree.Element("context") 384 etree.SubElement(elem, "name").text = name 385 386 for msg in sorted(self.msgs.values(), key=lambda x: x.sourcetext): 387 elem.append(msg.toXml()) 388 389 return elem
390
391 392 -class Translation(object):
393 """ 394 Wrapper for a Qt linguist translation file 395 """ 396
397 - def __init__(self, filename, language):
398 """ 399 Instantiates a new Translation object 400 @param filename: path to read from resp. write to 401 @type filename: str 402 @param language: the target language code 403 @type language: str 404 """ 405 self.contexts = {} 406 407 self.filename = filename 408 self.language = language
409
410 - def _loadXml(self):
411 """ 412 Parses the xml document from self.filename and creates corresponding 413 context objects 414 """ 415 tree = etree.ElementTree(file=self.filename) 416 if tree.getroot().tag != "TS": 417 raise Exception("No valid translation file (roottag)") 418 419 for ele in tree.getroot().findall("context"): 420 (name, ctx) = Context.fromXml(ele) 421 self.contexts[name] = ctx
422
423 - def read(self, filename=None):
424 """ 425 Read the translation file given by the filename 426 @param filename: if given, the path to the file to read. 427 Defaults to None 428 @type filename: str 429 """ 430 if filename: 431 self.filename = filename 432 433 self._loadXml()
434
435 - def write(self, language, dtd=None, filename=None):
436 """ 437 Writes the data to a file. 438 @param language: the target language code 439 @type language: str 440 @param dtd: if set, the xml is validated before written, defaults 441 to None, throws Exception if validation failed 442 @type dtd: str 443 @param filename: if given, the path to write to, defaults to None 444 @type filename: str 445 """ 446 if filename: 447 self.filename = filename 448 449 root = etree.Element("TS", attrib={"version": "2.1", 450 "language": language}) 451 452 for ctxname in sorted(self.contexts.keys()): 453 root.append(self.contexts[ctxname].toXml(ctxname)) 454 455 tree = etree.ElementTree(root) 456 457 if dtd: 458 val = etree.DTD(StringIO(dtd)) 459 if not val.validate(tree): 460 raise Exception("Error validating: %s" % 461 val.error_log.filter_from_errors()[0]) 462 463 tree.write(self.filename, encoding='utf-8', xml_declaration=True, 464 pretty_print=True)
465
466 - def __contains__(self, key):
467 """ 468 Checks, if a context is contained 469 @param key: the context name 470 @type key: str 471 @return: returns True, if a context is contained by key's name 472 @rtype: bool 473 """ 474 return key in self.contexts
475
476 - def __getitem__(self, key):
477 """ 478 Returns the context object references by its name 479 @param key: the name of the context 480 @type key: str 481 @return: the context 482 @rtype: Context 483 """ 484 return self.contexts[key]
485
486 - def __iter__(self):
487 """ 488 Yields each context name contained in the translation 489 @return: the context names 490 @rtype: str 491 """ 492 for ctx in self.contexts: 493 yield ctx
494
495 - def addContext(self, key):
496 """ 497 Creates a new context. A previous context with that name will 498 be overwritten 499 @param key: the name of the context 500 @type key: str 501 """ 502 self.contexts[key] = Context()
503
504 - def removeContext(self, key):
505 """ 506 Remove a context. 507 @param key: the name of the context 508 @type key: str 509 """ 510 del self.contexts[key]
511
512 513 -class ParentVisitor(ast.NodeVisitor):
514 """ 515 ast Visitor which sets links to the parent node 516 """ 517
518 - def generic_visit(self, node):
519 if not hasattr(node, "parent"): 520 node.parent = None 521 522 for _, child in ast.iter_fields(node): 523 if isinstance(child, ast.AST): 524 child.parent = node 525 elif isinstance(child, list): 526 for item in child: 527 item.parent = node 528 529 super().generic_visit(node)
530
531 532 -class FunctionValidator(ParentVisitor):
533 """ 534 ast Visitor to find calls to pyTSon's translation functions 535 """ 536
537 - def __init__(self):
538 super().__init__() 539 540 self.modname = None 541 self.classname = None 542 self.funcname = None 543 544 self.calls = [] 545 546 self.log = logging.getLogger("pylupdate")
547
548 - def _hasImport(self):
549 """ 550 Checks, if the translation functions were already imported 551 @return: Returns True, if any function is available 552 @rtype: bool 553 """ 554 return any([self.modname, self.classname, self.funcname])
555
556 - def _getClass(self, node):
557 """ 558 Returns the parent class of some ast node 559 @param node: the node to get the parent of (must be previously visited 560 with ParentVisitor) 561 @type node: ast.AST 562 @return: the node of the parent class 563 @rtype: ast.ClassDef 564 """ 565 parent = node.parent 566 while parent: 567 if isinstance(parent, ast.ClassDef): 568 return parent 569 parent = parent.parent 570 571 return None
572
573 - def _classIsTranslatable(self, node):
574 """ 575 Checks, if a class is a subclass of pytson.Translatable 576 @param node: the node of the classdef 577 @type node: ast.ClassDef 578 @return: True or False 579 @rtype: bool 580 """ 581 for base in node.bases: 582 if isinstance(base, ast.Attribute): 583 if "%s.%s" % (base.value.id, base.attr) == self.classname: 584 return True 585 elif isinstance(base, ast.Name): 586 if base.id == self.classname: 587 return True 588 return False
589
590 - def _getKeywords(self, node):
591 """ 592 Gets the keyword parameters of a translate function call 593 @param node: 594 @type node: ast.AST 595 @return: a tuple containing the contextname, the source string, 596 the disambiguation string and if the numerous form is requested 597 @rtype: tuple(str, str or None, bool) 598 """ 599 ctx = None 600 disambiguation = None 601 nused = False 602 sourcetext = None 603 604 for kw in node.keywords: 605 if kw.arg == "context": 606 if not isinstance(kw.value, ast.Str): 607 self.log.debug("Type of argument context needs to be raw " 608 "string (line %s)" % node.lineno) 609 return None 610 else: 611 ctx = kw.value.s 612 elif kw.arg == "disambiguation": 613 if not isinstance(kw.value, ast.Str): 614 self.log.debug("Type of argument disambiguation needs to " 615 "be raw string (line %s)" % node.lineno) 616 return None 617 else: 618 disambiguation = kw.value.s 619 elif kw.arg == "n": 620 nused = True 621 elif kw.arg == "sourcetext": 622 if not isinstance(kw.value, ast.Str): 623 self.log.debug("Type of argument sourcetext needs to " 624 "be raw string (line %s)" % node.lineno) 625 return None 626 else: 627 sourcetext = kw.value.s 628 else: 629 self.log.debug("Unknown keyword %s (line %s)" % 630 (kw.arg, node.lineno)) 631 return None 632 633 return (ctx, sourcetext, disambiguation, nused)
634 635 @staticmethod
636 - def _checkArguments(node, count):
637 """ 638 Checks, if the argument list of a translation function is valid 639 @param node: the node of the call 640 @type node: ast.AST 641 @param count: number of arguments to check for 642 @type count: int 643 @return: True or False 644 @rtype: bool 645 """ 646 if len(node.args) != count: 647 return False 648 649 for i in range(count): 650 if not isinstance(node.args[i], ast.Str): 651 return False 652 653 return True
654
655 - def _extractMethodCall(self, node):
656 """ 657 Extracts all info from a Translatable._tr call 658 @param node: the method call 659 @type node: ast.Attribute 660 @return: a tuple containing the contextname and the message 661 or None, if the call is not valid 662 @rtype: tuple(str, str, str or None, bool) or None 663 """ 664 call = "%s.%s" % (node.func.value.id, node.func.attr) 665 if call in ["self._tr", "cls._tr"]: 666 (ctx, source, disambiguation, nused) = self._getKeywords(node) 667 if not ctx: 668 cl = self._getClass(node) 669 if not cl: 670 self.log.debug("No context found (line %s)" % node.lineno) 671 return None 672 673 if self._classIsTranslatable(cl): 674 ctx = cl.name 675 else: 676 self.log.debug("Class is not translatable (line %s)" % 677 node.lineno) 678 679 # if source was not given as keyword arg 680 if not source: 681 if not self._checkArguments(node, 1): 682 self.log.debug("Argument list not matched (line %s)" % 683 node.lineno) 684 return None 685 else: 686 source = node.args[0].s 687 688 return (ctx, Message(source, disambiguation, nused)) 689 690 return None
691
692 - def _extractModuleCall(self, node):
693 """ 694 Extracts all info from a pytson.tr call 695 @param node: the function call 696 @type node: ast.Attribute 697 @return: a tuple containing the contextname and the message 698 or None, if the call is not valid 699 @rtype: tuple(str, Message) or None 700 """ 701 if "%s.%s" % (node.func.value.id, node.func.attr) == self.funcname: 702 (ctx, source, disambiguation, nused) = self._getKeywords(node) 703 704 count = [ctx, source].count(None) 705 if count > 0 and not self._checkArguments(node, count): 706 self.log.debug("Argument list not matched (line %s)" % 707 node.lineno) 708 return None 709 710 # if source was no keyword, context can't be either 711 if not source: 712 if ctx: 713 self.log.debug("Argument list not matched, wrong order " 714 "(line %s)" % node.lineno) 715 return None 716 717 ctx = node.args[0].s 718 source = node.args[1].s 719 elif not ctx: 720 ctx = node.args[0].s 721 722 return (ctx, Message(source, disambiguation, nused)) 723 724 return None
725
726 - def _extractFunctionCall(self, node):
727 """ 728 Extracts all info from a tr call (imported with alias) 729 @param node: the function call 730 @type node: ast.Attribute 731 @return: a tuple containing the contextname and the message 732 or None, if the call is not valid 733 @rtype: tuple(str, Message) or None 734 """ 735 if node.func.id == self.funcname: 736 (ctx, source, disambiguation, nused) = self._getKeywords(node) 737 738 count = [ctx, source].count(None) 739 if count > 0 and not self._checkArguments(node, count): 740 self.log.debug("Argument list not matched (line %s)" % 741 node.lineno) 742 return None 743 744 # if source was no keyword, context can't be either 745 if not source: 746 if ctx: 747 self.log.debug("Argument list not matched, wrong order " 748 "(line %s)" % node.lineno) 749 return None 750 751 ctx = node.args[0].s 752 source = node.args[1].s 753 elif not ctx: 754 ctx = node.args[0].s 755 756 return (ctx, Message(source, disambiguation, nused)) 757 758 return None
759
760 - def _validateExtract(self, node):
761 """ 762 Extracts all info from a translation call 763 @param node: the method call 764 @type node: ast.Attribute 765 @return: a tuple containing the contextname and the message 766 or None, if the call is not valid 767 @rtype: tuple(str, str, str or None, bool) or None 768 """ 769 if not self._hasImport(): 770 return None 771 772 if (isinstance(node.func, ast.Attribute) and 773 isinstance(node.func.value, ast.Name)): 774 args = self._extractMethodCall(node) 775 if args: 776 return args 777 778 args = self._extractModuleCall(node) 779 if args: 780 return args 781 elif isinstance(node.func, ast.Name): 782 args = self._extractFunctionCall(node) 783 if args: 784 return args 785 786 return None
787
788 - def visit_Import(self, node):
789 """ 790 Visits each import of the ast 791 @param node: the import node 792 @type node: ast.Import 793 """ 794 for imp in node.names: 795 if imp.name == "pytson": 796 if imp.asname: 797 self.modname = imp.asname 798 if not self.funcname: 799 self.funcname = "%s.tr" % imp.asname 800 if not self.classname: 801 self.classname = "%s.Translatable" % imp.asname 802 else: 803 self.modname = "pytson" 804 if not self.funcname: 805 self.funcname = "pytson.tr" 806 if not self.classname: 807 self.classname = "pytson.Translatable"
808
809 - def visit_ImportFrom(self, node):
810 """ 811 Visits each import (from) of the ast 812 @param node: the import node 813 @type node: ast.ImportFrom 814 """ 815 if node.module == "pytson": 816 for imp in node.names: 817 if imp.name == "tr": 818 if imp.asname: 819 self.funcname = imp.asname 820 else: 821 self.funcname = "tr" 822 elif imp.name == "Translatable": 823 if imp.asname: 824 self.classname = imp.asname 825 else: 826 self.classname = "Translatable"
827
828 - def visit_Call(self, node):
829 """ 830 Visits each function call of the ast 831 @param node: the call node 832 @type node: ast.Call 833 """ 834 args = self._validateExtract(node) 835 836 if args: 837 self.calls.append(args) 838 else: 839 self.generic_visit(node)
840
841 842 -def _findallRecursive(node, path):
843 """ 844 Finds all matching subelements, by tag name or path, searches recursively 845 @param node: the xml element to search in 846 @type node: ElementTree.Element 847 @param path: what element to look for 848 @type path: str 849 @return: yields each matching element 850 @rtype: ElementTree.Element 851 """ 852 for ele in node.getchildren(): 853 if ele.tag == path: 854 yield ele 855 for sub in _findallRecursive(ele, path): 856 yield sub
857
858 859 -def _getUiTexts(inputfile):
860 """ 861 Extracts all translatable string properties from a Qt ui file 862 @param inputfile: path to the ui file 863 @type inputfile: str 864 @return: a list of tuples, containing the contextname and the message 865 @rtype: list[tuple(str, Message)] 866 """ 867 tree = etree.ElementTree(file=inputfile) 868 if tree.getroot().tag != "ui": 869 raise Exception("No valid uifile") 870 871 ctx = tree.find("class").text 872 ret = [] 873 874 for ele in _findallRecursive(tree.getroot(), "string"): 875 if "notr" not in ele.attrib or ele.attrib["notr"] == "false": 876 if "comment" in ele.attrib: 877 msg = Message(ele.text or "", ele.attrib["comment"], False) 878 else: 879 msg = Message(ele.text or "", None, False) 880 881 ret.append((ctx, msg)) 882 883 return ret
884
885 886 -def getSourceTexts(inputfile):
887 """ 888 Extracts all translate function info from a python source file or 889 Qt ui file 890 @param inputfile: path to the python source file/ui file 891 @type inputfile: str 892 @return: a list of tuples, containing the contextname and the message 893 @rtype: list[tuple(str, Message)] 894 """ 895 logging.getLogger("pylupdate").info("Parsing %s" % inputfile) 896 897 if os.path.splitext(inputfile)[1] == ".py": 898 tree = ast.parse(open(inputfile).read()) 899 900 val = FunctionValidator() 901 val.visit(tree) 902 903 ret = val.calls 904 else: 905 ret = _getUiTexts(inputfile) 906 907 logging.getLogger("pylupdate").debug("Found sources: %s" % ret) 908 return ret
909
910 911 -def main(argv):
912 """ 913 Main function of pylupdate 914 @param argv: the arguments passed to the scripts 915 @type argv: list[str] 916 """ 917 parser = ArgumentParser(argv) 918 parser.add_argument('input', help='Input file or directory') 919 parser.add_argument('output', help='The file to write to') 920 parser.add_argument('-l', '--language', dest='language', action='store', 921 default='de_DE', help='language code') 922 parser.add_argument('-e', '--exclude', dest='excludes', action='store', 923 nargs='*', help='Ignore file(s)', default=[]) 924 parser.add_argument('-f', '--force', dest='force', action='store_true', 925 help='Force rewrite of outputfile if already exists') 926 parser.add_argument('-n', '--no-obsolete', dest='clear', 927 action='store_true', 928 help='Delete sourcetexts not found in contexts') 929 parser.add_argument('-c', '--check', dest='check', action='store_true', 930 help='validate output file against DTD') 931 parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', 932 help='Verbose output') 933 934 args = parser.parse_args() 935 936 log = logging.getLogger("pylupdate") 937 ch = logging.StreamHandler() 938 log.addHandler(ch) 939 940 if args.verbose: 941 log.setLevel(logging.DEBUG) 942 else: 943 log.setLevel(logging.INFO) 944 945 if os.path.isfile(args.input): 946 files = [args.input] 947 elif os.path.isdir(args.input): 948 files = [] 949 for root, dirnames, filenames in os.walk(args.input): 950 for f in filenames: 951 if os.path.splitext(f)[1] in [".py", ".ui"]: 952 files.append(os.path.join(root, f)) 953 else: 954 raise Exception("input has to be a valid file- or directorypath") 955 956 trans = Translation(args.output, args.language) 957 if os.path.isfile(args.output) and not args.force: 958 log.info("There might be information loss by reading a translation " 959 "file generated by other tools") 960 trans.read() 961 962 contexts = {} 963 for f in filter(lambda x: x not in args.excludes, files): 964 for (ctxname, msg) in getSourceTexts(f): 965 if args.clear and ctxname not in contexts: 966 if ctxname in trans: 967 contexts[ctxname] = trans[ctxname] 968 trans.addContext(ctxname) 969 else: 970 contexts[ctxname] = None 971 972 if ctxname not in trans: 973 trans.addContext(ctxname) 974 975 trans[ctxname].addMessage(msg) 976 977 for name, ctx in contexts.items(): 978 if ctx: 979 trans[name].update(ctx) 980 981 if args.clear: 982 for name in list(trans): 983 if name not in contexts: 984 trans.removeContext(name) 985 986 trans.write(args.language, DTD if args.check else None)
987 988 989 if __name__ == "__main__": 990 main(sys.argv) 991