#!/usr/bin/env python """Run `python script_name.py -h` for info. WARNING: Save a backup of your code base before running this! """ import argparse import re import sys from pathlib import Path from typing import Callable, Sequence, Pattern, Tuple CALLBACK_TRANSITION_PATTERN = re.compile( r"(? str: contents = re.sub( CALLBACK_TRANSITION_PATTERN, r"async def \1(\2\3, \5: \7ContextTypes.DEFAULT_TYPE)", contents, ) contents = re.sub( JOB_TRANSITION_PATTERN, r"async def \1(\2: \4ContextTypes.DEFAULT_TYPE)", contents ) # this line is for users transitioning from v20.0a0 to 20.0a1 return contents.replace("CallbackContext.DEFAULT_TYPE", "ContextTypes.DEFAULT_TYPE") AWAIT_BOT_METHODS_PATTERNS: Sequence[Tuple[Pattern, str]] = [ (re.compile(r"(? str: for pattern, replace in AWAIT_BOT_METHODS_PATTERNS: contents = re.sub(pattern, replace, contents) return contents RUN_ASYNC_PATTERNS = [ (re.compile(r"run_async=True"), "block=False"), (re.compile(r"run_async=False"), "block=True"), ] def run_async_transition(_: Path, contents: str) -> str: for pattern, replace in RUN_ASYNC_PATTERNS: contents = re.sub(pattern, replace, contents) return contents def init_transition(_: Path, contents: str) -> str: return re.sub( r"Updater\((token=|)([\w\d:\'\"\[\]\.\(\)\s]+)\)", r"Application.builder().token(\2).build()", contents, ) def updater_transition(_: Path, contents: str) -> str: return re.sub(r"updater\.start_(polling|webhook)\(", r"application.run_\1(", contents) def dispatcher_transition(_: Path, contents: str) -> str: return contents.replace("dispatcher", "application") def run_async_decorator_transition(_: Path, contents: str) -> str: return contents.replace("@run_async\n", "") def use_context_transition(_: Path, contents: str) -> str: contents = re.sub(r"use_context=(True|False)(,|\n)", "", contents) return re.sub(r"use_context=(True|False)\)", ")", contents) def filters_transition(_: Path, contents: str) -> str: # Order of the replacements matters! # Filters.text -> Filters.TEXT contents = re.sub( r"Filters\.(\w+)([ ~&\^,\)])", lambda match: f"filters.{match.group(1).upper()}{match.group(2)}", contents, ) # Filters.chat(…) -> filters.Chat(…) contents = re.sub( r"Filters\.(\w)(\w+)\(", lambda match: f"filters.{match.group(1).upper()}{match.group(2)}(", contents, ) # Filters.status_update.new_chat_member -> filters.StatusUpdate.new_chat_member contents = re.sub( r"Filters\.([\w_]+)\.", lambda match: f"filters.{''.join(word.title() for word in match.group(1).split('_'))}.", contents, ) # Filters.Dice.darts(…) -> filters.Dice.Darts(…) contents = re.sub( r"filters\.([\w]+)\.([\w_]+)\(", lambda match: ( f"filters.{match.group(1)}.{''.join(word.title() for word in match.group(2).split('_'))}(" ), contents, ) # filters.StatusUpdate.new_chat_member -> filters.StatusUpdate.NEW_CHAT_MEMBER contents = re.sub( r"filters\.([\w]+)\.([\w_]+)([ ~&\^,\)])", lambda match: f"filters.{match.group(1)}.{match.group(2).upper()}{match.group(3)}", contents, ) return contents def job_pass_data_transition(_: Path, contents: str) -> str: return re.sub(r"context=context.(user|chat)_data", r"\1_id=update.effective_\1.id", contents) def job_context_to_data_rename_transition(_: Path, contents: str) -> str: contents = re.sub( r"run_(\w+)\(([\w,].*)?(context=)(\w+)([\w,].*)\)", r"run_\1\(\2data=\4\5\)", contents ) return contents.replace("context.job.context", "context.job.data") def video_chat_transiton(_: Path, contents: str) -> str: return ( contents.replace("voice_chat", "video_chat") .replace("VoiceChat", "VideoChat") .replace("VOICE_CHAT", "VIDEO_CHAT") ) def updater_start_arguments_transition(_: Path, contents: str) -> str: return ( contents.replace("clean=True", "drop_pending_updates=True") .replace("force_event_loop=True,", "") .replace("force_event_loop=True),", ")") ) def renamed_bot_methods_transition(_: Path, contents: str) -> str: return ( contents.replace("kick_chat_member", "ban_chat_member") .replace("kickChatMember", "banChatMember") .replace("kick_member", "ban_member") .replace("get_chat_members_count", "get_chat_member_count") .replace("getChatMembersCount", "getChatMemberCount") .replace("get_members_count", "get_member_count") ) def rename_handler_to_base_handler(_: Path, contents: str) -> str: return re.sub(r"(class \w+)\((telegram\.ext\.|)Handler\)", r"\1\(\2BaseHandler)\)", contents) TRANSITIONS: Sequence[Callable[[Path, str], str]] = [ callback_signature_transition, await_bot_methods_transition, run_async_transition, use_context_transition, # place before init_transition init_transition, updater_transition, dispatcher_transition, run_async_decorator_transition, filters_transition, job_pass_data_transition, job_context_to_data_rename_transition, # place after job_pass_data_transition video_chat_transiton, updater_start_arguments_transition, renamed_bot_methods_transition, rename_handler_to_base_handler, ] def transition_file(file: Path) -> None: file_contents = file.read_text(encoding="utf8") for transition in TRANSITIONS: file_contents = transition(file, file_contents) file.write_text(file_contents, encoding="utf8") def run_transition(files: Sequence[str], recurse: bool) -> None: for file in files: path = Path(file) if path.is_dir(): if recurse: for file_path in path.rglob("*.py"): transition_file(file_path) else: transition_file(path) if __name__ == "__main__": parser = argparse.ArgumentParser( description=( "Helper script ease transition to PTB v20. Manual work will still be required! " "This script will likely produce some false transitions. " "If you encounter import errors, try passing the files one by one. " "Example usage: `python script_name.py path/to/file.py path/to/directory -r`." ) ) parser.add_argument( "files", nargs="*", help="Use to specify the names of the files or directories that should be transitioned. ", ) parser.add_argument( "-r", "--recurse", default=False, dest="recurse", action="store_true", help="Whether to recurse through directories searching for Python files.", ) args = parser.parse_args() sys.exit(run_transition(files=args.files, recurse=args.recurse))
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: