diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 3e4905b1..337377cb 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -775,3 +775,5 @@ raise_last_call_error is now the default rather than ValueError self-produced updates like getmessage now also trigger a handler input_peer removed from get_me; input peers should remain mostly an impl detail + +raw api types and fns are now immutable. this can enable optimizations in the future. diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 480263e4..92dc1b65 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -8,6 +8,7 @@ import time import typing import ipaddress import dataclasses +import functools from .. import version, __name__ as __base_name__, _tl from .._crypto import rsa @@ -182,7 +183,8 @@ def init( default_device_model = system.machine default_system_version = re.sub(r'-.+','',system.release) - self._init_request = _tl.fn.InitConnection( + self._init_request = functools.partial( + _tl.fn.InitConnection, api_id=self._api_id, device_model=device_model or default_device_model or 'Unknown', system_version=system_version or default_system_version or '1.0', @@ -190,8 +192,6 @@ def init( lang_code=lang_code, system_lang_code=system_lang_code, lang_pack='', # "langPacks are for official apps only" - query=None, - proxy=None ) self._sender = MTProtoSender( @@ -272,10 +272,8 @@ async def connect(self: 'TelegramClient') -> None: # Need to send invokeWithLayer for things to work out. # Make the most out of this opportunity by also refreshing our state. # During the v1 to v2 migration, this also correctly sets the IPv* columns. - self._init_request.query = _tl.fn.help.GetConfig() - config = await self._sender.send(_tl.fn.InvokeWithLayer( - _tl.LAYER, self._init_request + _tl.LAYER, self._init_request(query=_tl.fn.help.GetConfig()) )) for dc in config.dc_options: @@ -318,7 +316,6 @@ async def disconnect(self: 'TelegramClient'): def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): init_proxy = None - self._init_request.proxy = init_proxy self._proxy = proxy # While `await client.connect()` passes new proxy on each new call, @@ -408,8 +405,9 @@ async def _create_exported_sender(self: 'TelegramClient', dc_id): )) self._log[__name__].info('Exporting auth for new borrowed sender in %s', dc) auth = await self(_tl.fn.auth.ExportAuthorization(dc_id)) - self._init_request.query = _tl.fn.auth.ImportAuthorization(id=auth.id, bytes=auth.bytes) - req = _tl.fn.InvokeWithLayer(_tl.LAYER, self._init_request) + req = _tl.fn.InvokeWithLayer(_tl.LAYER, self._init_request( + query=_tl.fn.auth.ImportAuthorization(id=auth.id, bytes=auth.bytes) + )) await sender.send(req) return sender diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 05f925bd..763a31c4 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -35,10 +35,11 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl if flood_sleep_threshold is None: flood_sleep_threshold = self.flood_sleep_threshold requests = (request if utils.is_list_like(request) else (request,)) + new_requests = [] for r in requests: if not isinstance(r, _tl.TLRequest): raise _NOT_A_REQUEST() - await r.resolve(self, utils) + r = await r.resolve(self, utils) # Avoid making the request if it's already in a flood wait if r.CONSTRUCTOR_ID in self._flood_waited_requests: @@ -59,6 +60,9 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl if self._no_updates: r = _tl.fn.InvokeWithoutUpdates(r) + new_requests.append(r) + request = new_requests if utils.is_list_like(request) else new_requests[0] + request_index = 0 last_error = None self._last_request = time.time() diff --git a/telethon/_misc/tlobject.py b/telethon/_misc/tlobject.py index 6b4bddf8..397dc1b0 100644 --- a/telethon/_misc/tlobject.py +++ b/telethon/_misc/tlobject.py @@ -155,4 +155,4 @@ class TLRequest(TLObject): return reader.tgread_object() async def resolve(self, client, utils): - pass + return self diff --git a/telethon_generator/generators/tlobject.py b/telethon_generator/generators/tlobject.py index c1e5e14a..3c0959c1 100644 --- a/telethon_generator/generators/tlobject.py +++ b/telethon_generator/generators/tlobject.py @@ -85,6 +85,9 @@ def _write_modules( # Import struct for the .__bytes__(self) serialization builder.writeln('import struct') + # Import dataclasses in order to freeze the instances + builder.writeln('import dataclasses') + # Import datetime for type hinting builder.writeln('from datetime import datetime') @@ -187,37 +190,9 @@ def _write_source_code(tlobject, kind, builder, type_constructors): def _write_class_init(tlobject, kind, type_constructors, builder): builder.writeln() builder.writeln() + builder.writeln('@dataclasses.dataclass(init=False, frozen=True)') builder.writeln('class {}({}):', tlobject.class_name, kind) - # Define slots to help reduce the size of the objects a little bit. - # It's also good for knowing what fields an object has. - builder.write('__slots__ = (') - sep = '' - for arg in tlobject.real_args: - builder.write('{}{!r},', sep, arg.name) - sep = ' ' - builder.writeln(')') - - # 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 = ['{}: {}{}'.format( - a.name, a.type_hint(), '=None' if a.is_flag or a.can_be_inferred else '') - for a in tlobject.real_args - ] - - # Write the __init__ function if it has any argument - if not tlobject.real_args: - return - - if any(a.name in dir(builtins) for a in tlobject.real_args): - builder.writeln('# noinspection PyShadowingBuiltins') - - builder.writeln("def __init__({}):", ', '.join(['self'] + args)) builder.writeln('"""') if tlobject.is_function: builder.write(':returns {}: ', tlobject.result) @@ -236,47 +211,83 @@ def _write_class_init(tlobject, kind, type_constructors, builder): builder.writeln('"""') - # Set the arguments + # Define slots to help reduce the size of the objects a little bit. + # It's also good for knowing what fields an object has. + builder.write('__slots__ = (') + sep = '' for arg in tlobject.real_args: - if not arg.can_be_inferred: - builder.writeln('self.{0} = {0}', arg.name) + builder.write('{}{!r},', sep, arg.name) + sep = ' ' + builder.writeln(')') - # 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) + # 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() - 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) + # Because we're using __slots__ and frozen instances, we cannot have flags = None directly. + # See https://stackoverflow.com/q/50180735 (Python 3.10 does offer a solution). + # Write the __init__ function if it has any argument. + if tlobject.real_args: + # Convert the args to string parameters + for a in tlobject.real_args: + builder.writeln('{}: {}', a.name, a.type_hint()) - builder.writeln( - "self.random_id = random_id if random_id " - "is not None else {}", code - ) - else: - raise ValueError('Cannot infer a value for ', arg) + # Convert the args to string parameters, flags having =None + args = ['{}: {}{}'.format( + a.name, a.type_hint(), '=None' if a.is_flag or a.can_be_inferred else '') + for a in tlobject.real_args + ] - builder.end_block() + if any(a.name in dir(builtins) for a in tlobject.real_args): + builder.writeln('# noinspection PyShadowingBuiltins') + + builder.writeln("def __init__({}):", ', '.join(['self'] + args)) + + # Set the arguments + for arg in tlobject.real_args: + builder.writeln("object.__setattr__(self, '{0}', {0})", arg.name) + + builder.end_block() def _write_resolve(tlobject, builder): if tlobject.is_function and any( - (arg.type in AUTO_CASTS - or ((arg.name, arg.type) in NAMED_AUTO_CASTS - and tlobject.fullname not in NAMED_BLACKLIST)) - for arg in tlobject.real_args + (arg.can_be_inferred + or arg.type in AUTO_CASTS + or ((arg.name, arg.type) in NAMED_AUTO_CASTS and tlobject.fullname not in NAMED_BLACKLIST)) + for arg in tlobject.real_args ): builder.writeln('async def resolve(self, client, utils):') + builder.writeln('r = {}') # hold replacements + for arg in tlobject.real_args: + if arg.can_be_inferred: + builder.writeln('if self.{} is None:', arg.name) + + # 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 tlobject.real_args if a.name == 'id').is_vector: + raise ValueError('Cannot infer list of random ids for ', tlobject) + + code = '[{} for _ in range(len(self.id))]'.format(code) + + builder.writeln("r['{}'] = {}", arg.name, code) + else: + raise ValueError('Cannot infer a value for ', arg) + + builder.end_block() + continue + ac = AUTO_CASTS.get(arg.type) if not ac: ac = NAMED_AUTO_CASTS.get((arg.name, arg.type)) @@ -287,17 +298,17 @@ def _write_resolve(tlobject, builder): builder.writeln('if self.{}:', arg.name) if arg.is_vector: - builder.writeln('_tmp = []') - builder.writeln('for _x in self.{0}:', arg.name) - builder.writeln('_tmp.append({})', ac.format('_x')) + builder.writeln("r['{}'] = []", arg.name) + builder.writeln('for x in self.{0}:', arg.name) + builder.writeln("r['{}'].append({})", arg.name, ac.format('x')) builder.end_block() - builder.writeln('self.{} = _tmp', arg.name) else: - builder.writeln('self.{} = {}', arg.name, - ac.format('self.' + arg.name)) + builder.writeln("r['{}'] = {}", arg.name, ac.format('self.' + arg.name)) if arg.is_flag: builder.end_block() + + builder.writeln('return dataclasses.replace(self, **r)') builder.end_block()