Based on the different answers, the only thing that I missed was the local variables (couldn't make them be global).
Here is what I did :
async def async_eval_or_exec(message, vars):
# Translating the command into a function
# The function returns the main result AND the local variables to make them accessible outside
if message.count("\n"):
function = "async def __execute__():\n " + message.replace("\n", " ") + "\n return None, locals()\n"
else:
function = "async def __execute__():\n return " + message + ", locals()\n"
# The execution - get the result of
try:
exec(function, vars, vars)
result = await vars["__execute__"] ()
if result[ 0 ] is not None:
return result[ 0 ]
vars.update(result[ 1 ]) # forces the local variables (inside __execute__) to be global
except SyntaxError: # for commands like "import os"
exec(message, vars, vars)
And then, I can run :
>>> vars = {}
>>> await async_eval_or_exec('x = 1', vars)
>>> await async_eval_or_exec('await asyncio.sleep(x)', vars)
>>> await async_eval_or_exec('print(x)', vars)
1
>>>
It can be useful if you create an async interpreter (to not lose objects you create inside the execute function).
I think it is better than using the "global" command every time you create a variable.
await eval('asyncio.sleep(1))await eval('asyncio.sleep(x)', globals(), {'x': 1})