Created
December 22, 2016 08:56
-
-
Save gergelypolonkai/1a16a47e5a1971ca33e58bdfd88c5059 to your computer and use it in GitHub Desktop.
Finding untranslated Python strings
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env python3 | |
import ast | |
import gettext | |
from gettext import gettext as _ | |
import sys | |
def get_func_name(node): | |
cls = node.__class__.__name__ | |
if cls == 'Call': | |
return get_func_name(node.func) | |
elif cls == 'Attribute': | |
return '{}.{}'.format( | |
get_func_name(node.value), | |
node.attr) | |
elif cls == 'Name': | |
return get_func_name(node.id) | |
elif cls == 'str': | |
return node | |
elif cls == 'Str': | |
return "<String literal>" | |
elif cls == 'Subscript': | |
return '{}[{}]'.format(get_func_name(node.value), | |
get_func_name(node.slice)) | |
elif cls == 'Index': | |
return get_func_name(node.value) | |
else: | |
print('ERROR: Unknown class: {}'.format(cls)) | |
class ShowStrings(ast.NodeVisitor): | |
TRANSLATION_FUNCTIONS = [ | |
'_', # gettext.gettext is often imported under this name | |
'gettext', | |
'gettext.gettext', | |
# FIXME: this list is pretty much incomplete | |
] | |
UNTRANSLATED = 'untranslated 9' | |
def __init__(self, filename=None): | |
super(ShowStrings, self).__init__() | |
self.in_call = [] | |
self.filename = filename or '<parsed string>' | |
def visit_with_trace(self, node, func): | |
self.in_call.append((func, node.lineno, node.col_offset)) | |
self.visit(node) | |
self.in_call.pop() | |
def visit_Str(self, node): | |
# TODO: make it possible to ignore untranslated strings | |
# TODO: make this ignore docstrings | |
# if we are not in a translator function, issue a warning | |
if not self.in_call or \ | |
self.in_call[-1][0] not in self.TRANSLATION_FUNCTIONS: | |
try: | |
funcname = self.in_call[-1][0] | |
except IndexError: | |
funcname = None | |
funcall_msg = "outside a function call" if funcname is None \ | |
else "inside a call to {funcname}".format( | |
funcname=funcname) | |
print("WARNING: Untranslated string found at " | |
"{filename}:{line}:{col} {funcall_msg}".format( | |
filename=self.filename, | |
line=node.lineno, | |
col=node.col_offset, | |
funcall_msg=funcall_msg)) | |
def visit_Call(self, node): | |
# if we are in a translator function, issue a warninc | |
if self.in_call and self.in_call[-1][0] in self.TRANSLATION_FUNCTIONS: | |
print("WARNING: function call within a translation function at " | |
"{filename}:{line}:{col}".format(filename=self.filename, | |
line=node.lineno, | |
col=node.col_offset)) | |
funcname = get_func_name(node) | |
for arg in node.args: | |
self.visit_with_trace(arg, funcname) | |
for kwarg in node.keywords: | |
self.visit_with_trace(kwarg.value, funcname) | |
def generic_visit(self, node): | |
# if we are inside a translator function, issue a warning | |
if self.in_call and self.in_call[-1][0] in self.TRANSLATION_FUNCTIONS: | |
# Some ast nodes, like Add don’t have position information | |
if hasattr(node, 'lineno'): | |
print("WARNING: something not a string ({klass}) found in a " | |
"translation function at {filename}:{line}:{col}".format( | |
filename=self.filename, | |
klass=node.__class__.__name__, | |
line=node.lineno, | |
col=node.col_offset)) | |
else: | |
print("WARNING: something not a string ({klass}) found in a " | |
"translation function. Position unknown; function call " | |
"is at {filename}:{line}:{col}".format( | |
filename=self.filename, | |
klass=node.__class__.__name__, | |
line=self.in_call[-1][1], | |
col=self.in_call[-1][2])) | |
super(ShowStrings, self).generic_visit(node) | |
def tst(*args, **kwargs): | |
pass | |
def actual_tests(): | |
_('translated 1') | |
tst(_('translated 2')) | |
tst(gettext.gettext('translated 3')) | |
tst(_('translated 4') + 'native 1') | |
tst('native 2' | |
'native 3') | |
tst(_('native 4' + 'native 5')) | |
tst('native 6', b='native 7') | |
tst(_(tst('hello!'))) | |
if __name__ == '__main__': | |
try: | |
filename = sys.argv[1] | |
except IndexError: | |
filename = __file__ | |
print("INFO: No filename specified, checking myself.") | |
with open(filename, 'r') as f: | |
code = f.read() | |
root = ast.parse(code) | |
show_strings = ShowStrings(filename=filename) | |
show_strings.visit(root) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment