mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-03-11 06:45:57 +03:00
Merge pull request #766 from LonamiWebs/tidygenerator
Tidy up the telethon-generator package
This commit is contained in:
commit
ffdfa8f262
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
||||||
# Docs
|
# Docs
|
||||||
_build/
|
_build/
|
||||||
|
docs/
|
||||||
|
|
||||||
# Generated code
|
# Generated code
|
||||||
telethon/tl/functions/
|
telethon/tl/functions/
|
||||||
|
|
1
docs/.gitignore
vendored
1
docs/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
generated/
|
|
|
@ -54,7 +54,7 @@ Manual Installation
|
||||||
|
|
||||||
3. Enter the cloned repository: ``cd Telethon``
|
3. Enter the cloned repository: ``cd Telethon``
|
||||||
|
|
||||||
4. Run the code generator: ``python3 setup.py gen_tl``
|
4. Run the code generator: ``python3 setup.py gen tl errors``
|
||||||
|
|
||||||
5. Done!
|
5. Done!
|
||||||
|
|
||||||
|
|
117
setup.py
117
setup.py
|
@ -6,16 +6,16 @@ https://packaging.python.org/en/latest/distributing.html
|
||||||
https://github.com/pypa/sampleproject
|
https://github.com/pypa/sampleproject
|
||||||
|
|
||||||
Extra supported commands are:
|
Extra supported commands are:
|
||||||
* gen_tl, to generate the classes required for Telethon to run
|
* gen, to generate the classes required for Telethon to run or docs
|
||||||
* clean_tl, to clean these generated classes
|
|
||||||
* pypi, to generate sdist, bdist_wheel, and push to PyPi
|
* pypi, to generate sdist, bdist_wheel, and push to PyPi
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# To use a consistent encoding
|
|
||||||
from codecs import open
|
|
||||||
from sys import argv, version_info
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
# To use a consistent encoding
|
||||||
|
import shutil
|
||||||
|
from codecs import open
|
||||||
|
from sys import argv, version_info
|
||||||
|
|
||||||
# Always prefer setuptools over distutils
|
# Always prefer setuptools over distutils
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
@ -37,44 +37,85 @@ class TempWorkDir:
|
||||||
os.chdir(self.original)
|
os.chdir(self.original)
|
||||||
|
|
||||||
|
|
||||||
ERROR_LIST = 'telethon/errors/rpc_error_list.py'
|
GENERATOR_DIR = 'telethon_generator'
|
||||||
ERRORS_JSON = 'telethon_generator/errors.json'
|
LIBRARY_DIR = 'telethon'
|
||||||
ERRORS_DESC = 'telethon_generator/error_descriptions'
|
|
||||||
SCHEME_TL = 'telethon_generator/scheme.tl'
|
ERRORS_IN_JSON = os.path.join(GENERATOR_DIR, 'data', 'errors.json')
|
||||||
GENERATOR_DIR = 'telethon/tl'
|
ERRORS_IN_DESC = os.path.join(GENERATOR_DIR, 'data', 'error_descriptions')
|
||||||
|
ERRORS_OUT = os.path.join(LIBRARY_DIR, 'errors', 'rpc_error_list.py')
|
||||||
|
|
||||||
|
TLOBJECT_IN_TL = os.path.join(GENERATOR_DIR, 'data', 'scheme.tl')
|
||||||
|
TLOBJECT_OUT = os.path.join(LIBRARY_DIR, 'tl')
|
||||||
IMPORT_DEPTH = 2
|
IMPORT_DEPTH = 2
|
||||||
|
|
||||||
|
DOCS_IN_RES = os.path.join(GENERATOR_DIR, 'data', 'html')
|
||||||
|
DOCS_OUT = 'docs'
|
||||||
|
|
||||||
def gen_tl(force=True):
|
|
||||||
from telethon_generator.tl_generator import TLGenerator
|
|
||||||
from telethon_generator.error_generator import generate_code
|
|
||||||
generator = TLGenerator(GENERATOR_DIR)
|
|
||||||
if generator.tlobjects_exist():
|
|
||||||
if not force:
|
|
||||||
return
|
|
||||||
print('Detected previous TLObjects. Cleaning...')
|
|
||||||
generator.clean_tlobjects()
|
|
||||||
|
|
||||||
print('Generating TLObjects...')
|
def generate(which):
|
||||||
generator.generate_tlobjects(SCHEME_TL, import_depth=IMPORT_DEPTH)
|
from telethon_generator.parsers import parse_errors, parse_tl, find_layer
|
||||||
print('Generating errors...')
|
from telethon_generator.generators import\
|
||||||
generate_code(ERROR_LIST, json_file=ERRORS_JSON, errors_desc=ERRORS_DESC)
|
generate_errors, generate_tlobjects, generate_docs, clean_tlobjects
|
||||||
print('Done.')
|
|
||||||
|
tlobjects = list(parse_tl(TLOBJECT_IN_TL, ignore_core=True))
|
||||||
|
errors = list(parse_errors(ERRORS_IN_JSON, ERRORS_IN_DESC))
|
||||||
|
layer = find_layer(TLOBJECT_IN_TL)
|
||||||
|
|
||||||
|
if not which:
|
||||||
|
which.extend(('tl', 'errors'))
|
||||||
|
|
||||||
|
clean = 'clean' in which
|
||||||
|
action = 'Cleaning' if clean else 'Generating'
|
||||||
|
if clean:
|
||||||
|
which.remove('clean')
|
||||||
|
|
||||||
|
if 'all' in which:
|
||||||
|
which.remove('all')
|
||||||
|
for x in ('tl', 'errors', 'docs'):
|
||||||
|
if x not in which:
|
||||||
|
which.append(x)
|
||||||
|
|
||||||
|
if 'tl' in which:
|
||||||
|
which.remove('tl')
|
||||||
|
print(action, 'TLObjects...')
|
||||||
|
if clean:
|
||||||
|
clean_tlobjects(TLOBJECT_OUT)
|
||||||
|
else:
|
||||||
|
generate_tlobjects(tlobjects, layer, IMPORT_DEPTH, TLOBJECT_OUT)
|
||||||
|
|
||||||
|
if 'errors' in which:
|
||||||
|
which.remove('errors')
|
||||||
|
print(action, 'RPCErrors...')
|
||||||
|
if clean:
|
||||||
|
if os.path.isfile(ERRORS_OUT):
|
||||||
|
os.remove(ERRORS_OUT)
|
||||||
|
else:
|
||||||
|
with open(ERRORS_OUT, 'w', encoding='utf-8') as file:
|
||||||
|
generate_errors(errors, file)
|
||||||
|
|
||||||
|
if 'docs' in which:
|
||||||
|
which.remove('docs')
|
||||||
|
print(action, 'documentation...')
|
||||||
|
if clean:
|
||||||
|
if os.path.isdir(DOCS_OUT):
|
||||||
|
shutil.rmtree(DOCS_OUT)
|
||||||
|
else:
|
||||||
|
generate_docs(tlobjects, errors, layer, DOCS_IN_RES, DOCS_OUT)
|
||||||
|
|
||||||
|
if which:
|
||||||
|
print('The following items were not understood:', which)
|
||||||
|
print(' Consider using only "tl", "errors" and/or "docs".')
|
||||||
|
print(' Using only "clean" will clean them. "all" to act on all.')
|
||||||
|
print(' For instance "gen tl errors".')
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if len(argv) >= 2 and argv[1] == 'gen_tl':
|
if len(argv) >= 2 and argv[1] == 'gen':
|
||||||
gen_tl()
|
generate(argv[2:])
|
||||||
|
|
||||||
elif len(argv) >= 2 and argv[1] == 'clean_tl':
|
|
||||||
from telethon_generator.tl_generator import TLGenerator
|
|
||||||
print('Cleaning...')
|
|
||||||
TLGenerator(GENERATOR_DIR).clean_tlobjects()
|
|
||||||
print('Done.')
|
|
||||||
|
|
||||||
elif len(argv) >= 2 and argv[1] == 'pypi':
|
elif len(argv) >= 2 and argv[1] == 'pypi':
|
||||||
# (Re)generate the code to make sure we don't push without it
|
# (Re)generate the code to make sure we don't push without it
|
||||||
gen_tl()
|
generate(['clean', 'tl', 'errors'])
|
||||||
|
|
||||||
# Try importing the telethon module to assert it has no errors
|
# Try importing the telethon module to assert it has no errors
|
||||||
try:
|
try:
|
||||||
|
@ -96,14 +137,10 @@ def main():
|
||||||
for x in ('build', 'dist', 'Telethon.egg-info'):
|
for x in ('build', 'dist', 'Telethon.egg-info'):
|
||||||
rmtree(x, ignore_errors=True)
|
rmtree(x, ignore_errors=True)
|
||||||
|
|
||||||
elif len(argv) >= 2 and argv[1] == 'fetch_errors':
|
|
||||||
from telethon_generator.error_generator import fetch_errors
|
|
||||||
fetch_errors(ERRORS_JSON)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Call gen_tl() if the scheme.tl file exists, e.g. install from GitHub
|
# e.g. install from GitHub
|
||||||
if os.path.isfile(SCHEME_TL):
|
if os.path.isfile(GENERATOR_DIR):
|
||||||
gen_tl(force=False)
|
generate(['clean', 'tl', 'errors'])
|
||||||
|
|
||||||
# Get the long description from the README file
|
# Get the long description from the README file
|
||||||
with open('README.rst', encoding='utf-8') as f:
|
with open('README.rst', encoding='utf-8') as f:
|
||||||
|
|
|
@ -12,6 +12,7 @@ from .common import (
|
||||||
)
|
)
|
||||||
|
|
||||||
# This imports the base errors too, as they're imported there
|
# This imports the base errors too, as they're imported there
|
||||||
|
from .rpc_base_errors import *
|
||||||
from .rpc_error_list import *
|
from .rpc_error_list import *
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -189,6 +189,6 @@ class TLObject:
|
||||||
def __bytes__(self):
|
def __bytes__(self):
|
||||||
return b''
|
return b''
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_reader(reader):
|
def from_reader(cls, reader):
|
||||||
return TLObject()
|
return TLObject()
|
||||||
|
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
@ -4,7 +4,7 @@ import re
|
||||||
|
|
||||||
class DocsWriter:
|
class DocsWriter:
|
||||||
"""Utility class used to write the HTML files used on the documentation"""
|
"""Utility class used to write the HTML files used on the documentation"""
|
||||||
def __init__(self, filename, type_to_path_function):
|
def __init__(self, filename, type_to_path):
|
||||||
"""Initializes the writer to the specified output file,
|
"""Initializes the writer to the specified output file,
|
||||||
creating the parent directories when used if required.
|
creating the parent directories when used if required.
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ class DocsWriter:
|
||||||
self.menu_separator_tag = None
|
self.menu_separator_tag = None
|
||||||
|
|
||||||
# Utility functions TODO There must be a better way
|
# Utility functions TODO There must be a better way
|
||||||
self.type_to_path = lambda t: type_to_path_function(
|
self.type_to_path = lambda t: type_to_path(
|
||||||
t, relative_to=self.filename
|
t, relative_to=self.filename
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,177 +0,0 @@
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import urllib.request
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
URL = 'https://rpc.pwrtelegram.xyz/?all'
|
|
||||||
|
|
||||||
known_base_classes = {
|
|
||||||
303: 'InvalidDCError',
|
|
||||||
400: 'BadRequestError',
|
|
||||||
401: 'UnauthorizedError',
|
|
||||||
403: 'ForbiddenError',
|
|
||||||
404: 'NotFoundError',
|
|
||||||
406: 'AuthKeyError',
|
|
||||||
420: 'FloodError',
|
|
||||||
500: 'ServerError',
|
|
||||||
}
|
|
||||||
|
|
||||||
# The API doesn't return the code for some (vital) errors. They are
|
|
||||||
# all assumed to be 400, except these well-known ones that aren't.
|
|
||||||
known_codes = {
|
|
||||||
'ACTIVE_USER_REQUIRED': 401,
|
|
||||||
'AUTH_KEY_UNREGISTERED': 401,
|
|
||||||
'USER_DEACTIVATED': 401
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_errors(output, url=URL):
|
|
||||||
print('Opening a connection to', url, '...')
|
|
||||||
r = urllib.request.urlopen(urllib.request.Request(
|
|
||||||
url, headers={'User-Agent' : 'Mozilla/5.0'}
|
|
||||||
))
|
|
||||||
print('Checking response...')
|
|
||||||
data = json.loads(
|
|
||||||
r.read().decode(r.info().get_param('charset') or 'utf-8')
|
|
||||||
)
|
|
||||||
if data.get('ok'):
|
|
||||||
print('Response was okay, saving data')
|
|
||||||
with open(output, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(data, f, sort_keys=True)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print('The data received was not okay:')
|
|
||||||
print(json.dumps(data, indent=4, sort_keys=True))
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_class_name(error_code):
|
|
||||||
if isinstance(error_code, int):
|
|
||||||
return known_base_classes.get(
|
|
||||||
error_code, 'RPCError' + str(error_code).replace('-', 'Neg')
|
|
||||||
)
|
|
||||||
|
|
||||||
if 'FIRSTNAME' in error_code:
|
|
||||||
error_code = error_code.replace('FIRSTNAME', 'FIRST_NAME')
|
|
||||||
|
|
||||||
result = re.sub(
|
|
||||||
r'_([a-z])', lambda m: m.group(1).upper(), error_code.lower()
|
|
||||||
)
|
|
||||||
return result[:1].upper() + result[1:].replace('_', '') + 'Error'
|
|
||||||
|
|
||||||
|
|
||||||
def write_error(f, code, name, desc, capture_name):
|
|
||||||
f.write(
|
|
||||||
'\n\nclass {}({}):\n def __init__(self, **kwargs):\n '
|
|
||||||
''.format(name, get_class_name(code))
|
|
||||||
)
|
|
||||||
if capture_name:
|
|
||||||
f.write(
|
|
||||||
"self.{} = int(kwargs.get('capture', 0))\n ".format(capture_name)
|
|
||||||
)
|
|
||||||
f.write('super(Exception, self).__init__({}'.format(repr(desc)))
|
|
||||||
if capture_name:
|
|
||||||
f.write('.format(self.{})'.format(capture_name))
|
|
||||||
f.write(')\n')
|
|
||||||
|
|
||||||
|
|
||||||
def generate_code(output, json_file, errors_desc):
|
|
||||||
with open(json_file, encoding='utf-8') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
errors = defaultdict(set)
|
|
||||||
# PWRTelegram's API doesn't return all errors, which we do need here.
|
|
||||||
# Add some special known-cases manually first.
|
|
||||||
errors[420].update((
|
|
||||||
'FLOOD_WAIT_X', 'FLOOD_TEST_PHONE_WAIT_X'
|
|
||||||
))
|
|
||||||
errors[401].update((
|
|
||||||
'AUTH_KEY_INVALID', 'SESSION_EXPIRED', 'SESSION_REVOKED'
|
|
||||||
))
|
|
||||||
errors[303].update((
|
|
||||||
'FILE_MIGRATE_X', 'PHONE_MIGRATE_X',
|
|
||||||
'NETWORK_MIGRATE_X', 'USER_MIGRATE_X'
|
|
||||||
))
|
|
||||||
for error_code, method_errors in data['result'].items():
|
|
||||||
for error_list in method_errors.values():
|
|
||||||
for error in error_list:
|
|
||||||
errors[int(error_code)].add(re.sub('_\d+', '_X', error).upper())
|
|
||||||
|
|
||||||
# Some errors are in the human result, but not with a code. Assume code 400
|
|
||||||
for error in data['human_result']:
|
|
||||||
if error[0] != '-' and not error.isdigit():
|
|
||||||
error = re.sub('_\d+', '_X', error).upper()
|
|
||||||
if not any(error in es for es in errors.values()):
|
|
||||||
errors[known_codes.get(error, 400)].add(error)
|
|
||||||
|
|
||||||
# Some error codes are not known, so create custom base classes if needed
|
|
||||||
needed_base_classes = [
|
|
||||||
(e, get_class_name(e)) for e in errors if e not in known_base_classes
|
|
||||||
]
|
|
||||||
|
|
||||||
# Prefer the descriptions that are related with Telethon way of coding to
|
|
||||||
# those that PWRTelegram's API provides.
|
|
||||||
telethon_descriptions = {}
|
|
||||||
with open(errors_desc, encoding='utf-8') as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
if line and not line.startswith('#'):
|
|
||||||
equal = line.index('=')
|
|
||||||
message, description = line[:equal], line[equal + 1:]
|
|
||||||
telethon_descriptions[message.rstrip()] = description.lstrip()
|
|
||||||
|
|
||||||
# Names for the captures, or 'x' if unknown
|
|
||||||
capture_names = {
|
|
||||||
'FloodWaitError': 'seconds',
|
|
||||||
'FloodTestPhoneWaitError': 'seconds',
|
|
||||||
'FileMigrateError': 'new_dc',
|
|
||||||
'NetworkMigrateError': 'new_dc',
|
|
||||||
'PhoneMigrateError': 'new_dc',
|
|
||||||
'UserMigrateError': 'new_dc',
|
|
||||||
'FilePartMissingError': 'which'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Everything ready, generate the code
|
|
||||||
with open(output, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(
|
|
||||||
'from .rpc_base_errors import RPCError, BadMessageError, {}\n'.format(
|
|
||||||
", ".join(known_base_classes.values()))
|
|
||||||
)
|
|
||||||
for code, cls in needed_base_classes:
|
|
||||||
f.write(
|
|
||||||
'\n\nclass {}(RPCError):\n code = {}\n'.format(cls, code)
|
|
||||||
)
|
|
||||||
|
|
||||||
patterns = [] # Save this dictionary later in the generated code
|
|
||||||
for error_code, error_set in errors.items():
|
|
||||||
for error in sorted(error_set):
|
|
||||||
description = telethon_descriptions.get(
|
|
||||||
error, '\n'.join(data['human_result'].get(
|
|
||||||
error, ['No description known.']
|
|
||||||
))
|
|
||||||
)
|
|
||||||
has_captures = '_X' in error
|
|
||||||
if has_captures:
|
|
||||||
name = get_class_name(error.replace('_X', ''))
|
|
||||||
pattern = error.replace('_X', r'_(\d+)')
|
|
||||||
else:
|
|
||||||
name, pattern = get_class_name(error), error
|
|
||||||
|
|
||||||
patterns.append((pattern, name))
|
|
||||||
capture = capture_names.get(name, 'x') if has_captures else None
|
|
||||||
# TODO Some errors have the same name but different code,
|
|
||||||
# split this across different files?
|
|
||||||
write_error(f, error_code, name, description, capture)
|
|
||||||
|
|
||||||
f.write('\n\nrpc_errors_all = {\n')
|
|
||||||
for pattern, name in patterns:
|
|
||||||
f.write(' {}: {},\n'.format(repr(pattern), name))
|
|
||||||
f.write('}\n')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if input('generate (y/n)?: ').lower() == 'y':
|
|
||||||
generate_code('../telethon/errors/rpc_error_list.py',
|
|
||||||
'errors.json', 'error_descriptions')
|
|
||||||
elif input('fetch (y/n)?: ').lower() == 'y':
|
|
||||||
fetch_errors('errors.json')
|
|
32
telethon_generator/fetch_errors.py
Normal file
32
telethon_generator/fetch_errors.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
OUT = 'data/errors.json'
|
||||||
|
URL = 'https://rpc.pwrtelegram.xyz/?all'
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_errors(output, url=URL):
|
||||||
|
print('Opening a connection to', url, '...')
|
||||||
|
r = urllib.request.urlopen(urllib.request.Request(
|
||||||
|
url, headers={'User-Agent' : 'Mozilla/5.0'}
|
||||||
|
))
|
||||||
|
print('Checking response...')
|
||||||
|
data = json.loads(
|
||||||
|
r.read().decode(r.info().get_param('charset') or 'utf-8')
|
||||||
|
)
|
||||||
|
if data.get('ok'):
|
||||||
|
print('Response was okay, saving data')
|
||||||
|
with open(output, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, sort_keys=True)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print('The data received was not okay:')
|
||||||
|
print(json.dumps(data, indent=4, sort_keys=True))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
out = OUT if len(sys.argv) < 2 else sys.argv[2]
|
||||||
|
url = URL if len(sys.argv) < 3 else sys.argv[3]
|
||||||
|
fetch_errors(out, url)
|
26
telethon_generator/generator.py
Normal file
26
telethon_generator/generator.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
from telethon_generator.parsers import parse_errors, parse_tl, find_layer
|
||||||
|
from telethon_generator.generators import\
|
||||||
|
generate_errors, generate_tlobjects, generate_docs
|
||||||
|
|
||||||
|
|
||||||
|
ERRORS_INPUT_JSON = 'data/errors.json'
|
||||||
|
ERRORS_INPUT_DESC = 'data/error_descriptions'
|
||||||
|
ERRORS_OUTPUT = '../telethon/errors/rpc_error_list.py'
|
||||||
|
|
||||||
|
TLOBJECT_INPUT_TL = 'data/scheme.tl'
|
||||||
|
TLOBJECT_OUTPUT = '../telethon/tl'
|
||||||
|
|
||||||
|
DOCS_INPUT_RES = 'data/html'
|
||||||
|
DOCS_OUTPUT = '../docs'
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
tlobjects = list(parse_tl(TLOBJECT_INPUT_TL, ignore_core=True))
|
||||||
|
errors = list(parse_errors(ERRORS_INPUT_JSON, ERRORS_INPUT_DESC))
|
||||||
|
layer = find_layer(TLOBJECT_INPUT_TL)
|
||||||
|
|
||||||
|
generate_tlobjects(tlobjects, layer, TLOBJECT_OUTPUT)
|
||||||
|
with open(ERRORS_OUTPUT, 'w', encoding='utf-8') as file:
|
||||||
|
generate_errors(errors, file)
|
||||||
|
|
||||||
|
generate_docs(tlobjects, errors, layer, DOCS_INPUT_RES, DOCS_OUTPUT)
|
3
telethon_generator/generators/__init__.py
Normal file
3
telethon_generator/generators/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from .errors import generate_errors
|
||||||
|
from .tlobject import generate_tlobjects, clean_tlobjects
|
||||||
|
from .docs import generate_docs
|
|
@ -1,124 +1,74 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
import shutil
|
import shutil
|
||||||
try:
|
from collections import defaultdict
|
||||||
from .docs_writer import DocsWriter
|
|
||||||
except (ImportError, SystemError):
|
|
||||||
from docs_writer import DocsWriter
|
|
||||||
|
|
||||||
# Small trick so importing telethon_generator works
|
from ..docs_writer import DocsWriter
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
from ..parsers import TLObject
|
||||||
|
from ..utils import snake_to_camel_case
|
||||||
from telethon_generator.parser import TLParser, TLObject
|
|
||||||
|
|
||||||
|
|
||||||
# TLObject -> Python class name
|
CORE_TYPES = {
|
||||||
def get_class_name(tlobject):
|
'int', 'long', 'int128', 'int256', 'double',
|
||||||
"""Gets the class name following the Python style guidelines"""
|
'vector', 'string', 'bool', 'true', 'bytes', 'date'
|
||||||
# Courtesy of http://stackoverflow.com/a/31531797/4759433
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_file_name(tlobject):
|
||||||
|
"""``ClassName -> class_name.html``."""
|
||||||
name = tlobject.name if isinstance(tlobject, TLObject) else tlobject
|
name = tlobject.name if isinstance(tlobject, TLObject) else tlobject
|
||||||
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)
|
|
||||||
|
|
||||||
# Replace '_' with '' once again to make sure it doesn't appear on the name
|
|
||||||
result = result[:1].upper() + result[1:].replace('_', '')
|
|
||||||
|
|
||||||
# If it's a function, let it end with "Request" to identify them more easily
|
|
||||||
if isinstance(tlobject, TLObject) and tlobject.is_function:
|
|
||||||
result += 'Request'
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# TLObject -> filename
|
|
||||||
def get_file_name(tlobject, add_extension=False):
|
|
||||||
"""Gets the file name in file_name_format.html for the given TLObject.
|
|
||||||
Only its name may also be given if the full TLObject is not available"""
|
|
||||||
if isinstance(tlobject, TLObject):
|
|
||||||
name = tlobject.name
|
|
||||||
else:
|
|
||||||
name = tlobject
|
|
||||||
|
|
||||||
# Courtesy of http://stackoverflow.com/a/1176023/4759433
|
# Courtesy of http://stackoverflow.com/a/1176023/4759433
|
||||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||||
result = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
result = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||||
if add_extension:
|
return '{}.html'.format(result)
|
||||||
return result + '.html'
|
|
||||||
else:
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# TLObject -> from ... import ...
|
|
||||||
def get_import_code(tlobject):
|
def get_import_code(tlobject):
|
||||||
|
"""``TLObject -> from ... import ...``."""
|
||||||
kind = 'functions' if tlobject.is_function else 'types'
|
kind = 'functions' if tlobject.is_function else 'types'
|
||||||
ns = '.' + tlobject.namespace if tlobject.namespace else ''
|
ns = '.' + tlobject.namespace if tlobject.namespace else ''
|
||||||
|
|
||||||
return 'from telethon.tl.{}{} import {}'\
|
return 'from telethon.tl.{}{} import {}'\
|
||||||
.format(kind, ns, get_class_name(tlobject))
|
.format(kind, ns, tlobject.class_name)
|
||||||
|
|
||||||
|
|
||||||
def get_create_path_for(tlobject):
|
def _get_create_path_for(root, tlobject):
|
||||||
"""Gets the file path (and creates the parent directories)
|
"""Creates and returns the path for the given TLObject at root."""
|
||||||
for the given 'tlobject', relative to nothing; only its local path"""
|
|
||||||
|
|
||||||
# Determine the output directory
|
|
||||||
out_dir = 'methods' if tlobject.is_function else 'constructors'
|
out_dir = 'methods' if tlobject.is_function else 'constructors'
|
||||||
|
|
||||||
if tlobject.namespace:
|
if tlobject.namespace:
|
||||||
out_dir = os.path.join(out_dir, tlobject.namespace)
|
out_dir = os.path.join(out_dir, tlobject.namespace)
|
||||||
|
|
||||||
# Ensure that it exists
|
out_dir = os.path.join(root, out_dir)
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
return os.path.join(out_dir, _get_file_name(tlobject))
|
||||||
# Return the resulting filename
|
|
||||||
return os.path.join(out_dir, get_file_name(tlobject, add_extension=True))
|
|
||||||
|
|
||||||
|
|
||||||
def is_core_type(type_):
|
def get_path_for_type(root, type_, relative_to='.'):
|
||||||
"""Returns "true" if the type is considered a core type"""
|
"""Similar to `_get_create_path_for` but for only type names."""
|
||||||
return type_.lower() in {
|
if type_.lower() in CORE_TYPES:
|
||||||
'int', 'long', 'int128', 'int256', 'double',
|
|
||||||
'vector', 'string', 'bool', 'true', 'bytes', 'date'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_path_for_type(type_, relative_to='.'):
|
|
||||||
"""Similar to getting the path for a TLObject, it might not be possible
|
|
||||||
to have the TLObject itself but rather its name (the type);
|
|
||||||
this method works in the same way, returning a relative path"""
|
|
||||||
if is_core_type(type_):
|
|
||||||
path = 'index.html#%s' % type_.lower()
|
path = 'index.html#%s' % type_.lower()
|
||||||
|
|
||||||
elif '.' in type_:
|
elif '.' in type_:
|
||||||
# If it's not a core type, then it has to be a custom Telegram type
|
|
||||||
namespace, name = type_.split('.')
|
namespace, name = type_.split('.')
|
||||||
path = 'types/%s/%s' % (namespace, get_file_name(name, True))
|
path = 'types/%s/%s' % (namespace, _get_file_name(name))
|
||||||
else:
|
else:
|
||||||
path = 'types/%s' % get_file_name(type_, True)
|
path = 'types/%s' % _get_file_name(type_)
|
||||||
|
|
||||||
return get_relative_path(path, relative_to)
|
return _get_relative_path(os.path.join(root, path), relative_to)
|
||||||
|
|
||||||
|
|
||||||
# Destination path from the current position -> relative to the given path
|
def _get_relative_path(destination, relative_to, folder=False):
|
||||||
def get_relative_path(destination, relative_to):
|
"""Return the relative path to destination from relative_to."""
|
||||||
if os.path.isfile(relative_to):
|
if not folder:
|
||||||
relative_to = os.path.dirname(relative_to)
|
relative_to = os.path.dirname(relative_to)
|
||||||
|
|
||||||
return os.path.relpath(destination, start=relative_to)
|
return os.path.relpath(destination, start=relative_to)
|
||||||
|
|
||||||
|
|
||||||
def get_relative_paths(original, relative_to):
|
def _find_title(html_file):
|
||||||
"""Converts the dictionary of 'original' paths to relative paths
|
"""Finds the <title> for the given HTML file, or (Unknown)."""
|
||||||
starting from the given 'relative_to' file"""
|
with open(html_file) as fp:
|
||||||
return {k: get_relative_path(v, relative_to) for k, v in original.items()}
|
for line in fp:
|
||||||
|
|
||||||
|
|
||||||
# Generate a index.html file for the given folder
|
|
||||||
def find_title(html_file):
|
|
||||||
"""Finds the <title> for the given HTML file, or (Unknown)"""
|
|
||||||
with open(html_file) as handle:
|
|
||||||
for line in handle:
|
|
||||||
if '<title>' in line:
|
if '<title>' in line:
|
||||||
# + 7 to skip len('<title>')
|
# + 7 to skip len('<title>')
|
||||||
return line[line.index('<title>') + 7:line.index('</title>')]
|
return line[line.index('<title>') + 7:line.index('</title>')]
|
||||||
|
@ -126,10 +76,11 @@ def find_title(html_file):
|
||||||
return '(Unknown)'
|
return '(Unknown)'
|
||||||
|
|
||||||
|
|
||||||
def build_menu(docs, filename, relative_main_index):
|
def _build_menu(docs, filename, root, relative_main_index):
|
||||||
"""Builds the menu using the given DocumentWriter up to 'filename',
|
"""Builds the menu using the given DocumentWriter up to 'filename',
|
||||||
which must be a file (it cannot be a directory)"""
|
which must be a file (it cannot be a directory)"""
|
||||||
# TODO Maybe this could be part of DocsWriter itself, "build path menu"
|
# TODO Maybe this could be part of DocsWriter itself, "build path menu"
|
||||||
|
filename = _get_relative_path(filename, root)
|
||||||
docs.add_menu('API', relative_main_index)
|
docs.add_menu('API', relative_main_index)
|
||||||
|
|
||||||
items = filename.split('/')
|
items = filename.split('/')
|
||||||
|
@ -144,9 +95,8 @@ def build_menu(docs, filename, relative_main_index):
|
||||||
docs.end_menu()
|
docs.end_menu()
|
||||||
|
|
||||||
|
|
||||||
def generate_index(folder, original_paths):
|
def _generate_index(folder, original_paths, root):
|
||||||
"""Generates the index file for the specified folder"""
|
"""Generates the index file for the specified folder"""
|
||||||
|
|
||||||
# Determine the namespaces listed here (as sub folders)
|
# Determine the namespaces listed here (as sub folders)
|
||||||
# and the files (.html files) that we should link to
|
# and the files (.html files) that we should link to
|
||||||
namespaces = []
|
namespaces = []
|
||||||
|
@ -157,27 +107,28 @@ def generate_index(folder, original_paths):
|
||||||
elif item != 'index.html':
|
elif item != 'index.html':
|
||||||
files.append(item)
|
files.append(item)
|
||||||
|
|
||||||
# We work with relative paths
|
paths = {k: _get_relative_path(v, folder, folder=True)
|
||||||
paths = get_relative_paths(original_paths, relative_to=folder)
|
for k, v in original_paths.items()}
|
||||||
|
|
||||||
# Now that everything is setup, write the index.html file
|
# Now that everything is setup, write the index.html file
|
||||||
filename = os.path.join(folder, 'index.html')
|
filename = os.path.join(folder, 'index.html')
|
||||||
with DocsWriter(filename, type_to_path_function=get_path_for_type) as docs:
|
with DocsWriter(filename, type_to_path=get_path_for_type) as docs:
|
||||||
# Title should be the current folder name
|
# Title should be the current folder name
|
||||||
docs.write_head(folder.title(), relative_css_path=paths['css'])
|
docs.write_head(folder.title(), relative_css_path=paths['css'])
|
||||||
|
|
||||||
docs.set_menu_separator(paths['arrow'])
|
docs.set_menu_separator(paths['arrow'])
|
||||||
build_menu(docs, filename, relative_main_index=paths['index_all'])
|
_build_menu(docs, filename, root,
|
||||||
|
relative_main_index=paths['index_all'])
|
||||||
docs.write_title(folder.title())
|
|
||||||
|
|
||||||
|
docs.write_title(_get_relative_path(folder, root, folder=True).title())
|
||||||
if namespaces:
|
if namespaces:
|
||||||
docs.write_title('Namespaces', level=3)
|
docs.write_title('Namespaces', level=3)
|
||||||
docs.begin_table(4)
|
docs.begin_table(4)
|
||||||
namespaces.sort()
|
namespaces.sort()
|
||||||
for namespace in namespaces:
|
for namespace in namespaces:
|
||||||
# For every namespace, also write the index of it
|
# For every namespace, also write the index of it
|
||||||
generate_index(os.path.join(folder, namespace), original_paths)
|
_generate_index(os.path.join(folder, namespace),
|
||||||
|
original_paths, root)
|
||||||
docs.add_row(namespace.title(),
|
docs.add_row(namespace.title(),
|
||||||
link=os.path.join(namespace, 'index.html'))
|
link=os.path.join(namespace, 'index.html'))
|
||||||
|
|
||||||
|
@ -186,7 +137,7 @@ def generate_index(folder, original_paths):
|
||||||
docs.write_title('Available items')
|
docs.write_title('Available items')
|
||||||
docs.begin_table(2)
|
docs.begin_table(2)
|
||||||
|
|
||||||
files = [(f, find_title(os.path.join(folder, f))) for f in files]
|
files = [(f, _find_title(os.path.join(folder, f))) for f in files]
|
||||||
files.sort(key=lambda t: t[1])
|
files.sort(key=lambda t: t[1])
|
||||||
|
|
||||||
for file, title in files:
|
for file, title in files:
|
||||||
|
@ -196,8 +147,8 @@ def generate_index(folder, original_paths):
|
||||||
docs.end_body()
|
docs.end_body()
|
||||||
|
|
||||||
|
|
||||||
def get_description(arg):
|
def _get_description(arg):
|
||||||
"""Generates a proper description for the given argument"""
|
"""Generates a proper description for the given argument."""
|
||||||
desc = []
|
desc = []
|
||||||
otherwise = False
|
otherwise = False
|
||||||
if arg.can_be_inferred:
|
if arg.can_be_inferred:
|
||||||
|
@ -235,7 +186,7 @@ def get_description(arg):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def copy_replace(src, dst, replacements):
|
def _copy_replace(src, dst, replacements):
|
||||||
"""Copies the src file into dst applying the replacements dict"""
|
"""Copies the src file into dst applying the replacements dict"""
|
||||||
with open(src) as infile, open(dst, 'w') as outfile:
|
with open(src) as infile, open(dst, 'w') as outfile:
|
||||||
outfile.write(re.sub(
|
outfile.write(re.sub(
|
||||||
|
@ -245,10 +196,15 @@ def copy_replace(src, dst, replacements):
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
def generate_documentation(scheme_file):
|
def _write_html_pages(tlobjects, errors, layer, input_res, output_dir):
|
||||||
"""Generates the documentation HTML files from from scheme.tl to
|
|
||||||
/methods and /constructors, etc.
|
|
||||||
"""
|
"""
|
||||||
|
Generates the documentation HTML files from from ``scheme.tl``
|
||||||
|
to ``/methods`` and ``/constructors``, etc.
|
||||||
|
"""
|
||||||
|
# Save 'Type: [Constructors]' for use in both:
|
||||||
|
# * Seeing the return type or constructors belonging to the same type.
|
||||||
|
# * Generating the types documentation, showing available constructors.
|
||||||
|
# TODO Tried using 'defaultdict(list)' with strange results, make it work.
|
||||||
original_paths = {
|
original_paths = {
|
||||||
'css': 'css/docs.css',
|
'css': 'css/docs.css',
|
||||||
'arrow': 'img/arrow.svg',
|
'arrow': 'img/arrow.svg',
|
||||||
|
@ -259,46 +215,46 @@ def generate_documentation(scheme_file):
|
||||||
'index_methods': 'methods/index.html',
|
'index_methods': 'methods/index.html',
|
||||||
'index_constructors': 'constructors/index.html'
|
'index_constructors': 'constructors/index.html'
|
||||||
}
|
}
|
||||||
tlobjects = tuple(TLParser.parse_file(scheme_file))
|
original_paths = {k: os.path.join(output_dir, v)
|
||||||
|
for k, v in original_paths.items()}
|
||||||
|
|
||||||
print('Generating constructors and functions documentation...')
|
type_to_constructors = {}
|
||||||
|
type_to_functions = {}
|
||||||
# Save 'Type: [Constructors]' for use in both:
|
|
||||||
# * Seeing the return type or constructors belonging to the same type.
|
|
||||||
# * Generating the types documentation, showing available constructors.
|
|
||||||
# TODO Tried using 'defaultdict(list)' with strange results, make it work.
|
|
||||||
tltypes = {}
|
|
||||||
tlfunctions = {}
|
|
||||||
for tlobject in tlobjects:
|
for tlobject in tlobjects:
|
||||||
# Select to which dictionary we want to store this type
|
d = type_to_functions if tlobject.is_function else type_to_constructors
|
||||||
dictionary = tlfunctions if tlobject.is_function else tltypes
|
if tlobject.result in d:
|
||||||
|
d[tlobject.result].append(tlobject)
|
||||||
if tlobject.result in dictionary:
|
|
||||||
dictionary[tlobject.result].append(tlobject)
|
|
||||||
else:
|
else:
|
||||||
dictionary[tlobject.result] = [tlobject]
|
d[tlobject.result] = [tlobject]
|
||||||
|
|
||||||
for tltype, constructors in tltypes.items():
|
for t, cs in type_to_constructors.items():
|
||||||
tltypes[tltype] = list(sorted(constructors, key=lambda c: c.name))
|
type_to_constructors[t] = list(sorted(cs, key=lambda c: c.name))
|
||||||
|
|
||||||
|
method_causes_errors = defaultdict(list)
|
||||||
|
for error in errors:
|
||||||
|
for method in error.caused_by:
|
||||||
|
method_causes_errors[method].append(error)
|
||||||
|
|
||||||
|
# Since the output directory is needed everywhere partially apply it now
|
||||||
|
create_path_for = functools.partial(_get_create_path_for, output_dir)
|
||||||
|
path_for_type = functools.partial(get_path_for_type, output_dir)
|
||||||
|
|
||||||
for tlobject in tlobjects:
|
for tlobject in tlobjects:
|
||||||
filename = get_create_path_for(tlobject)
|
filename = create_path_for(tlobject)
|
||||||
|
paths = {k: _get_relative_path(v, filename)
|
||||||
|
for k, v in original_paths.items()}
|
||||||
|
|
||||||
# Determine the relative paths for this file
|
with DocsWriter(filename, type_to_path=path_for_type) as docs:
|
||||||
paths = get_relative_paths(original_paths, relative_to=filename)
|
docs.write_head(title=tlobject.class_name,
|
||||||
|
relative_css_path=paths['css'])
|
||||||
with DocsWriter(filename, type_to_path_function=get_path_for_type) \
|
|
||||||
as docs:
|
|
||||||
docs.write_head(
|
|
||||||
title=get_class_name(tlobject),
|
|
||||||
relative_css_path=paths['css'])
|
|
||||||
|
|
||||||
# Create the menu (path to the current TLObject)
|
# Create the menu (path to the current TLObject)
|
||||||
docs.set_menu_separator(paths['arrow'])
|
docs.set_menu_separator(paths['arrow'])
|
||||||
build_menu(docs, filename, relative_main_index=paths['index_all'])
|
_build_menu(docs, filename, output_dir,
|
||||||
|
relative_main_index=paths['index_all'])
|
||||||
|
|
||||||
# Create the page title
|
# Create the page title
|
||||||
docs.write_title(get_class_name(tlobject))
|
docs.write_title(tlobject.class_name)
|
||||||
|
|
||||||
# Write the code definition for this TLObject
|
# Write the code definition for this TLObject
|
||||||
docs.write_code(tlobject)
|
docs.write_code(tlobject)
|
||||||
|
@ -328,24 +284,24 @@ def generate_documentation(scheme_file):
|
||||||
inner = tlobject.result
|
inner = tlobject.result
|
||||||
|
|
||||||
docs.begin_table(column_count=1)
|
docs.begin_table(column_count=1)
|
||||||
docs.add_row(inner, link=get_path_for_type(
|
docs.add_row(inner, link=path_for_type(
|
||||||
inner, relative_to=filename
|
inner, relative_to=filename
|
||||||
))
|
))
|
||||||
docs.end_table()
|
docs.end_table()
|
||||||
|
|
||||||
constructors = tltypes.get(inner, [])
|
cs = type_to_constructors.get(inner, [])
|
||||||
if not constructors:
|
if not cs:
|
||||||
docs.write_text('This type has no instances available.')
|
docs.write_text('This type has no instances available.')
|
||||||
elif len(constructors) == 1:
|
elif len(cs) == 1:
|
||||||
docs.write_text('This type can only be an instance of:')
|
docs.write_text('This type can only be an instance of:')
|
||||||
else:
|
else:
|
||||||
docs.write_text('This type can be an instance of either:')
|
docs.write_text('This type can be an instance of either:')
|
||||||
|
|
||||||
docs.begin_table(column_count=2)
|
docs.begin_table(column_count=2)
|
||||||
for constructor in constructors:
|
for constructor in cs:
|
||||||
link = get_create_path_for(constructor)
|
link = create_path_for(constructor)
|
||||||
link = get_relative_path(link, relative_to=filename)
|
link = _get_relative_path(link, relative_to=filename)
|
||||||
docs.add_row(get_class_name(constructor), link=link)
|
docs.add_row(constructor.class_name, link=link)
|
||||||
docs.end_table()
|
docs.end_table()
|
||||||
|
|
||||||
# Return (or similar types) written. Now parameters/members
|
# Return (or similar types) written. Now parameters/members
|
||||||
|
@ -375,11 +331,11 @@ def generate_documentation(scheme_file):
|
||||||
else:
|
else:
|
||||||
docs.add_row(
|
docs.add_row(
|
||||||
arg.type, align='center', link=
|
arg.type, align='center', link=
|
||||||
get_path_for_type(arg.type, relative_to=filename)
|
path_for_type(arg.type, relative_to=filename)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add a description for this argument
|
# Add a description for this argument
|
||||||
docs.add_row(get_description(arg))
|
docs.add_row(_get_description(arg))
|
||||||
|
|
||||||
docs.end_table()
|
docs.end_table()
|
||||||
else:
|
else:
|
||||||
|
@ -388,6 +344,25 @@ def generate_documentation(scheme_file):
|
||||||
else:
|
else:
|
||||||
docs.write_text('This type has no members.')
|
docs.write_text('This type has no members.')
|
||||||
|
|
||||||
|
if tlobject.is_function:
|
||||||
|
docs.write_title('Known RPC errors')
|
||||||
|
errors = method_causes_errors[tlobject.fullname]
|
||||||
|
if not errors:
|
||||||
|
docs.write_text("This request can't cause any RPC error "
|
||||||
|
"as far as we know.")
|
||||||
|
else:
|
||||||
|
docs.write_text(
|
||||||
|
'This request can cause {} known error{}:'.format(
|
||||||
|
len(errors), '' if len(errors) == 1 else 's'
|
||||||
|
))
|
||||||
|
docs.begin_table(column_count=2)
|
||||||
|
for error in errors:
|
||||||
|
docs.add_row('<code>{}</code>'.format(error.name))
|
||||||
|
docs.add_row('{}.'.format(error.description))
|
||||||
|
docs.end_table()
|
||||||
|
docs.write_text('You can import these from '
|
||||||
|
'<code>telethon.errors</code>.')
|
||||||
|
|
||||||
# TODO Bit hacky, make everything like this? (prepending '../')
|
# TODO Bit hacky, make everything like this? (prepending '../')
|
||||||
depth = '../' * (2 if tlobject.namespace else 1)
|
depth = '../' * (2 if tlobject.namespace else 1)
|
||||||
docs.add_script(src='prependPath = "{}";'.format(depth))
|
docs.add_script(src='prependPath = "{}";'.format(depth))
|
||||||
|
@ -396,55 +371,54 @@ def generate_documentation(scheme_file):
|
||||||
|
|
||||||
# Find all the available types (which are not the same as the constructors)
|
# Find all the available types (which are not the same as the constructors)
|
||||||
# Each type has a list of constructors associated to it, hence is a map
|
# Each type has a list of constructors associated to it, hence is a map
|
||||||
print('Generating types documentation...')
|
for t, cs in type_to_constructors.items():
|
||||||
for tltype, constructors in tltypes.items():
|
filename = path_for_type(t)
|
||||||
filename = get_path_for_type(tltype)
|
|
||||||
out_dir = os.path.dirname(filename)
|
out_dir = os.path.dirname(filename)
|
||||||
if out_dir:
|
if out_dir:
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
# Since we don't have access to the full TLObject, split the type
|
# Since we don't have access to the full TLObject, split the type
|
||||||
if '.' in tltype:
|
if '.' in t:
|
||||||
namespace, name = tltype.split('.')
|
namespace, name = t.split('.')
|
||||||
else:
|
else:
|
||||||
namespace, name = None, tltype
|
namespace, name = None, t
|
||||||
|
|
||||||
# Determine the relative paths for this file
|
paths = {k: _get_relative_path(v, out_dir, folder=True)
|
||||||
paths = get_relative_paths(original_paths, relative_to=out_dir)
|
for k, v in original_paths.items()}
|
||||||
|
|
||||||
with DocsWriter(filename, type_to_path_function=get_path_for_type) \
|
with DocsWriter(filename, type_to_path=path_for_type) as docs:
|
||||||
as docs:
|
|
||||||
docs.write_head(
|
docs.write_head(
|
||||||
title=get_class_name(name),
|
title=snake_to_camel_case(name),
|
||||||
relative_css_path=paths['css'])
|
relative_css_path=paths['css'])
|
||||||
|
|
||||||
docs.set_menu_separator(paths['arrow'])
|
docs.set_menu_separator(paths['arrow'])
|
||||||
build_menu(docs, filename, relative_main_index=paths['index_all'])
|
_build_menu(docs, filename, output_dir,
|
||||||
|
relative_main_index=paths['index_all'])
|
||||||
|
|
||||||
# Main file title
|
# Main file title
|
||||||
docs.write_title(get_class_name(name))
|
docs.write_title(snake_to_camel_case(name))
|
||||||
|
|
||||||
# List available constructors for this type
|
# List available constructors for this type
|
||||||
docs.write_title('Available constructors', level=3)
|
docs.write_title('Available constructors', level=3)
|
||||||
if not constructors:
|
if not cs:
|
||||||
docs.write_text('This type has no constructors available.')
|
docs.write_text('This type has no constructors available.')
|
||||||
elif len(constructors) == 1:
|
elif len(cs) == 1:
|
||||||
docs.write_text('This type has one constructor available.')
|
docs.write_text('This type has one constructor available.')
|
||||||
else:
|
else:
|
||||||
docs.write_text('This type has %d constructors available.' %
|
docs.write_text('This type has %d constructors available.' %
|
||||||
len(constructors))
|
len(cs))
|
||||||
|
|
||||||
docs.begin_table(2)
|
docs.begin_table(2)
|
||||||
for constructor in constructors:
|
for constructor in cs:
|
||||||
# Constructor full name
|
# Constructor full name
|
||||||
link = get_create_path_for(constructor)
|
link = create_path_for(constructor)
|
||||||
link = get_relative_path(link, relative_to=filename)
|
link = _get_relative_path(link, relative_to=filename)
|
||||||
docs.add_row(get_class_name(constructor), link=link)
|
docs.add_row(constructor.class_name, link=link)
|
||||||
docs.end_table()
|
docs.end_table()
|
||||||
|
|
||||||
# List all the methods which return this type
|
# List all the methods which return this type
|
||||||
docs.write_title('Methods returning this type', level=3)
|
docs.write_title('Methods returning this type', level=3)
|
||||||
functions = tlfunctions.get(tltype, [])
|
functions = type_to_functions.get(t, [])
|
||||||
if not functions:
|
if not functions:
|
||||||
docs.write_text('No method returns this type.')
|
docs.write_text('No method returns this type.')
|
||||||
elif len(functions) == 1:
|
elif len(functions) == 1:
|
||||||
|
@ -457,16 +431,16 @@ def generate_documentation(scheme_file):
|
||||||
|
|
||||||
docs.begin_table(2)
|
docs.begin_table(2)
|
||||||
for func in functions:
|
for func in functions:
|
||||||
link = get_create_path_for(func)
|
link = create_path_for(func)
|
||||||
link = get_relative_path(link, relative_to=filename)
|
link = _get_relative_path(link, relative_to=filename)
|
||||||
docs.add_row(get_class_name(func), link=link)
|
docs.add_row(func.class_name, link=link)
|
||||||
docs.end_table()
|
docs.end_table()
|
||||||
|
|
||||||
# List all the methods which take this type as input
|
# List all the methods which take this type as input
|
||||||
docs.write_title('Methods accepting this type as input', level=3)
|
docs.write_title('Methods accepting this type as input', level=3)
|
||||||
other_methods = sorted(
|
other_methods = sorted(
|
||||||
(t for t in tlobjects
|
(t for t in tlobjects
|
||||||
if any(tltype == a.type for a in t.args) and t.is_function),
|
if any(t == a.type for a in t.args) and t.is_function),
|
||||||
key=lambda t: t.name
|
key=lambda t: t.name
|
||||||
)
|
)
|
||||||
if not other_methods:
|
if not other_methods:
|
||||||
|
@ -482,16 +456,16 @@ def generate_documentation(scheme_file):
|
||||||
|
|
||||||
docs.begin_table(2)
|
docs.begin_table(2)
|
||||||
for ot in other_methods:
|
for ot in other_methods:
|
||||||
link = get_create_path_for(ot)
|
link = create_path_for(ot)
|
||||||
link = get_relative_path(link, relative_to=filename)
|
link = _get_relative_path(link, relative_to=filename)
|
||||||
docs.add_row(get_class_name(ot), link=link)
|
docs.add_row(ot.class_name, link=link)
|
||||||
docs.end_table()
|
docs.end_table()
|
||||||
|
|
||||||
# List every other type which has this type as a member
|
# List every other type which has this type as a member
|
||||||
docs.write_title('Other types containing this type', level=3)
|
docs.write_title('Other types containing this type', level=3)
|
||||||
other_types = sorted(
|
other_types = sorted(
|
||||||
(t for t in tlobjects
|
(t for t in tlobjects
|
||||||
if any(tltype == a.type for a in t.args)
|
if any(t == a.type for a in t.args)
|
||||||
and not t.is_function
|
and not t.is_function
|
||||||
), key=lambda t: t.name
|
), key=lambda t: t.name
|
||||||
)
|
)
|
||||||
|
@ -509,9 +483,9 @@ def generate_documentation(scheme_file):
|
||||||
|
|
||||||
docs.begin_table(2)
|
docs.begin_table(2)
|
||||||
for ot in other_types:
|
for ot in other_types:
|
||||||
link = get_create_path_for(ot)
|
link = create_path_for(ot)
|
||||||
link = get_relative_path(link, relative_to=filename)
|
link = _get_relative_path(link, relative_to=filename)
|
||||||
docs.add_row(get_class_name(ot), link=link)
|
docs.add_row(ot.class_name, link=link)
|
||||||
docs.end_table()
|
docs.end_table()
|
||||||
docs.end_body()
|
docs.end_body()
|
||||||
|
|
||||||
|
@ -519,22 +493,21 @@ def generate_documentation(scheme_file):
|
||||||
# This will be done automatically and not taking into account any extra
|
# This will be done automatically and not taking into account any extra
|
||||||
# information that we have available, simply a file listing all the others
|
# information that we have available, simply a file listing all the others
|
||||||
# accessible by clicking on their title
|
# accessible by clicking on their title
|
||||||
print('Generating indices...')
|
|
||||||
for folder in ['types', 'methods', 'constructors']:
|
for folder in ['types', 'methods', 'constructors']:
|
||||||
generate_index(folder, original_paths)
|
_generate_index(os.path.join(output_dir, folder), original_paths,
|
||||||
|
output_dir)
|
||||||
|
|
||||||
# Write the final core index, the main index for the rest of files
|
# Write the final core index, the main index for the rest of files
|
||||||
layer = TLParser.find_layer(scheme_file)
|
|
||||||
types = set()
|
types = set()
|
||||||
methods = []
|
methods = []
|
||||||
constructors = []
|
cs = []
|
||||||
for tlobject in tlobjects:
|
for tlobject in tlobjects:
|
||||||
if tlobject.is_function:
|
if tlobject.is_function:
|
||||||
methods.append(tlobject)
|
methods.append(tlobject)
|
||||||
else:
|
else:
|
||||||
constructors.append(tlobject)
|
cs.append(tlobject)
|
||||||
|
|
||||||
if not is_core_type(tlobject.result):
|
if not tlobject.result.lower() in CORE_TYPES:
|
||||||
if re.search('^vector<', tlobject.result, re.IGNORECASE):
|
if re.search('^vector<', tlobject.result, re.IGNORECASE):
|
||||||
types.add(tlobject.result.split('<')[1].strip('>'))
|
types.add(tlobject.result.split('<')[1].strip('>'))
|
||||||
else:
|
else:
|
||||||
|
@ -542,41 +515,43 @@ def generate_documentation(scheme_file):
|
||||||
|
|
||||||
types = sorted(types)
|
types = sorted(types)
|
||||||
methods = sorted(methods, key=lambda m: m.name)
|
methods = sorted(methods, key=lambda m: m.name)
|
||||||
constructors = sorted(constructors, key=lambda c: c.name)
|
cs = sorted(cs, key=lambda c: c.name)
|
||||||
|
|
||||||
def fmt(xs):
|
shutil.copy(os.path.join(input_res, '404.html'), original_paths['404'])
|
||||||
ys = {x: get_class_name(x) for x in xs} # cache TLObject: display
|
_copy_replace(os.path.join(input_res, 'core.html'),
|
||||||
zs = {} # create a dict to hold those which have duplicated keys
|
original_paths['index_all'], {
|
||||||
for y in ys.values():
|
|
||||||
zs[y] = y in zs
|
|
||||||
return ', '.join(
|
|
||||||
'"{}.{}"'.format(x.namespace, ys[x])
|
|
||||||
if zs[ys[x]] and getattr(x, 'namespace', None)
|
|
||||||
else '"{}"'.format(ys[x]) for x in xs
|
|
||||||
)
|
|
||||||
|
|
||||||
request_names = fmt(methods)
|
|
||||||
type_names = fmt(types)
|
|
||||||
constructor_names = fmt(constructors)
|
|
||||||
|
|
||||||
def fmt(xs, formatter):
|
|
||||||
return ', '.join('"{}"'.format(formatter(x)) for x in xs)
|
|
||||||
|
|
||||||
request_urls = fmt(methods, get_create_path_for)
|
|
||||||
type_urls = fmt(types, get_path_for_type)
|
|
||||||
constructor_urls = fmt(constructors, get_create_path_for)
|
|
||||||
|
|
||||||
shutil.copy('../res/404.html', original_paths['404'])
|
|
||||||
copy_replace('../res/core.html', original_paths['index_all'], {
|
|
||||||
'{type_count}': len(types),
|
'{type_count}': len(types),
|
||||||
'{method_count}': len(methods),
|
'{method_count}': len(methods),
|
||||||
'{constructor_count}': len(tlobjects) - len(methods),
|
'{constructor_count}': len(tlobjects) - len(methods),
|
||||||
'{layer}': layer,
|
'{layer}': layer,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def fmt(xs):
|
||||||
|
zs = {} # create a dict to hold those which have duplicated keys
|
||||||
|
for x in xs:
|
||||||
|
zs[x.class_name] = x.class_name in zs
|
||||||
|
return ', '.join(
|
||||||
|
'"{}.{}"'.format(x.namespace, x.class_name)
|
||||||
|
if zs[x.class_name] and x.namespace
|
||||||
|
else '"{}"'.format(x.class_name) for x in xs
|
||||||
|
)
|
||||||
|
|
||||||
|
request_names = fmt(methods)
|
||||||
|
constructor_names = fmt(cs)
|
||||||
|
|
||||||
|
def fmt(xs, formatter):
|
||||||
|
return ', '.join('"{}"'.format(formatter(x)) for x in xs)
|
||||||
|
|
||||||
|
type_names = fmt(types, formatter=lambda x: x)
|
||||||
|
request_urls = fmt(methods, create_path_for)
|
||||||
|
type_urls = fmt(types, path_for_type)
|
||||||
|
constructor_urls = fmt(cs, create_path_for)
|
||||||
|
|
||||||
os.makedirs(os.path.abspath(os.path.join(
|
os.makedirs(os.path.abspath(os.path.join(
|
||||||
original_paths['search.js'], os.path.pardir
|
original_paths['search.js'], os.path.pardir
|
||||||
)), exist_ok=True)
|
)), exist_ok=True)
|
||||||
copy_replace('../res/js/search.js', original_paths['search.js'], {
|
_copy_replace(os.path.join(input_res, 'js', 'search.js'),
|
||||||
|
original_paths['search.js'], {
|
||||||
'{request_names}': request_names,
|
'{request_names}': request_names,
|
||||||
'{type_names}': type_names,
|
'{type_names}': type_names,
|
||||||
'{constructor_names}': constructor_names,
|
'{constructor_names}': constructor_names,
|
||||||
|
@ -585,23 +560,16 @@ def generate_documentation(scheme_file):
|
||||||
'{constructor_urls}': constructor_urls
|
'{constructor_urls}': constructor_urls
|
||||||
})
|
})
|
||||||
|
|
||||||
# Everything done
|
|
||||||
print('Documentation generated.')
|
def _copy_resources(res_dir, out_dir):
|
||||||
|
for dirname, files in [('css', ['docs.css']), ('img', ['arrow.svg'])]:
|
||||||
|
dirpath = os.path.join(out_dir, dirname)
|
||||||
|
os.makedirs(dirpath, exist_ok=True)
|
||||||
|
for file in files:
|
||||||
|
shutil.copy(os.path.join(res_dir, dirname, file), dirpath)
|
||||||
|
|
||||||
|
|
||||||
def copy_resources():
|
def generate_docs(tlobjects, errors, layer, input_res, output_dir):
|
||||||
for d in ('css', 'img'):
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
os.makedirs(d, exist_ok=True)
|
_write_html_pages(tlobjects, errors, layer, input_res, output_dir)
|
||||||
|
_copy_resources(input_res, output_dir)
|
||||||
shutil.copy('../res/img/arrow.svg', 'img')
|
|
||||||
shutil.copy('../res/css/docs.css', 'css')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
os.makedirs('generated', exist_ok=True)
|
|
||||||
os.chdir('generated')
|
|
||||||
try:
|
|
||||||
generate_documentation('../../telethon_generator/scheme.tl')
|
|
||||||
copy_resources()
|
|
||||||
finally:
|
|
||||||
os.chdir(os.pardir)
|
|
52
telethon_generator/generators/errors.py
Normal file
52
telethon_generator/generators/errors.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
|
||||||
|
def generate_errors(errors, f):
|
||||||
|
# Exact/regex match to create {CODE: ErrorClassName}
|
||||||
|
exact_match = []
|
||||||
|
regex_match = []
|
||||||
|
|
||||||
|
# Find out what subclasses to import and which to create
|
||||||
|
import_base, create_base = set(), {}
|
||||||
|
for error in errors:
|
||||||
|
if error.subclass_exists:
|
||||||
|
import_base.add(error.subclass)
|
||||||
|
else:
|
||||||
|
create_base[error.subclass] = error.int_code
|
||||||
|
|
||||||
|
if error.has_captures:
|
||||||
|
regex_match.append(error)
|
||||||
|
else:
|
||||||
|
exact_match.append(error)
|
||||||
|
|
||||||
|
# Imports and new subclass creation
|
||||||
|
f.write('from .rpc_base_errors import RPCError, {}\n'
|
||||||
|
.format(", ".join(sorted(import_base))))
|
||||||
|
|
||||||
|
for cls, int_code in sorted(create_base.items(), key=lambda t: t[1]):
|
||||||
|
f.write('\n\nclass {}(RPCError):\n code = {}\n'
|
||||||
|
.format(cls, int_code))
|
||||||
|
|
||||||
|
# Error classes generation
|
||||||
|
for error in errors:
|
||||||
|
f.write('\n\nclass {}({}):\n def __init__(self, **kwargs):\n'
|
||||||
|
' '.format(error.name, error.subclass))
|
||||||
|
|
||||||
|
if error.has_captures:
|
||||||
|
f.write("self.{} = int(kwargs.get('capture', 0))\n "
|
||||||
|
.format(error.capture_name))
|
||||||
|
|
||||||
|
f.write('super(Exception, self).__init__({}'
|
||||||
|
.format(repr(error.description)))
|
||||||
|
|
||||||
|
if error.has_captures:
|
||||||
|
f.write('.format(self.{})'.format(error.capture_name))
|
||||||
|
|
||||||
|
f.write(')\n')
|
||||||
|
|
||||||
|
# Create the actual {CODE: ErrorClassName} dict once classes are defined
|
||||||
|
# TODO Actually make a difference between regex/exact
|
||||||
|
f.write('\n\nrpc_errors_all = {\n')
|
||||||
|
for error in itertools.chain(regex_match, exact_match):
|
||||||
|
f.write(' {}: {},\n'.format(repr(error.pattern), error.name))
|
||||||
|
f.write('}\n')
|
660
telethon_generator/generators/tlobject.py
Normal file
660
telethon_generator/generators/tlobject.py
Normal file
|
@ -0,0 +1,660 @@
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
from collections import defaultdict
|
||||||
|
from zlib import crc32
|
||||||
|
|
||||||
|
from ..source_builder import SourceBuilder
|
||||||
|
from ..utils import snake_to_camel_case
|
||||||
|
|
||||||
|
AUTO_GEN_NOTICE = \
|
||||||
|
'"""File generated by TLObjects\' generator. All changes will be ERASED"""'
|
||||||
|
|
||||||
|
|
||||||
|
AUTO_CASTS = {
|
||||||
|
'InputPeer': 'utils.get_input_peer(client.get_input_entity({}))',
|
||||||
|
'InputChannel': 'utils.get_input_channel(client.get_input_entity({}))',
|
||||||
|
'InputUser': 'utils.get_input_user(client.get_input_entity({}))',
|
||||||
|
'InputMedia': 'utils.get_input_media({})',
|
||||||
|
'InputPhoto': 'utils.get_input_photo({})'
|
||||||
|
}
|
||||||
|
|
||||||
|
BASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128',
|
||||||
|
'int256', 'double', 'Bool', 'true', 'date')
|
||||||
|
|
||||||
|
|
||||||
|
def _write_modules(out_dir, depth, namespace_tlobjects, type_constructors):
|
||||||
|
# namespace_tlobjects: {'namespace', [TLObject]}
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
for ns, tlobjects in namespace_tlobjects.items():
|
||||||
|
file = os.path.join(out_dir, '{}.py'.format(ns or '__init__'))
|
||||||
|
with open(file, 'w', encoding='utf-8') as f,\
|
||||||
|
SourceBuilder(f) as builder:
|
||||||
|
builder.writeln(AUTO_GEN_NOTICE)
|
||||||
|
|
||||||
|
builder.writeln('from {}.tl.tlobject import TLObject', '.' * depth)
|
||||||
|
builder.writeln('from typing import Optional, List, '
|
||||||
|
'Union, TYPE_CHECKING')
|
||||||
|
|
||||||
|
# Add the relative imports to the namespaces,
|
||||||
|
# unless we already are in a namespace.
|
||||||
|
if not ns:
|
||||||
|
builder.writeln('from . import {}', ', '.join(
|
||||||
|
x for x in namespace_tlobjects.keys() if x
|
||||||
|
))
|
||||||
|
|
||||||
|
# Import 'os' for those needing access to 'os.urandom()'
|
||||||
|
# Currently only 'random_id' needs 'os' to be imported,
|
||||||
|
# for all those TLObjects with arg.can_be_inferred.
|
||||||
|
builder.writeln('import os')
|
||||||
|
|
||||||
|
# Import struct for the .__bytes__(self) serialization
|
||||||
|
builder.writeln('import struct')
|
||||||
|
|
||||||
|
tlobjects.sort(key=lambda x: x.name)
|
||||||
|
|
||||||
|
type_names = set()
|
||||||
|
type_defs = []
|
||||||
|
|
||||||
|
# Find all the types in this file and generate type definitions
|
||||||
|
# based on the types. The type definitions are written to the
|
||||||
|
# file at the end.
|
||||||
|
for t in tlobjects:
|
||||||
|
if not t.is_function:
|
||||||
|
type_name = t.result
|
||||||
|
if '.' in type_name:
|
||||||
|
type_name = type_name[type_name.rindex('.'):]
|
||||||
|
if type_name in type_names:
|
||||||
|
continue
|
||||||
|
type_names.add(type_name)
|
||||||
|
constructors = type_constructors[type_name]
|
||||||
|
if not constructors:
|
||||||
|
pass
|
||||||
|
elif len(constructors) == 1:
|
||||||
|
type_defs.append('Type{} = {}'.format(
|
||||||
|
type_name, constructors[0].class_name))
|
||||||
|
else:
|
||||||
|
type_defs.append('Type{} = Union[{}]'.format(
|
||||||
|
type_name, ','.join(c.class_name
|
||||||
|
for c in constructors)))
|
||||||
|
|
||||||
|
imports = {}
|
||||||
|
primitives = ('int', 'long', 'int128', 'int256', 'string',
|
||||||
|
'date', 'bytes', 'true')
|
||||||
|
# Find all the types in other files that are used in this file
|
||||||
|
# and generate the information required to import those types.
|
||||||
|
for t in tlobjects:
|
||||||
|
for arg in t.args:
|
||||||
|
name = arg.type
|
||||||
|
if not name or name in primitives:
|
||||||
|
continue
|
||||||
|
|
||||||
|
import_space = '{}.tl.types'.format('.' * depth)
|
||||||
|
if '.' in name:
|
||||||
|
namespace = name.split('.')[0]
|
||||||
|
name = name.split('.')[1]
|
||||||
|
import_space += '.{}'.format(namespace)
|
||||||
|
|
||||||
|
if name not in type_names:
|
||||||
|
type_names.add(name)
|
||||||
|
if name == 'date':
|
||||||
|
imports['datetime'] = ['datetime']
|
||||||
|
continue
|
||||||
|
elif import_space not in imports:
|
||||||
|
imports[import_space] = set()
|
||||||
|
imports[import_space].add('Type{}'.format(name))
|
||||||
|
|
||||||
|
# Add imports required for type checking
|
||||||
|
if imports:
|
||||||
|
builder.writeln('if TYPE_CHECKING:')
|
||||||
|
for namespace, names in imports.items():
|
||||||
|
builder.writeln('from {} import {}',
|
||||||
|
namespace, ', '.join(names))
|
||||||
|
|
||||||
|
builder.end_block()
|
||||||
|
|
||||||
|
# Generate the class for every TLObject
|
||||||
|
for t in tlobjects:
|
||||||
|
_write_source_code(t, builder, type_constructors)
|
||||||
|
builder.current_indent = 0
|
||||||
|
|
||||||
|
# Write the type definitions generated earlier.
|
||||||
|
builder.writeln('')
|
||||||
|
for line in type_defs:
|
||||||
|
builder.writeln(line)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_source_code(tlobject, builder, type_constructors):
|
||||||
|
"""
|
||||||
|
Writes the source code corresponding to the given TLObject
|
||||||
|
by making use of the ``builder`` `SourceBuilder`.
|
||||||
|
|
||||||
|
Additional information such as file path depth and
|
||||||
|
the ``Type: [Constructors]`` must be given for proper
|
||||||
|
importing and documentation strings.
|
||||||
|
"""
|
||||||
|
_write_class_init(tlobject, type_constructors, builder)
|
||||||
|
_write_resolve(tlobject, builder)
|
||||||
|
_write_to_dict(tlobject, builder)
|
||||||
|
_write_to_bytes(tlobject, builder)
|
||||||
|
_write_from_reader(tlobject, builder)
|
||||||
|
_write_on_response(tlobject, builder)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_class_init(tlobject, type_constructors, builder):
|
||||||
|
builder.writeln()
|
||||||
|
builder.writeln()
|
||||||
|
builder.writeln('class {}(TLObject):', tlobject.class_name)
|
||||||
|
|
||||||
|
# Class-level variable to store its Telegram's constructor ID
|
||||||
|
builder.writeln('CONSTRUCTOR_ID = {:#x}', tlobject.id)
|
||||||
|
builder.writeln('SUBCLASS_OF_ID = {:#x}',
|
||||||
|
crc32(tlobject.result.encode('ascii')))
|
||||||
|
builder.writeln()
|
||||||
|
|
||||||
|
# Convert the args to string parameters, flags having =None
|
||||||
|
args = [(a.name if not a.is_flag and not a.can_be_inferred
|
||||||
|
else '{}=None'.format(a.name)) for a in tlobject.real_args]
|
||||||
|
|
||||||
|
# Write the __init__ function
|
||||||
|
builder.writeln('def __init__({}):', ', '.join(['self'] + args))
|
||||||
|
if tlobject.real_args:
|
||||||
|
# Write the docstring, to know the type of the args
|
||||||
|
builder.writeln('"""')
|
||||||
|
for arg in tlobject.real_args:
|
||||||
|
if not arg.flag_indicator:
|
||||||
|
builder.writeln(':param {} {}:', arg.type_hint(), arg.name)
|
||||||
|
builder.current_indent -= 1 # It will auto-indent (':')
|
||||||
|
|
||||||
|
# We also want to know what type this request returns
|
||||||
|
# or to which type this constructor belongs to
|
||||||
|
builder.writeln()
|
||||||
|
if tlobject.is_function:
|
||||||
|
builder.write(':returns {}: ', tlobject.result)
|
||||||
|
else:
|
||||||
|
builder.write('Constructor for {}: ', tlobject.result)
|
||||||
|
|
||||||
|
constructors = type_constructors[tlobject.result]
|
||||||
|
if not constructors:
|
||||||
|
builder.writeln('This type has no constructors.')
|
||||||
|
elif len(constructors) == 1:
|
||||||
|
builder.writeln('Instance of {}.',
|
||||||
|
constructors[0].class_name)
|
||||||
|
else:
|
||||||
|
builder.writeln('Instance of either {}.', ', '.join(
|
||||||
|
c.class_name for c in constructors))
|
||||||
|
|
||||||
|
builder.writeln('"""')
|
||||||
|
|
||||||
|
builder.writeln('super().__init__()')
|
||||||
|
# Functions have a result object and are confirmed by default
|
||||||
|
if tlobject.is_function:
|
||||||
|
builder.writeln('self.result = None')
|
||||||
|
builder.writeln('self.content_related = True')
|
||||||
|
|
||||||
|
# Set the arguments
|
||||||
|
if tlobject.real_args:
|
||||||
|
builder.writeln()
|
||||||
|
|
||||||
|
for arg in tlobject.real_args:
|
||||||
|
if not arg.can_be_inferred:
|
||||||
|
builder.writeln('self.{0} = {0} # type: {1}',
|
||||||
|
arg.name, arg.type_hint())
|
||||||
|
|
||||||
|
# Currently the only argument that can be
|
||||||
|
# inferred are those called 'random_id'
|
||||||
|
elif arg.name == 'random_id':
|
||||||
|
# Endianness doesn't really matter, and 'big' is shorter
|
||||||
|
code = "int.from_bytes(os.urandom({}), 'big', signed=True)" \
|
||||||
|
.format(8 if arg.type == 'long' else 4)
|
||||||
|
|
||||||
|
if arg.is_vector:
|
||||||
|
# Currently for the case of "messages.forwardMessages"
|
||||||
|
# Ensure we can infer the length from id:Vector<>
|
||||||
|
if not next(a for a in tlobject.real_args
|
||||||
|
if a.name == 'id').is_vector:
|
||||||
|
raise ValueError(
|
||||||
|
'Cannot infer list of random ids for ', tlobject
|
||||||
|
)
|
||||||
|
code = '[{} for _ in range(len(id))]'.format(code)
|
||||||
|
|
||||||
|
builder.writeln(
|
||||||
|
"self.random_id = random_id if random_id "
|
||||||
|
"is not None else {}", code
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError('Cannot infer a value for ', arg)
|
||||||
|
|
||||||
|
builder.end_block()
|
||||||
|
|
||||||
|
|
||||||
|
def _write_resolve(tlobject, builder):
|
||||||
|
if any(arg.type in AUTO_CASTS for arg in tlobject.real_args):
|
||||||
|
builder.writeln('def resolve(self, client, utils):')
|
||||||
|
for arg in tlobject.real_args:
|
||||||
|
ac = AUTO_CASTS.get(arg.type, None)
|
||||||
|
if not ac:
|
||||||
|
continue
|
||||||
|
if arg.is_vector:
|
||||||
|
builder.write('self.{0} = [{1} for _x in self.{0}]',
|
||||||
|
arg.name, ac.format('_x'))
|
||||||
|
else:
|
||||||
|
builder.write('self.{} = {}', arg.name,
|
||||||
|
ac.format('self.' + arg.name))
|
||||||
|
builder.writeln(' if self.{} else None'.format(arg.name)
|
||||||
|
if arg.is_flag else '')
|
||||||
|
builder.end_block()
|
||||||
|
|
||||||
|
|
||||||
|
def _write_to_dict(tlobject, builder):
|
||||||
|
builder.writeln('def to_dict(self):')
|
||||||
|
builder.writeln('return {')
|
||||||
|
builder.current_indent += 1
|
||||||
|
|
||||||
|
builder.write("'_': '{}'", tlobject.class_name)
|
||||||
|
for arg in tlobject.real_args:
|
||||||
|
builder.writeln(',')
|
||||||
|
builder.write("'{}': ", arg.name)
|
||||||
|
if arg.type in BASE_TYPES:
|
||||||
|
if arg.is_vector:
|
||||||
|
builder.write('[] if self.{0} is None else self.{0}[:]',
|
||||||
|
arg.name)
|
||||||
|
else:
|
||||||
|
builder.write('self.{}', arg.name)
|
||||||
|
else:
|
||||||
|
if arg.is_vector:
|
||||||
|
builder.write(
|
||||||
|
'[] if self.{0} is None else [None '
|
||||||
|
'if x is None else x.to_dict() for x in self.{0}]',
|
||||||
|
arg.name
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
builder.write(
|
||||||
|
'None if self.{0} is None else self.{0}.to_dict()',
|
||||||
|
arg.name
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.writeln()
|
||||||
|
builder.current_indent -= 1
|
||||||
|
builder.writeln("}")
|
||||||
|
|
||||||
|
builder.end_block()
|
||||||
|
|
||||||
|
|
||||||
|
def _write_to_bytes(tlobject, builder):
|
||||||
|
builder.writeln('def __bytes__(self):')
|
||||||
|
|
||||||
|
# Some objects require more than one flag parameter to be set
|
||||||
|
# at the same time. In this case, add an assertion.
|
||||||
|
repeated_args = defaultdict(list)
|
||||||
|
for arg in tlobject.args:
|
||||||
|
if arg.is_flag:
|
||||||
|
repeated_args[arg.flag_index].append(arg)
|
||||||
|
|
||||||
|
for ra in repeated_args.values():
|
||||||
|
if len(ra) > 1:
|
||||||
|
cnd1 = ('(self.{0} or self.{0} is not None)'
|
||||||
|
.format(a.name) for a in ra)
|
||||||
|
cnd2 = ('(self.{0} is None or self.{0} is False)'
|
||||||
|
.format(a.name) for a in ra)
|
||||||
|
builder.writeln(
|
||||||
|
"assert ({}) or ({}), '{} parameters must all "
|
||||||
|
"be False-y (like None) or all me True-y'",
|
||||||
|
' and '.join(cnd1), ' and '.join(cnd2),
|
||||||
|
', '.join(a.name for a in ra)
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.writeln("return b''.join((")
|
||||||
|
builder.current_indent += 1
|
||||||
|
|
||||||
|
# First constructor code, we already know its bytes
|
||||||
|
builder.writeln('{},', repr(struct.pack('<I', tlobject.id)))
|
||||||
|
|
||||||
|
for arg in tlobject.args:
|
||||||
|
if _write_arg_to_bytes(builder, arg, tlobject.args):
|
||||||
|
builder.writeln(',')
|
||||||
|
|
||||||
|
builder.current_indent -= 1
|
||||||
|
builder.writeln('))')
|
||||||
|
builder.end_block()
|
||||||
|
|
||||||
|
|
||||||
|
def _write_from_reader(tlobject, builder):
|
||||||
|
builder.writeln('@classmethod')
|
||||||
|
builder.writeln('def from_reader(cls, reader):')
|
||||||
|
for arg in tlobject.args:
|
||||||
|
_write_arg_read_code(builder, arg, tlobject.args, name='_' + arg.name)
|
||||||
|
|
||||||
|
builder.writeln('return cls({})', ', '.join(
|
||||||
|
'{0}=_{0}'.format(a.name) for a in tlobject.real_args))
|
||||||
|
|
||||||
|
|
||||||
|
def _write_on_response(tlobject, builder):
|
||||||
|
# Only requests can have a different response that's not their
|
||||||
|
# serialized body, that is, we'll be setting their .result.
|
||||||
|
#
|
||||||
|
# The default behaviour is reading a TLObject too, so no need
|
||||||
|
# to override it unless necessary.
|
||||||
|
if not tlobject.is_function:
|
||||||
|
return
|
||||||
|
|
||||||
|
# https://core.telegram.org/mtproto/serialize#boxed-and-bare-types
|
||||||
|
# TL;DR; boxed types start with uppercase always, so we can use
|
||||||
|
# this to check whether everything in it is boxed or not.
|
||||||
|
#
|
||||||
|
# Currently only un-boxed responses are Vector<int>/Vector<long>.
|
||||||
|
# If this weren't the case, we should check upper case after
|
||||||
|
# max(index('<'), index('.')) (and if it is, it's boxed, so return).
|
||||||
|
m = re.match(r'Vector<(int|long)>', tlobject.result)
|
||||||
|
if not m:
|
||||||
|
return
|
||||||
|
|
||||||
|
builder.end_block()
|
||||||
|
builder.writeln('def on_response(self, reader):')
|
||||||
|
builder.writeln('reader.read_int() # Vector ID')
|
||||||
|
builder.writeln('self.result = [reader.read_{}() '
|
||||||
|
'for _ in range(reader.read_int())]', m.group(1))
|
||||||
|
|
||||||
|
|
||||||
|
def _write_arg_to_bytes(builder, arg, args, name=None):
|
||||||
|
"""
|
||||||
|
Writes the .__bytes__() code for the given argument
|
||||||
|
:param builder: The source code builder
|
||||||
|
:param arg: The argument to write
|
||||||
|
:param args: All the other arguments in TLObject same __bytes__.
|
||||||
|
This is required to determine the flags value
|
||||||
|
:param name: The name of the argument. Defaults to "self.argname"
|
||||||
|
This argument is an option because it's required when
|
||||||
|
writing Vectors<>
|
||||||
|
"""
|
||||||
|
if arg.generic_definition:
|
||||||
|
return # Do nothing, this only specifies a later type
|
||||||
|
|
||||||
|
if name is None:
|
||||||
|
name = 'self.{}'.format(arg.name)
|
||||||
|
|
||||||
|
# The argument may be a flag, only write if it's not None AND
|
||||||
|
# if it's not a True type.
|
||||||
|
# True types are not actually sent, but instead only used to
|
||||||
|
# determine the flags.
|
||||||
|
if arg.is_flag:
|
||||||
|
if arg.type == 'true':
|
||||||
|
return # Exit, since True type is never written
|
||||||
|
elif arg.is_vector:
|
||||||
|
# Vector flags are special since they consist of 3 values,
|
||||||
|
# so we need an extra join here. Note that empty vector flags
|
||||||
|
# should NOT be sent either!
|
||||||
|
builder.write("b'' if {0} is None or {0} is False "
|
||||||
|
"else b''.join((", name)
|
||||||
|
else:
|
||||||
|
builder.write("b'' if {0} is None or {0} is False "
|
||||||
|
"else (", name)
|
||||||
|
|
||||||
|
if arg.is_vector:
|
||||||
|
if arg.use_vector_id:
|
||||||
|
# vector code, unsigned 0x1cb5c415 as little endian
|
||||||
|
builder.write(r"b'\x15\xc4\xb5\x1c',")
|
||||||
|
|
||||||
|
builder.write("struct.pack('<i', len({})),", name)
|
||||||
|
|
||||||
|
# Cannot unpack the values for the outer tuple through *[(
|
||||||
|
# since that's a Python >3.5 feature, so add another join.
|
||||||
|
builder.write("b''.join(")
|
||||||
|
|
||||||
|
# Temporary disable .is_vector, not to enter this if again
|
||||||
|
# Also disable .is_flag since it's not needed per element
|
||||||
|
old_flag = arg.is_flag
|
||||||
|
arg.is_vector = arg.is_flag = False
|
||||||
|
_write_arg_to_bytes(builder, arg, args, name='x')
|
||||||
|
arg.is_vector = True
|
||||||
|
arg.is_flag = old_flag
|
||||||
|
|
||||||
|
builder.write(' for x in {})', name)
|
||||||
|
|
||||||
|
elif arg.flag_indicator:
|
||||||
|
# Calculate the flags with those items which are not None
|
||||||
|
if not any(f.is_flag for f in args):
|
||||||
|
# There's a flag indicator, but no flag arguments so it's 0
|
||||||
|
builder.write(r"b'\0\0\0\0'")
|
||||||
|
else:
|
||||||
|
builder.write("struct.pack('<I', ")
|
||||||
|
builder.write(
|
||||||
|
' | '.join('(0 if {0} is None or {0} is False else {1})'
|
||||||
|
.format('self.{}'.format(flag.name),
|
||||||
|
1 << flag.flag_index)
|
||||||
|
for flag in args if flag.is_flag)
|
||||||
|
)
|
||||||
|
builder.write(')')
|
||||||
|
|
||||||
|
elif 'int' == arg.type:
|
||||||
|
# struct.pack is around 4 times faster than int.to_bytes
|
||||||
|
builder.write("struct.pack('<i', {})", name)
|
||||||
|
|
||||||
|
elif 'long' == arg.type:
|
||||||
|
builder.write("struct.pack('<q', {})", name)
|
||||||
|
|
||||||
|
elif 'int128' == arg.type:
|
||||||
|
builder.write("{}.to_bytes(16, 'little', signed=True)", name)
|
||||||
|
|
||||||
|
elif 'int256' == arg.type:
|
||||||
|
builder.write("{}.to_bytes(32, 'little', signed=True)", name)
|
||||||
|
|
||||||
|
elif 'double' == arg.type:
|
||||||
|
builder.write("struct.pack('<d', {})", name)
|
||||||
|
|
||||||
|
elif 'string' == arg.type:
|
||||||
|
builder.write('TLObject.serialize_bytes({})', name)
|
||||||
|
|
||||||
|
elif 'Bool' == arg.type:
|
||||||
|
# 0x997275b5 if boolean else 0xbc799737
|
||||||
|
builder.write(r"b'\xb5ur\x99' if {} else b'7\x97y\xbc'", name)
|
||||||
|
|
||||||
|
elif 'true' == arg.type:
|
||||||
|
pass # These are actually NOT written! Only used for flags
|
||||||
|
|
||||||
|
elif 'bytes' == arg.type:
|
||||||
|
builder.write('TLObject.serialize_bytes({})', name)
|
||||||
|
|
||||||
|
elif 'date' == arg.type: # Custom format
|
||||||
|
builder.write('TLObject.serialize_datetime({})', name)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Else it may be a custom type
|
||||||
|
builder.write('bytes({})', name)
|
||||||
|
|
||||||
|
if arg.is_flag:
|
||||||
|
builder.write(')')
|
||||||
|
if arg.is_vector:
|
||||||
|
builder.write(')') # We were using a tuple
|
||||||
|
|
||||||
|
return True # Something was written
|
||||||
|
|
||||||
|
|
||||||
|
def _write_arg_read_code(builder, arg, args, name):
|
||||||
|
"""
|
||||||
|
Writes the read code for the given argument, setting the
|
||||||
|
arg.name variable to its read value.
|
||||||
|
|
||||||
|
:param builder: The source code builder
|
||||||
|
:param arg: The argument to write
|
||||||
|
:param args: All the other arguments in TLObject same on_send.
|
||||||
|
This is required to determine the flags value
|
||||||
|
:param name: The name of the argument. Defaults to "self.argname"
|
||||||
|
This argument is an option because it's required when
|
||||||
|
writing Vectors<>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if arg.generic_definition:
|
||||||
|
return # Do nothing, this only specifies a later type
|
||||||
|
|
||||||
|
# The argument may be a flag, only write that flag was given!
|
||||||
|
was_flag = False
|
||||||
|
if arg.is_flag:
|
||||||
|
# Treat 'true' flags as a special case, since they're true if
|
||||||
|
# they're set, and nothing else needs to actually be read.
|
||||||
|
if 'true' == arg.type:
|
||||||
|
builder.writeln('{} = bool(flags & {})',
|
||||||
|
name, 1 << arg.flag_index)
|
||||||
|
return
|
||||||
|
|
||||||
|
was_flag = True
|
||||||
|
builder.writeln('if flags & {}:', 1 << arg.flag_index)
|
||||||
|
# Temporary disable .is_flag not to enter this if
|
||||||
|
# again when calling the method recursively
|
||||||
|
arg.is_flag = False
|
||||||
|
|
||||||
|
if arg.is_vector:
|
||||||
|
if arg.use_vector_id:
|
||||||
|
# We have to read the vector's constructor ID
|
||||||
|
builder.writeln("reader.read_int()")
|
||||||
|
|
||||||
|
builder.writeln('{} = []', name)
|
||||||
|
builder.writeln('for _ in range(reader.read_int()):')
|
||||||
|
# Temporary disable .is_vector, not to enter this if again
|
||||||
|
arg.is_vector = False
|
||||||
|
_write_arg_read_code(builder, arg, args, name='_x')
|
||||||
|
builder.writeln('{}.append(_x)', name)
|
||||||
|
arg.is_vector = True
|
||||||
|
|
||||||
|
elif arg.flag_indicator:
|
||||||
|
# Read the flags, which will indicate what items we should read next
|
||||||
|
builder.writeln('flags = reader.read_int()')
|
||||||
|
builder.writeln()
|
||||||
|
|
||||||
|
elif 'int' == arg.type:
|
||||||
|
builder.writeln('{} = reader.read_int()', name)
|
||||||
|
|
||||||
|
elif 'long' == arg.type:
|
||||||
|
builder.writeln('{} = reader.read_long()', name)
|
||||||
|
|
||||||
|
elif 'int128' == arg.type:
|
||||||
|
builder.writeln('{} = reader.read_large_int(bits=128)', name)
|
||||||
|
|
||||||
|
elif 'int256' == arg.type:
|
||||||
|
builder.writeln('{} = reader.read_large_int(bits=256)', name)
|
||||||
|
|
||||||
|
elif 'double' == arg.type:
|
||||||
|
builder.writeln('{} = reader.read_double()', name)
|
||||||
|
|
||||||
|
elif 'string' == arg.type:
|
||||||
|
builder.writeln('{} = reader.tgread_string()', name)
|
||||||
|
|
||||||
|
elif 'Bool' == arg.type:
|
||||||
|
builder.writeln('{} = reader.tgread_bool()', name)
|
||||||
|
|
||||||
|
elif 'true' == arg.type:
|
||||||
|
# Arbitrary not-None value, don't actually read "true" flags
|
||||||
|
builder.writeln('{} = True', name)
|
||||||
|
|
||||||
|
elif 'bytes' == arg.type:
|
||||||
|
builder.writeln('{} = reader.tgread_bytes()', name)
|
||||||
|
|
||||||
|
elif 'date' == arg.type: # Custom format
|
||||||
|
builder.writeln('{} = reader.tgread_date()', name)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Else it may be a custom type
|
||||||
|
if not arg.skip_constructor_id:
|
||||||
|
builder.writeln('{} = reader.tgread_object()', name)
|
||||||
|
else:
|
||||||
|
# Import the correct type inline to avoid cyclic imports.
|
||||||
|
# There may be better solutions so that we can just access
|
||||||
|
# all the types before the files have been parsed, but I
|
||||||
|
# don't know of any.
|
||||||
|
sep_index = arg.type.find('.')
|
||||||
|
if sep_index == -1:
|
||||||
|
ns, t = '.', arg.type
|
||||||
|
else:
|
||||||
|
ns, t = '.' + arg.type[:sep_index], arg.type[sep_index+1:]
|
||||||
|
class_name = snake_to_camel_case(t)
|
||||||
|
|
||||||
|
# There would be no need to import the type if we're in the
|
||||||
|
# file with the same namespace, but since it does no harm
|
||||||
|
# and we don't have information about such thing in the
|
||||||
|
# method we just ignore that case.
|
||||||
|
builder.writeln('from {} import {}', ns, class_name)
|
||||||
|
builder.writeln('{} = {}.from_reader(reader)',
|
||||||
|
name, class_name)
|
||||||
|
|
||||||
|
# End vector and flag blocks if required (if we opened them before)
|
||||||
|
if arg.is_vector:
|
||||||
|
builder.end_block()
|
||||||
|
|
||||||
|
if was_flag:
|
||||||
|
builder.current_indent -= 1
|
||||||
|
builder.writeln('else:')
|
||||||
|
builder.writeln('{} = None', name)
|
||||||
|
builder.current_indent -= 1
|
||||||
|
# Restore .is_flag
|
||||||
|
arg.is_flag = True
|
||||||
|
|
||||||
|
|
||||||
|
def _write_all_tlobjects(tlobjects, layer, builder):
|
||||||
|
builder.writeln(AUTO_GEN_NOTICE)
|
||||||
|
builder.writeln()
|
||||||
|
|
||||||
|
builder.writeln('from . import types, functions')
|
||||||
|
builder.writeln()
|
||||||
|
|
||||||
|
# Create a constant variable to indicate which layer this is
|
||||||
|
builder.writeln('LAYER = {}', layer)
|
||||||
|
builder.writeln()
|
||||||
|
|
||||||
|
# Then create the dictionary containing constructor_id: class
|
||||||
|
builder.writeln('tlobjects = {')
|
||||||
|
builder.current_indent += 1
|
||||||
|
|
||||||
|
# Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class)
|
||||||
|
for tlobject in tlobjects:
|
||||||
|
builder.write('{:#010x}: ', tlobject.id)
|
||||||
|
builder.write('functions' if tlobject.is_function else 'types')
|
||||||
|
if tlobject.namespace:
|
||||||
|
builder.write('.' + tlobject.namespace)
|
||||||
|
|
||||||
|
builder.writeln('.{},', tlobject.class_name)
|
||||||
|
|
||||||
|
builder.current_indent -= 1
|
||||||
|
builder.writeln('}')
|
||||||
|
|
||||||
|
|
||||||
|
def generate_tlobjects(tlobjects, layer, import_depth, output_dir):
|
||||||
|
get_file = functools.partial(os.path.join, output_dir)
|
||||||
|
os.makedirs(get_file('functions'), exist_ok=True)
|
||||||
|
os.makedirs(get_file('types'), exist_ok=True)
|
||||||
|
|
||||||
|
# Group everything by {namespace: [tlobjects]} to generate __init__.py
|
||||||
|
namespace_functions = defaultdict(list)
|
||||||
|
namespace_types = defaultdict(list)
|
||||||
|
|
||||||
|
# Group {type: [constructors]} to generate the documentation
|
||||||
|
type_constructors = defaultdict(list)
|
||||||
|
for tlobject in tlobjects:
|
||||||
|
if tlobject.is_function:
|
||||||
|
namespace_functions[tlobject.namespace].append(tlobject)
|
||||||
|
else:
|
||||||
|
namespace_types[tlobject.namespace].append(tlobject)
|
||||||
|
type_constructors[tlobject.result].append(tlobject)
|
||||||
|
|
||||||
|
_write_modules(get_file('functions'), import_depth,
|
||||||
|
namespace_functions, type_constructors)
|
||||||
|
_write_modules(get_file('types'), import_depth,
|
||||||
|
namespace_types, type_constructors)
|
||||||
|
|
||||||
|
filename = os.path.join(get_file('all_tlobjects.py'))
|
||||||
|
with open(filename, 'w', encoding='utf-8') as file:
|
||||||
|
with SourceBuilder(file) as builder:
|
||||||
|
_write_all_tlobjects(tlobjects, layer, builder)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_tlobjects(output_dir):
|
||||||
|
get_file = functools.partial(os.path.join, output_dir)
|
||||||
|
for d in ('functions', 'types'):
|
||||||
|
d = get_file(d)
|
||||||
|
if os.path.isdir(d):
|
||||||
|
shutil.rmtree(d)
|
||||||
|
|
||||||
|
tl = get_file('all_tlobjects.py')
|
||||||
|
if os.path.isfile(tl):
|
||||||
|
os.remove(tl)
|
|
@ -1,3 +0,0 @@
|
||||||
from .source_builder import SourceBuilder
|
|
||||||
from .tl_parser import TLParser
|
|
||||||
from .tl_object import TLObject
|
|
|
@ -1,323 +0,0 @@
|
||||||
import re
|
|
||||||
from zlib import crc32
|
|
||||||
|
|
||||||
|
|
||||||
class TLObject:
|
|
||||||
""".tl core types IDs (such as vector, booleans, etc.)"""
|
|
||||||
CORE_TYPES = (
|
|
||||||
0xbc799737, # boolFalse#bc799737 = Bool;
|
|
||||||
0x997275b5, # boolTrue#997275b5 = Bool;
|
|
||||||
0x3fedd339, # true#3fedd339 = True;
|
|
||||||
0x1cb5c415, # vector#1cb5c415 {t:Type} # [ t ] = Vector t;
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, fullname, object_id, args, result, is_function):
|
|
||||||
"""
|
|
||||||
Initializes a new TLObject, given its properties.
|
|
||||||
Usually, this will be called from `from_tl` instead
|
|
||||||
:param fullname: The fullname of the TL object (namespace.name)
|
|
||||||
The namespace can be omitted
|
|
||||||
:param object_id: The hexadecimal string representing the object ID
|
|
||||||
:param args: The arguments, if any, of the TL object
|
|
||||||
:param result: The result type of the TL object
|
|
||||||
:param is_function: Is the object a function or a type?
|
|
||||||
"""
|
|
||||||
# The name can or not have a namespace
|
|
||||||
if '.' in fullname:
|
|
||||||
self.namespace = fullname.split('.')[0]
|
|
||||||
self.name = fullname.split('.')[1]
|
|
||||||
else:
|
|
||||||
self.namespace = None
|
|
||||||
self.name = fullname
|
|
||||||
|
|
||||||
self.args = args
|
|
||||||
self.result = result
|
|
||||||
self.is_function = is_function
|
|
||||||
|
|
||||||
# The ID should be an hexadecimal string or None to be inferred
|
|
||||||
if object_id is None:
|
|
||||||
self.id = self.infer_id()
|
|
||||||
else:
|
|
||||||
self.id = int(object_id, base=16)
|
|
||||||
assert self.id == self.infer_id(),\
|
|
||||||
'Invalid inferred ID for ' + repr(self)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_tl(tl, is_function):
|
|
||||||
"""Returns a TL object from the given TL scheme line"""
|
|
||||||
|
|
||||||
# Regex to match the whole line
|
|
||||||
match = re.match(r'''
|
|
||||||
^ # We want to match from the beginning to the end
|
|
||||||
([\w.]+) # The .tl object can contain alpha_name or namespace.alpha_name
|
|
||||||
(?:
|
|
||||||
\# # After the name, comes the ID of the object
|
|
||||||
([0-9a-f]+) # The constructor ID is in hexadecimal form
|
|
||||||
)? # If no constructor ID was given, CRC32 the 'tl' to determine it
|
|
||||||
|
|
||||||
(?:\s # After that, we want to match its arguments (name:type)
|
|
||||||
{? # For handling the start of the '{X:Type}' case
|
|
||||||
\w+ # The argument name will always be an alpha-only name
|
|
||||||
: # Then comes the separator between name:type
|
|
||||||
[\w\d<>#.?!]+ # The type is slightly more complex, since it's alphanumeric and it can
|
|
||||||
# also have Vector<type>, flags:# and flags.0?default, plus :!X as type
|
|
||||||
}? # For handling the end of the '{X:Type}' case
|
|
||||||
)* # Match 0 or more arguments
|
|
||||||
\s # Leave a space between the arguments and the equal
|
|
||||||
=
|
|
||||||
\s # Leave another space between the equal and the result
|
|
||||||
([\w\d<>#.?]+) # The result can again be as complex as any argument type
|
|
||||||
;$ # Finally, the line should always end with ;
|
|
||||||
''', tl, re.IGNORECASE | re.VERBOSE)
|
|
||||||
|
|
||||||
if match is None:
|
|
||||||
# Probably "vector#1cb5c415 {t:Type} # [ t ] = Vector t;"
|
|
||||||
raise ValueError('Cannot parse TLObject', tl)
|
|
||||||
|
|
||||||
# Sub-regex to match the arguments (sadly, it cannot be embedded in the first regex)
|
|
||||||
args_match = re.findall(r'''
|
|
||||||
({)? # We may or may not capture the opening brace
|
|
||||||
(\w+) # First we capture any alpha name with length 1 or more
|
|
||||||
: # Which is separated from its type by a colon
|
|
||||||
([\w\d<>#.?!]+) # The type is slightly more complex, since it's alphanumeric and it can
|
|
||||||
# also have Vector<type>, flags:# and flags.0?default, plus :!X as type
|
|
||||||
(})? # We may or not capture the closing brace
|
|
||||||
''', tl, re.IGNORECASE | re.VERBOSE)
|
|
||||||
|
|
||||||
# Retrieve the matched arguments
|
|
||||||
args = [TLArg(name, arg_type, brace != '')
|
|
||||||
for brace, name, arg_type, _ in args_match]
|
|
||||||
|
|
||||||
# And initialize the TLObject
|
|
||||||
return TLObject(
|
|
||||||
fullname=match.group(1),
|
|
||||||
object_id=match.group(2),
|
|
||||||
args=args,
|
|
||||||
result=match.group(3),
|
|
||||||
is_function=is_function)
|
|
||||||
|
|
||||||
def class_name(self):
|
|
||||||
"""Gets the class name following the Python style guidelines"""
|
|
||||||
return self.class_name_for(self.name, self.is_function)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def class_name_for(typename, is_function=False):
|
|
||||||
"""Gets the class name following the Python style guidelines"""
|
|
||||||
# Courtesy of http://stackoverflow.com/a/31531797/4759433
|
|
||||||
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), typename)
|
|
||||||
result = result[:1].upper() + result[1:].replace('_', '')
|
|
||||||
# If it's a function, let it end with "Request" to identify them
|
|
||||||
if is_function:
|
|
||||||
result += 'Request'
|
|
||||||
return result
|
|
||||||
|
|
||||||
def sorted_args(self):
|
|
||||||
"""Returns the arguments properly sorted and ready to plug-in
|
|
||||||
into a Python's method header (i.e., flags and those which
|
|
||||||
can be inferred will go last so they can default =None)
|
|
||||||
"""
|
|
||||||
return sorted(self.args,
|
|
||||||
key=lambda x: x.is_flag or x.can_be_inferred)
|
|
||||||
|
|
||||||
def is_core_type(self):
|
|
||||||
"""Determines whether the TLObject is a "core type"
|
|
||||||
(and thus should be embedded in the generated code) or not"""
|
|
||||||
return self.id in TLObject.CORE_TYPES
|
|
||||||
|
|
||||||
def __repr__(self, ignore_id=False):
|
|
||||||
fullname = ('{}.{}'.format(self.namespace, self.name)
|
|
||||||
if self.namespace is not None else self.name)
|
|
||||||
|
|
||||||
if getattr(self, 'id', None) is None or ignore_id:
|
|
||||||
hex_id = ''
|
|
||||||
else:
|
|
||||||
# Skip 0x and add 0's for padding
|
|
||||||
hex_id = '#' + hex(self.id)[2:].rjust(8, '0')
|
|
||||||
|
|
||||||
if self.args:
|
|
||||||
args = ' ' + ' '.join([repr(arg) for arg in self.args])
|
|
||||||
else:
|
|
||||||
args = ''
|
|
||||||
|
|
||||||
return '{}{}{} = {}'.format(fullname, hex_id, args, self.result)
|
|
||||||
|
|
||||||
def infer_id(self):
|
|
||||||
representation = self.__repr__(ignore_id=True)
|
|
||||||
|
|
||||||
# Clean the representation
|
|
||||||
representation = representation\
|
|
||||||
.replace(':bytes ', ':string ')\
|
|
||||||
.replace('?bytes ', '?string ')\
|
|
||||||
.replace('<', ' ').replace('>', '')\
|
|
||||||
.replace('{', '').replace('}', '')
|
|
||||||
|
|
||||||
representation = re.sub(
|
|
||||||
r' \w+:flags\.\d+\?true',
|
|
||||||
r'',
|
|
||||||
representation
|
|
||||||
)
|
|
||||||
return crc32(representation.encode('ascii'))
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
fullname = ('{}.{}'.format(self.namespace, self.name)
|
|
||||||
if self.namespace is not None else self.name)
|
|
||||||
|
|
||||||
# Some arguments are not valid for being represented, such as the flag indicator or generic definition
|
|
||||||
# (these have no explicit values until used)
|
|
||||||
valid_args = [arg for arg in self.args
|
|
||||||
if not arg.flag_indicator and not arg.generic_definition]
|
|
||||||
|
|
||||||
args = ', '.join(['{}={{}}'.format(arg.name) for arg in valid_args])
|
|
||||||
|
|
||||||
# Since Python's default representation for lists is using repr(), we need to str() manually on every item
|
|
||||||
args_format = ', '.join(
|
|
||||||
['str(self.{})'.format(arg.name) if not arg.is_vector else
|
|
||||||
'None if not self.{0} else [str(_) for _ in self.{0}]'.format(
|
|
||||||
arg.name) for arg in valid_args])
|
|
||||||
|
|
||||||
return ("'({} (ID: {}) = ({}))'.format({})"
|
|
||||||
.format(fullname, hex(self.id), args, args_format))
|
|
||||||
|
|
||||||
|
|
||||||
class TLArg:
|
|
||||||
def __init__(self, name, arg_type, generic_definition):
|
|
||||||
"""
|
|
||||||
Initializes a new .tl argument
|
|
||||||
:param name: The name of the .tl argument
|
|
||||||
:param arg_type: The type of the .tl argument
|
|
||||||
:param generic_definition: Is the argument a generic definition?
|
|
||||||
(i.e. {X:Type})
|
|
||||||
"""
|
|
||||||
if name == 'self': # This very only name is restricted
|
|
||||||
self.name = 'is_self'
|
|
||||||
else:
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
# Default values
|
|
||||||
self.is_vector = False
|
|
||||||
self.is_flag = False
|
|
||||||
self.skip_constructor_id = False
|
|
||||||
self.flag_index = -1
|
|
||||||
|
|
||||||
# Special case: some types can be inferred, which makes it
|
|
||||||
# less annoying to type. Currently the only type that can
|
|
||||||
# be inferred is if the name is 'random_id', to which a
|
|
||||||
# random ID will be assigned if left as None (the default)
|
|
||||||
self.can_be_inferred = name == 'random_id'
|
|
||||||
|
|
||||||
# The type can be an indicator that other arguments will be flags
|
|
||||||
if arg_type == '#':
|
|
||||||
self.flag_indicator = True
|
|
||||||
self.type = None
|
|
||||||
self.is_generic = False
|
|
||||||
else:
|
|
||||||
self.flag_indicator = False
|
|
||||||
self.is_generic = arg_type.startswith('!')
|
|
||||||
# Strip the exclamation mark always to have only the name
|
|
||||||
self.type = arg_type.lstrip('!')
|
|
||||||
|
|
||||||
# The type may be a flag (flags.IDX?REAL_TYPE)
|
|
||||||
# Note that 'flags' is NOT the flags name; this is determined by a previous argument
|
|
||||||
# However, we assume that the argument will always be called 'flags'
|
|
||||||
flag_match = re.match(r'flags.(\d+)\?([\w<>.]+)', self.type)
|
|
||||||
if flag_match:
|
|
||||||
self.is_flag = True
|
|
||||||
self.flag_index = int(flag_match.group(1))
|
|
||||||
# Update the type to match the exact type, not the "flagged" one
|
|
||||||
self.type = flag_match.group(2)
|
|
||||||
|
|
||||||
# Then check if the type is a Vector<REAL_TYPE>
|
|
||||||
vector_match = re.match(r'[Vv]ector<([\w\d.]+)>', self.type)
|
|
||||||
if vector_match:
|
|
||||||
self.is_vector = True
|
|
||||||
|
|
||||||
# If the type's first letter is not uppercase, then
|
|
||||||
# it is a constructor and we use (read/write) its ID
|
|
||||||
# as pinpointed on issue #81.
|
|
||||||
self.use_vector_id = self.type[0] == 'V'
|
|
||||||
|
|
||||||
# Update the type to match the one inside the vector
|
|
||||||
self.type = vector_match.group(1)
|
|
||||||
|
|
||||||
# See use_vector_id. An example of such case is ipPort in
|
|
||||||
# help.configSpecial
|
|
||||||
if self.type.split('.')[-1][0].islower():
|
|
||||||
self.skip_constructor_id = True
|
|
||||||
|
|
||||||
# The name may contain "date" in it, if this is the case and the type is "int",
|
|
||||||
# we can safely assume that this should be treated as a "date" object.
|
|
||||||
# Note that this is not a valid Telegram object, but it's easier to work with
|
|
||||||
if self.type == 'int' and (
|
|
||||||
re.search(r'(\b|_)date\b', name) or
|
|
||||||
name in ('expires', 'expires_at', 'was_online')):
|
|
||||||
self.type = 'date'
|
|
||||||
|
|
||||||
self.generic_definition = generic_definition
|
|
||||||
|
|
||||||
def doc_type_hint(self):
|
|
||||||
result = {
|
|
||||||
'int': 'int',
|
|
||||||
'long': 'int',
|
|
||||||
'int128': 'int',
|
|
||||||
'int256': 'int',
|
|
||||||
'string': 'str',
|
|
||||||
'date': 'datetime.datetime | None', # None date = 0 timestamp
|
|
||||||
'bytes': 'bytes',
|
|
||||||
'true': 'bool',
|
|
||||||
}.get(self.type, self.type)
|
|
||||||
if self.is_vector:
|
|
||||||
result = 'list[{}]'.format(result)
|
|
||||||
if self.is_flag and self.type != 'date':
|
|
||||||
result += ' | None'
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def python_type_hint(self):
|
|
||||||
type = self.type
|
|
||||||
if '.' in type:
|
|
||||||
type = type.split('.')[1]
|
|
||||||
result = {
|
|
||||||
'int': 'int',
|
|
||||||
'long': 'int',
|
|
||||||
'int128': 'int',
|
|
||||||
'int256': 'int',
|
|
||||||
'string': 'str',
|
|
||||||
'date': 'Optional[datetime]', # None date = 0 timestamp
|
|
||||||
'bytes': 'bytes',
|
|
||||||
'true': 'bool',
|
|
||||||
}.get(type, "Type{}".format(type))
|
|
||||||
if self.is_vector:
|
|
||||||
result = 'List[{}]'.format(result)
|
|
||||||
if self.is_flag and type != 'date':
|
|
||||||
result = 'Optional[{}]'.format(result)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
# Find the real type representation by updating it as required
|
|
||||||
real_type = self.type
|
|
||||||
if self.flag_indicator:
|
|
||||||
real_type = '#'
|
|
||||||
|
|
||||||
if self.is_vector:
|
|
||||||
if self.use_vector_id:
|
|
||||||
real_type = 'Vector<{}>'.format(real_type)
|
|
||||||
else:
|
|
||||||
real_type = 'vector<{}>'.format(real_type)
|
|
||||||
|
|
||||||
if self.is_generic:
|
|
||||||
real_type = '!{}'.format(real_type)
|
|
||||||
|
|
||||||
if self.is_flag:
|
|
||||||
real_type = 'flags.{}?{}'.format(self.flag_index, real_type)
|
|
||||||
|
|
||||||
if self.generic_definition:
|
|
||||||
return '{{{}:{}}}'.format(self.name, real_type)
|
|
||||||
else:
|
|
||||||
return '{}:{}'.format(self.name, real_type)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
# Get rid of our special type
|
|
||||||
return str(self)\
|
|
||||||
.replace(':date', ':int')\
|
|
||||||
.replace('?date', '?int')
|
|
|
@ -1,51 +0,0 @@
|
||||||
import re
|
|
||||||
|
|
||||||
from .tl_object import TLObject
|
|
||||||
|
|
||||||
|
|
||||||
class TLParser:
|
|
||||||
"""Class used to parse .tl files"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_file(file_path, ignore_core=False):
|
|
||||||
"""This method yields TLObjects from a given .tl file"""
|
|
||||||
|
|
||||||
with open(file_path, encoding='utf-8') as file:
|
|
||||||
# Start by assuming that the next found line won't
|
|
||||||
# be a function (and will hence be a type)
|
|
||||||
is_function = False
|
|
||||||
|
|
||||||
# Read all the lines from the .tl file
|
|
||||||
for line in file:
|
|
||||||
# Strip comments from the line
|
|
||||||
comment_index = line.find('//')
|
|
||||||
if comment_index != -1:
|
|
||||||
line = line[:comment_index]
|
|
||||||
|
|
||||||
line = line.strip()
|
|
||||||
if line:
|
|
||||||
# Check whether the line is a type change
|
|
||||||
# (types <-> functions) or not
|
|
||||||
match = re.match('---(\w+)---', line)
|
|
||||||
if match:
|
|
||||||
following_types = match.group(1)
|
|
||||||
is_function = following_types == 'functions'
|
|
||||||
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
result = TLObject.from_tl(line, is_function)
|
|
||||||
if not ignore_core or not result.is_core_type():
|
|
||||||
yield result
|
|
||||||
except ValueError as e:
|
|
||||||
if 'vector#1cb5c415' not in str(e):
|
|
||||||
raise
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def find_layer(file_path):
|
|
||||||
"""Finds the layer used on the specified scheme.tl file"""
|
|
||||||
layer_regex = re.compile(r'^//\s*LAYER\s*(\d+)$')
|
|
||||||
with open(file_path, encoding='utf-8') as file:
|
|
||||||
for line in file:
|
|
||||||
match = layer_regex.match(line)
|
|
||||||
if match:
|
|
||||||
return int(match.group(1))
|
|
2
telethon_generator/parsers/__init__.py
Normal file
2
telethon_generator/parsers/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from .errors import Error, parse_errors
|
||||||
|
from .tlobject import TLObject, parse_tl, find_layer
|
146
telethon_generator/parsers/errors.py
Normal file
146
telethon_generator/parsers/errors.py
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from ..utils import snake_to_camel_case
|
||||||
|
|
||||||
|
# Core base classes depending on the integer error code
|
||||||
|
KNOWN_BASE_CLASSES = {
|
||||||
|
303: 'InvalidDCError',
|
||||||
|
400: 'BadRequestError',
|
||||||
|
401: 'UnauthorizedError',
|
||||||
|
403: 'ForbiddenError',
|
||||||
|
404: 'NotFoundError',
|
||||||
|
406: 'AuthKeyError',
|
||||||
|
420: 'FloodError',
|
||||||
|
500: 'ServerError',
|
||||||
|
}
|
||||||
|
|
||||||
|
# The API doesn't return the code for some (vital) errors. They are
|
||||||
|
# all assumed to be 400, except these well-known ones that aren't.
|
||||||
|
KNOWN_CODES = {
|
||||||
|
'ACTIVE_USER_REQUIRED': 401,
|
||||||
|
'AUTH_KEY_UNREGISTERED': 401,
|
||||||
|
'USER_DEACTIVATED': 401
|
||||||
|
}
|
||||||
|
|
||||||
|
# Give better semantic names to some captures
|
||||||
|
CAPTURE_NAMES = {
|
||||||
|
'FloodWaitError': 'seconds',
|
||||||
|
'FloodTestPhoneWaitError': 'seconds',
|
||||||
|
'FileMigrateError': 'new_dc',
|
||||||
|
'NetworkMigrateError': 'new_dc',
|
||||||
|
'PhoneMigrateError': 'new_dc',
|
||||||
|
'UserMigrateError': 'new_dc',
|
||||||
|
'FilePartMissingError': 'which'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_class_name(error_code):
|
||||||
|
"""
|
||||||
|
Gets the corresponding class name for the given error code,
|
||||||
|
this either being an integer (thus base error name) or str.
|
||||||
|
"""
|
||||||
|
if isinstance(error_code, int):
|
||||||
|
return KNOWN_BASE_CLASSES.get(
|
||||||
|
error_code, 'RPCError' + str(error_code).replace('-', 'Neg')
|
||||||
|
)
|
||||||
|
|
||||||
|
return snake_to_camel_case(
|
||||||
|
error_code.replace('FIRSTNAME', 'FIRST_NAME').lower(), suffix='Error')
|
||||||
|
|
||||||
|
|
||||||
|
class Error:
|
||||||
|
def __init__(self, int_code, str_code, description, caused_by):
|
||||||
|
# TODO Some errors have the same str_code but different int_code
|
||||||
|
# Should these be split into different files or doesn't really matter?
|
||||||
|
# Telegram isn't exactly consistent with returned errors anyway.
|
||||||
|
self.int_code = int_code
|
||||||
|
self.str_code = str_code
|
||||||
|
self.subclass = _get_class_name(int_code)
|
||||||
|
self.subclass_exists = int_code in KNOWN_BASE_CLASSES
|
||||||
|
self.description = description
|
||||||
|
self.caused_by = list(sorted(caused_by))
|
||||||
|
|
||||||
|
self.has_captures = '_X' in str_code
|
||||||
|
if self.has_captures:
|
||||||
|
self.name = _get_class_name(str_code.replace('_X', ''))
|
||||||
|
self.pattern = str_code.replace('_X', r'_(\d+)')
|
||||||
|
self.capture_name = CAPTURE_NAMES.get(self.name, 'x')
|
||||||
|
else:
|
||||||
|
self.name = _get_class_name(str_code)
|
||||||
|
self.pattern = str_code
|
||||||
|
self.capture_name = None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_errors(json_file, descriptions_file):
|
||||||
|
"""
|
||||||
|
Parses the given JSON file in the following format:
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"human_result": {"int_code": ["descriptions"]},
|
||||||
|
"result": {"int_code": {"full_method_name": ["str_error"]}}
|
||||||
|
}
|
||||||
|
|
||||||
|
The descriptions file, which has precedence over the JSON's human_result,
|
||||||
|
should have the following format:
|
||||||
|
# comment
|
||||||
|
str_error=Description
|
||||||
|
|
||||||
|
The method yields `Error` instances as a result.
|
||||||
|
"""
|
||||||
|
with open(json_file, encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
errors = defaultdict(set)
|
||||||
|
error_to_method = defaultdict(set)
|
||||||
|
# PWRTelegram's API doesn't return all errors, which we do need here.
|
||||||
|
# Add some special known-cases manually first.
|
||||||
|
errors[420].update((
|
||||||
|
'FLOOD_WAIT_X', 'FLOOD_TEST_PHONE_WAIT_X'
|
||||||
|
))
|
||||||
|
errors[401].update((
|
||||||
|
'AUTH_KEY_INVALID', 'SESSION_EXPIRED', 'SESSION_REVOKED'
|
||||||
|
))
|
||||||
|
errors[303].update((
|
||||||
|
'FILE_MIGRATE_X', 'PHONE_MIGRATE_X',
|
||||||
|
'NETWORK_MIGRATE_X', 'USER_MIGRATE_X'
|
||||||
|
))
|
||||||
|
for int_code, method_errors in data['result'].items():
|
||||||
|
for method, error_list in method_errors.items():
|
||||||
|
for error in error_list:
|
||||||
|
error = re.sub('_\d+', '_X', error).upper()
|
||||||
|
errors[int(int_code)].add(error)
|
||||||
|
error_to_method[error].add(method)
|
||||||
|
|
||||||
|
# Some errors are in the human result, but not with a code. Assume 400
|
||||||
|
for error in data['human_result']:
|
||||||
|
if error[0] != '-' and not error.isdigit():
|
||||||
|
error = re.sub('_\d+', '_X', error).upper()
|
||||||
|
if not any(error in es for es in errors.values()):
|
||||||
|
errors[KNOWN_CODES.get(error, 400)].add(error)
|
||||||
|
|
||||||
|
# Prefer the descriptions that are related with Telethon way of coding
|
||||||
|
# to those that PWRTelegram's API provides.
|
||||||
|
telethon_descriptions = {}
|
||||||
|
with open(descriptions_file, encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith('#'):
|
||||||
|
equal = line.index('=')
|
||||||
|
message, description = line[:equal], line[equal + 1:]
|
||||||
|
telethon_descriptions[message.rstrip()] = description.lstrip()
|
||||||
|
|
||||||
|
for int_code, error_set in errors.items():
|
||||||
|
for str_code in sorted(error_set):
|
||||||
|
description = telethon_descriptions.get(
|
||||||
|
str_code, '\n'.join(data['human_result'].get(
|
||||||
|
str_code, ['No description known']
|
||||||
|
))
|
||||||
|
)
|
||||||
|
yield Error(
|
||||||
|
int_code=int_code,
|
||||||
|
str_code=str_code,
|
||||||
|
description=description,
|
||||||
|
caused_by=error_to_method[str_code]
|
||||||
|
)
|
274
telethon_generator/parsers/tlobject.py
Normal file
274
telethon_generator/parsers/tlobject.py
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
import re
|
||||||
|
from zlib import crc32
|
||||||
|
|
||||||
|
from ..utils import snake_to_camel_case
|
||||||
|
|
||||||
|
CORE_TYPES = (
|
||||||
|
0xbc799737, # boolFalse#bc799737 = Bool;
|
||||||
|
0x997275b5, # boolTrue#997275b5 = Bool;
|
||||||
|
0x3fedd339, # true#3fedd339 = True;
|
||||||
|
0x1cb5c415, # vector#1cb5c415 {t:Type} # [ t ] = Vector t;
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TLObject:
|
||||||
|
def __init__(self, fullname, object_id, args, result, is_function):
|
||||||
|
"""
|
||||||
|
Initializes a new TLObject, given its properties.
|
||||||
|
|
||||||
|
:param fullname: The fullname of the TL object (namespace.name)
|
||||||
|
The namespace can be omitted.
|
||||||
|
:param object_id: The hexadecimal string representing the object ID
|
||||||
|
:param args: The arguments, if any, of the TL object
|
||||||
|
:param result: The result type of the TL object
|
||||||
|
:param is_function: Is the object a function or a type?
|
||||||
|
"""
|
||||||
|
# The name can or not have a namespace
|
||||||
|
self.fullname = fullname
|
||||||
|
if '.' in fullname:
|
||||||
|
self.namespace, self.name = fullname.split('.', maxsplit=1)
|
||||||
|
else:
|
||||||
|
self.namespace, self.name = None, fullname
|
||||||
|
|
||||||
|
self.args = args
|
||||||
|
self.result = result
|
||||||
|
self.is_function = is_function
|
||||||
|
self.id = None
|
||||||
|
if object_id is None:
|
||||||
|
self.id = self.infer_id()
|
||||||
|
else:
|
||||||
|
self.id = int(object_id, base=16)
|
||||||
|
assert self.id == self.infer_id(),\
|
||||||
|
'Invalid inferred ID for ' + repr(self)
|
||||||
|
|
||||||
|
self.class_name = snake_to_camel_case(
|
||||||
|
self.name, suffix='Request' if self.is_function else '')
|
||||||
|
|
||||||
|
self.real_args = list(a for a in self.sorted_args() if not
|
||||||
|
(a.flag_indicator or a.generic_definition))
|
||||||
|
|
||||||
|
def sorted_args(self):
|
||||||
|
"""Returns the arguments properly sorted and ready to plug-in
|
||||||
|
into a Python's method header (i.e., flags and those which
|
||||||
|
can be inferred will go last so they can default =None)
|
||||||
|
"""
|
||||||
|
return sorted(self.args,
|
||||||
|
key=lambda x: x.is_flag or x.can_be_inferred)
|
||||||
|
|
||||||
|
def __repr__(self, ignore_id=False):
|
||||||
|
if self.id is None or ignore_id:
|
||||||
|
hex_id = ''
|
||||||
|
else:
|
||||||
|
hex_id = '#{:08x}'.format(self.id)
|
||||||
|
|
||||||
|
if self.args:
|
||||||
|
args = ' ' + ' '.join([repr(arg) for arg in self.args])
|
||||||
|
else:
|
||||||
|
args = ''
|
||||||
|
|
||||||
|
return '{}{}{} = {}'.format(self.fullname, hex_id, args, self.result)
|
||||||
|
|
||||||
|
def infer_id(self):
|
||||||
|
representation = self.__repr__(ignore_id=True)
|
||||||
|
representation = representation\
|
||||||
|
.replace(':bytes ', ':string ')\
|
||||||
|
.replace('?bytes ', '?string ')\
|
||||||
|
.replace('<', ' ').replace('>', '')\
|
||||||
|
.replace('{', '').replace('}', '')
|
||||||
|
|
||||||
|
representation = re.sub(
|
||||||
|
r' \w+:flags\.\d+\?true',
|
||||||
|
r'',
|
||||||
|
representation
|
||||||
|
)
|
||||||
|
return crc32(representation.encode('ascii'))
|
||||||
|
|
||||||
|
|
||||||
|
class TLArg:
|
||||||
|
def __init__(self, name, arg_type, generic_definition):
|
||||||
|
"""
|
||||||
|
Initializes a new .tl argument
|
||||||
|
:param name: The name of the .tl argument
|
||||||
|
:param arg_type: The type of the .tl argument
|
||||||
|
:param generic_definition: Is the argument a generic definition?
|
||||||
|
(i.e. {X:Type})
|
||||||
|
"""
|
||||||
|
self.name = 'is_self' if name == 'self' else name
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
self.is_vector = False
|
||||||
|
self.is_flag = False
|
||||||
|
self.skip_constructor_id = False
|
||||||
|
self.flag_index = -1
|
||||||
|
|
||||||
|
# Special case: some types can be inferred, which makes it
|
||||||
|
# less annoying to type. Currently the only type that can
|
||||||
|
# be inferred is if the name is 'random_id', to which a
|
||||||
|
# random ID will be assigned if left as None (the default)
|
||||||
|
self.can_be_inferred = name == 'random_id'
|
||||||
|
|
||||||
|
# The type can be an indicator that other arguments will be flags
|
||||||
|
if arg_type == '#':
|
||||||
|
self.flag_indicator = True
|
||||||
|
self.type = None
|
||||||
|
self.is_generic = False
|
||||||
|
else:
|
||||||
|
self.flag_indicator = False
|
||||||
|
self.is_generic = arg_type.startswith('!')
|
||||||
|
# Strip the exclamation mark always to have only the name
|
||||||
|
self.type = arg_type.lstrip('!')
|
||||||
|
|
||||||
|
# The type may be a flag (flags.IDX?REAL_TYPE)
|
||||||
|
# Note that 'flags' is NOT the flags name; this
|
||||||
|
# is determined by a previous argument
|
||||||
|
# However, we assume that the argument will always be called 'flags'
|
||||||
|
flag_match = re.match(r'flags.(\d+)\?([\w<>.]+)', self.type)
|
||||||
|
if flag_match:
|
||||||
|
self.is_flag = True
|
||||||
|
self.flag_index = int(flag_match.group(1))
|
||||||
|
# Update the type to match the exact type, not the "flagged" one
|
||||||
|
self.type = flag_match.group(2)
|
||||||
|
|
||||||
|
# Then check if the type is a Vector<REAL_TYPE>
|
||||||
|
vector_match = re.match(r'[Vv]ector<([\w\d.]+)>', self.type)
|
||||||
|
if vector_match:
|
||||||
|
self.is_vector = True
|
||||||
|
|
||||||
|
# If the type's first letter is not uppercase, then
|
||||||
|
# it is a constructor and we use (read/write) its ID
|
||||||
|
# as pinpointed on issue #81.
|
||||||
|
self.use_vector_id = self.type[0] == 'V'
|
||||||
|
|
||||||
|
# Update the type to match the one inside the vector
|
||||||
|
self.type = vector_match.group(1)
|
||||||
|
|
||||||
|
# See use_vector_id. An example of such case is ipPort in
|
||||||
|
# help.configSpecial
|
||||||
|
if self.type.split('.')[-1][0].islower():
|
||||||
|
self.skip_constructor_id = True
|
||||||
|
|
||||||
|
# The name may contain "date" in it, if this is the case and the type is "int",
|
||||||
|
# we can safely assume that this should be treated as a "date" object.
|
||||||
|
# Note that this is not a valid Telegram object, but it's easier to work with
|
||||||
|
if self.type == 'int' and (
|
||||||
|
re.search(r'(\b|_)date\b', name) or
|
||||||
|
name in ('expires', 'expires_at', 'was_online')):
|
||||||
|
self.type = 'date'
|
||||||
|
|
||||||
|
self.generic_definition = generic_definition
|
||||||
|
|
||||||
|
def type_hint(self):
|
||||||
|
type = self.type
|
||||||
|
if '.' in type:
|
||||||
|
type = type.split('.')[1]
|
||||||
|
result = {
|
||||||
|
'int': 'int',
|
||||||
|
'long': 'int',
|
||||||
|
'int128': 'int',
|
||||||
|
'int256': 'int',
|
||||||
|
'string': 'str',
|
||||||
|
'date': 'Optional[datetime]', # None date = 0 timestamp
|
||||||
|
'bytes': 'bytes',
|
||||||
|
'true': 'bool',
|
||||||
|
}.get(type, "Type{}".format(type))
|
||||||
|
if self.is_vector:
|
||||||
|
result = 'List[{}]'.format(result)
|
||||||
|
if self.is_flag and type != 'date':
|
||||||
|
result = 'Optional[{}]'.format(result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# Find the real type representation by updating it as required
|
||||||
|
real_type = self.type
|
||||||
|
if self.flag_indicator:
|
||||||
|
real_type = '#'
|
||||||
|
|
||||||
|
if self.is_vector:
|
||||||
|
if self.use_vector_id:
|
||||||
|
real_type = 'Vector<{}>'.format(real_type)
|
||||||
|
else:
|
||||||
|
real_type = 'vector<{}>'.format(real_type)
|
||||||
|
|
||||||
|
if self.is_generic:
|
||||||
|
real_type = '!{}'.format(real_type)
|
||||||
|
|
||||||
|
if self.is_flag:
|
||||||
|
real_type = 'flags.{}?{}'.format(self.flag_index, real_type)
|
||||||
|
|
||||||
|
if self.generic_definition:
|
||||||
|
return '{{{}:{}}}'.format(self.name, real_type)
|
||||||
|
else:
|
||||||
|
return '{}:{}'.format(self.name, real_type)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self).replace(':date', ':int').replace('?date', '?int')
|
||||||
|
|
||||||
|
|
||||||
|
def _from_line(line, is_function):
|
||||||
|
match = re.match(
|
||||||
|
r'^([\w.]+)' # 'name'
|
||||||
|
r'(?:#([0-9a-fA-F]+))?' # '#optionalcode'
|
||||||
|
r'(?:\s{?\w+:[\w\d<>#.?!]+}?)*' # '{args:.0?type}'
|
||||||
|
r'\s=\s' # ' = '
|
||||||
|
r'([\w\d<>#.?]+);$', # '<result.type>;'
|
||||||
|
line
|
||||||
|
)
|
||||||
|
if match is None:
|
||||||
|
# Probably "vector#1cb5c415 {t:Type} # [ t ] = Vector t;"
|
||||||
|
raise ValueError('Cannot parse TLObject {}'.format(line))
|
||||||
|
|
||||||
|
args_match = re.findall(
|
||||||
|
r'({)?'
|
||||||
|
r'(\w+)'
|
||||||
|
r':'
|
||||||
|
r'([\w\d<>#.?!]+)'
|
||||||
|
r'}?',
|
||||||
|
line
|
||||||
|
)
|
||||||
|
return TLObject(
|
||||||
|
fullname=match.group(1),
|
||||||
|
object_id=match.group(2),
|
||||||
|
result=match.group(3),
|
||||||
|
is_function=is_function,
|
||||||
|
args=[TLArg(name, arg_type, brace != '')
|
||||||
|
for brace, name, arg_type in args_match]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_tl(file_path, ignore_core=False):
|
||||||
|
"""This method yields TLObjects from a given .tl file."""
|
||||||
|
with open(file_path, encoding='utf-8') as file:
|
||||||
|
is_function = False
|
||||||
|
for line in file:
|
||||||
|
comment_index = line.find('//')
|
||||||
|
if comment_index != -1:
|
||||||
|
line = line[:comment_index]
|
||||||
|
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = re.match('---(\w+)---', line)
|
||||||
|
if match:
|
||||||
|
following_types = match.group(1)
|
||||||
|
is_function = following_types == 'functions'
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = _from_line(line, is_function)
|
||||||
|
if not ignore_core or result.id not in CORE_TYPES:
|
||||||
|
yield result
|
||||||
|
except ValueError as e:
|
||||||
|
if 'vector#1cb5c415' not in str(e):
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def find_layer(file_path):
|
||||||
|
"""Finds the layer used on the specified scheme.tl file."""
|
||||||
|
layer_regex = re.compile(r'^//\s*LAYER\s*(\d+)$')
|
||||||
|
with open(file_path, encoding='utf-8') as file:
|
||||||
|
for line in file:
|
||||||
|
match = layer_regex.match(line)
|
||||||
|
if match:
|
||||||
|
return int(match.group(1))
|
|
@ -1,762 +0,0 @@
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import struct
|
|
||||||
from zlib import crc32
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
from .parser import SourceBuilder, TLParser, TLObject
|
|
||||||
AUTO_GEN_NOTICE = \
|
|
||||||
'"""File generated by TLObjects\' generator. All changes will be ERASED"""'
|
|
||||||
|
|
||||||
|
|
||||||
AUTO_CASTS = {
|
|
||||||
'InputPeer': 'utils.get_input_peer(client.get_input_entity({}))',
|
|
||||||
'InputChannel': 'utils.get_input_channel(client.get_input_entity({}))',
|
|
||||||
'InputUser': 'utils.get_input_user(client.get_input_entity({}))',
|
|
||||||
'InputMedia': 'utils.get_input_media({})',
|
|
||||||
'InputPhoto': 'utils.get_input_photo({})'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TLGenerator:
|
|
||||||
def __init__(self, output_dir):
|
|
||||||
self.output_dir = output_dir
|
|
||||||
|
|
||||||
def _get_file(self, *paths):
|
|
||||||
"""Wrapper around ``os.path.join()`` with output as first path."""
|
|
||||||
return os.path.join(self.output_dir, *paths)
|
|
||||||
|
|
||||||
def _rm_if_exists(self, filename):
|
|
||||||
"""Recursively deletes the given filename if it exists."""
|
|
||||||
file = self._get_file(filename)
|
|
||||||
if os.path.exists(file):
|
|
||||||
if os.path.isdir(file):
|
|
||||||
shutil.rmtree(file)
|
|
||||||
else:
|
|
||||||
os.remove(file)
|
|
||||||
|
|
||||||
def tlobjects_exist(self):
|
|
||||||
"""
|
|
||||||
Determines whether the TLObjects were previously
|
|
||||||
generated (hence exist) or not.
|
|
||||||
"""
|
|
||||||
return os.path.isfile(self._get_file('all_tlobjects.py'))
|
|
||||||
|
|
||||||
def clean_tlobjects(self):
|
|
||||||
"""Cleans the automatically generated TLObjects from disk."""
|
|
||||||
for name in ('functions', 'types', 'all_tlobjects.py'):
|
|
||||||
self._rm_if_exists(name)
|
|
||||||
|
|
||||||
def generate_tlobjects(self, scheme_file, import_depth):
|
|
||||||
"""
|
|
||||||
Generates all the TLObjects from the ``scheme_file`` to
|
|
||||||
``tl/functions`` and ``tl/types``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# First ensure that the required parent directories exist
|
|
||||||
os.makedirs(self._get_file('functions'), exist_ok=True)
|
|
||||||
os.makedirs(self._get_file('types'), exist_ok=True)
|
|
||||||
|
|
||||||
# Step 0: Cache the parsed file on a tuple
|
|
||||||
tlobjects = tuple(TLParser.parse_file(scheme_file, ignore_core=True))
|
|
||||||
|
|
||||||
# Step 1: Group everything by {namespace: [tlobjects]} so we can
|
|
||||||
# easily generate __init__.py files with all the TLObjects on them.
|
|
||||||
namespace_functions = defaultdict(list)
|
|
||||||
namespace_types = defaultdict(list)
|
|
||||||
|
|
||||||
# Make use of this iteration to also store 'Type: [Constructors]',
|
|
||||||
# used when generating the documentation for the classes.
|
|
||||||
type_constructors = defaultdict(list)
|
|
||||||
for tlobject in tlobjects:
|
|
||||||
if tlobject.is_function:
|
|
||||||
namespace_functions[tlobject.namespace].append(tlobject)
|
|
||||||
else:
|
|
||||||
namespace_types[tlobject.namespace].append(tlobject)
|
|
||||||
type_constructors[tlobject.result].append(tlobject)
|
|
||||||
|
|
||||||
# Step 2: Generate the actual code
|
|
||||||
self._write_init_py(
|
|
||||||
self._get_file('functions'), import_depth,
|
|
||||||
namespace_functions, type_constructors
|
|
||||||
)
|
|
||||||
self._write_init_py(
|
|
||||||
self._get_file('types'), import_depth,
|
|
||||||
namespace_types, type_constructors
|
|
||||||
)
|
|
||||||
|
|
||||||
# Step 4: Once all the objects have been generated,
|
|
||||||
# we can now group them in a single file
|
|
||||||
filename = os.path.join(self._get_file('all_tlobjects.py'))
|
|
||||||
with open(filename, 'w', encoding='utf-8') as file,\
|
|
||||||
SourceBuilder(file) as builder:
|
|
||||||
builder.writeln(AUTO_GEN_NOTICE)
|
|
||||||
builder.writeln()
|
|
||||||
|
|
||||||
builder.writeln('from . import types, functions')
|
|
||||||
builder.writeln()
|
|
||||||
|
|
||||||
# Create a constant variable to indicate which layer this is
|
|
||||||
builder.writeln('LAYER = {}', TLParser.find_layer(scheme_file))
|
|
||||||
builder.writeln()
|
|
||||||
|
|
||||||
# Then create the dictionary containing constructor_id: class
|
|
||||||
builder.writeln('tlobjects = {')
|
|
||||||
builder.current_indent += 1
|
|
||||||
|
|
||||||
# Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class)
|
|
||||||
for tlobject in tlobjects:
|
|
||||||
builder.write('{:#010x}: ', tlobject.id)
|
|
||||||
builder.write('functions' if tlobject.is_function else 'types')
|
|
||||||
if tlobject.namespace:
|
|
||||||
builder.write('.' + tlobject.namespace)
|
|
||||||
|
|
||||||
builder.writeln('.{},', tlobject.class_name())
|
|
||||||
|
|
||||||
builder.current_indent -= 1
|
|
||||||
builder.writeln('}')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _write_init_py(out_dir, depth, namespace_tlobjects, type_constructors):
|
|
||||||
# namespace_tlobjects: {'namespace', [TLObject]}
|
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
|
||||||
for ns, tlobjects in namespace_tlobjects.items():
|
|
||||||
file = os.path.join(out_dir, ns + '.py' if ns else '__init__.py')
|
|
||||||
with open(file, 'w', encoding='utf-8') as f, \
|
|
||||||
SourceBuilder(f) as builder:
|
|
||||||
builder.writeln(AUTO_GEN_NOTICE)
|
|
||||||
|
|
||||||
# Both types and functions inherit from the TLObject class
|
|
||||||
# so they all can be serialized and sent, however, only the
|
|
||||||
# functions are "content_related".
|
|
||||||
builder.writeln(
|
|
||||||
'from {}.tl.tlobject import TLObject', '.' * depth
|
|
||||||
)
|
|
||||||
builder.writeln('from typing import Optional, List, '
|
|
||||||
'Union, TYPE_CHECKING')
|
|
||||||
|
|
||||||
# Add the relative imports to the namespaces,
|
|
||||||
# unless we already are in a namespace.
|
|
||||||
if not ns:
|
|
||||||
builder.writeln('from . import {}', ', '.join(
|
|
||||||
x for x in namespace_tlobjects.keys() if x
|
|
||||||
))
|
|
||||||
|
|
||||||
# Import 'os' for those needing access to 'os.urandom()'
|
|
||||||
# Currently only 'random_id' needs 'os' to be imported,
|
|
||||||
# for all those TLObjects with arg.can_be_inferred.
|
|
||||||
builder.writeln('import os')
|
|
||||||
|
|
||||||
# Import struct for the .__bytes__(self) serialization
|
|
||||||
builder.writeln('import struct')
|
|
||||||
|
|
||||||
tlobjects.sort(key=lambda x: x.name)
|
|
||||||
|
|
||||||
type_names = set()
|
|
||||||
type_defs = []
|
|
||||||
|
|
||||||
# Find all the types in this file and generate type definitions
|
|
||||||
# based on the types. The type definitions are written to the
|
|
||||||
# file at the end.
|
|
||||||
for t in tlobjects:
|
|
||||||
if not t.is_function:
|
|
||||||
type_name = t.result
|
|
||||||
if '.' in type_name:
|
|
||||||
type_name = type_name[type_name.rindex('.'):]
|
|
||||||
if type_name in type_names:
|
|
||||||
continue
|
|
||||||
type_names.add(type_name)
|
|
||||||
constructors = type_constructors[type_name]
|
|
||||||
if not constructors:
|
|
||||||
pass
|
|
||||||
elif len(constructors) == 1:
|
|
||||||
type_defs.append('Type{} = {}'.format(
|
|
||||||
type_name, constructors[0].class_name()))
|
|
||||||
else:
|
|
||||||
type_defs.append('Type{} = Union[{}]'.format(
|
|
||||||
type_name, ','.join(c.class_name()
|
|
||||||
for c in constructors)))
|
|
||||||
|
|
||||||
imports = {}
|
|
||||||
primitives = ('int', 'long', 'int128', 'int256', 'string',
|
|
||||||
'date', 'bytes', 'true')
|
|
||||||
# Find all the types in other files that are used in this file
|
|
||||||
# and generate the information required to import those types.
|
|
||||||
for t in tlobjects:
|
|
||||||
for arg in t.args:
|
|
||||||
name = arg.type
|
|
||||||
if not name or name in primitives:
|
|
||||||
continue
|
|
||||||
|
|
||||||
import_space = '{}.tl.types'.format('.' * depth)
|
|
||||||
if '.' in name:
|
|
||||||
namespace = name.split('.')[0]
|
|
||||||
name = name.split('.')[1]
|
|
||||||
import_space += '.{}'.format(namespace)
|
|
||||||
|
|
||||||
if name not in type_names:
|
|
||||||
type_names.add(name)
|
|
||||||
if name == 'date':
|
|
||||||
imports['datetime'] = ['datetime']
|
|
||||||
continue
|
|
||||||
elif import_space not in imports:
|
|
||||||
imports[import_space] = set()
|
|
||||||
imports[import_space].add('Type{}'.format(name))
|
|
||||||
|
|
||||||
# Add imports required for type checking
|
|
||||||
if imports:
|
|
||||||
builder.writeln('if TYPE_CHECKING:')
|
|
||||||
for namespace, names in imports.items():
|
|
||||||
builder.writeln('from {} import {}',
|
|
||||||
namespace, ', '.join(names))
|
|
||||||
|
|
||||||
builder.end_block()
|
|
||||||
|
|
||||||
# Generate the class for every TLObject
|
|
||||||
for t in tlobjects:
|
|
||||||
TLGenerator._write_source_code(
|
|
||||||
t, builder, depth, type_constructors
|
|
||||||
)
|
|
||||||
builder.current_indent = 0
|
|
||||||
|
|
||||||
# Write the type definitions generated earlier.
|
|
||||||
builder.writeln('')
|
|
||||||
for line in type_defs:
|
|
||||||
builder.writeln(line)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _write_source_code(tlobject, builder, depth, type_constructors):
|
|
||||||
"""
|
|
||||||
Writes the source code corresponding to the given TLObject
|
|
||||||
by making use of the ``builder`` `SourceBuilder`.
|
|
||||||
|
|
||||||
Additional information such as file path depth and
|
|
||||||
the ``Type: [Constructors]`` must be given for proper
|
|
||||||
importing and documentation strings.
|
|
||||||
"""
|
|
||||||
builder.writeln()
|
|
||||||
builder.writeln()
|
|
||||||
builder.writeln('class {}(TLObject):', tlobject.class_name())
|
|
||||||
|
|
||||||
# Class-level variable to store its Telegram's constructor ID
|
|
||||||
builder.writeln('CONSTRUCTOR_ID = {:#x}', tlobject.id)
|
|
||||||
builder.writeln('SUBCLASS_OF_ID = {:#x}',
|
|
||||||
crc32(tlobject.result.encode('ascii')))
|
|
||||||
builder.writeln()
|
|
||||||
|
|
||||||
# Flag arguments must go last
|
|
||||||
args = [
|
|
||||||
a for a in tlobject.sorted_args()
|
|
||||||
if not a.flag_indicator and not a.generic_definition
|
|
||||||
]
|
|
||||||
|
|
||||||
# Convert the args to string parameters, flags having =None
|
|
||||||
args = [
|
|
||||||
(a.name if not a.is_flag and not a.can_be_inferred
|
|
||||||
else '{}=None'.format(a.name))
|
|
||||||
for a in args
|
|
||||||
]
|
|
||||||
|
|
||||||
# Write the __init__ function
|
|
||||||
if args:
|
|
||||||
builder.writeln('def __init__(self, {}):', ', '.join(args))
|
|
||||||
else:
|
|
||||||
builder.writeln('def __init__(self):')
|
|
||||||
|
|
||||||
# Now update args to have the TLObject arguments, _except_
|
|
||||||
# those which are calculated on send or ignored, this is
|
|
||||||
# flag indicator and generic definitions.
|
|
||||||
#
|
|
||||||
# We don't need the generic definitions in Python
|
|
||||||
# because arguments can be any type
|
|
||||||
args = [arg for arg in tlobject.args
|
|
||||||
if not arg.flag_indicator and
|
|
||||||
not arg.generic_definition]
|
|
||||||
|
|
||||||
if args:
|
|
||||||
# Write the docstring, to know the type of the args
|
|
||||||
builder.writeln('"""')
|
|
||||||
for arg in args:
|
|
||||||
if not arg.flag_indicator:
|
|
||||||
builder.writeln(':param {} {}:',
|
|
||||||
arg.doc_type_hint(), arg.name)
|
|
||||||
builder.current_indent -= 1 # It will auto-indent (':')
|
|
||||||
|
|
||||||
# We also want to know what type this request returns
|
|
||||||
# or to which type this constructor belongs to
|
|
||||||
builder.writeln()
|
|
||||||
if tlobject.is_function:
|
|
||||||
builder.write(':returns {}: ', tlobject.result)
|
|
||||||
else:
|
|
||||||
builder.write('Constructor for {}: ', tlobject.result)
|
|
||||||
|
|
||||||
constructors = type_constructors[tlobject.result]
|
|
||||||
if not constructors:
|
|
||||||
builder.writeln('This type has no constructors.')
|
|
||||||
elif len(constructors) == 1:
|
|
||||||
builder.writeln('Instance of {}.',
|
|
||||||
constructors[0].class_name())
|
|
||||||
else:
|
|
||||||
builder.writeln('Instance of either {}.', ', '.join(
|
|
||||||
c.class_name() for c in constructors))
|
|
||||||
|
|
||||||
builder.writeln('"""')
|
|
||||||
|
|
||||||
builder.writeln('super().__init__()')
|
|
||||||
# Functions have a result object and are confirmed by default
|
|
||||||
if tlobject.is_function:
|
|
||||||
builder.writeln('self.result = None')
|
|
||||||
builder.writeln(
|
|
||||||
'self.content_related = True')
|
|
||||||
|
|
||||||
# Set the arguments
|
|
||||||
if args:
|
|
||||||
# Leave an empty line if there are any args
|
|
||||||
builder.writeln()
|
|
||||||
|
|
||||||
for arg in args:
|
|
||||||
if not arg.can_be_inferred:
|
|
||||||
builder.writeln('self.{0} = {0} # type: {1}',
|
|
||||||
arg.name, arg.python_type_hint())
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Currently the only argument that can be
|
|
||||||
# inferred are those called 'random_id'
|
|
||||||
if arg.name == 'random_id':
|
|
||||||
# Endianness doesn't really matter, and 'big' is shorter
|
|
||||||
code = "int.from_bytes(os.urandom({}), 'big', signed=True)" \
|
|
||||||
.format(8 if arg.type == 'long' else 4)
|
|
||||||
|
|
||||||
if arg.is_vector:
|
|
||||||
# Currently for the case of "messages.forwardMessages"
|
|
||||||
# Ensure we can infer the length from id:Vector<>
|
|
||||||
if not next(
|
|
||||||
a for a in args if a.name == 'id').is_vector:
|
|
||||||
raise ValueError(
|
|
||||||
'Cannot infer list of random ids for ', tlobject
|
|
||||||
)
|
|
||||||
code = '[{} for _ in range(len(id))]'.format(code)
|
|
||||||
|
|
||||||
builder.writeln(
|
|
||||||
"self.random_id = random_id if random_id "
|
|
||||||
"is not None else {}", code
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError('Cannot infer a value for ', arg)
|
|
||||||
|
|
||||||
builder.end_block()
|
|
||||||
|
|
||||||
# Write the resolve(self, client, utils) method
|
|
||||||
if any(arg.type in AUTO_CASTS for arg in args):
|
|
||||||
builder.writeln('def resolve(self, client, utils):')
|
|
||||||
for arg in args:
|
|
||||||
ac = AUTO_CASTS.get(arg.type, None)
|
|
||||||
if ac:
|
|
||||||
TLGenerator._write_self_assign(builder, arg, ac)
|
|
||||||
builder.end_block()
|
|
||||||
|
|
||||||
# Write the to_dict(self) method
|
|
||||||
builder.writeln('def to_dict(self):')
|
|
||||||
builder.writeln('return {')
|
|
||||||
builder.current_indent += 1
|
|
||||||
|
|
||||||
base_types = ('string', 'bytes', 'int', 'long', 'int128',
|
|
||||||
'int256', 'double', 'Bool', 'true', 'date')
|
|
||||||
|
|
||||||
builder.write("'_': '{}'", tlobject.class_name())
|
|
||||||
for arg in args:
|
|
||||||
builder.writeln(',')
|
|
||||||
builder.write("'{}': ", arg.name)
|
|
||||||
if arg.type in base_types:
|
|
||||||
if arg.is_vector:
|
|
||||||
builder.write('[] if self.{0} is None else self.{0}[:]',
|
|
||||||
arg.name)
|
|
||||||
else:
|
|
||||||
builder.write('self.{}', arg.name)
|
|
||||||
else:
|
|
||||||
if arg.is_vector:
|
|
||||||
builder.write(
|
|
||||||
'[] if self.{0} is None else [None '
|
|
||||||
'if x is None else x.to_dict() for x in self.{0}]',
|
|
||||||
arg.name
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
builder.write(
|
|
||||||
'None if self.{0} is None else self.{0}.to_dict()',
|
|
||||||
arg.name
|
|
||||||
)
|
|
||||||
|
|
||||||
builder.writeln()
|
|
||||||
builder.current_indent -= 1
|
|
||||||
builder.writeln("}")
|
|
||||||
|
|
||||||
builder.end_block()
|
|
||||||
|
|
||||||
# Write the .__bytes__() function
|
|
||||||
builder.writeln('def __bytes__(self):')
|
|
||||||
|
|
||||||
# Some objects require more than one flag parameter to be set
|
|
||||||
# at the same time. In this case, add an assertion.
|
|
||||||
repeated_args = defaultdict(list)
|
|
||||||
for arg in tlobject.args:
|
|
||||||
if arg.is_flag:
|
|
||||||
repeated_args[arg.flag_index].append(arg)
|
|
||||||
|
|
||||||
for ra in repeated_args.values():
|
|
||||||
if len(ra) > 1:
|
|
||||||
cnd1 = ('(self.{0} or self.{0} is not None)'
|
|
||||||
.format(a.name) for a in ra)
|
|
||||||
cnd2 = ('(self.{0} is None or self.{0} is False)'
|
|
||||||
.format(a.name) for a in ra)
|
|
||||||
builder.writeln(
|
|
||||||
"assert ({}) or ({}), '{} parameters must all "
|
|
||||||
"be False-y (like None) or all me True-y'",
|
|
||||||
' and '.join(cnd1), ' and '.join(cnd2),
|
|
||||||
', '.join(a.name for a in ra)
|
|
||||||
)
|
|
||||||
|
|
||||||
builder.writeln("return b''.join((")
|
|
||||||
builder.current_indent += 1
|
|
||||||
|
|
||||||
# First constructor code, we already know its bytes
|
|
||||||
builder.writeln('{},', repr(struct.pack('<I', tlobject.id)))
|
|
||||||
|
|
||||||
for arg in tlobject.args:
|
|
||||||
if TLGenerator.write_to_bytes(builder, arg, tlobject.args):
|
|
||||||
builder.writeln(',')
|
|
||||||
|
|
||||||
builder.current_indent -= 1
|
|
||||||
builder.writeln('))')
|
|
||||||
builder.end_block()
|
|
||||||
|
|
||||||
# Write the static from_reader(reader) function
|
|
||||||
builder.writeln('@staticmethod')
|
|
||||||
builder.writeln('def from_reader(reader):')
|
|
||||||
for arg in tlobject.args:
|
|
||||||
TLGenerator.write_read_code(
|
|
||||||
builder, arg, tlobject.args, name='_' + arg.name
|
|
||||||
)
|
|
||||||
|
|
||||||
builder.writeln(
|
|
||||||
'return {}({})',
|
|
||||||
tlobject.class_name(),
|
|
||||||
', '.join(
|
|
||||||
'{0}=_{0}'.format(a.name) for a in tlobject.sorted_args()
|
|
||||||
if not a.flag_indicator and not a.generic_definition
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Only requests can have a different response that's not their
|
|
||||||
# serialized body, that is, we'll be setting their .result.
|
|
||||||
#
|
|
||||||
# The default behaviour is reading a TLObject too, so no need
|
|
||||||
# to override it unless necessary.
|
|
||||||
if tlobject.is_function and not TLGenerator._is_boxed(tlobject.result):
|
|
||||||
builder.end_block()
|
|
||||||
builder.writeln('def on_response(self, reader):')
|
|
||||||
TLGenerator.write_request_result_code(builder, tlobject)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _is_boxed(type_):
|
|
||||||
# https://core.telegram.org/mtproto/serialize#boxed-and-bare-types
|
|
||||||
# TL;DR; boxed types start with uppercase always, so we can use
|
|
||||||
# this to check whether everything in it is boxed or not.
|
|
||||||
#
|
|
||||||
# The API always returns a boxed type, but it may inside a Vector<>
|
|
||||||
# or a namespace, and the Vector may have a not-boxed type. For this
|
|
||||||
# reason we find whatever index, '<' or '.'. If neither are present
|
|
||||||
# we will get -1, and the 0th char is always upper case thus works.
|
|
||||||
# For Vector types and namespaces, it will check in the right place.
|
|
||||||
check_after = max(type_.find('<'), type_.find('.'))
|
|
||||||
return type_[check_after + 1].isupper()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _write_self_assign(builder, arg, get_input_code):
|
|
||||||
"""Writes self.arg = input.format(self.arg), considering vectors."""
|
|
||||||
if arg.is_vector:
|
|
||||||
builder.write('self.{0} = [{1} for _x in self.{0}]',
|
|
||||||
arg.name, get_input_code.format('_x'))
|
|
||||||
else:
|
|
||||||
builder.write('self.{} = {}',
|
|
||||||
arg.name, get_input_code.format('self.' + arg.name))
|
|
||||||
|
|
||||||
builder.writeln(
|
|
||||||
' if self.{} else None'.format(arg.name) if arg.is_flag else ''
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_file_name(tlobject, add_extension=False):
|
|
||||||
"""Gets the file name in file_name_format.py for the given TLObject"""
|
|
||||||
|
|
||||||
# Courtesy of http://stackoverflow.com/a/1176023/4759433
|
|
||||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', tlobject.name)
|
|
||||||
result = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
|
||||||
if add_extension:
|
|
||||||
return result + '.py'
|
|
||||||
else:
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def write_to_bytes(builder, arg, args, name=None):
|
|
||||||
"""
|
|
||||||
Writes the .__bytes__() code for the given argument
|
|
||||||
:param builder: The source code builder
|
|
||||||
:param arg: The argument to write
|
|
||||||
:param args: All the other arguments in TLObject same __bytes__.
|
|
||||||
This is required to determine the flags value
|
|
||||||
:param name: The name of the argument. Defaults to "self.argname"
|
|
||||||
This argument is an option because it's required when
|
|
||||||
writing Vectors<>
|
|
||||||
"""
|
|
||||||
if arg.generic_definition:
|
|
||||||
return # Do nothing, this only specifies a later type
|
|
||||||
|
|
||||||
if name is None:
|
|
||||||
name = 'self.{}'.format(arg.name)
|
|
||||||
|
|
||||||
# The argument may be a flag, only write if it's not None AND
|
|
||||||
# if it's not a True type.
|
|
||||||
# True types are not actually sent, but instead only used to
|
|
||||||
# determine the flags.
|
|
||||||
if arg.is_flag:
|
|
||||||
if arg.type == 'true':
|
|
||||||
return # Exit, since True type is never written
|
|
||||||
elif arg.is_vector:
|
|
||||||
# Vector flags are special since they consist of 3 values,
|
|
||||||
# so we need an extra join here. Note that empty vector flags
|
|
||||||
# should NOT be sent either!
|
|
||||||
builder.write("b'' if {0} is None or {0} is False "
|
|
||||||
"else b''.join((", name)
|
|
||||||
else:
|
|
||||||
builder.write("b'' if {0} is None or {0} is False "
|
|
||||||
"else (", name)
|
|
||||||
|
|
||||||
if arg.is_vector:
|
|
||||||
if arg.use_vector_id:
|
|
||||||
# vector code, unsigned 0x1cb5c415 as little endian
|
|
||||||
builder.write(r"b'\x15\xc4\xb5\x1c',")
|
|
||||||
|
|
||||||
builder.write("struct.pack('<i', len({})),", name)
|
|
||||||
|
|
||||||
# Cannot unpack the values for the outer tuple through *[(
|
|
||||||
# since that's a Python >3.5 feature, so add another join.
|
|
||||||
builder.write("b''.join(")
|
|
||||||
|
|
||||||
# Temporary disable .is_vector, not to enter this if again
|
|
||||||
# Also disable .is_flag since it's not needed per element
|
|
||||||
old_flag = arg.is_flag
|
|
||||||
arg.is_vector = arg.is_flag = False
|
|
||||||
TLGenerator.write_to_bytes(builder, arg, args, name='x')
|
|
||||||
arg.is_vector = True
|
|
||||||
arg.is_flag = old_flag
|
|
||||||
|
|
||||||
builder.write(' for x in {})', name)
|
|
||||||
|
|
||||||
elif arg.flag_indicator:
|
|
||||||
# Calculate the flags with those items which are not None
|
|
||||||
if not any(f.is_flag for f in args):
|
|
||||||
# There's a flag indicator, but no flag arguments so it's 0
|
|
||||||
builder.write(r"b'\0\0\0\0'")
|
|
||||||
else:
|
|
||||||
builder.write("struct.pack('<I', ")
|
|
||||||
builder.write(
|
|
||||||
' | '.join('(0 if {0} is None or {0} is False else {1})'
|
|
||||||
.format('self.{}'.format(flag.name),
|
|
||||||
1 << flag.flag_index)
|
|
||||||
for flag in args if flag.is_flag)
|
|
||||||
)
|
|
||||||
builder.write(')')
|
|
||||||
|
|
||||||
elif 'int' == arg.type:
|
|
||||||
# struct.pack is around 4 times faster than int.to_bytes
|
|
||||||
builder.write("struct.pack('<i', {})", name)
|
|
||||||
|
|
||||||
elif 'long' == arg.type:
|
|
||||||
builder.write("struct.pack('<q', {})", name)
|
|
||||||
|
|
||||||
elif 'int128' == arg.type:
|
|
||||||
builder.write("{}.to_bytes(16, 'little', signed=True)", name)
|
|
||||||
|
|
||||||
elif 'int256' == arg.type:
|
|
||||||
builder.write("{}.to_bytes(32, 'little', signed=True)", name)
|
|
||||||
|
|
||||||
elif 'double' == arg.type:
|
|
||||||
builder.write("struct.pack('<d', {})", name)
|
|
||||||
|
|
||||||
elif 'string' == arg.type:
|
|
||||||
builder.write('TLObject.serialize_bytes({})', name)
|
|
||||||
|
|
||||||
elif 'Bool' == arg.type:
|
|
||||||
# 0x997275b5 if boolean else 0xbc799737
|
|
||||||
builder.write(r"b'\xb5ur\x99' if {} else b'7\x97y\xbc'", name)
|
|
||||||
|
|
||||||
elif 'true' == arg.type:
|
|
||||||
pass # These are actually NOT written! Only used for flags
|
|
||||||
|
|
||||||
elif 'bytes' == arg.type:
|
|
||||||
builder.write('TLObject.serialize_bytes({})', name)
|
|
||||||
|
|
||||||
elif 'date' == arg.type: # Custom format
|
|
||||||
builder.write('TLObject.serialize_datetime({})', name)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Else it may be a custom type
|
|
||||||
builder.write('bytes({})', name)
|
|
||||||
|
|
||||||
if arg.is_flag:
|
|
||||||
builder.write(')')
|
|
||||||
if arg.is_vector:
|
|
||||||
builder.write(')') # We were using a tuple
|
|
||||||
|
|
||||||
return True # Something was written
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def write_read_code(builder, arg, args, name):
|
|
||||||
"""
|
|
||||||
Writes the read code for the given argument, setting the
|
|
||||||
arg.name variable to its read value.
|
|
||||||
|
|
||||||
:param builder: The source code builder
|
|
||||||
:param arg: The argument to write
|
|
||||||
:param args: All the other arguments in TLObject same on_send.
|
|
||||||
This is required to determine the flags value
|
|
||||||
:param name: The name of the argument. Defaults to "self.argname"
|
|
||||||
This argument is an option because it's required when
|
|
||||||
writing Vectors<>
|
|
||||||
"""
|
|
||||||
|
|
||||||
if arg.generic_definition:
|
|
||||||
return # Do nothing, this only specifies a later type
|
|
||||||
|
|
||||||
# The argument may be a flag, only write that flag was given!
|
|
||||||
was_flag = False
|
|
||||||
if arg.is_flag:
|
|
||||||
# Treat 'true' flags as a special case, since they're true if
|
|
||||||
# they're set, and nothing else needs to actually be read.
|
|
||||||
if 'true' == arg.type:
|
|
||||||
builder.writeln('{} = bool(flags & {})',
|
|
||||||
name, 1 << arg.flag_index)
|
|
||||||
return
|
|
||||||
|
|
||||||
was_flag = True
|
|
||||||
builder.writeln('if flags & {}:', 1 << arg.flag_index)
|
|
||||||
# Temporary disable .is_flag not to enter this if
|
|
||||||
# again when calling the method recursively
|
|
||||||
arg.is_flag = False
|
|
||||||
|
|
||||||
if arg.is_vector:
|
|
||||||
if arg.use_vector_id:
|
|
||||||
# We have to read the vector's constructor ID
|
|
||||||
builder.writeln("reader.read_int()")
|
|
||||||
|
|
||||||
builder.writeln('{} = []', name)
|
|
||||||
builder.writeln('for _ in range(reader.read_int()):')
|
|
||||||
# Temporary disable .is_vector, not to enter this if again
|
|
||||||
arg.is_vector = False
|
|
||||||
TLGenerator.write_read_code(builder, arg, args, name='_x')
|
|
||||||
builder.writeln('{}.append(_x)', name)
|
|
||||||
arg.is_vector = True
|
|
||||||
|
|
||||||
elif arg.flag_indicator:
|
|
||||||
# Read the flags, which will indicate what items we should read next
|
|
||||||
builder.writeln('flags = reader.read_int()')
|
|
||||||
builder.writeln()
|
|
||||||
|
|
||||||
elif 'int' == arg.type:
|
|
||||||
builder.writeln('{} = reader.read_int()', name)
|
|
||||||
|
|
||||||
elif 'long' == arg.type:
|
|
||||||
builder.writeln('{} = reader.read_long()', name)
|
|
||||||
|
|
||||||
elif 'int128' == arg.type:
|
|
||||||
builder.writeln('{} = reader.read_large_int(bits=128)', name)
|
|
||||||
|
|
||||||
elif 'int256' == arg.type:
|
|
||||||
builder.writeln('{} = reader.read_large_int(bits=256)', name)
|
|
||||||
|
|
||||||
elif 'double' == arg.type:
|
|
||||||
builder.writeln('{} = reader.read_double()', name)
|
|
||||||
|
|
||||||
elif 'string' == arg.type:
|
|
||||||
builder.writeln('{} = reader.tgread_string()', name)
|
|
||||||
|
|
||||||
elif 'Bool' == arg.type:
|
|
||||||
builder.writeln('{} = reader.tgread_bool()', name)
|
|
||||||
|
|
||||||
elif 'true' == arg.type:
|
|
||||||
# Arbitrary not-None value, don't actually read "true" flags
|
|
||||||
builder.writeln('{} = True', name)
|
|
||||||
|
|
||||||
elif 'bytes' == arg.type:
|
|
||||||
builder.writeln('{} = reader.tgread_bytes()', name)
|
|
||||||
|
|
||||||
elif 'date' == arg.type: # Custom format
|
|
||||||
builder.writeln('{} = reader.tgread_date()', name)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Else it may be a custom type
|
|
||||||
if not arg.skip_constructor_id:
|
|
||||||
builder.writeln('{} = reader.tgread_object()', name)
|
|
||||||
else:
|
|
||||||
# Import the correct type inline to avoid cyclic imports.
|
|
||||||
# There may be better solutions so that we can just access
|
|
||||||
# all the types before the files have been parsed, but I
|
|
||||||
# don't know of any.
|
|
||||||
sep_index = arg.type.find('.')
|
|
||||||
if sep_index == -1:
|
|
||||||
ns, t = '.', arg.type
|
|
||||||
else:
|
|
||||||
ns, t = '.' + arg.type[:sep_index], arg.type[sep_index+1:]
|
|
||||||
class_name = TLObject.class_name_for(t)
|
|
||||||
|
|
||||||
# There would be no need to import the type if we're in the
|
|
||||||
# file with the same namespace, but since it does no harm
|
|
||||||
# and we don't have information about such thing in the
|
|
||||||
# method we just ignore that case.
|
|
||||||
builder.writeln('from {} import {}', ns, class_name)
|
|
||||||
builder.writeln('{} = {}.from_reader(reader)',
|
|
||||||
name, class_name)
|
|
||||||
|
|
||||||
# End vector and flag blocks if required (if we opened them before)
|
|
||||||
if arg.is_vector:
|
|
||||||
builder.end_block()
|
|
||||||
|
|
||||||
if was_flag:
|
|
||||||
builder.current_indent -= 1
|
|
||||||
builder.writeln('else:')
|
|
||||||
builder.writeln('{} = None', name)
|
|
||||||
builder.current_indent -= 1
|
|
||||||
# Restore .is_flag
|
|
||||||
arg.is_flag = True
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def write_request_result_code(builder, tlobject):
|
|
||||||
"""
|
|
||||||
Writes the receive code for the given function
|
|
||||||
|
|
||||||
:param builder: The source code builder
|
|
||||||
:param tlobject: The TLObject for which the 'self.result = '
|
|
||||||
will be written
|
|
||||||
"""
|
|
||||||
if tlobject.result.startswith('Vector<'):
|
|
||||||
# Vector results are a bit special since they can also be composed
|
|
||||||
# of integer values and such; however, the result of requests is
|
|
||||||
# not parsed as arguments are and it's a bit harder to tell which
|
|
||||||
# is which.
|
|
||||||
if tlobject.result == 'Vector<int>':
|
|
||||||
builder.writeln('reader.read_int() # Vector ID')
|
|
||||||
builder.writeln('count = reader.read_int()')
|
|
||||||
builder.writeln(
|
|
||||||
'self.result = [reader.read_int() for _ in range(count)]'
|
|
||||||
)
|
|
||||||
elif tlobject.result == 'Vector<long>':
|
|
||||||
builder.writeln('reader.read_int() # Vector ID')
|
|
||||||
builder.writeln('count = reader.read_long()')
|
|
||||||
builder.writeln(
|
|
||||||
'self.result = [reader.read_long() for _ in range(count)]'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
builder.writeln('self.result = reader.tgread_vector()')
|
|
||||||
else:
|
|
||||||
builder.writeln('self.result = reader.tgread_object()')
|
|
8
telethon_generator/utils.py
Normal file
8
telethon_generator/utils.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def snake_to_camel_case(name, suffix=None):
|
||||||
|
# Courtesy of http://stackoverflow.com/a/31531797/4759433
|
||||||
|
result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)
|
||||||
|
result = result[:1].upper() + result[1:].replace('_', '')
|
||||||
|
return result + suffix if suffix else result
|
Loading…
Reference in New Issue
Block a user