A cool, but likely finicky, exec command for a discord.py-based bot
- Features
- Feature Details
- Command Usage
- Examples
- Project Layout
- How To Add To Your Bot
- Bugs
- Random Info
- No need use awaits in your code! (Example 1, Example 2)
- Displays expression values just like an interactive interpretter! (Example)
- Can call async code in synchronous code!!! (Example)
- Barely tested code that uses asyncio internals in ways they never should!
- BUGS!!! (hopefully not, but who knows)
Note: This is not accurate when it comes to code in
async def
functions.
They will still need awaits in their function body
This command effectively causes async and sync functions to be treated the same in the passed in code.
asynchronous code is executed asynchronously so as not to block the rest of the bot, but in such a way that to the passed in code it appears as if it is just another synchronous call.
This means that you don't have to worry about mixing async and synchronous code. They can both be executed from each other and mixed to your hearts desire.
Just like how the Python Interactive Interpreter will display values of expressions and return values that aren't being assigned
So too will this function.
Debugging has never been easier! (And with this command, you will likely be doing a lot of it)
I'm sure you have had the struggle where you are trying to interop your bot with a synchronous library and how difficult that is.
Having that brought over to quick use commands that will likely be used for debugging is annoying and frustrating.
This command avoids that.
(To this command, EVERYTHING is synchronous)
There are a few little things and definitions that are important to know for the commands operation
-
Definitions
"Top level": Top level refer to anything that is not wrapped in a function or class definition
i.e. it is being executed in the global scope# Top level print("I'm top level!") try: print("Me too!") # Not top level def func(): print("I am not top level")
-
Awaits
This command simplifies calling async code. It removes the need to be in an async function to call async code.
It also removes the need to use "await" when calling async functions.
And even if you do use "await", it will still work! (If it doesn't, just get rid of the "await" and it probably will. Nobody likes awaits, not even this command)You can even use await on non-async functions due to the way this command modifies function calls in the passed in source code. It doesn't care if it is an async or sync function, they are both called the same way!
This also adds an interesting, and very useful side-effect. Because both async and sync are treated as sync (it is slightly more complex than that), you can do things you wouldn't normally be able such as using an async function call as an argument to a function (Async Call As Value For Function Argument)
Important: At this point in time, none of this applies inside async function definitions in the passed in code. That remains unmodified and still needs awaits -
Interactive Interpreter Style Expression Printing
The command will print all top level statements just like the python interactive interpreter would.
Internally this works by wrapping expressions in a function call that prints out the value, skipping over None.
Expressions by Pythons definition are function calls or expressions that aren't part of an assignment (Example)
Those are the only real important things to remember as compared to a normal exec command.
Just pass in any normal python code. Nothing special
Here are some examples showing what it will print.
1 + 1
"Hello World"
bot.send_message(channel, "hello").content
=== Output ===
2
Hello World
hello
bot.send_message(channel, text)
Yes, it is that simple
This will wait until the specified user posts a message in the given channel.
It will then send another message that contains what the user just sent.
This is not possible (so easily at least) in normal exec commands. This command is different
bot.send_message(channel, bot.wait_for_message(channel=channel, author=author).content)
-
eval_ast_gen.py
This contains all of the functions for generating and modifying the AST of the passed in code.
It is where almost all of the action is going on. -
exec_command.py This contains the implementation of the exec command.
It is a slightly edited version of the one that my bot is currently using.
All of the code is pretty much uncommented. It should still be at least partially possible to understand the code without them.
The file "exec_command.py" contains the command
Do note that it is not in a form that you can just "load a cog"
You will need to edit it so that your bot is able to call and use the command
Do note that this was designed for the d.py "async" branch
I have not tested the provided command implementation on d.py "rewrite" The actual execution and AST generation on the otherhand is generalised and should work on either
While the exec's wont block other bot functions, they will block each other.
If you run one exec that waits for a message, then run another. The new exec will block the old one until it exits.
This is due to the execs being synchronous. This means that in order to get values out of coroutines, the synchronous code has to take manual control of the event loop.
In short: They nest
While I can't say for sure that I am not (I mean, seriously. Just look at the code!), I didn't do it for that reason.
I did it for fun and to see if I could.
Asyncio really does not like synchronous code and if you ever need to use some you will end up either doing some very strange things or, in the case of synchronous code that needs to use async code, just forget about doing it at all.
The easy way would have been to use threads or multiple processes (or even to just turn it into an async function). But I get annoyed with people always saying how "great" and "good" async is and how terrible threads are and so I thought I would twist that in and make an exec function that uses no extra threads to do synchronous code.
There is at least one thing I am "crazy" for doing, and that is not wrapping the users code in
an async def.
It wouldn't matter if I just stripped away the async def at the end, it would just make things far better. And if I left it there, it would mean that I would not need to do any of this needless "Sync calling Async" rubbish.
But... That wouldn't be any fun!
The way python parses "await" is pretty hacky as-is. I didn't want to modify the user passed in code by wrapping it in an async func def just to strip it away after. Instead I am tokenizing and modifying each syntax error until there are none left (or unless it hits more than 10).
It is very hacky and repeated tokenization, modification, and then retokenisation eventually ends up messing with the line of source it is modifying.
May change to just wrap in an async func def then strip away after ast generation. May not strip At which point this will likely go away
Best answer: "I hope so, because this is a pretty terrible way of doing things"
Maybe, maybe not. I have no idea and I can't be bothered looking.
Likely someone else has tried something similar.