├── .circleci └── config.yml ├── .gitignore ├── LICENSE.md ├── MANIFEST ├── README.rst ├── async2rewrite ├── __init__.py ├── __main__.py ├── main.py ├── tests │ ├── __init__.py │ ├── test_ext_changes.py │ ├── test_game_to_activity.py │ ├── test_property_changes.py │ ├── test_server_to_guild.py │ ├── test_snowflakes.py │ └── test_stateful_models.py └── transformers.py ├── logo.png ├── requirements.txt ├── sample_code.py ├── setup.cfg ├── setup.py ├── test-requirements.txt └── tox.ini /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | 2 | version: 2 3 | jobs: 4 | tox: 5 | docker: 6 | - image: themattrix/tox 7 | 8 | working_directory: ~/repo 9 | 10 | steps: 11 | - run: 12 | name: Install git for codecov 13 | command: apt update && apt install git -y 14 | 15 | - checkout 16 | 17 | - run: 18 | name: pytest via tox 19 | command: tox 20 | 21 | workflows: 22 | version: 2 23 | build: 24 | jobs: 25 | - tox 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | \.idea/ 3 | *.pyc 4 | run\.py 5 | .pypirc 6 | .vscode/* 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2017 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.cfg 3 | setup.py 4 | async2rewrite\__init__.py 5 | async2rewrite\__main__.py 6 | async2rewrite\main.py 7 | async2rewrite\transformers.py 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | DEPRECATED 2 | ---------- 3 | It was fun while it lasted, everyone. async2rewrite will remain here as an archived repo. It will still be on pip (for 4 | the time being), and it will still work for converting some of async to rewrite. This project will receive no more updates. 5 | 6 | .. image:: https://github.com/TheTrain2000/async2rewrite/blob/master/logo.png?raw=true 7 | :align: center 8 | 9 | .. image:: https://img.shields.io/pypi/v/async2rewrite.svg 10 | :target: https://pypi.python.org/pypi/async2rewrite 11 | .. image:: https://img.shields.io/codecov/c/github/TheTrain2000/async2rewrite.svg 12 | :target: https://codecov.io/gh/TheTrain2000/async2rewrite 13 | 14 | Automatically convert discord.py async branch code to rewrite branch code. 15 | 16 | async2rewrite does not complete 100% of the necessary conversions. It is a tool designed to minimize the amount of 17 | tedious work required. async2rewrite will warn upon changes that it cannot make itself. Make sure to read the migration 18 | documentation for rewrite when using this tool. 19 | 20 | `Migration Documentation`_ 21 | `Commercial 1`_ 22 | 23 | .. _Migration Documentation: https://discordpy.readthedocs.io/en/rewrite/migrating.html 24 | 25 | .. _Commercial 1: https://youtu.be/R-ZLNU-MQL8 26 | 27 | Installation 28 | ------------ 29 | 30 | .. code:: sh 31 | 32 | python -m pip install -U async2rewrite 33 | 34 | Usage 35 | ----- 36 | 37 | Command Line 38 | ~~~~~~~~~~~~ 39 | 40 | When converting files via the command line, async2rewrite will create a new Python 41 | file with the specified suffix. If no suffix is specified, the default suffix is used. 42 | 43 | For file paths, a path to a directory may also be passed. The library will locate all 44 | Python files recursively inside of the passed directory. 45 | 46 | Single File 47 | ^^^^^^^^^^^ 48 | 49 | .. code:: sh 50 | 51 | python -m async2rewrite file/path 52 | 53 | Multiple Files 54 | ^^^^^^^^^^^^^^ 55 | 56 | async2rewrite can automatically convert multiple files at once. 57 | 58 | .. code:: sh 59 | 60 | python -m async2rewrite file/path1 file/path2 ... 61 | 62 | Specifying a Suffix 63 | ^^^^^^^^^^^^^^^^^^^ 64 | 65 | Use the ``--suffix`` flag to denote a custom suffix to add the the new file. 66 | The default suffix is ``.a2r.py``. 67 | 68 | Example: 69 | 70 | .. code:: sh 71 | 72 | python -m async2rewrite file/path --suffix .my_suffix.py 73 | 74 | Printing the Output 75 | ^^^^^^^^^^^^^^^^^^^ 76 | 77 | If you would like to print the output instead of writing to a new file, 78 | the ``--print`` flag can be used. 79 | 80 | Example: 81 | 82 | .. code:: sh 83 | 84 | python -m async2rewrite file/path --print 85 | 86 | Module 87 | ~~~~~~ 88 | 89 | Converting a File 90 | ^^^^^^^^^^^^^^^^^ 91 | 92 | The ``from_file()`` method returns a dictionary. The dictionary keys are the file names, 93 | and the values can either be a tuple or a string. If ``stats=True`` or ``include_ast=True``, then 94 | ``from_file()`` will return a tuple. The 0th index in the tuple will always be the converted code. 95 | 96 | .. code:: py 97 | 98 | import async2rewrite 99 | 100 | file_result = async2rewrite.from_file('file/path') 101 | print(file_result['file/path']) # file_result contains the converted code. 102 | 103 | Multiple files can be converted by passing an unpacked list into ``from_file()``. 104 | 105 | Example: 106 | 107 | .. code:: py 108 | 109 | results = async2rewrite.from_file('file/path', 'file/path2', 'file/path3', ...) 110 | 111 | for converted_file in results: # from_file() returns a dictionary. 112 | print(converted_file) # Print out the result of each file. 113 | 114 | Converting from Text 115 | ^^^^^^^^^^^^^^^^^^^^ 116 | 117 | .. code:: py 118 | 119 | import async2rewrite 120 | 121 | text_result = async2rewrite.from_text('async def on_command_error(ctx, error): pass') 122 | print(text_result) # text_result contains the converted code. 123 | 124 | Getting Statistics 125 | ^^^^^^^^^^^^^^^^^^ 126 | 127 | .. code:: py 128 | 129 | import async2rewrite 130 | 131 | stats = async2rewrite.from_file('file/path', stats=True) 132 | print(stats['file/path']) # stats=True makes from_x return a collections Counter. 133 | 134 | Thanks 135 | ------ 136 | 137 | * Pantsu for forking and editing `astunparse `_ to not insert unnecessary parentheses. 138 | * Reina for the logo idea 139 | * Beta for making sweet commercials -------------------------------------------------------------------------------- /async2rewrite/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Convert discord.py code using abstract syntax trees. 4 | 5 | """ 6 | 7 | __title__ = 'async2rewrite' 8 | __author__ = 'Tyler Gibbs' 9 | __version__ = '0.1.9' 10 | __copyright__ = 'Copyright 2017 TheTrain2000' 11 | __license__ = 'MIT' 12 | 13 | from .main import * 14 | -------------------------------------------------------------------------------- /async2rewrite/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import difflib 3 | 4 | from async2rewrite.main import from_file 5 | 6 | parser = argparse.ArgumentParser(description='Automatically convert discord.py async branch code to rewrite.') 7 | 8 | parser.add_argument('paths', type=str, nargs='+') 9 | parser.add_argument('--suffix', dest='suffix', action='store', type=str, default='.a2r.py', 10 | help='the suffix to use for file names when writing (default: \'.a2r.py\'') 11 | parser.add_argument('--print', dest='print', action='store_true', 12 | help='print the output instead of writing for a file (default: false)') 13 | parser.add_argument('--diff', dest='diff', action='store_true', 14 | help='create a diff file for every file converted (default: false)') 15 | parser.set_defaults(print=False, interactive=False, diff=False) 16 | 17 | results = parser.parse_args() 18 | 19 | converted = from_file(*results.paths, interactive=results.interactive) 20 | d = difflib.Differ() 21 | 22 | for key, value in converted.items(): 23 | if not results.print: 24 | with open(key + results.suffix, 'w', encoding='utf-8') as f: 25 | f.write(value) 26 | else: 27 | print('{}\n{}'.format(key + results.suffix, value)) 28 | 29 | if results.diff: 30 | with open(key, 'r', encoding='utf-8') as f: 31 | original = f.readlines() 32 | with open(key + results.suffix, 'r', encoding='utf-8') as f: 33 | new = f.readlines() 34 | 35 | differences = d.compare(original, new) 36 | with open(key + '.diff', 'w', encoding='utf-8') as f: 37 | f.writelines(differences) 38 | -------------------------------------------------------------------------------- /async2rewrite/main.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | 4 | import astunparse_noparen as ast_unparse 5 | 6 | from .transformers import * 7 | 8 | # Detects 17-19 digit integers within quotes (snowflake detection) 9 | snowflake_regex = re.compile(r"['\"](\d{17,19})['\"]") 10 | 11 | 12 | def get_result(code, **kwargs): 13 | """Performs conversion of a given string of code. 14 | 15 | This attempts to parse async(str) snowflakes and convert them 16 | into rewrite(int) snowflakes, fix ordering for Messageables 17 | and converting to shorthands where available. 18 | """ 19 | 20 | stats = kwargs.pop('stats', False) 21 | include_ast = kwargs.pop('include_ast', False) 22 | 23 | def snowflake_repl(match): 24 | # Cast the snowflake string into an integer 25 | # This should never fail, but this will raise in case it does. 26 | possible_snowflake = int(match.group(1)) 27 | 28 | # Cast the snowflake back into a string (for substitution) 29 | return str(possible_snowflake) 30 | 31 | # Perform the substitution 32 | code = snowflake_regex.sub(snowflake_repl, code) 33 | 34 | expr_ast = ast.parse(code) 35 | 36 | if stats: 37 | return find_stats(expr_ast) 38 | 39 | # Instantiate a new transformer and start walking through 40 | # this syntax tree. 41 | new_ast = DiscordTransformer().generic_visit(expr_ast) 42 | 43 | unparsed = ast_unparse.unparse(new_ast) 44 | 45 | unparsed = unparsed.replace('ctx.message.guild', 'ctx.guild').replace( 46 | 'ctx.message.author', 'ctx.author') 47 | unparsed = unparsed.replace('ctx.message.channel', 'ctx.channel').replace('ctx.guild.me', 'ctx.me') 48 | 49 | # This compiles our new code, ensuring that the syntax is valid 50 | # and allowing us to return the syntax tree if requested. 51 | final_ast = ast.parse(unparsed) 52 | 53 | unparsed = unparsed.strip() 54 | 55 | if include_ast: 56 | return unparsed, final_ast 57 | return unparsed 58 | 59 | 60 | def process_file(file, **kwargs): 61 | """Opens, reads and processes a file.""" 62 | with open(file, 'r', encoding='utf-8') as f: 63 | return get_result(f.read(), **kwargs) 64 | 65 | 66 | def from_file(*files, **kwargs): 67 | """Process a list of files or directories. 68 | 69 | Abstraction for get_result, returns batch results for a file or 70 | files in a given directory 71 | """ 72 | res = {} 73 | 74 | for path in files: 75 | if path.endswith('.py'): 76 | # The user has passed a direct file, convert on its own. 77 | res[path] = process_file(path, **kwargs) 78 | else: 79 | # This is either a directory or a symlink, walk through and 80 | # modify any files we detect. 81 | for dir_path, _, file_names in os.walk(path, followlinks=True): 82 | for file in file_names: 83 | file_path = '{}/{}'.format(dir_path, file) 84 | 85 | if file_path.endswith('.py'): 86 | # Only process python files 87 | res[file_path] = process_file(file_path, **kwargs) 88 | 89 | return res 90 | 91 | 92 | def from_text(text, **kwargs): 93 | """Frontend interface for processing raw text.""" 94 | return get_result(text, **kwargs) 95 | -------------------------------------------------------------------------------- /async2rewrite/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylergibbs2/async2rewrite/aab3dd65a70bcdf8c9a69d86f1f99994b1d92ebf/async2rewrite/tests/__init__.py -------------------------------------------------------------------------------- /async2rewrite/tests/test_ext_changes.py: -------------------------------------------------------------------------------- 1 | import async2rewrite 2 | 3 | 4 | def test_remove_pass_context_true(): 5 | converted_code = async2rewrite.from_text("@bot.command(pass_context=True)\nasync def test(ctx):\n pass") 6 | assert converted_code == "@bot.command()\nasync def test(ctx):\n pass" 7 | 8 | 9 | def test_dont_remove_pass_context_false(): 10 | converted_code = async2rewrite.from_text("@bot.command(pass_context=False)\nasync def test():\n pass") 11 | assert converted_code == "@bot.command()\nasync def test(ctx):\n pass" 12 | 13 | 14 | def test_say_to_send(): 15 | converted_code = async2rewrite.from_text("bot.say('Test')") 16 | assert converted_code == "ctx.send('Test')" 17 | 18 | 19 | def test_shortcut_author(): 20 | converted_code = async2rewrite.from_text("ctx.message.author") 21 | assert converted_code == "ctx.author" 22 | 23 | 24 | def test_shortcut_guild(): 25 | converted_code = async2rewrite.from_text("ctx.message.server") 26 | assert converted_code == "ctx.guild" 27 | 28 | 29 | def test_shortcut_channel(): 30 | converted_code = async2rewrite.from_text("ctx.message.channel") 31 | assert converted_code == "ctx.channel" 32 | 33 | 34 | def test_shortcut_me(): 35 | converted_code = async2rewrite.from_text("ctx.message.server.me") 36 | assert converted_code == "ctx.me" 37 | -------------------------------------------------------------------------------- /async2rewrite/tests/test_game_to_activity.py: -------------------------------------------------------------------------------- 1 | import async2rewrite 2 | 3 | 4 | def test_change_presence_activity_keyword(): 5 | converted_code = async2rewrite.from_text("client.change_presence(game=discord.Game(name='a game'))") 6 | assert converted_code == "client.change_presence(activity=discord.Game(name='a game'))" 7 | 8 | 9 | def test_game_type_1_to_streaming(): 10 | converted_code = async2rewrite.from_text("client.change_presence(game=discord.Game(" 11 | "name='Streaming', url=my_url, type=1))") 12 | assert converted_code == "client.change_presence(activity=discord.Streaming(name='Streaming', url=my_url))" 13 | 14 | 15 | def test_game_type_2_to_activity(): 16 | converted_code = async2rewrite.from_text("client.change_presence(game=discord.Game(name='music', type=2))") 17 | assert converted_code == "client.change_presence(activity=discord.Activity(" \ 18 | "type=discord.ActivityType.listening, name='music'))" 19 | 20 | 21 | def test_game_type_3_to_activity(): 22 | converted_code = async2rewrite.from_text("client.change_presence(game=discord.Game(name='a movie', type=3))") 23 | assert converted_code == "client.change_presence(activity=discord.Activity(" \ 24 | "type=discord.ActivityType.watching, name='a movie'))" 25 | 26 | 27 | def test_member_game_to_activity(): 28 | converted_code = async2rewrite.from_text("member.game") 29 | assert converted_code == "member.activity" 30 | 31 | 32 | def test_game_to_activity_client_keywork(): 33 | converted_code = async2rewrite.from_text("bot = discord.Bot(game=discord.Game(name='a game'))") 34 | assert converted_code == "bot = discord.Bot(activity=discord.Game(name='a game'))" 35 | -------------------------------------------------------------------------------- /async2rewrite/tests/test_property_changes.py: -------------------------------------------------------------------------------- 1 | import async2rewrite 2 | 3 | 4 | def test_is_default(): 5 | converted_code = async2rewrite.from_text("role.is_default") 6 | assert converted_code == "role.is_default()" 7 | 8 | 9 | def test_is_ready(): 10 | converted_code = async2rewrite.from_text("bot.is_ready") 11 | assert converted_code == "bot.is_ready()" 12 | 13 | 14 | def test_is_closed(): 15 | converted_code = async2rewrite.from_text("bot.is_closed") 16 | assert converted_code == "bot.is_closed()" 17 | -------------------------------------------------------------------------------- /async2rewrite/tests/test_server_to_guild.py: -------------------------------------------------------------------------------- 1 | import async2rewrite 2 | 3 | 4 | def test_messageserver_to_messageguild(): 5 | converted_code = async2rewrite.from_text("message.server") 6 | assert converted_code == "message.guild" 7 | 8 | 9 | def test_ctxmsgserver_to_ctxguild(): 10 | converted_code = async2rewrite.from_text("server = ctx.message.server") 11 | assert converted_code == "guild = ctx.guild" 12 | 13 | 14 | def test_createserver_to_createguild(): 15 | converted_code = async2rewrite.from_text("bot.create_server()") 16 | assert converted_code == "bot.create_guild()" 17 | 18 | 19 | def test_manageserver_to_manageguild(): 20 | converted_code = async2rewrite.from_text("perms.manage_server") 21 | assert converted_code == "perms.manage_guild" 22 | -------------------------------------------------------------------------------- /async2rewrite/tests/test_snowflakes.py: -------------------------------------------------------------------------------- 1 | import async2rewrite 2 | 3 | 4 | def test_snowflake_to_int_in_method(): 5 | converted_code = async2rewrite.from_text("ch = client.get_channel('84319995256905728')") 6 | assert converted_code == "ch = client.get_channel(84319995256905728)" 7 | 8 | 9 | def test_snowflake_to_int_in_comparison(): 10 | converted_code = async2rewrite.from_text("if message.author.id == '80528701850124288': pass") 11 | assert converted_code == "if message.author.id == 80528701850124288:\n pass" 12 | 13 | 14 | def test_snowflake_to_int(): 15 | converted_code = async2rewrite.from_text("'123456789012345678'") 16 | assert converted_code == "123456789012345678" 17 | -------------------------------------------------------------------------------- /async2rewrite/tests/test_stateful_models.py: -------------------------------------------------------------------------------- 1 | import async2rewrite 2 | import pytest 3 | 4 | 5 | def test_add_reaction(): 6 | converted_code = async2rewrite.from_text("bot.add_reaction(msg, rxn)") 7 | assert converted_code == "msg.add_reaction(rxn)" 8 | 9 | 10 | def test_add_roles(): 11 | converted_code = async2rewrite.from_text("bot.add_roles(member, role1, role2, role3)") 12 | assert converted_code == "member.add_roles(role1, role2, role3)" 13 | 14 | 15 | def test_ban(): 16 | converted_code = async2rewrite.from_text("bot.ban(member)") 17 | assert converted_code == "member.ban()" 18 | 19 | 20 | def test_change_nickname(): 21 | converted_code = async2rewrite.from_text("bot.change_nickname(member, 'New Nick')") 22 | assert converted_code == "member.edit(nick='New Nick')" 23 | 24 | 25 | def test_clear_reactions(): 26 | converted_code = async2rewrite.from_text("bot.clear_reactions(msg)") 27 | assert converted_code == "msg.clear_reactions()" 28 | 29 | 30 | def test_create_custom_emoji(): 31 | converted_code = async2rewrite.from_text("bot.create_custom_emoji(server, name='Name', image=img_obj)") 32 | assert converted_code == "guild.create_custom_emoji(name='Name', image=img_obj)" 33 | 34 | 35 | def test_create_text_channel(): 36 | converted_code = async2rewrite.from_text("bot.create_channel(server, 'Text', type=discord.ChannelType.text)") 37 | assert converted_code == "guild.create_text_channel('Text')" 38 | 39 | 40 | def test_create_voice_channel(): 41 | converted_code = async2rewrite.from_text("bot.create_channel(server, 'Voice', type=discord.ChannelType.voice)") 42 | assert converted_code == "guild.create_voice_channel('Voice')" 43 | 44 | 45 | def test_create_invite(): 46 | converted_code = async2rewrite.from_text("bot.create_invite(destination, max_age=10, max_uses=3)") 47 | assert converted_code == "destination.create_invite(max_age=10, max_uses=3)" 48 | 49 | 50 | def test_create_role(): 51 | converted_code = async2rewrite.from_text("bot.create_role(server, name='New Role', hoist=True)") 52 | assert converted_code == "guild.create_role(name='New Role', hoist=True)" 53 | 54 | 55 | def test_delete_channel(): 56 | converted_code = async2rewrite.from_text("bot.delete_channel(channel)") 57 | assert converted_code == "channel.delete()" 58 | 59 | 60 | def test_delete_channel_perms(): 61 | converted_code = async2rewrite.from_text("bot.delete_channel_permissions(channel)") 62 | assert converted_code == "channel.set_permissions(overwrite=None)" 63 | 64 | 65 | def test_delete_custom_emoji(): 66 | converted_code = async2rewrite.from_text("bot.delete_custom_emoji(emoji)") 67 | assert converted_code == "emoji.delete()" 68 | 69 | 70 | def test_delete_invite(): 71 | converted_code = async2rewrite.from_text("bot.delete_invite(inv)") 72 | assert converted_code == "inv.delete()" 73 | 74 | 75 | def test_delete_message(): 76 | converted_code = async2rewrite.from_text("bot.delete_message(msg)") 77 | assert converted_code == "msg.delete()" 78 | 79 | 80 | def test_delete_role(): 81 | converted_code = async2rewrite.from_text("bot.delete_role(server, role)") 82 | assert converted_code == "role.delete()" 83 | 84 | 85 | def test_delete_server(): 86 | converted_code = async2rewrite.from_text("bot.delete_server(server)") 87 | assert converted_code == "guild.delete()" 88 | 89 | 90 | def test_edit_channel(): 91 | converted_code = async2rewrite.from_text("bot.edit_channel(channel, topic='GG', user_limit=10)") 92 | assert converted_code == "channel.edit(topic='GG', user_limit=10)" 93 | 94 | 95 | def test_edit_channel_permissions(): 96 | converted_code = async2rewrite.from_text("bot.edit_channel_permissions(channel, target, overwrite=some_overwrite)") 97 | assert converted_code == "channel.set_permissions(target, overwrite=some_overwrite)" 98 | 99 | 100 | def test_edit_custom_emoji(): 101 | converted_code = async2rewrite.from_text("bot.edit_custom_emoji(emoji, name='Test')") 102 | assert converted_code == "emoji.edit(name='Test')" 103 | 104 | 105 | def test_edit_message(): 106 | converted_code = async2rewrite.from_text("bot.edit_message(msg, 'New Content', embed=new_embed)") 107 | assert converted_code == "msg.edit(embed=new_embed, content='New Content')" 108 | 109 | 110 | def test_edit_profile(): 111 | converted_code = async2rewrite.from_text("bot.edit_profile(username='New Username')") 112 | assert converted_code == "bot.user.edit(username='New Username')" 113 | 114 | 115 | def test_edit_role(): 116 | converted_code = async2rewrite.from_text("bot.edit_role(server, role, name='New Name')") 117 | assert converted_code == "role.edit(name='New Name')" 118 | 119 | 120 | def test_edit_server(): 121 | converted_code = async2rewrite.from_text("bot.edit_server(server, name='New Name')") 122 | assert converted_code == "guild.edit(name='New Name')" 123 | 124 | 125 | def test_estimate_pruned_members(): 126 | converted_code = async2rewrite.from_text("bot.estimate_pruned_members(location)") 127 | assert converted_code == "location.estimate_pruned_members()" 128 | 129 | 130 | def test_get_all_emojis(): 131 | converted_code = async2rewrite.from_text("bot.get_all_emojis()") 132 | assert converted_code == "bot.emojis" 133 | 134 | 135 | def test_get_bans(): 136 | converted_code = async2rewrite.from_text("bot.get_bans(guild)") 137 | assert converted_code == "guild.bans()" 138 | 139 | 140 | def test_get_message(): 141 | converted_code = async2rewrite.from_text("bot.get_message(channel, id)") 142 | assert converted_code == "channel.get_message(id)" 143 | 144 | 145 | def test_get_reaction_users(): 146 | converted_code = async2rewrite.from_text("bot.get_reaction_users(rxn, limit=10)") 147 | assert converted_code == "rxn.users(limit=10)" 148 | 149 | 150 | def test_invites_from(): 151 | converted_code = async2rewrite.from_text("bot.invites_from(server=guild)") 152 | assert converted_code == "guild.invites()" 153 | 154 | 155 | def test_kick(): 156 | converted_code = async2rewrite.from_text("bot.kick(member)") 157 | assert converted_code == "member.kick()" 158 | 159 | 160 | def test_leave_server(): 161 | converted_code = async2rewrite.from_text("bot.leave_server(server)") 162 | assert converted_code == "guild.leave()" 163 | 164 | 165 | def test_logs_from(): 166 | converted_code = async2rewrite.from_text("bot.logs_from(chan, limit=50, reverse=True)") 167 | assert converted_code == "chan.history(limit=50, reverse=True)" 168 | 169 | 170 | def test_move_channel(): 171 | converted_code = async2rewrite.from_text("bot.move_channel(channel, position)") 172 | assert converted_code == "channel.edit(position=position)" 173 | 174 | 175 | def test_move_role(): 176 | converted_code = async2rewrite.from_text("bot.move_role(server, role, pos)") 177 | assert converted_code == "role.edit(position=pos)" 178 | 179 | 180 | def test_move_member(): 181 | converted_code = async2rewrite.from_text("bot.move_member(mem, chan)") 182 | assert converted_code == "mem.edit(voice_channel=chan)" 183 | 184 | 185 | def test_pin_message(): 186 | converted_code = async2rewrite.from_text("bot.pin_message(msg)") 187 | assert converted_code == "msg.pin()" 188 | 189 | 190 | def test_pins_from(): 191 | converted_code = async2rewrite.from_text("bot.pins_from(dest)") 192 | assert converted_code == "dest.pins()" 193 | 194 | 195 | def test_prune_members(): 196 | converted_code = async2rewrite.from_text("bot.prune_members(server)") 197 | assert converted_code == "guild.prune_members()" 198 | 199 | 200 | def test_purge_from(): 201 | converted_code = async2rewrite.from_text("bot.purge_from(dest, limit=5, check=my_check)") 202 | assert converted_code == "dest.purge(limit=5, check=my_check)" 203 | 204 | 205 | def test_remove_reaction(): 206 | converted_code = async2rewrite.from_text("bot.remove_reaction(msg, emoji, member)") 207 | assert converted_code == "msg.remove_reaction(emoji, member)" 208 | 209 | 210 | def test_remove_roles(): 211 | converted_code = async2rewrite.from_text("bot.remove_roles(member, role1, role2, role3)") 212 | assert converted_code == "member.remove_roles(role1, role2, role3)" 213 | 214 | 215 | def test_replace_roles(): 216 | converted_code = async2rewrite.from_text("bot.replace_roles(member, role1, role2)") 217 | assert converted_code == "member.edit(roles=[role1, role2])" 218 | 219 | 220 | def test_send_file(): 221 | converted_code = async2rewrite.from_text("bot.send_file(dest, 'to_send.png'," 222 | " filename='my_file.png', content='File')") 223 | assert converted_code == "dest.send('File', file=discord.File('to_send.png', filename='my_file.png'))" 224 | 225 | 226 | def test_send_message(): 227 | converted_code = async2rewrite.from_text("bot.send_message(dest, 'Content')") 228 | assert converted_code == "dest.send('Content')" 229 | 230 | 231 | def test_send_typing(): 232 | converted_code = async2rewrite.from_text("bot.send_typing(dest)") 233 | assert converted_code == "dest.trigger_typing()" 234 | 235 | 236 | def test_server_voice_state(): 237 | converted_code = async2rewrite.from_text("bot.server_voice_state(member, mute=True, deafen=True)") 238 | assert converted_code == "member.edit(mute=True, deafen=True)" 239 | 240 | 241 | def test_stateful_start_private_message(): 242 | converted_code = async2rewrite.from_text("bot.start_private_message(user)") 243 | assert converted_code == "user.create_dm()" 244 | 245 | 246 | def test_unban(): 247 | converted_code = async2rewrite.from_text("bot.unban(server, user)") 248 | assert converted_code == "guild.unban(user)" 249 | 250 | 251 | def test_unpin_message(): 252 | converted_code = async2rewrite.from_text("bot.unpin_message(msg)") 253 | assert converted_code == "msg.unpin()" 254 | 255 | 256 | def test_wait_for_message_working(): 257 | converted_code = async2rewrite.from_text("bot.wait_for_message(check=my_check)") 258 | assert converted_code == "bot.wait_for('message', check=my_check)" 259 | 260 | 261 | def test_wait_for_message_warning(): 262 | with pytest.warns(UserWarning): 263 | async2rewrite.from_text("bot.wait_for_message(author=member)") 264 | 265 | 266 | def test_wait_for_reaction_working(): 267 | converted_code = async2rewrite.from_text("bot.wait_for_reaction(check=my_check)") 268 | assert converted_code == "bot.wait_for('reaction_add', check=my_check)" 269 | 270 | 271 | def test_wait_for_reaction_warning(): 272 | with pytest.warns(UserWarning): 273 | async2rewrite.from_text("bot.wait_for_reaction(author=member)") 274 | -------------------------------------------------------------------------------- /async2rewrite/transformers.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | import warnings 3 | import ast 4 | 5 | 6 | easy_stateful_list = ['add_reaction', 'add_roles', 'ban', 'clear_reactions', 'create_invite', 'create_custom_emoji', 7 | 'create_role', 'kick', 'remove_reaction', 'remove_roles', 'prune_members', 'unban', 8 | 'get_message', 'estimate_pruned_members'] 9 | 10 | easy_deletes_list = ['delete_custom_emoji', 'delete_channel', 'delete_invite', 'delete_message', 'delete_guild'] 11 | 12 | easy_edits_list = ['edit_channel', 'edit_custom_emoji', 'edit_guild'] 13 | 14 | removed_methods = ['wait_until_login', 'messages'] 15 | 16 | stats_counter = Counter() 17 | 18 | 19 | def find_arg(call: ast.Call, arg_name: str, arg_pos: int=None): 20 | found_value = None 21 | for kw in call.keywords: 22 | if arg_name == kw.arg: 23 | found_value = kw.value 24 | break 25 | else: 26 | try: 27 | found_value = call.args[arg_pos] 28 | except (IndexError, TypeError): 29 | pass 30 | return found_value 31 | 32 | 33 | class DiscordTransformer(ast.NodeTransformer): 34 | 35 | def visit_FormattedValue(self, node): 36 | self.generic_visit(node) 37 | 38 | return node 39 | 40 | def visit_Module(self, node): 41 | self.generic_visit(node) 42 | 43 | def visit_keyword(self, node): 44 | if node.arg == "game" and isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Attribute): 45 | node = self.game_to_activity(node) 46 | 47 | return node 48 | 49 | def visit_Expr(self, node): 50 | self.generic_visit(node) 51 | 52 | node = self.attr_to_meth(node) 53 | 54 | return node 55 | 56 | def visit_Call(self, node): 57 | """Modify calls to their appropriate rewrite counterparts.""" 58 | self.generic_visit(node) 59 | 60 | # this all has to do with the stateful model changes 61 | node = self.to_messageable(node) 62 | node = self.easy_statefuls(node) 63 | node = self.stateful_change_nickname(node) 64 | node = self.stateful_create_channel(node) 65 | node = self.easy_deletes(node) 66 | node = self.stateful_edit_message(node) 67 | node = self.easy_edits(node) 68 | node = self.stateful_edit_role(node) 69 | node = self.stateful_edit_channel_perms(node) 70 | node = self.stateful_leave_server(node) 71 | node = self.stateful_pin_message(node) 72 | node = self.stateful_get_bans(node) 73 | node = self.stateful_pins_from(node) 74 | node = self.stateful_send_typing(node) 75 | node = self.stateful_wait_for(node) 76 | node = self.to_tuple_to_to_rgb(node) 77 | node = self.channel_history(node) 78 | node = self.stateful_send_file(node) 79 | node = self.stateful_delete_channel_perms(node) 80 | node = self.stateful_delete_role(node) 81 | node = self.stateful_edit_profile(node) 82 | node = self.stateful_invites_from(node) 83 | node = self.stateful_get_reaction_users(node) 84 | node = self.stateful_move_channel(node) 85 | node = self.stateful_move_role(node) 86 | node = self.stateful_move_member(node) 87 | node = self.stateful_purge_from(node) 88 | node = self.stateful_replace_roles(node) 89 | node = self.stateful_server_voice_state(node) 90 | node = self.stateful_start_private_message(node) 91 | 92 | if isinstance(node.func, ast.Attribute) and node.func.attr == "delete_messages": 93 | warnings.warn("Cannot convert delete_messages. Must be done manually.") 94 | 95 | if isinstance(node.func, ast.Attribute) and node.func.attr in removed_methods: 96 | warnings.warn("{} was removed in rewrite. Fix your code accordingly.".format(node.func.attr)) 97 | 98 | # Transforms below this comment change the node type. 99 | 100 | node = self.stateful_get_all_emojis(node) 101 | 102 | return node 103 | 104 | def visit_arg(self, node): 105 | self.generic_visit(node) 106 | 107 | node.arg = node.arg.replace('server', 'guild').replace('Server', 'Guild') 108 | return node 109 | 110 | def visit_Attribute(self, node): 111 | self.generic_visit(node) 112 | 113 | node = self.to_edited_at(node) 114 | 115 | self.detect_voice(node) 116 | 117 | if node.attr == "game": 118 | node.attr = "activity" 119 | 120 | node.attr = node.attr.replace('server', 'guild').replace('Server', 'Guild') 121 | return node 122 | 123 | def visit_Name(self, node): 124 | self.generic_visit(node) 125 | 126 | node.id = node.id.replace('server', 'guild').replace('Server', 'Guild') 127 | return node 128 | 129 | def visit_Await(self, node): 130 | self.generic_visit(node) 131 | 132 | return node 133 | 134 | def visit_AsyncFunctionDef(self, node): 135 | self.generic_visit(node) 136 | 137 | node = self.ext_event_changes(node) 138 | node = self.ensure_ctx_var(node) 139 | node = self.remove_passcontext(node) 140 | node = self.event_changes(node) 141 | 142 | node.name = node.name.replace('server', 'guild').replace('Server', 'Guild') 143 | 144 | return node 145 | 146 | def visit_Assign(self, node): 147 | self.generic_visit(node) 148 | 149 | return node 150 | 151 | def ext_event_changes(self, coro): 152 | if coro.name == 'on_command' or coro.name == 'on_command_completion': 153 | 154 | coro.args.args = coro.args.args[1:] 155 | stats_counter['coro_changes'] += 1 156 | return coro 157 | elif coro.name == 'on_command_error': 158 | 159 | coro.args.args.reverse() 160 | stats_counter['coro_changes'] += 1 161 | return coro 162 | 163 | return coro 164 | 165 | def event_changes(self, coro): 166 | if coro.name == 'on_voice_state_update': 167 | coro.args.args.insert(0, ast.arg(arg='member', annotation=None)) 168 | 169 | elif coro.name in ['on_guild_emojis_update', 'on_member_ban']: 170 | coro.args.args.insert(0, ast.arg(arg='guild', annotation=None)) 171 | 172 | elif coro.name in ['on_channel_delete', 'on_channel_create', 'on_channel_update']: 173 | coro.name = coro.name.replace('on_channel', 'on_guild_channel') 174 | 175 | stats_counter['coro_changes'] += 1 176 | return coro 177 | 178 | def game_to_activity(self, node): 179 | new_keyword = ast.keyword(arg="activity", value=None) 180 | 181 | activity_wrapper = ast.Call(func=ast.Attribute(value=ast.Name(id="discord", ctx=ast.Load()), 182 | attr="Activity", ctx=ast.Load()), args=[], keywords=[]) 183 | 184 | enum = ast.Attribute(value=ast.Attribute( 185 | value=ast.Name(id="discord", ctx=ast.Load()), 186 | attr="ActivityType", ctx=ast.Load()), 187 | attr=None, ctx=ast.Load()) 188 | 189 | game_type = 0 190 | keywords = list(node.value.keywords) 191 | for kw in node.value.keywords: 192 | if kw.arg == "type": 193 | keywords.remove(kw) 194 | game_type = kw.value.n 195 | 196 | if game_type == 0: 197 | node.arg = "activity" 198 | return node 199 | elif game_type == 1: 200 | enum = ast.Attribute(value=ast.Name(id="discord", ctx=ast.Load()), attr="Streaming", ctx=ast.Load()) 201 | new_keyword.value = ast.Call(func=enum, keywords=keywords, args=[]) 202 | else: 203 | enum.attr = "listening" if game_type == 2 else "watching" 204 | activity_wrapper.keywords = [ast.keyword(arg="type", value=enum)] 205 | activity_wrapper.keywords += keywords 206 | new_keyword.value = activity_wrapper 207 | 208 | return new_keyword 209 | 210 | def detect_voice(self, node): 211 | if getattr(node, 'attr', None) in ["create_ffmpeg_player", "create_ytdl_player", 212 | "create_stream_player", "play_audio"]: 213 | warnings.warn("Voice implementation detected. This library does not convert voice.") 214 | 215 | return node 216 | 217 | def ensure_ctx_var(self, coro): 218 | 219 | d_list = [] 220 | for d in coro.decorator_list: 221 | if isinstance(d, ast.Attribute): 222 | d_list.append(d.attr) 223 | elif isinstance(d, ast.Call): 224 | if isinstance(d.func, ast.Attribute): 225 | d_list.append(d.func.attr) 226 | if 'command' not in d_list: 227 | return coro 228 | 229 | coro_args = [arg.arg for arg in coro.args.args] 230 | 231 | if not coro_args: 232 | coro.args.args.append(ast.arg(arg='ctx', annotation=None)) 233 | elif 'self' in coro_args and 'ctx' not in coro_args: 234 | coro.args.args.insert(1, ast.arg(arg='ctx', annotation=None)) 235 | elif 'self' not in coro_args and 'ctx' not in coro_args: 236 | coro.args.args.insert(0, ast.arg(arg='ctx', annotation=None)) 237 | 238 | stats_counter['coro_changes'] += 1 239 | 240 | return coro 241 | 242 | def to_edited_at(self, attribute): 243 | if attribute.attr == 'edited_timestamp': 244 | attribute.attr = 'edited_at' 245 | stats_counter['attribute_changes'] += 1 246 | 247 | return attribute 248 | 249 | def attr_to_meth(self, expr): 250 | if isinstance(expr.value, ast.Attribute): 251 | if expr.value.attr in ['is_ready', 'is_default', 'is_closed']: 252 | call = ast.Call() 253 | call.args = [] 254 | call.keywords = [] 255 | call.func = expr.value 256 | expr.value = call 257 | stats_counter['expr_changes'] += 1 258 | 259 | return expr 260 | 261 | def remove_passcontext(self, n): 262 | for d in n.decorator_list: 263 | if not isinstance(d, ast.Call): 264 | continue 265 | for kw in list(d.keywords): # iterate over a copy of the list to avoid removing while iterating 266 | if not isinstance(kw.value, ast.NameConstant): 267 | continue 268 | if kw.arg == 'pass_context': # if the pass_context kwarg is set to True 269 | d.keywords.remove(kw) 270 | stats_counter['coro_changes'] += 1 271 | return n 272 | 273 | def stateful_get_all_emojis(self, call): 274 | if not isinstance(call.func, ast.Attribute): 275 | return call 276 | if call.func.attr != 'get_all_emojis': 277 | return call 278 | 279 | new_expr = ast.Expr() 280 | ast.copy_location(new_expr, call) 281 | call.func.attr = 'emojis' 282 | new_expr.value = call.func 283 | return new_expr 284 | 285 | def to_messageable(self, call): 286 | if not isinstance(call.func, ast.Attribute): 287 | return call 288 | if call.func.attr == 'say': 289 | call.func.value = ast.Name(id='ctx', ctx=ast.Load()) 290 | call.func.attr = 'send' 291 | stats_counter['call_changes'] += 1 292 | elif call.func.attr == 'send_message': 293 | destination = find_arg(call, "destination", 0) 294 | 295 | for kw in call.keywords.copy(): 296 | if kw.arg == "destination": 297 | call.keywords.remove(kw) 298 | 299 | wrap_attr = ast.Attribute() 300 | wrap_attr.value = destination 301 | wrap_attr.attr = 'send' 302 | wrap_attr.ctx = ast.Load() 303 | 304 | newcall = ast.Call() 305 | newcall.func = wrap_attr 306 | newcall.args = call.args[1:] 307 | newcall.keywords = call.keywords 308 | 309 | newcall = ast.copy_location(newcall, call) 310 | stats_counter['call_changes'] += 1 311 | 312 | return newcall 313 | 314 | return call 315 | 316 | def stateful_purge_from(self, call): 317 | if isinstance(call.func, ast.Attribute): 318 | if call.func.attr == "purge_from": 319 | 320 | call.func.attr = 'purge' 321 | dest = find_arg(call, "channel", 0) 322 | call.args = [] 323 | call.func.value = dest 324 | 325 | return call 326 | 327 | def stateful_replace_roles(self, call): 328 | if isinstance(call.func, ast.Attribute): 329 | if call.func.attr == "replace_roles": 330 | call.func.attr = 'edit' 331 | call.func.value = find_arg(call, 'member', 0) 332 | roles = call.args[1:] 333 | call.args = [] 334 | call.keywords = [ast.keyword(arg='roles', value=ast.List(elts=roles, ctx=ast.Store()))] 335 | 336 | return call 337 | 338 | def stateful_server_voice_state(self, call): 339 | if isinstance(call.func, ast.Attribute): 340 | if call.func.attr == "guild_voice_state": 341 | call.func.attr = 'edit' 342 | call.func.value = find_arg(call, 'member', 0) 343 | call.args = [] 344 | 345 | return call 346 | 347 | def stateful_move_channel(self, call): 348 | if isinstance(call.func, ast.Attribute): 349 | if call.func.attr == 'move_channel': 350 | call.func.attr = 'edit' 351 | obj = find_arg(call, 'channel', 0) 352 | pos = find_arg(call, 'position', 1) 353 | call.func.value = obj 354 | call.args = [] 355 | call.keywords = [ast.keyword(arg='position', value=pos)] 356 | 357 | return call 358 | 359 | def stateful_start_private_message(self, call): 360 | if isinstance(call.func, ast.Attribute): 361 | if call.func.attr == "start_private_message": 362 | call.func.attr = 'create_dm' 363 | call.func.value = find_arg(call, 'user', 0) 364 | call.args = [] 365 | call.keywords = [] 366 | 367 | return call 368 | 369 | def stateful_move_role(self, call): 370 | if isinstance(call.func, ast.Attribute): 371 | if call.func.attr == 'move_role': 372 | call.func.attr = 'edit' 373 | obj = find_arg(call, 'role', 1) 374 | pos = find_arg(call, 'position', 2) 375 | call.func.value = obj 376 | call.args = [] 377 | call.keywords = [ast.keyword(arg='position', value=pos)] 378 | 379 | return call 380 | 381 | def easy_statefuls(self, call): 382 | if isinstance(call.func, ast.Attribute): 383 | if call.func.attr in easy_stateful_list: 384 | message = call.args[0] 385 | call.func.value = message 386 | call.args = call.args[1:] 387 | stats_counter['call_changes'] += 1 388 | return call 389 | 390 | def easy_deletes(self, call): 391 | if isinstance(call.func, ast.Attribute): 392 | if call.func.attr in easy_deletes_list: 393 | to_delete = call.args[0] 394 | call.func.value = to_delete 395 | call.args = call.args[1:] 396 | call.func.attr = 'delete' 397 | stats_counter['call_changes'] += 1 398 | return call 399 | 400 | def easy_edits(self, call): 401 | if isinstance(call.func, ast.Attribute): 402 | if call.func.attr in easy_edits_list: 403 | to_edit = call.args[0] 404 | call.func.value = to_edit 405 | call.args = call.args[1:] 406 | call.func.attr = 'edit' 407 | stats_counter['call_changes'] += 1 408 | return call 409 | 410 | def stateful_delete_role(self, call): 411 | if isinstance(call.func, ast.Attribute): 412 | if call.func.attr == 'delete_role': 413 | role = find_arg(call, "role", 1) 414 | call.func.value = role 415 | call.func.attr = "delete" 416 | call.args = [] 417 | call.keywords = [] 418 | 419 | return call 420 | 421 | def stateful_send_file(self, call): 422 | if isinstance(call.func, ast.Attribute): 423 | if call.func.attr == 'send_file': 424 | dest = find_arg(call, "destination", 0) 425 | send_as = find_arg(call, "fp", 1) 426 | content = None 427 | filename = None 428 | for kw in list(call.keywords): 429 | if kw.arg in ["destination", "fp"]: 430 | call.keywords.remove(kw) 431 | elif kw.arg == 'filename': 432 | filename = kw 433 | elif kw.arg == 'content': 434 | content = kw 435 | if filename is None: 436 | filename = ast.keyword(arg='filename', value=send_as) 437 | call.func.value = dest 438 | call.func.attr = 'send' 439 | call.args = [] 440 | if content: 441 | call.args.append(content.value) 442 | call.keywords = [] 443 | file_kw = ast.keyword() 444 | file_kw.arg = 'file' 445 | discord_file_call = ast.Call() 446 | discord_file_call.func = ast.Attribute(value=ast.Name(id='discord', ctx=ast.Load()), attr='File', 447 | ctx=ast.Load()) 448 | discord_file_call.args = [send_as] 449 | discord_file_call.keywords = [ast.keyword(arg='filename', value=filename.value)] 450 | file_kw.value = discord_file_call 451 | call.keywords.append(file_kw) 452 | stats_counter['call_changes'] += 1 453 | 454 | return call 455 | 456 | def channel_history(self, call): 457 | if isinstance(call.func, ast.Attribute): 458 | if call.func.attr == 'logs_from': 459 | dest = find_arg(call, "channel", 0) 460 | call.args.remove(dest) 461 | if call.args: 462 | limit = call.args[0] 463 | call.keywords.append(ast.keyword(arg='limit', value=limit)) 464 | call.args = [] 465 | call.func.value = dest 466 | call.func.attr = 'history' 467 | stats_counter['call_changes'] += 1 468 | return call 469 | 470 | def stateful_change_nickname(self, call): 471 | if isinstance(call.func, ast.Attribute): 472 | if call.func.attr == 'change_nickname': 473 | member = find_arg(call, "member", 0) 474 | call.func.value = member 475 | call.func.attr = 'edit' 476 | nick = find_arg(call, "nickname", 1) 477 | call.args = [] 478 | call.keywords = [ast.keyword(arg='nick', value=nick)] 479 | stats_counter['call_changes'] += 1 480 | return call 481 | 482 | def stateful_pins_from(self, call): 483 | if isinstance(call.func, ast.Attribute): 484 | if call.func.attr == 'pins_from': 485 | dest = find_arg(call, "channel", 0) 486 | call.func.value = dest 487 | call.func.attr = 'pins' 488 | call.args = [] 489 | stats_counter['call_changes'] += 1 490 | return call 491 | 492 | def stateful_wait_for(self, call): 493 | if isinstance(call.func, ast.Attribute): 494 | if call.func.attr in ['wait_for_message', 'wait_for_reaction']: 495 | event = call.func.attr.split('_')[2] 496 | event = 'message' if event == 'message' else 'reaction_add' 497 | call.func.attr = 'wait_for' 498 | if call.args: 499 | timeout = call.args[0] 500 | call.args = [] 501 | call.keywords.append(ast.keyword(arg='timeout', value=timeout)) 502 | 503 | call.args.insert(0, ast.Str(s=event)) 504 | for kw in list(call.keywords): 505 | if kw.arg != 'check' and kw.arg != 'timeout': 506 | call.keywords.remove(kw) 507 | warnings.warn('wait_for keyword breaking change detected. Rewrite removes the {} keyword' 508 | ' from wait_for.'.format(kw.arg)) 509 | elif kw.arg == 'timeout': 510 | warnings.warn('wait_for timeout breaking change detected. Timeouts now raise ' 511 | 'asyncio.TimeoutError instead of returning None.') 512 | 513 | stats_counter['call_changes'] += 1 514 | return call 515 | 516 | def stateful_edit_role(self, call): 517 | if isinstance(call.func, ast.Attribute): 518 | if call.func.attr == 'edit_role': 519 | to_edit = find_arg(call, "role", 1) 520 | call.func.value = to_edit 521 | call.args = call.args[2:] 522 | call.func.attr = 'edit' 523 | stats_counter['call_changes'] += 1 524 | return call 525 | 526 | def stateful_get_reaction_users(self, call): 527 | if isinstance(call.func, ast.Attribute): 528 | if call.func.attr == 'get_reaction_users': 529 | call.func.attr = 'users' 530 | rxn = find_arg(call, 'reaction', 0) 531 | call.func.value = rxn 532 | call.args = call.args[1:] 533 | return call 534 | 535 | def stateful_invites_from(self, call): 536 | if isinstance(call.func, ast.Attribute): 537 | if call.func.attr == 'invites_from': 538 | call.func.attr = 'invites' 539 | call.func.value = find_arg(call, 'server', 0) 540 | call.args = [] 541 | call.keywords = [] 542 | 543 | return call 544 | 545 | def to_tuple_to_to_rgb(self, call): 546 | if isinstance(call.func, ast.Attribute): 547 | if call.func.attr == 'to_tuple': 548 | call.func.attr = 'to_rgb' 549 | stats_counter['call_changes'] += 1 550 | 551 | return call 552 | 553 | def stateful_send_typing(self, call): 554 | if isinstance(call.func, ast.Attribute): 555 | if call.func.attr == 'send_typing': 556 | dest = find_arg(call, "destination", 0) 557 | call.func.value = dest 558 | call.args = call.args[1:] 559 | call.func.attr = 'trigger_typing' 560 | stats_counter['call_changes'] += 1 561 | return call 562 | 563 | def stateful_create_channel(self, call): 564 | if isinstance(call.func, ast.Attribute): 565 | if call.func.attr == 'create_channel': 566 | for kw in list(call.keywords): 567 | if isinstance(kw.value, ast.Attribute): 568 | channel_type = kw.value.attr 569 | call.keywords.remove(kw) 570 | break 571 | else: 572 | channel_type = 'text' 573 | call.func.attr = 'create_{}_channel'.format(channel_type) 574 | guild = find_arg(call, "guild", 0) 575 | if guild: 576 | call.args = call.args[1:] 577 | call.func.value = guild 578 | stats_counter['call_changes'] += 1 579 | return call 580 | 581 | def stateful_edit_message(self, call): 582 | if isinstance(call.func, ast.Attribute): 583 | if call.func.attr == 'edit_message': 584 | call.func.attr = 'edit' 585 | message = find_arg(call, "message", 0) 586 | call.func.value = message 587 | content = find_arg(call, "new_content", 1) 588 | call.args = call.args[2:] 589 | if content is not None: 590 | call.keywords.append(ast.keyword(arg='content', value=content)) 591 | stats_counter['call_changes'] += 1 592 | return call 593 | 594 | def stateful_edit_profile(self, call): 595 | if isinstance(call.func, ast.Attribute): 596 | if call.func.attr == 'edit_profile': 597 | call.func.attr = 'user.edit' 598 | 599 | return call 600 | 601 | def stateful_edit_channel_perms(self, call): 602 | if isinstance(call.func, ast.Attribute): 603 | if call.func.attr == 'edit_channel_permissions': 604 | call.func.attr = 'set_permissions' 605 | channel = find_arg(call, "channel", 0) 606 | call.func.value = channel 607 | target = find_arg(call, "target", 1) 608 | call.args = [target] 609 | stats_counter['call_changes'] += 1 610 | return call 611 | 612 | def stateful_delete_channel_perms(self, call): 613 | if isinstance(call.func, ast.Attribute): 614 | if call.func.attr == 'delete_channel_permissions': 615 | call.func.attr = 'set_permissions' 616 | channel = find_arg(call, "channel", 0) 617 | call.func.value = channel 618 | call.args = [] 619 | call.keywords = [] 620 | call.keywords.append(ast.keyword(arg='overwrite', value=ast.NameConstant(None))) 621 | stats_counter['call_changes'] += 1 622 | 623 | return call 624 | 625 | def stateful_leave_server(self, call): 626 | if isinstance(call.func, ast.Attribute): 627 | if call.func.attr == 'leave_guild': 628 | server = find_arg(call, "server", 0) 629 | call.func.value = server 630 | call.func.attr = 'leave' 631 | call.args = [] 632 | stats_counter['call_changes'] += 1 633 | return call 634 | 635 | def stateful_move_member(self, call): 636 | if isinstance(call.func, ast.Attribute): 637 | if call.func.attr == 'move_member': 638 | call.func.attr = 'edit' 639 | member = find_arg(call, 'member', 0) 640 | channel = find_arg(call, 'channel', 1) 641 | call.func.value = member 642 | call.args = [] 643 | call.keywords = [ast.keyword(arg='voice_channel', value=channel)] 644 | 645 | return call 646 | 647 | def stateful_pin_message(self, call): 648 | if isinstance(call.func, ast.Attribute): 649 | if call.func.attr == 'pin_message': 650 | message = find_arg(call, "message", 0) 651 | call.func.value = message 652 | call.func.attr = 'pin' 653 | call.args = [] 654 | elif call.func.attr == 'unpin_message': 655 | message = find_arg(call, "message", 0) 656 | call.func.value = message 657 | call.func.attr = 'unpin' 658 | call.args = [] 659 | stats_counter['call_changes'] += 1 660 | return call 661 | 662 | def stateful_get_bans(self, call): 663 | if isinstance(call.func, ast.Attribute): 664 | if call.func.attr == 'get_bans': 665 | guild = find_arg(call, "server", 0) 666 | call.func.value = guild 667 | call.func.attr = 'bans' 668 | call.args = [] 669 | stats_counter['call_changes'] += 1 670 | return call 671 | 672 | 673 | def find_stats(ast): 674 | DiscordTransformer().generic_visit(ast) 675 | return stats_counter 676 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tylergibbs2/async2rewrite/aab3dd65a70bcdf8c9a69d86f1f99994b1d92ebf/logo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astunparse-noparen>=1.5.6 -------------------------------------------------------------------------------- /sample_code.py: -------------------------------------------------------------------------------- 1 | # This sample code is from the readme on the async branch of the official discord.py repo. 2 | # Author: Rapptz Link: https://github.com/Rapptz/discord.py/ 3 | 4 | import discord 5 | import asyncio 6 | 7 | client = discord.Client() 8 | 9 | @client.event 10 | async def on_ready(): 11 | print('Logged in as') 12 | print(client.user.name) 13 | print(client.user.id) 14 | print('------') 15 | 16 | @client.event 17 | async def on_message(message): 18 | if message.content.startswith('!test'): 19 | counter = 0 20 | tmp = await client.send_message(message.channel, 'Calculating messages...') 21 | async for log in client.logs_from(message.channel, limit=100): 22 | if log.author == message.author: 23 | counter += 1 24 | 25 | await client.edit_message(tmp, 'You have {} messages.'.format(counter)) 26 | elif message.content.startswith('!sleep'): 27 | await asyncio.sleep(5) 28 | await client.send_message(message.channel, 'Done sleeping') 29 | 30 | client.run('token') 31 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from distutils.core import setup 3 | 4 | # Thanks Laura 5 | rootpath = os.path.abspath(os.path.dirname(__file__)) 6 | 7 | 8 | def extract_version(module='async2rewrite'): 9 | version = None 10 | fname = os.path.join(rootpath, module, '__init__.py') 11 | with open(fname) as f: 12 | for line in f: 13 | if line.startswith('__version__'): 14 | _, version = line.split('=') 15 | version = version.strip()[1:-1] # Remove quotation characters. 16 | break 17 | return version 18 | 19 | 20 | version = extract_version() 21 | 22 | setup( 23 | name='async2rewrite', 24 | packages=['async2rewrite'], 25 | version=version, 26 | description='Convert discord.py code using abstract syntax trees.', 27 | author='Tyler Gibbs', 28 | author_email='gibbstyler7@gmail.com', 29 | url='https://github.com/TheTrain2000/async2rewrite', 30 | download_url='https://github.com/TheTrain2000/async2rewrite/archive/{}.tar.gz'.format(version), 31 | keywords=['discord', 'discordpy', 'ast'], 32 | classifiers=[], 33 | install_requires=['astunparse-noparen>=1.5.6', 'yapf'], 34 | test_requires=['pytest', 'pytest-cov'] 35 | ) 36 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py35,py36,py37,nightly,pypy35 3 | 4 | [testenv] 5 | passenv = * 6 | commands = 7 | py.test --cov=async2rewrite --strict 8 | codecov -e TOXENV 9 | 10 | deps = 11 | pytest 12 | pytest-cov 13 | codecov 14 | --------------------------------------------------------------------------------