anki/pylib/tools/genbackend.py

145 lines
3.7 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import re
from anki import backend_pb2 as pb
import stringcase
TYPE_DOUBLE = 1
TYPE_FLOAT = 2
TYPE_INT64 = 3
TYPE_UINT64 = 4
TYPE_INT32 = 5
TYPE_FIXED64 = 6
TYPE_FIXED32 = 7
TYPE_BOOL = 8
TYPE_STRING = 9
TYPE_GROUP = 10
TYPE_MESSAGE = 11
TYPE_BYTES = 12
TYPE_UINT32 = 13
TYPE_ENUM = 14
TYPE_SFIXED32 = 15
TYPE_SFIXED64 = 16
TYPE_SINT32 = 17
TYPE_SINT64 = 18
LABEL_OPTIONAL = 1
LABEL_REQUIRED = 2
LABEL_REPEATED = 3
2020-05-24 00:36:50 +02:00
# messages we don't want to unroll in codegen
SKIP_UNROLL_INPUT = {"TranslateString"}
def python_type(field):
type = python_type_inner(field)
if field.label == LABEL_REPEATED:
2020-05-22 13:25:25 +02:00
type = f"Sequence[{type}]"
return type
def python_type_inner(field):
type = field.type
if type == TYPE_BOOL:
return "bool"
elif type in (1, 2):
return "float"
elif type in (3, 4, 5, 6, 7, 13, 15, 16, 17, 18):
return "int"
elif type == TYPE_STRING:
return "str"
elif type == TYPE_BYTES:
return "bytes"
2020-05-23 12:43:55 +02:00
elif type == TYPE_MESSAGE:
2020-05-24 00:36:50 +02:00
return fullname(field.message_type.full_name)
2020-05-23 12:43:55 +02:00
elif type == TYPE_ENUM:
2020-05-24 00:36:50 +02:00
return fullname(field.enum_type.full_name)
else:
raise Exception(f"unknown type: {type}")
2020-05-24 00:36:50 +02:00
def fullname(fullname):
if "FluentString" in fullname:
return fullname.replace("backend_proto", "anki.fluent_pb2")
else:
return fullname.replace("backend_proto", "pb")
2020-05-23 04:58:13 +02:00
# get_deck_i_d -> get_deck_id etc
def fix_snakecase(name):
for fix in "a_v", "i_d":
name = re.sub(
f"(\w)({fix})(\w)",
lambda m: m.group(1) + m.group(2).replace("_", "") + m.group(3),
name,
)
return name
def get_input_args(msg):
fields = sorted(msg.fields, key=lambda x: x.number)
self_star = ["self"]
if len(fields) >= 2:
self_star.append("*")
return ", ".join(self_star + [f"{f.name}: {python_type(f)}" for f in fields])
def get_input_assign(msg):
fields = sorted(msg.fields, key=lambda x: x.number)
return ", ".join(f"{f.name}={f.name}" for f in fields)
def render_method(method, idx):
2020-05-23 08:19:48 +02:00
input_name = method.input_type.name
2020-05-24 00:36:50 +02:00
if (
(input_name.endswith("In") or len(method.input_type.fields) < 2)
and not method.input_type.oneofs
and not method.name in SKIP_UNROLL_INPUT
):
2020-05-23 08:19:48 +02:00
input_args = get_input_args(method.input_type)
input_assign = get_input_assign(method.input_type)
input_assign_outer = (
2020-05-24 00:36:50 +02:00
f"input = {fullname(method.input_type.full_name)}({input_assign})\n "
2020-05-23 08:19:48 +02:00
)
else:
2020-05-24 00:36:50 +02:00
input_args = f"self, input: {fullname(method.input_type.full_name)}"
2020-05-23 08:19:48 +02:00
input_assign_outer = ""
2020-05-23 04:58:13 +02:00
name = fix_snakecase(stringcase.snakecase(method.name))
if len(method.output_type.fields) == 1:
# unwrap single return arg
f = method.output_type.fields[0]
single_field = f".{f.name}"
return_type = python_type(f)
else:
single_field = ""
return_type = f"pb.{method.output_type.name}"
return f"""\
def {name}({input_args}) -> {return_type}:
2020-05-23 08:19:48 +02:00
{input_assign_outer}output = pb.{method.output_type.name}()
2020-05-24 00:36:50 +02:00
output.ParseFromString(self._run_command({idx+1}, input))
return output{single_field}
"""
out = []
for idx, method in enumerate(pb._BACKENDSERVICE.methods):
out.append(render_method(method, idx))
out = "\n".join(out)
path = "anki/rsbackend.py"
with open(path) as file:
orig = file.read()
new = re.sub(
"(?s)# @@AUTOGEN@@.*?# @@AUTOGEN@@\n",
f"# @@AUTOGEN@@\n\n{out}\n # @@AUTOGEN@@\n",
orig,
)
with open(path, "wb") as file:
file.write(new.encode("utf8"))