# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import re import tokenize from hacking import core FORMAT_RE = re.compile(r"%(?:" r"%|" # Ignore plain percents r"(\(\w+\))?" # mapping key r"([#0 +-]?" # flag r"(?:\d+|\*)?" # width r"(?:\.\d+)?" # precision r"[hlL]?" # length mod r"\w))") # type class LocalizationError(Exception): pass def check_i18n(): """Generator that checks token stream for localization errors. Expects tokens to be ``send``ed one by one. Raises LocalizationError if some error is found. """ while True: try: token_type, text, _, _, line = yield except GeneratorExit: return if text == "def" and token_type == tokenize.NAME: # explicitly ignore function definitions, as oslo defines these return if (token_type == tokenize.NAME and text in ["_", "_LI", "_LW", "_LE", "_LC"]): while True: token_type, text, start, _, _ = yield if token_type != tokenize.NL: break if token_type != tokenize.OP or text != "(": continue # not a localization call format_string = '' while True: token_type, text, start, _, _ = yield if token_type == tokenize.STRING: format_string += eval(text) elif token_type == tokenize.NL: pass else: break if not format_string: raise LocalizationError( start, "H701: Empty localization string") if token_type != tokenize.OP: raise LocalizationError( start, "H701: Invalid localization call") if text != ")": if text == "%": raise LocalizationError( start, "H702: Formatting operation should be outside" " of localization method call") elif text == "+": raise LocalizationError( start, "H702: Use bare string concatenation instead of +") else: raise LocalizationError( start, "H702: Argument to _, _LI, _LW, _LC, or _LE " "must be just a string") format_specs = FORMAT_RE.findall(format_string) positional_specs = [(key, spec) for key, spec in format_specs if not key and spec] # not spec means %%, key means %(smth)s if len(positional_specs) > 1: raise LocalizationError( start, "H703: Multiple positional placeholders") @core.flake8ext def hacking_localization_strings(logical_line, tokens, noqa): r"""Check localization in line. Okay: _("This is fine") Okay: _LI("This is fine") Okay: _LW("This is fine") Okay: _LE("This is fine") Okay: _LC("This is fine") Okay: _("This is also fine %s") Okay: _("So is this %s, %(foo)s") % {foo: 'foo'} H701: _('') Okay: def _(msg):\n pass Okay: def _LE(msg):\n pass H701: _LI('') H701: _LW('') H701: _LE('') H701: _LC('') Okay: _('') # noqa H702: _("Bob" + " foo") H702: _LI("Bob" + " foo") H702: _LW("Bob" + " foo") H702: _LE("Bob" + " foo") H702: _LC("Bob" + " foo") Okay: _("Bob" + " foo") # noqa H702: _("Bob %s" % foo) H702: _LI("Bob %s" % foo) H702: _LW("Bob %s" % foo) H702: _LE("Bob %s" % foo) H702: _LC("Bob %s" % foo) H702: _("%s %s" % (foo, bar)) H703: _("%s %s") % (foo, bar) """ if noqa: return gen = check_i18n() next(gen) try: list(map(gen.send, tokens)) gen.close() except LocalizationError as e: yield e.args # TODO(jogo) Dict and list objects