[V3 Fuzzy search] fix several issues with this feature (#1788)

* [V3 Fuzzy search] fix several issues with this feature

* Make it check if parent commands are hidden

* Check if compiler available in setup.py

* Let's just compile a dummy C file to check compiler availability

* Add a missing import + remove unneeded code
This commit is contained in:
palmtree5 2018-06-05 12:14:11 -08:00 committed by Kowlin
parent db5d4d5158
commit 0b78664792
7 changed files with 160 additions and 20 deletions

View File

@ -50,6 +50,7 @@ class RedBase(BotBase):
locale="en", locale="en",
embeds=True, embeds=True,
color=15158332, color=15158332,
fuzzy=False,
help__page_char_limit=1000, help__page_char_limit=1000,
help__max_pages_in_guild=2, help__max_pages_in_guild=2,
help__tagline="", help__tagline="",
@ -63,6 +64,7 @@ class RedBase(BotBase):
mod_role=None, mod_role=None,
embeds=None, embeds=None,
use_bot_color=False, use_bot_color=False,
fuzzy=False,
) )
self.db.register_user(embeds=None) self.db.register_user(embeds=None)

View File

@ -48,6 +48,21 @@ class Command(commands.Command):
# We don't want our help property to be overwritten, namely by super() # We don't want our help property to be overwritten, namely by super()
pass pass
@property
def parents(self):
"""
Returns all parent commands of this command.
This is a list, sorted by the length of :attr:`.qualified_name` from highest to lowest.
If the command has no parents, this will be an empty list.
"""
cmd = self.parent
entries = []
while cmd is not None:
entries.append(cmd)
cmd = cmd.parent
return sorted(entries, key=lambda x: len(x.qualified_name), reverse=True)
def command(self, cls=None, *args, **kwargs): def command(self, cls=None, *args, **kwargs):
"""A shortcut decorator that invokes :func:`.command` and adds it to """A shortcut decorator that invokes :func:`.command` and adds it to
the internal command list via :meth:`~.GroupMixin.add_command`. the internal command list via :meth:`~.GroupMixin.add_command`.

View File

@ -653,6 +653,39 @@ class Core(CoreLogic):
) )
) )
@_set.command()
@checks.guildowner()
@commands.guild_only()
async def serverfuzzy(self, ctx):
"""
Toggle whether to enable fuzzy command search for the server.
Default is for fuzzy command search to be disabled.
"""
current_setting = await ctx.bot.db.guild(ctx.guild).fuzzy()
await ctx.bot.db.guild(ctx.guild).fuzzy.set(not current_setting)
await ctx.send(
_("Fuzzy command search has been {} for this server.").format(
_("disabled") if current_setting else _("enabled")
)
)
@_set.command()
@checks.is_owner()
async def fuzzy(self, ctx):
"""
Toggle whether to enable fuzzy command search in DMs.
Default is for fuzzy command search to be disabled.
"""
current_setting = await ctx.bot.db.fuzzy()
await ctx.bot.db.fuzzy.set(not current_setting)
await ctx.send(
_("Fuzzy command search has been {} in DMs.").format(
_("disabled") if current_setting else _("enabled")
)
)
@_set.command(aliases=["color"]) @_set.command(aliases=["color"])
@checks.is_owner() @checks.is_owner()
async def colour(self, ctx, *, colour: discord.Colour = None): async def colour(self, ctx, *, colour: discord.Colour = None):

View File

@ -225,7 +225,9 @@ def init_events(bot, cli_flags):
term = ctx.invoked_with + " " term = ctx.invoked_with + " "
if len(ctx.args) > 1: if len(ctx.args) > 1:
term += " ".join(ctx.args[1:]) term += " ".join(ctx.args[1:])
await ctx.maybe_send_embed(fuzzy_command_search(ctx, ctx.invoked_with)) fuzzy_result = await fuzzy_command_search(ctx, ctx.invoked_with)
if fuzzy_result is not None:
await ctx.maybe_send_embed(fuzzy_result)
elif isinstance(error, commands.CheckFailure): elif isinstance(error, commands.CheckFailure):
pass pass
elif isinstance(error, commands.NoPrivateMessage): elif isinstance(error, commands.NoPrivateMessage):

View File

@ -281,11 +281,10 @@ class Help(formatter.HelpFormatter):
embed.set_author(**self.author) embed.set_author(**self.author)
return embed return embed
async def cmd_not_found(self, ctx, cmd, color=None): async def cmd_not_found(self, ctx, cmd, description=None, color=None):
# Shortcut for a shortcut. Sue me # Shortcut for a shortcut. Sue me
out = fuzzy_command_search(ctx, " ".join(ctx.args[1:]))
embed = await self.simple_embed( embed = await self.simple_embed(
ctx, title="Command {} not found.".format(cmd), description=out, color=color ctx, title="Command {} not found.".format(cmd), description=description, color=color
) )
return embed return embed
@ -326,10 +325,18 @@ async def help(ctx, *cmds: str):
command = ctx.bot.all_commands.get(name) command = ctx.bot.all_commands.get(name)
if command is None: if command is None:
if use_embeds: if use_embeds:
await destination.send(embed=await ctx.bot.formatter.cmd_not_found(ctx, name)) fuzzy_result = await fuzzy_command_search(ctx, name)
else: if fuzzy_result is not None:
await destination.send( await destination.send(
ctx.bot.command_not_found.format(name, fuzzy_command_search(ctx, name)) embed=await ctx.bot.formatter.cmd_not_found(
ctx, name, description=fuzzy_result
)
)
else:
fuzzy_result = await fuzzy_command_search(ctx, name)
if fuzzy_result is not None:
await destination.send(
ctx.bot.command_not_found.format(name, fuzzy_result)
) )
return return
if use_embeds: if use_embeds:
@ -341,11 +348,17 @@ async def help(ctx, *cmds: str):
command = ctx.bot.all_commands.get(name) command = ctx.bot.all_commands.get(name)
if command is None: if command is None:
if use_embeds: if use_embeds:
await destination.send(embed=await ctx.bot.formatter.cmd_not_found(ctx, name)) fuzzy_result = await fuzzy_command_search(ctx, name)
else: if fuzzy_result is not None:
await destination.send( await destination.send(
ctx.bot.command_not_found.format(name, fuzzy_command_search(ctx, name)) embed=await ctx.bot.formatter.cmd_not_found(
ctx, name, description=fuzzy_result
) )
)
else:
fuzzy_result = await fuzzy_command_search(ctx, name)
if fuzzy_result is not None:
await destination.send(ctx.bot.command_not_found.format(name, fuzzy_result))
return return
for key in cmds[1:]: for key in cmds[1:]:
@ -354,12 +367,18 @@ async def help(ctx, *cmds: str):
command = command.all_commands.get(key) command = command.all_commands.get(key)
if command is None: if command is None:
if use_embeds: if use_embeds:
fuzzy_result = await fuzzy_command_search(ctx, name)
if fuzzy_result is not None:
await destination.send( await destination.send(
embed=await ctx.bot.formatter.cmd_not_found(ctx, key) embed=await ctx.bot.formatter.cmd_not_found(
ctx, name, description=fuzzy_result
)
) )
else: else:
fuzzy_result = await fuzzy_command_search(ctx, name)
if fuzzy_result is not None:
await destination.send( await destination.send(
ctx.bot.command_not_found.format(key, fuzzy_command_search(ctx, name)) ctx.bot.command_not_found.format(name, fuzzy_result)
) )
return return
except AttributeError: except AttributeError:

View File

@ -3,11 +3,19 @@ __all__ = ["safe_delete", "fuzzy_command_search"]
from pathlib import Path from pathlib import Path
import os import os
import shutil import shutil
import logging
from redbot.core import commands from redbot.core import commands
from fuzzywuzzy import process from fuzzywuzzy import process
from .chat_formatting import box from .chat_formatting import box
def fuzzy_filter(record):
return record.funcName != "extractWithoutOrder"
logging.getLogger().addFilter(fuzzy_filter)
def safe_delete(pth: Path): def safe_delete(pth: Path):
if pth.exists(): if pth.exists():
for root, dirs, files in os.walk(str(pth)): for root, dirs, files in os.walk(str(pth)):
@ -19,9 +27,49 @@ def safe_delete(pth: Path):
shutil.rmtree(str(pth), ignore_errors=True) shutil.rmtree(str(pth), ignore_errors=True)
def fuzzy_command_search(ctx: commands.Context, term: str): async def filter_commands(ctx: commands.Context, extracted: list):
return [
i
for i in extracted
if i[1] >= 90
and not i[0].hidden
and await i[0].can_run(ctx)
and all([await p.can_run(ctx) for p in i[0].parents])
and not any([p.hidden for p in i[0].parents])
]
async def fuzzy_command_search(ctx: commands.Context, term: str):
out = "" out = ""
for pos, extracted in enumerate(process.extract(term, ctx.bot.walk_commands(), limit=5), 1): if ctx.guild is not None:
enabled = await ctx.bot.db.guild(ctx.guild).fuzzy()
else:
enabled = await ctx.bot.db.fuzzy()
if not enabled:
return None
alias_cog = ctx.bot.get_cog("Alias")
if alias_cog is not None:
is_alias, alias = await alias_cog.is_alias(ctx.guild, term)
if is_alias:
return None
customcom_cog = ctx.bot.get_cog("CustomCommands")
if customcom_cog is not None:
cmd_obj = customcom_cog.commandobj
try:
ccinfo = await cmd_obj.get(ctx.message, term)
except:
pass
else:
return None
extracted_cmds = await filter_commands(
ctx, process.extract(term, ctx.bot.walk_commands(), limit=5)
)
if not extracted_cmds:
return None
for pos, extracted in enumerate(extracted_cmds, 1):
out += "{0}. {1.prefix}{2.qualified_name}{3}\n".format( out += "{0}. {1.prefix}{2.qualified_name}{3}\n".format(
pos, pos,
ctx, ctx,

View File

@ -1,6 +1,10 @@
from distutils.core import setup from distutils.core import setup
from distutils import ccompiler
from distutils.errors import CCompilerError
from pathlib import Path from pathlib import Path
import re import re
import importlib
import tempfile
import os import os
import sys import sys
@ -21,6 +25,20 @@ def get_package_list():
return core return core
def check_compiler_available():
m = ccompiler.new_compiler()
with tempfile.TemporaryDirectory() as tdir:
with tempfile.NamedTemporaryFile(prefix="dummy", suffix=".c", dir=tdir) as tfile:
tfile.write(b"int main(int argc, char** argv) {return 0;}")
tfile.seek(0)
try:
m.compile([tfile.name])
except CCompilerError:
return False
return True
def get_requirements(): def get_requirements():
with open("requirements.txt") as f: with open("requirements.txt") as f:
requirements = f.read().splitlines() requirements = f.read().splitlines()
@ -31,6 +49,9 @@ def get_requirements():
except ValueError: except ValueError:
pass pass
if not check_compiler_available(): # Can't compile python-Levensthein, so drop extra
requirements.remove("fuzzywuzzy[speedup]<=0.16.0")
requirements.append("fuzzywuzzy<=0.16.0")
if IS_DEPLOYING or not (IS_TRAVIS or IS_RTD): if IS_DEPLOYING or not (IS_TRAVIS or IS_RTD):
requirements.append("discord.py>=1.0.0a0") requirements.append("discord.py>=1.0.0a0")
if sys.platform.startswith("linux"): if sys.platform.startswith("linux"):