[V3] Start work on fuzzy command search (#1600)

* [V3] Start work on fuzzy command search

* Implement in command error handler

* Something isn't working here, try fixing

* Style compliance

* Add fuzzywuzzy to pipfile

* Dump the short doc part if there is no short doc

* Add fuzzy command search on command not found in help

* Move things around, implement for use of default d.py help formatter

* Formatting compliance

* Undo pipfile changes
This commit is contained in:
palmtree5 2018-05-27 18:57:10 -08:00 committed by Kowlin
parent 4028dd3009
commit 4f270f3aab
5 changed files with 39 additions and 11 deletions

View File

@ -86,6 +86,9 @@ class RedBase(BotBase):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.run_until_complete(self._dict_abuse(kwargs)) loop.run_until_complete(self._dict_abuse(kwargs))
if "command_not_found" not in kwargs:
kwargs["command_not_found"] = "Command {} not found.\n{}"
self.counter = Counter() self.counter = Counter()
self.uptime = None self.uptime = None

View File

@ -15,7 +15,8 @@ from discord.ext import commands
from . import __version__ from . import __version__
from .data_manager import storage_type from .data_manager import storage_type
from .utils.chat_formatting import inline, bordered from .utils.chat_formatting import inline, bordered, pagify, box
from .utils import fuzzy_command_search
from colorama import Fore, Style, init from colorama import Fore, Style, init
log = logging.getLogger("red") log = logging.getLogger("red")
@ -221,7 +222,10 @@ def init_events(bot, cli_flags):
if not hasattr(ctx.cog, "_{0.command.cog_name}__error".format(ctx)): if not hasattr(ctx.cog, "_{0.command.cog_name}__error".format(ctx)):
await ctx.send(inline(message)) await ctx.send(inline(message))
elif isinstance(error, commands.CommandNotFound): elif isinstance(error, commands.CommandNotFound):
pass term = ctx.invoked_with + " "
if len(ctx.args) > 1:
term += " ".join(ctx.args[1:])
await ctx.maybe_send_embed(fuzzy_command_search(ctx, ctx.invoked_with))
elif isinstance(error, commands.CheckFailure): elif isinstance(error, commands.CheckFailure):
await ctx.send("⛔ You are not authorized to issue that command.") await ctx.send("⛔ You are not authorized to issue that command.")
elif isinstance(error, commands.NoPrivateMessage): elif isinstance(error, commands.NoPrivateMessage):

View File

@ -36,7 +36,8 @@ import sys
import traceback import traceback
from . import commands from . import commands
from redbot.core.utils.chat_formatting import pagify from redbot.core.utils.chat_formatting import pagify, box
from redbot.core.utils import fuzzy_command_search
EMPTY_STRING = u"\u200b" EMPTY_STRING = u"\u200b"
@ -277,11 +278,9 @@ class Help(formatter.HelpFormatter):
def cmd_not_found(self, ctx, cmd, color=None): def cmd_not_found(self, ctx, cmd, color=None):
# Shortcut for a shortcut. Sue me # Shortcut for a shortcut. Sue me
out = fuzzy_command_search(ctx, " ".join(ctx.args[1:]))
embed = self.simple_embed( embed = self.simple_embed(
ctx, ctx, title="Command {} not found.".format(cmd), description=out, color=color
title=ctx.bot.command_not_found.format(cmd),
description="Commands are case sensitive. Please check your spelling and try again",
color=color,
) )
return embed return embed
@ -324,7 +323,9 @@ async def help(ctx, *cmds: str):
if use_embeds: if use_embeds:
await destination.send(embed=ctx.bot.formatter.cmd_not_found(ctx, name)) await destination.send(embed=ctx.bot.formatter.cmd_not_found(ctx, name))
else: else:
await destination.send(ctx.bot.command_not_found.format(name)) await destination.send(
ctx.bot.command_not_found.format(name, fuzzy_command_search(ctx, name))
)
return return
if use_embeds: if use_embeds:
embeds = await ctx.bot.formatter.format_help_for(ctx, command) embeds = await ctx.bot.formatter.format_help_for(ctx, command)
@ -337,7 +338,9 @@ async def help(ctx, *cmds: str):
if use_embeds: if use_embeds:
await destination.send(embed=ctx.bot.formatter.cmd_not_found(ctx, name)) await destination.send(embed=ctx.bot.formatter.cmd_not_found(ctx, name))
else: else:
await destination.send(ctx.bot.command_not_found.format(name)) await destination.send(
ctx.bot.command_not_found.format(name, fuzzy_command_search(ctx, name))
)
return return
for key in cmds[1:]: for key in cmds[1:]:
@ -348,7 +351,9 @@ async def help(ctx, *cmds: str):
if use_embeds: if use_embeds:
await destination.send(embed=ctx.bot.formatter.cmd_not_found(ctx, key)) await destination.send(embed=ctx.bot.formatter.cmd_not_found(ctx, key))
else: else:
await destination.send(ctx.bot.command_not_found.format(key)) await destination.send(
ctx.bot.command_not_found.format(key, fuzzy_command_search(ctx, name))
)
return return
except AttributeError: except AttributeError:
if use_embeds: if use_embeds:

View File

@ -1,8 +1,11 @@
__all__ = ["TYPE_CHECKING", "NewType", "safe_delete"] __all__ = ["TYPE_CHECKING", "NewType", "safe_delete", "fuzzy_command_search"]
from pathlib import Path from pathlib import Path
import os import os
import shutil import shutil
from redbot.core import commands
from fuzzywuzzy import process
from .chat_formatting import box
try: try:
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -26,3 +29,15 @@ def safe_delete(pth: Path):
for f in files: for f in files:
os.chmod(os.path.join(root, f), 0o755) os.chmod(os.path.join(root, f), 0o755)
shutil.rmtree(str(pth), ignore_errors=True) shutil.rmtree(str(pth), ignore_errors=True)
def fuzzy_command_search(ctx: commands.Context, term: str):
out = ""
for pos, extracted in enumerate(process.extract(term, ctx.bot.walk_commands(), limit=5), 1):
out += "{0}. {1.prefix}{2.qualified_name}{3}\n".format(
pos,
ctx,
extracted[0],
" - {}".format(extracted[0].short_doc) if extracted[0].short_doc else "",
)
return box(out, lang="Perhaps you wanted one of these?")

View File

@ -5,5 +5,6 @@ raven==6.5.0
colorama==0.3.9 colorama==0.3.9
jsonrpcserver jsonrpcserver
pyyaml==3.12 pyyaml==3.12
fuzzywuzzy[speedup]<=0.16.0
Red-Trivia>=1.1.1 Red-Trivia>=1.1.1
async-timeout<3.0.0 async-timeout<3.0.0