[V3 Dev] Various fixes and improvements to Dev (#981)

* Include discord in [p]debug env

* Typecast eval output to string

* Use globals instead of locals for debug

* Fix up storing last result and non-string results

* Cleanup code and better help messages
This commit is contained in:
Tobotimus 2017-10-16 13:45:09 +11:00 committed by Will
parent 4993c5a675
commit 980a1a452c

View File

@ -6,12 +6,10 @@ import traceback
from contextlib import redirect_stdout from contextlib import redirect_stdout
import discord import discord
from discord.ext import commands
from . import checks from . import checks
from .i18n import CogI18n from .i18n import CogI18n
from discord.ext import commands
from .utils.chat_formatting import box, pagify from .utils.chat_formatting import box, pagify
""" """
Notice: Notice:
@ -24,7 +22,8 @@ _ = CogI18n("Dev", __file__)
class Dev: class Dev:
"""Various development focused utilities""" """Various development focused utilities."""
def __init__(self): def __init__(self):
self._last_result = None self._last_result = None
self.sessions = set() self.sessions = set()
@ -41,15 +40,23 @@ class Dev:
@staticmethod @staticmethod
def get_syntax_error(e): def get_syntax_error(e):
"""Format a syntax error to send to the user.
Returns a string representation of the error formatted as a codeblock.
"""
if e.text is None: if e.text is None:
return '```py\n{0.__class__.__name__}: {0}\n```'.format(e) return box('{0.__class__.__name__}: {0}'.format(e), lang="py")
return '```py\n{0.text}{1:>{0.offset}}\n{2}: {0}```'.format(e, '^', type(e).__name__) return box(
'{0.text}{1:>{0.offset}}\n{2}: {0}'
''.format(e, '^', type(e).__name__),
lang="py")
@staticmethod @staticmethod
def sanitize_output(ctx: commands.Context, input: str) -> str: def sanitize_output(ctx: commands.Context, input_: str) -> str:
"""Hides the bot's token from a string."""
token = ctx.bot.http.token token = ctx.bot.http.token
r = "[EXPUNGED]" r = "[EXPUNGED]"
result = input.replace(token, r) result = input_.replace(token, r)
result = result.replace(token.lower(), r) result = result.replace(token.lower(), r)
result = result.replace(token.upper(), r) result = result.replace(token.upper(), r)
return result return result
@ -57,49 +64,25 @@ class Dev:
@commands.command() @commands.command()
@checks.is_owner() @checks.is_owner()
async def debug(self, ctx, *, code): async def debug(self, ctx, *, code):
""" """Evaluate a statement of python code.
Executes code and prints the result to discord.
"""
env = {
'bot': ctx.bot,
'ctx': ctx,
'channel': ctx.channel,
'author': ctx.author,
'guild': ctx.guild,
'message': ctx.message
}
code = self.cleanup_code(code) The bot will always respond with the return value of the code.
If the return value of the code is a coroutine, it will be awaited,
and the result of that will be the bot's response.
try: Note: Only one statement may be evaluated. Using await, yield or
result = eval(code, env, locals()) similar restricted keywords will result in a syntax error. For multiple
except SyntaxError as e: lines or asynchronous code, see [p]repl or [p]eval.
await ctx.send(self.get_syntax_error(e))
return
except Exception as e:
await ctx.send('```py\n{}: {}```'.format(type(e).__name__, str(e)), )
return
if asyncio.iscoroutine(result): Environment Variables:
result = await result ctx - command invokation context
bot - bot object
result = str(result) channel - the current channel object
author - command author's member object
result = self.sanitize_output(ctx, result) message - the command's message object
discord - discord.py library
await ctx.send(box(result, lang="py")) commands - discord.py commands extension
_ - The result of the last dev command.
@commands.command(name='eval')
@checks.is_owner()
async def _eval(self, ctx, *, body: str):
"""
Executes code as if it was the body of an async function
code MUST be in a code block using three ticks and
there MUST be a newline after the first set and
before the last set. This function will ONLY output
the return value of the function code AND anything
that is output to stdout (e.g. using a print()
statement).
""" """
env = { env = {
'bot': ctx.bot, 'bot': ctx.bot,
@ -108,10 +91,65 @@ class Dev:
'author': ctx.author, 'author': ctx.author,
'guild': ctx.guild, 'guild': ctx.guild,
'message': ctx.message, 'message': ctx.message,
'discord': discord,
'commands': commands,
'_': self._last_result '_': self._last_result
} }
env.update(globals()) code = self.cleanup_code(code)
try:
result = eval(code, env)
except SyntaxError as e:
await ctx.send(self.get_syntax_error(e))
return
except Exception as e:
await ctx.send(
box('{}: {!s}'.format(type(e).__name__, e), lang='py'))
return
if asyncio.iscoroutine(result):
result = await result
self._last_result = result
result = self.sanitize_output(ctx, str(result))
await ctx.send(box(result, lang="py"))
@commands.command(name='eval')
@checks.is_owner()
async def _eval(self, ctx, *, body: str):
"""Execute asynchronous code.
This command wraps code into the body of an async function and then
calls and awaits it. The bot will respond with anything printed to
stdout, as well as the return value of the function.
The code can be within a codeblock, inline code or neither, as long
as they are not mixed and they are formatted correctly.
Environment Variables:
ctx - command invokation context
bot - bot object
channel - the current channel object
author - command author's member object
message - the command's message object
discord - discord.py library
commands - discord.py commands extension
_ - The result of the last dev command.
"""
env = {
'bot': ctx.bot,
'ctx': ctx,
'channel': ctx.channel,
'author': ctx.author,
'guild': ctx.guild,
'message': ctx.message,
'discord': discord,
'commands': commands,
'_': self._last_result
}
body = self.cleanup_code(body) body = self.cleanup_code(body)
stdout = io.StringIO() stdout = io.StringIO()
@ -129,28 +167,35 @@ class Dev:
ret = await func() ret = await func()
except: except:
value = stdout.getvalue() value = stdout.getvalue()
await ctx.send(box('\n{}{}'.format(value, traceback.format_exc()), lang="py")) await ctx.send(
box('\n{}{}'.format(value, traceback.format_exc()), lang="py"))
else: else:
value = stdout.getvalue() value = stdout.getvalue()
try: try:
await ctx.bot.add_reaction(ctx.message, '\u2705') await ctx.message.add_reaction('\N{White Heavy Check Mark}')
except: except:
pass pass
if ret is None: if ret is None:
if value: if value:
value = self.sanitize_output(ctx, value) value = self.sanitize_output(ctx, str(value))
await ctx.send(box(value, lang="py")) await ctx.send(box(value, lang="py"))
else: else:
ret = self.sanitize_output(ctx, ret)
self._last_result = ret self._last_result = ret
ret = self.sanitize_output(ctx, str(ret))
await ctx.send(box("{}{}".format(value, ret), lang="py")) await ctx.send(box("{}{}".format(value, ret), lang="py"))
@commands.command() @commands.command()
@checks.is_owner() @checks.is_owner()
async def repl(self, ctx): async def repl(self, ctx):
""" """Open an interactive REPL.
Opens an interactive REPL.
The REPL will only recognise code as messages which start with a
backtick. This includes codeblocks, and as such multiple lines can be
evaluated.
You may not await any code in this REPL unless you define it inside an
async function.
""" """
variables = { variables = {
'ctx': ctx, 'ctx': ctx,
@ -163,20 +208,20 @@ class Dev:
} }
if ctx.channel.id in self.sessions: if ctx.channel.id in self.sessions:
await ctx.send(_('Already running a REPL session in this channel. Exit it with `quit`.')) await ctx.send(_('Already running a REPL session in this channel. '
'Exit it with `quit`.'))
return return
self.sessions.add(ctx.channel.id) self.sessions.add(ctx.channel.id)
await ctx.send(_('Enter code to execute or evaluate. `exit()` or `quit` to exit.')) await ctx.send(_('Enter code to execute or evaluate.'
' `exit()` or `quit` to exit.'))
def msg_check(m): msg_check = lambda m: (m.author == ctx.author and
return m.author == ctx.author and m.channel == ctx.channel and \ m.channel == ctx.channel and
m.content.startswith('`') m.content.startswith('`'))
while True: while True:
response = await ctx.bot.wait_for( response = await ctx.bot.wait_for("message", check=msg_check)
"message",
check=msg_check)
cleaned = self.cleanup_code(response.content) cleaned = self.cleanup_code(response.content)
@ -237,9 +282,10 @@ class Dev:
@commands.command() @commands.command()
@checks.is_owner() @checks.is_owner()
async def mock(self, ctx, user: discord.Member, *, command): async def mock(self, ctx, user: discord.Member, *, command):
"""Runs a command as if it was issued by a different user """Mock another user invoking a command.
The prefix must not be entered""" The prefix must not be entered.
"""
# Since we have stateful objects now this might be pretty bad # Since we have stateful objects now this might be pretty bad
# Sorry Danny # Sorry Danny
old_author = ctx.author old_author = ctx.author
@ -255,9 +301,11 @@ class Dev:
@commands.command(name="mockmsg") @commands.command(name="mockmsg")
@checks.is_owner() @checks.is_owner()
async def mock_msg(self, ctx, user: discord.Member, *, content: str): async def mock_msg(self, ctx, user: discord.Member, *, content: str):
"""Bot receives a message is if it were sent by a different user. """Dispatch a message event as if it were sent by a different user.
Only reads the raw content of the message. Attachments, embeds etc. are ignored.""" Only reads the raw content of the message. Attachments, embeds etc. are
ignored.
"""
old_author = ctx.author old_author = ctx.author
old_content = ctx.message.content old_content = ctx.message.content
ctx.message.author = user ctx.message.author = user
@ -265,7 +313,8 @@ class Dev:
ctx.bot.dispatch("message", ctx.message) ctx.bot.dispatch("message", ctx.message)
await asyncio.sleep(2) # If we change the author and content back too quickly, # If we change the author and content back too quickly,
# the bot won't process the mocked message in time. # the bot won't process the mocked message in time.
await asyncio.sleep(2)
ctx.message.author = old_author ctx.message.author = old_author
ctx.message.content = old_content ctx.message.content = old_content