2019-11-22 11:50:24 +00:00
|
|
|
|
"""Managment of `*.po` files."""
|
|
|
|
|
|
|
|
|
|
import re
|
2019-11-22 12:22:44 +00:00
|
|
|
|
from typing import List
|
2019-11-22 11:50:24 +00:00
|
|
|
|
|
|
|
|
|
import simplelogging
|
|
|
|
|
|
|
|
|
|
log = simplelogging.get_logger()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PoItem:
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Translation item."""
|
|
|
|
|
|
2019-11-22 11:50:24 +00:00
|
|
|
|
def __init__(self, path, lineno):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Initializer."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
self.path = path[3:]
|
|
|
|
|
self.lineno_start = lineno
|
|
|
|
|
self.lineno_end = lineno
|
|
|
|
|
self.parsing_msgid = None
|
|
|
|
|
self.msgid = []
|
|
|
|
|
self.msgstr = []
|
|
|
|
|
self.fuzzy = False
|
|
|
|
|
self.warnings = []
|
|
|
|
|
self.inside_pull_request = False
|
|
|
|
|
|
|
|
|
|
def append_line(self, line):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Append a line of a `*.po` file to the item."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
self.lineno_end += 1
|
|
|
|
|
if line.startswith("msgid"):
|
|
|
|
|
self.parsing_msgid = True
|
|
|
|
|
self.msgid.append(line[7:-2])
|
|
|
|
|
elif line.startswith("msgstr"):
|
|
|
|
|
self.parsing_msgid = False
|
|
|
|
|
self.msgstr.append(line[8:-2])
|
|
|
|
|
elif line.startswith("#, fuzzy"):
|
|
|
|
|
self.fuzzy = True
|
|
|
|
|
elif line.startswith('"'):
|
|
|
|
|
if self.parsing_msgid:
|
|
|
|
|
self.msgid.append(line[1:-2])
|
|
|
|
|
elif not self.parsing_msgid is None:
|
|
|
|
|
self.msgstr.append(line[1:-2])
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Return string representation."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
return (
|
|
|
|
|
f" - {self.msgid_full_content}\n"
|
|
|
|
|
f" => {self.msgstr_full_content}\n"
|
|
|
|
|
f" => {self.msgstr_rst2txt}\n"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def msgid_full_content(self):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Full content of the msgid."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
return "".join(self.msgid)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def msgstr_full_content(self):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Full content of the msgstr."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
return "".join(self.msgstr)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def msgid_rst2txt(self):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Full content of the msgid (reStructuredText escaped)."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
return self.rst2txt(self.msgid_full_content)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def msgstr_rst2txt(self):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Full content of the msgstr (reStructuredText escaped)."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
return self.rst2txt(self.msgstr_full_content)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def rst2txt(text):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""
|
|
|
|
|
Escape reStructuredText markup.
|
|
|
|
|
|
|
|
|
|
The text is modified to transform reStructuredText markup
|
|
|
|
|
in textual version. For instance:
|
|
|
|
|
|
|
|
|
|
* "::" becomes ":"
|
|
|
|
|
* ":class:`PoFile`" becomes "« PoFile »"
|
|
|
|
|
"""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
text = re.sub(r"::", r":", text)
|
|
|
|
|
text = re.sub(r"``(.*?)``", r"« \1 »", text)
|
|
|
|
|
text = re.sub(r"\"(.*?)\"", r"« \1 »", text)
|
|
|
|
|
text = re.sub(r":pep:`(.*?)`", r"PEP \1", text)
|
|
|
|
|
text = re.sub(r":[a-z:]+:`(.+?)`", r"« \1 »", text)
|
|
|
|
|
text = re.sub(r"\*\*(.*?)\*\*", r"« \1 »", text)
|
|
|
|
|
text = re.sub(
|
|
|
|
|
r"\*(.*?)\*", r"« \1 »", text
|
|
|
|
|
) # TODO sauf si déjà entre «»
|
2019-12-02 18:34:43 +00:00
|
|
|
|
text = re.sub(r"<((?:http|https|ftp)://.*?)>", r"« \1 »", text)
|
2019-11-22 11:50:24 +00:00
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
def add_warning(self, checker_name: str, text: str) -> None:
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Add a checker warning to the item."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
self.warnings.append(Warning(checker_name, text))
|
|
|
|
|
|
|
|
|
|
def add_error(self, checker_name: str, text: str) -> None:
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Add a checker error to the item."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
self.warnings.append(Error(checker_name, text))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PoFile:
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""A `*.po` file information."""
|
|
|
|
|
|
2019-11-22 11:50:24 +00:00
|
|
|
|
def __init__(self, path=None):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Initializer."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
self.content: List[PoItem] = []
|
|
|
|
|
self.path = path
|
|
|
|
|
if path:
|
|
|
|
|
self.parse_file(path)
|
|
|
|
|
|
|
|
|
|
def parse_file(self, path):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Parse a `*.po` file according to its path."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
# TODO assert path is a file, not a dir
|
|
|
|
|
item = None
|
|
|
|
|
with open(path, encoding="utf8") as f:
|
|
|
|
|
for lineno, line in enumerate(f):
|
|
|
|
|
if line.startswith("#: "):
|
|
|
|
|
if item:
|
|
|
|
|
self.content.append(item)
|
|
|
|
|
item = PoItem(line, lineno + 1)
|
|
|
|
|
elif item:
|
|
|
|
|
item.append_line(line)
|
|
|
|
|
if item:
|
|
|
|
|
self.content.append(item)
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Return string representation."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
ret = f"Po file: {self.path}\n"
|
|
|
|
|
ret += "\n".join(str(item) for item in self.content)
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
def rst2txt(self):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Escape reStructuredText markup."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
return "\n\n".join(item.msgstr_rst2txt for item in self.content)
|
|
|
|
|
|
|
|
|
|
def display_warnings(self, pull_request_info=None):
|
2019-12-02 17:57:27 +00:00
|
|
|
|
"""Log warnings and errors, return errors and warnings lists."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
self.tag_in_pull_request(pull_request_info)
|
2019-12-02 17:57:27 +00:00
|
|
|
|
errors = []
|
|
|
|
|
warnings = []
|
2019-11-22 11:50:24 +00:00
|
|
|
|
for item in self.content:
|
|
|
|
|
if not item.inside_pull_request:
|
|
|
|
|
continue
|
|
|
|
|
prefix = f"{self.path}:{item.lineno_start:-4} %s"
|
|
|
|
|
log.debug(prefix, "")
|
|
|
|
|
for message in item.warnings:
|
|
|
|
|
if isinstance(message, Error):
|
|
|
|
|
log.error(prefix, message)
|
2019-12-02 17:57:27 +00:00
|
|
|
|
errors.append(message)
|
2019-11-22 11:50:24 +00:00
|
|
|
|
elif isinstance(message, Warning):
|
|
|
|
|
log.warning(prefix, message)
|
2019-12-02 17:57:27 +00:00
|
|
|
|
warnings.append(message)
|
|
|
|
|
return errors, warnings
|
2019-11-22 11:50:24 +00:00
|
|
|
|
|
|
|
|
|
def tag_in_pull_request(self, pull_request_info):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Tag items being part of the pull request."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
if not pull_request_info:
|
|
|
|
|
for item in self.content:
|
|
|
|
|
item.inside_pull_request = True
|
|
|
|
|
else:
|
|
|
|
|
diff = pull_request_info.diff(self.path)
|
|
|
|
|
for item in self.content:
|
|
|
|
|
item.inside_pull_request = False
|
|
|
|
|
for lineno_diff in self.lines_in_diff(diff):
|
|
|
|
|
for item in self.content:
|
|
|
|
|
if item.lineno_start <= lineno_diff <= item.lineno_end:
|
|
|
|
|
item.inside_pull_request = True
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def lines_in_diff(diff):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Yield line numbers modified in a diff (new line numbers)."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
for line in diff.splitlines():
|
|
|
|
|
if line.startswith("@@"):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
match = re.search(r"@@\s*\-\d+,\d+\s+\+(\d+),(\d+)\s+@@", line)
|
|
|
|
|
if match:
|
|
|
|
|
line_start = int(match.group(1))
|
|
|
|
|
nb_lines = int(match.group(2))
|
2019-11-22 11:50:24 +00:00
|
|
|
|
# github add 3 extra lines around diff info
|
|
|
|
|
extra_info_lines = 3
|
|
|
|
|
for lineno in range(
|
|
|
|
|
line_start + extra_info_lines,
|
|
|
|
|
line_start + nb_lines - extra_info_lines,
|
|
|
|
|
):
|
|
|
|
|
yield lineno
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Message:
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Checker message."""
|
|
|
|
|
|
2019-11-22 11:50:24 +00:00
|
|
|
|
def __init__(self, checker_name: str, text: str):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Initializer."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
self.checker_name = checker_name
|
|
|
|
|
self.text = text
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Return string representation."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
return f"[{self.checker_name:^14}] {self.text}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Warning(Message):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Checker warning message."""
|
2019-11-22 11:50:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Error(Message):
|
2019-11-22 12:22:44 +00:00
|
|
|
|
"""Checker error message."""
|