You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/core/tools/translations/check_missing_upy.py

274 lines
8.4 KiB

from __future__ import annotations
import ast
import json
from pathlib import Path
from typing import Any
HERE = Path(__file__).parent
CORE = HERE.parent.parent
CORE_SRC = CORE / "src"
KEY_PREFIX = ""
MAPPING_FILE = HERE / "mapping_upy.json"
IGNORE_FILE = HERE / "ignore_upy.json"
if IGNORE_FILE.exists():
content = json.loads(IGNORE_FILE.read_text())
IGNORE_SET: set[str] = set(content.keys())
else:
IGNORE_SET = set() # type: ignore
def find_all_strings(filename: str | Path) -> list[str]:
with open(filename, "r") as file:
file_content = file.read()
tree = ast.parse(file_content)
strings: list[str] = []
class StringVisitor(ast.NodeVisitor):
def visit_Str(self, node: ast.Str):
strings.append(node.s)
def visit_JoinedStr(self, node: ast.JoinedStr):
for value in node.values:
if isinstance(value, ast.Str):
strings.append(value.s)
visitor = StringVisitor()
visitor.visit(tree)
return strings
def find_strings_to_ignore(filename: str | Path) -> list[str]:
with open(filename, "r") as file:
file_content = file.read()
tree = ast.parse(file_content)
strings: list[str] = []
def ignore_func(func_name: str) -> bool:
if not func_name:
return True
substrs = ["Error", "Exception", "wire.", "log.", "ensure", "mem_trace"]
if any(substr in func_name for substr in substrs):
return True
if func_name in (
"_log",
"info",
"warning",
"debug",
"Success",
"SdCardUnavailable",
"NotInitialized",
"ActionCancelled",
"UnexpectedMessage",
"NotEnoughFunds",
"Failure",
"PinCancelled",
"TypeVar",
"getattr",
"_validate_public_key",
"check_mem",
"halt",
"pack",
"mem_trace",
):
return True
return False
def get_final_attribute_name(node: ast.AST) -> str:
"""Recursively extracts the final attribute name from a nested attribute expression."""
if isinstance(node, ast.Name):
return node.id
elif isinstance(node, ast.Attribute):
return get_final_attribute_name(node.value) + "." + node.attr
return ""
def include_all_strings(arg: ast.expr) -> None:
if isinstance(arg, ast.Str):
strings.append(arg.s)
elif isinstance(arg, ast.JoinedStr):
for value in arg.values:
if isinstance(value, ast.Str):
strings.append(value.s)
elif isinstance(value, ast.FormattedValue):
# This part is an expression inside an f-string
expr_as_str = ast.dump(value.value, annotate_fields=False)
strings.append(expr_as_str)
class IgnoreStringVisitor(ast.NodeVisitor):
def visit_Call(self, node: ast.Call):
func_name = get_final_attribute_name(node.func)
if ignore_func(func_name):
for arg in node.args + [kw.value for kw in node.keywords]:
include_all_strings(arg)
# Continue visiting the children of this node (!!!Necessary!!!)
self.generic_visit(node)
def visit_Assert(self, node: ast.Assert):
error_message = node.msg
if error_message:
include_all_strings(error_message)
self.generic_visit(node)
def visit_Assign(self, node: ast.Assign):
ignore_variables = [
"msg_wire",
"msg_type",
]
for target in node.targets:
if isinstance(target, ast.Name) and target.id in ignore_variables:
value = node.value
include_all_strings(value)
self.generic_visit(node)
def visit_FunctionDef(self, node: ast.FunctionDef):
for arg in node.args.args:
annotation = arg.annotation
if annotation:
include_all_strings(annotation)
return_annotation = node.returns
if return_annotation:
include_all_strings(return_annotation)
self.generic_visit(node)
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> Any:
for arg in node.args.args:
annotation = arg.annotation
if annotation:
include_all_strings(annotation)
return_annotation = node.returns
if return_annotation:
include_all_strings(return_annotation)
self.generic_visit(node)
def visit_AnnAssign(self, node: ast.AnnAssign):
annotation = node.annotation
include_all_strings(annotation)
self.generic_visit(node)
visitor = IgnoreStringVisitor()
visitor.visit(tree)
# get all the top-level string comments
for node in tree.body:
if isinstance(node, ast.Expr) and isinstance(
node.value, (ast.Str, ast.JoinedStr)
):
strings.append(node.value.s) # type: ignore
return strings
def find_docstrings(filename: str | Path) -> list[str]:
with open(filename, "r") as file:
file_content = file.read()
tree = ast.parse(file_content)
functions = [
f
for f in ast.walk(tree)
if isinstance(f, (ast.FunctionDef, ast.AsyncFunctionDef))
]
function_docs = [ast.get_docstring(f) for f in functions]
classes = [c for c in ast.walk(tree) if isinstance(c, ast.ClassDef)]
class_docs = [ast.get_docstring(c) for c in classes]
all_docstrings = function_docs + class_docs
module_docstring = ast.get_docstring(tree)
if module_docstring:
all_docstrings.append(module_docstring)
return [doc for doc in all_docstrings if doc]
def check_file(file: str | Path) -> list[str]:
all_strings = find_all_strings(file)
def is_docstring(string: str) -> bool:
return "\n " in string or (string.startswith("\n") and string.endswith("\n"))
all_strings = [string for string in all_strings if not is_docstring(string)]
ignore_strings = find_strings_to_ignore(file)
docstrings = find_docstrings(file)
# Remove duplicates
all_strings = list(set(all_strings))
ignore_strings = set(ignore_strings)
docstrings = set(docstrings)
to_ignore = ignore_strings | docstrings
# Remove strings that are passed to error and other non-translatable functions
return [s for s in all_strings if s not in to_ignore]
def check_file_report(file: str | Path) -> None:
all_files = {str(file): check_file(file)}
report_all_files(all_files)
def check_folder_resursive_report(
folder: str | Path, ignore_files: list[str] | None = None
) -> None:
if ignore_files is None:
ignore_files = []
all_files: dict[str, list[str]] = {}
for file in Path(folder).rglob("*.py"):
if file.name in ignore_files:
continue
file_strings = check_file(file)
all_files[str(file)] = file_strings
report_all_files(all_files)
def report_all_files(all_files: dict[str, list[str]]) -> None:
str_mapping: dict[str, str] = {}
for _file, strings in all_files.items():
for string in strings:
if "_" in string:
continue
str_id = (
string.lower()
.strip()
.replace(" ", "_")
.replace("-", "_")
.replace(":", "")
.replace("?", "")
)
if KEY_PREFIX:
str_id = f"{KEY_PREFIX}__{str_id}"
if str_id in IGNORE_SET:
continue
str_mapping[str_id] = string
MAPPING_FILE.write_text(json.dumps(str_mapping, indent=4))
if __name__ == "__main__":
ignore_files = [
"coininfo.py",
"nem_mosaics.py",
"knownapps.py",
"networks.py",
"tokens.py",
"all_modules.py",
"workflow_handlers.py",
"messages.py",
"errors.py",
]
# folder = CORE_SRC / "apps"
# folder = CORE_SRC / "trezor"
folder = CORE_SRC
check_folder_resursive_report(folder, ignore_files=ignore_files)
# file = CORE_SRC / "trezor/ui/layouts/tt_v2/reset.py"
# KEY_PREFIX = "TR.reset" # type: ignore
# check_file_report(file)