MTPyroger/compiler/api/compiler.py

501 lines
18 KiB
Python
Raw Normal View History

2017-12-05 11:16:39 +00:00
# Pyrogram - Telegram MTProto API Client Library for Python
2019-01-01 11:36:16 +00:00
# Copyright (C) 2017-2019 Dan Tès <https://github.com/delivrance>
2017-12-05 11:16:39 +00:00
#
# This file is part of Pyrogram.
#
# Pyrogram is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pyrogram is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
import os
import re
import shutil
2018-01-04 15:29:10 +00:00
HOME = "compiler/api"
DESTINATION = "pyrogram/api"
2018-01-20 19:16:42 +00:00
NOTICE_PATH = "NOTICE"
2018-01-04 15:29:10 +00:00
SECTION_RE = re.compile(r"---(\w+)---")
LAYER_RE = re.compile(r"//\sLAYER\s(\d+)")
2018-03-26 11:51:56 +00:00
COMBINATOR_RE = re.compile(r"^([\w.]+)#([0-9a-f]+)\s(?:.*)=\s([\w<>.]+);(?: // Docs: (.+))?$", re.MULTILINE)
ARGS_RE = re.compile("[^{](\w+):([\w?!.<>#]+)")
2018-01-04 15:29:10 +00:00
FLAGS_RE = re.compile(r"flags\.(\d+)\?")
FLAGS_RE_2 = re.compile(r"flags\.(\d+)\?([\w<>.]+)")
2018-01-05 01:38:29 +00:00
FLAGS_RE_3 = re.compile(r"flags:#")
2018-01-21 11:35:36 +00:00
INT_RE = re.compile(r"int(\d+)")
2017-12-05 11:16:39 +00:00
core_types = ["int", "long", "int128", "int256", "double", "bytes", "string", "Bool"]
types_to_constructors = {}
types_to_functions = {}
constructors_to_functions = {}
def get_docstring_arg_type(t: str, is_list: bool = False, is_pyrogram_type: bool = False):
if t in core_types:
2018-01-21 11:35:36 +00:00
if t == "long":
2018-03-25 19:58:27 +00:00
return "``int`` ``64-bit``"
2018-01-21 11:35:36 +00:00
elif "int" in t:
size = INT_RE.match(t)
2018-03-25 19:58:27 +00:00
return "``int`` ``{}-bit``".format(size.group(1)) if size else "``int`` ``32-bit``"
elif t == "double":
2018-03-25 19:58:27 +00:00
return "``float`` ``64-bit``"
2018-01-08 08:27:47 +00:00
elif t == "string":
2018-03-25 19:58:27 +00:00
return "``str``"
else:
2018-03-25 19:58:27 +00:00
return "``{}``".format(t.lower())
elif t == "true":
2018-03-25 19:58:27 +00:00
return "``bool``"
2018-01-08 06:15:38 +00:00
elif t == "Object" or t == "X":
2018-03-25 19:58:27 +00:00
return "Any object from :obj:`pyrogram.api.types`"
elif t == "!X":
2018-01-21 11:35:36 +00:00
return "Any method from :obj:`pyrogram.api.functions`"
elif t.startswith("Vector"):
2018-04-11 13:34:58 +00:00
return "List of " + get_docstring_arg_type(t.split("<", 1)[1][:-1], True, is_pyrogram_type)
else:
if is_pyrogram_type:
t = "pyrogram." + t
t = types_to_constructors.get(t, [t])
n = len(t) - 1
t = (("e" if is_list else "E") + "ither " if n else "") + ", ".join(
2018-04-24 14:08:33 +00:00
":obj:`{1} <{0}.{1}>`".format(
"pyrogram.types" if is_pyrogram_type else "pyrogram.api.types",
i.replace("pyrogram.", "")
)
for i in t
)
if n:
t = t.split(", ")
t = ", ".join(t[:-1]) + " or " + t[-1]
return t
def get_references(t: str):
t = constructors_to_functions.get(t)
if t:
n = len(t) - 1
t = ", ".join(
":obj:`{0} <pyrogram.api.functions.{0}>`".format(i)
for i in t
)
if n:
t = t.split(", ")
t = ", ".join(t[:-1]) + " and " + t[-1]
return t
2017-12-05 11:16:39 +00:00
def get_argument_type(arg):
is_flag = FLAGS_RE.match(arg[1])
name, t = arg
if is_flag:
t = t.split("?")[1]
if t in core_types:
if t == "long" or "int" in t:
t = ": int"
elif t == "double":
t = ": float"
elif t == "string":
t = ": str"
else:
t = ": {}".format(t.lower())
elif t == "true":
t = ": bool"
elif t.startswith("Vector"):
t = ": list"
else:
return name + ("=None" if is_flag else "")
return name + t + (" = None" if is_flag else "")
2018-01-04 15:29:10 +00:00
class Combinator:
2018-03-26 11:51:56 +00:00
def __init__(self,
section: str,
namespace: str,
name: str,
id: str,
args: list,
has_flags: bool,
return_type: str,
docs: str):
2018-01-04 15:29:10 +00:00
self.section = section
self.namespace = namespace
self.name = name
self.id = id
self.args = args
self.has_flags = has_flags
self.return_type = return_type
2018-03-26 11:51:56 +00:00
self.docs = docs
2017-12-05 11:16:39 +00:00
2018-01-04 15:29:10 +00:00
def snek(s: str):
# https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case
s = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", s)
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s).lower()
2017-12-05 11:16:39 +00:00
2018-01-04 15:29:10 +00:00
def capit(s: str):
return "".join([i[0].upper() + i[1:] for i in s.split("_")])
2017-12-05 11:16:39 +00:00
2018-01-04 15:29:10 +00:00
def sort_args(args):
"""Put flags at the end"""
args = args.copy()
flags = [i for i in args if FLAGS_RE.match(i[1])]
2017-12-05 11:16:39 +00:00
2018-01-04 15:29:10 +00:00
for i in flags:
args.remove(i)
2017-12-05 11:16:39 +00:00
2018-01-04 15:29:10 +00:00
return args + flags
2017-12-05 11:16:39 +00:00
2018-01-04 15:29:10 +00:00
def start():
shutil.rmtree("{}/types".format(DESTINATION), ignore_errors=True)
shutil.rmtree("{}/functions".format(DESTINATION), ignore_errors=True)
with open("{}/source/auth_key.tl".format(HOME), encoding="utf-8") as auth, \
2019-03-16 16:51:37 +00:00
open("{}/source/sys_msgs.tl".format(HOME), encoding="utf-8") as system, \
open("{}/source/main_api.tl".format(HOME), encoding="utf-8") as api:
2018-09-05 14:44:07 +00:00
schema = (auth.read() + system.read() + api.read()).splitlines()
2018-01-04 15:29:10 +00:00
2018-04-01 11:09:32 +00:00
with open("{}/template/mtproto.txt".format(HOME), encoding="utf-8") as f:
mtproto_template = f.read()
with open("{}/template/pyrogram.txt".format(HOME), encoding="utf-8") as f:
pyrogram_template = f.read()
2018-01-04 15:29:10 +00:00
with open(NOTICE_PATH, encoding="utf-8") as f:
2018-01-04 15:29:10 +00:00
notice = []
for line in f.readlines():
notice.append("# {}".format(line).strip())
notice = "\n".join(notice)
section = None
layer = None
namespaces = {"types": set(), "functions": set()}
combinators = []
2018-01-04 16:09:28 +00:00
for line in schema:
2018-01-04 15:29:10 +00:00
# Check for section changer lines
s = SECTION_RE.match(line)
if s:
section = s.group(1)
continue
# Save the layer version
l = LAYER_RE.match(line)
if l:
layer = l.group(1)
continue
combinator = COMBINATOR_RE.match(line)
if combinator:
2018-03-26 11:51:56 +00:00
name, id, return_type, docs = combinator.groups()
2018-01-04 15:29:10 +00:00
namespace, name = name.split(".") if "." in name else ("", name)
2018-03-26 11:51:56 +00:00
args = ARGS_RE.findall(line.split(" //")[0])
2018-01-04 15:29:10 +00:00
2018-01-05 01:38:29 +00:00
# Pingu!
has_flags = not not FLAGS_RE_3.findall(line)
2018-01-04 15:29:10 +00:00
# Fix file and folder name collision
if name == "updates":
name = "update"
# Fix arg name being "self" (reserved keyword)
for i, item in enumerate(args):
if item[0] == "self":
args[i] = ("is_self", item[1])
if namespace:
namespaces[section].add(namespace)
combinators.append(
Combinator(
section,
namespace,
capit(name),
2018-01-04 15:29:10 +00:00
"0x{}".format(id.zfill(8)),
args,
has_flags,
".".join(
return_type.split(".")[:-1]
+ [capit(return_type.split(".")[-1])]
2018-03-26 11:51:56 +00:00
),
docs
2017-12-05 11:16:39 +00:00
)
2018-01-04 15:29:10 +00:00
)
2017-12-05 11:16:39 +00:00
2018-01-05 01:13:08 +00:00
for c in combinators:
return_type = c.return_type
if return_type.startswith("Vector"):
return_type = return_type.split("<")[1][:-1]
2018-01-05 01:13:08 +00:00
d = types_to_constructors if c.section == "types" else types_to_functions
2018-01-05 01:13:08 +00:00
if return_type not in d:
d[return_type] = []
d[return_type].append(".".join(filter(None, [c.namespace, c.name])))
for k, v in types_to_constructors.items():
for i in v:
try:
constructors_to_functions[i] = types_to_functions[k]
except KeyError:
pass
2018-01-05 01:13:08 +00:00
2018-01-04 16:09:28 +00:00
total = len(combinators)
current = 0
2018-01-04 15:29:10 +00:00
for c in combinators: # type: Combinator
2018-01-04 16:09:28 +00:00
print("Compiling APIs... [{}%] {}".format(
str(round(current * 100 / total)).rjust(3),
".".join(filter(None, [c.section, c.namespace, c.name]))
), end=" \r", flush=True)
current += 1
2018-01-04 15:29:10 +00:00
path = "{}/{}/{}".format(DESTINATION, c.section, c.namespace)
2017-12-05 11:16:39 +00:00
os.makedirs(path, exist_ok=True)
init = "{}/__init__.py".format(path)
if not os.path.exists(init):
with open(init, "w", encoding="utf-8") as f:
2018-01-04 15:29:10 +00:00
f.write(notice + "\n\n")
2017-12-05 11:16:39 +00:00
with open(init, "a", encoding="utf-8") as f:
2018-01-04 15:29:10 +00:00
f.write("from .{} import {}\n".format(snek(c.name), capit(c.name)))
2017-12-05 11:16:39 +00:00
2018-01-04 15:29:10 +00:00
sorted_args = sort_args(c.args)
2017-12-05 11:16:39 +00:00
arguments = (
", "
+ ("*, " if c.args else "")
+ (", ".join([get_argument_type(i) for i in sorted_args if i != ("flags", "#")]) if c.args else "")
)
2017-12-05 11:16:39 +00:00
fields = "\n ".join(
["self.{0} = {0} # {1}".format(i[0], i[1]) for i in c.args if i != ("flags", "#")]
2018-01-04 15:29:10 +00:00
) if c.args else "pass"
2017-12-05 11:16:39 +00:00
2018-01-03 16:40:38 +00:00
docstring_args = []
2018-04-24 14:08:33 +00:00
docs = c.docs.split("|")[1:] if c.docs else None
2018-01-03 16:40:38 +00:00
for i, arg in enumerate(sorted_args):
if arg == ("flags", "#"):
continue
2018-01-03 16:40:38 +00:00
arg_name, arg_type = arg
2018-01-21 11:35:36 +00:00
is_optional = FLAGS_RE.match(arg_type)
flag_number = is_optional.group(1) if is_optional else -1
2018-01-03 16:40:38 +00:00
arg_type = arg_type.split("?")[-1]
2018-04-24 14:08:33 +00:00
if docs:
docstring_args.append(
"{} ({}{}):\n {}\n".format(
arg_name,
get_docstring_arg_type(arg_type, is_pyrogram_type=True),
", optional" if "Optional" in docs[i] else "",
re.sub("Optional\. ", "", docs[i].split("§")[1].rstrip(".") + ".")
)
)
else:
docstring_args.append(
"{}{}: {}".format(
arg_name,
" (optional)".format(flag_number) if is_optional else "",
get_docstring_arg_type(arg_type, is_pyrogram_type=c.namespace == "pyrogram")
)
2018-01-03 16:40:38 +00:00
)
if docstring_args:
docstring_args = "Parameters:\n " + "\n ".join(docstring_args)
2018-01-03 16:40:38 +00:00
else:
docstring_args = "No parameters required."
2018-02-09 02:17:36 +00:00
docstring_args = "Attributes:\n ID: ``{}``\n\n ".format(c.id) + docstring_args
2018-01-05 01:13:08 +00:00
if c.section == "functions":
2018-01-08 06:15:38 +00:00
docstring_args += "\n\n Returns:\n " + get_docstring_arg_type(c.return_type)
docstring_args += "\n\n Raises:\n RPCError: In case of a Telegram RPC error."
else:
references = get_references(".".join(filter(None, [c.namespace, c.name])))
if references:
2018-03-24 16:16:45 +00:00
docstring_args += "\n\n See Also:\n This object can be returned by " + references + "."
write_types = read_types = "" if c.has_flags else "# No flags\n "
2017-12-05 11:16:39 +00:00
2018-01-04 15:29:10 +00:00
for arg_name, arg_type in c.args:
flag = FLAGS_RE_2.findall(arg_type)
2017-12-05 11:16:39 +00:00
if arg_name == "flags" and arg_type == "#":
write_flags = []
for i in c.args:
flag = FLAGS_RE.match(i[1])
if flag:
write_flags.append(
"flags |= (1 << {}) if self.{} is not None else 0".format(flag.group(1), i[0]))
write_flags = "\n ".join([
"flags = 0",
"\n ".join(write_flags),
"b.write(Int(flags))\n "
])
write_types += write_flags
read_types += "flags = Int.read(b)\n "
continue
2017-12-05 11:16:39 +00:00
if flag:
index, flag_type = flag[0]
if flag_type == "true":
read_types += "\n "
read_types += "{} = True if flags & (1 << {}) else False".format(arg_name, index)
elif flag_type in core_types:
write_types += "\n "
write_types += "if self.{} is not None:\n ".format(arg_name)
write_types += "b.write({}(self.{}))\n ".format(flag_type.title(), arg_name)
read_types += "\n "
read_types += "{} = {}.read(b) if flags & (1 << {}) else None".format(
arg_name, flag_type.title(), index
)
elif "vector" in flag_type.lower():
sub_type = arg_type.split("<")[1][:-1]
write_types += "\n "
write_types += "if self.{} is not None:\n ".format(arg_name)
write_types += "b.write(Vector(self.{}{}))\n ".format(
arg_name, ", {}".format(sub_type.title()) if sub_type in core_types else ""
)
read_types += "\n "
read_types += "{} = Object.read(b{}) if flags & (1 << {}) else []\n ".format(
arg_name, ", {}".format(sub_type.title()) if sub_type in core_types else "", index
)
else:
write_types += "\n "
write_types += "if self.{} is not None:\n ".format(arg_name)
write_types += "b.write(self.{}.write())\n ".format(arg_name)
read_types += "\n "
read_types += "{} = Object.read(b) if flags & (1 << {}) else None\n ".format(
arg_name, index
)
else:
if arg_type in core_types:
write_types += "\n "
write_types += "b.write({}(self.{}))\n ".format(arg_type.title(), arg_name)
read_types += "\n "
read_types += "{} = {}.read(b)\n ".format(arg_name, arg_type.title())
elif "vector" in arg_type.lower():
sub_type = arg_type.split("<")[1][:-1]
write_types += "\n "
write_types += "b.write(Vector(self.{}{}))\n ".format(
arg_name, ", {}".format(sub_type.title()) if sub_type in core_types else ""
)
read_types += "\n "
read_types += "{} = Object.read(b{})\n ".format(
arg_name, ", {}".format(sub_type.title()) if sub_type in core_types else ""
)
else:
write_types += "\n "
write_types += "b.write(self.{}.write())\n ".format(arg_name)
read_types += "\n "
read_types += "{} = Object.read(b)\n ".format(arg_name)
2018-03-26 11:51:56 +00:00
if c.docs:
2018-04-01 11:27:05 +00:00
description = c.docs.split("|")[0].split("§")[1]
2018-03-26 11:51:56 +00:00
docstring_args = description + "\n\n " + docstring_args
with open("{}/{}.py".format(path, snek(c.name)), "w", encoding="utf-8") as f:
2018-04-01 11:09:32 +00:00
if c.docs:
f.write(
pyrogram_template.format(
notice=notice,
class_name=capit(c.name),
docstring_args=docstring_args,
object_id=c.id,
arguments=arguments,
fields=fields
)
)
else:
f.write(
mtproto_template.format(
notice=notice,
class_name=capit(c.name),
docstring_args=docstring_args,
object_id=c.id,
arguments=arguments,
fields=fields,
read_types=read_types,
write_types=write_types,
return_arguments=", ".join(
["{0}={0}".format(i[0]) for i in sorted_args if i != ("flags", "#")]
),
2019-03-16 14:30:55 +00:00
slots=", ".join(['"{}"'.format(i[0]) for i in sorted_args if i != ("flags", "#")]),
qualname="{}.{}{}".format(c.section, "{}.".format(c.namespace) if c.namespace else "", c.name)
2018-04-01 11:09:32 +00:00
)
2017-12-05 11:16:39 +00:00
)
with open("{}/all.py".format(DESTINATION), "w", encoding="utf-8") as f:
2018-01-04 15:29:10 +00:00
f.write(notice + "\n\n")
f.write("layer = {}\n\n".format(layer))
f.write("objects = {")
2017-12-05 11:16:39 +00:00
2018-01-04 15:29:10 +00:00
for c in combinators:
path = ".".join(filter(None, [c.section, c.namespace, capit(c.name)]))
2018-04-24 14:08:33 +00:00
f.write("\n {}: \"pyrogram.api.{}\",".format(c.id, path))
f.write("\n 0xbc799737: \"pyrogram.api.core.BoolFalse\",")
f.write("\n 0x997275b5: \"pyrogram.api.core.BoolTrue\",")
f.write("\n 0x56730bcc: \"pyrogram.api.core.Null\",")
f.write("\n 0x1cb5c415: \"pyrogram.api.core.Vector\",")
f.write("\n 0x73f1f8dc: \"pyrogram.api.core.MsgContainer\",")
f.write("\n 0xae500895: \"pyrogram.api.core.FutureSalts\",")
f.write("\n 0x0949d9dc: \"pyrogram.api.core.FutureSalt\",")
f.write("\n 0x3072cfa1: \"pyrogram.api.core.GzipPacked\",")
f.write("\n 0x5bb8e511: \"pyrogram.api.core.Message\",")
2018-01-04 15:29:10 +00:00
f.write("\n}\n")
2017-12-05 11:16:39 +00:00
2018-01-04 15:29:10 +00:00
for k, v in namespaces.items():
with open("{}/{}/__init__.py".format(DESTINATION, k), "a", encoding="utf-8") as f:
2018-01-04 15:29:10 +00:00
f.write("from . import {}\n".format(", ".join([i for i in v])) if v else "")
2017-12-05 11:16:39 +00:00
if "__main__" == __name__:
2018-01-04 15:29:10 +00:00
HOME = "."
DESTINATION = "../../pyrogram/api"
2018-01-20 19:16:42 +00:00
NOTICE_PATH = "../../NOTICE"
2017-12-05 11:16:39 +00:00
start()