From 0b78664792c058f9a310fe1d2b08bfbac0148668 Mon Sep 17 00:00:00 2001 From: palmtree5 <3577255+palmtree5@users.noreply.github.com> Date: Tue, 5 Jun 2018 12:14:11 -0800 Subject: [PATCH] [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 --- redbot/core/bot.py | 2 ++ redbot/core/commands/commands.py | 15 +++++++++ redbot/core/core_commands.py | 33 ++++++++++++++++++++ redbot/core/events.py | 4 ++- redbot/core/help_formatter.py | 53 ++++++++++++++++++++++---------- redbot/core/utils/__init__.py | 52 +++++++++++++++++++++++++++++-- setup.py | 21 +++++++++++++ 7 files changed, 160 insertions(+), 20 deletions(-) diff --git a/redbot/core/bot.py b/redbot/core/bot.py index f0cf23d74..4e3e5fae5 100644 --- a/redbot/core/bot.py +++ b/redbot/core/bot.py @@ -50,6 +50,7 @@ class RedBase(BotBase): locale="en", embeds=True, color=15158332, + fuzzy=False, help__page_char_limit=1000, help__max_pages_in_guild=2, help__tagline="", @@ -63,6 +64,7 @@ class RedBase(BotBase): mod_role=None, embeds=None, use_bot_color=False, + fuzzy=False, ) self.db.register_user(embeds=None) diff --git a/redbot/core/commands/commands.py b/redbot/core/commands/commands.py index 8f2a87b19..7fd13a3d4 100644 --- a/redbot/core/commands/commands.py +++ b/redbot/core/commands/commands.py @@ -48,6 +48,21 @@ class Command(commands.Command): # We don't want our help property to be overwritten, namely by super() 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): """A shortcut decorator that invokes :func:`.command` and adds it to the internal command list via :meth:`~.GroupMixin.add_command`. diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py index ec8c82a5b..6c4c75d38 100644 --- a/redbot/core/core_commands.py +++ b/redbot/core/core_commands.py @@ -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"]) @checks.is_owner() async def colour(self, ctx, *, colour: discord.Colour = None): diff --git a/redbot/core/events.py b/redbot/core/events.py index 67046bfca..4acdf9075 100644 --- a/redbot/core/events.py +++ b/redbot/core/events.py @@ -225,7 +225,9 @@ def init_events(bot, cli_flags): 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)) + 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): pass elif isinstance(error, commands.NoPrivateMessage): diff --git a/redbot/core/help_formatter.py b/redbot/core/help_formatter.py index 147b0272c..8a983e981 100644 --- a/redbot/core/help_formatter.py +++ b/redbot/core/help_formatter.py @@ -281,11 +281,10 @@ class Help(formatter.HelpFormatter): embed.set_author(**self.author) 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 - out = fuzzy_command_search(ctx, " ".join(ctx.args[1:])) 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 @@ -326,11 +325,19 @@ async def help(ctx, *cmds: str): command = ctx.bot.all_commands.get(name) if command is None: if use_embeds: - await destination.send(embed=await ctx.bot.formatter.cmd_not_found(ctx, name)) + fuzzy_result = await fuzzy_command_search(ctx, name) + if fuzzy_result is not None: + await destination.send( + embed=await ctx.bot.formatter.cmd_not_found( + ctx, name, description=fuzzy_result + ) + ) else: - await destination.send( - ctx.bot.command_not_found.format(name, fuzzy_command_search(ctx, name)) - ) + 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 if use_embeds: embeds = await ctx.bot.formatter.format_help_for(ctx, command) @@ -341,11 +348,17 @@ async def help(ctx, *cmds: str): command = ctx.bot.all_commands.get(name) if command is None: if use_embeds: - await destination.send(embed=await ctx.bot.formatter.cmd_not_found(ctx, name)) + fuzzy_result = await fuzzy_command_search(ctx, name) + if fuzzy_result is not None: + await destination.send( + embed=await ctx.bot.formatter.cmd_not_found( + ctx, name, description=fuzzy_result + ) + ) else: - await destination.send( - ctx.bot.command_not_found.format(name, fuzzy_command_search(ctx, name)) - ) + 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 for key in cmds[1:]: @@ -354,13 +367,19 @@ async def help(ctx, *cmds: str): command = command.all_commands.get(key) if command is None: if use_embeds: - await destination.send( - embed=await ctx.bot.formatter.cmd_not_found(ctx, key) - ) + fuzzy_result = await fuzzy_command_search(ctx, name) + if fuzzy_result is not None: + await destination.send( + embed=await ctx.bot.formatter.cmd_not_found( + ctx, name, description=fuzzy_result + ) + ) else: - await destination.send( - ctx.bot.command_not_found.format(key, fuzzy_command_search(ctx, name)) - ) + 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 except AttributeError: if use_embeds: diff --git a/redbot/core/utils/__init__.py b/redbot/core/utils/__init__.py index c879fb871..f279f46c0 100644 --- a/redbot/core/utils/__init__.py +++ b/redbot/core/utils/__init__.py @@ -3,11 +3,19 @@ __all__ = ["safe_delete", "fuzzy_command_search"] from pathlib import Path import os import shutil +import logging from redbot.core import commands from fuzzywuzzy import process from .chat_formatting import box +def fuzzy_filter(record): + return record.funcName != "extractWithoutOrder" + + +logging.getLogger().addFilter(fuzzy_filter) + + def safe_delete(pth: Path): if pth.exists(): 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) -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 = "" - 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( pos, ctx, diff --git a/setup.py b/setup.py index c78ebbab1..35343c4de 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,10 @@ from distutils.core import setup +from distutils import ccompiler +from distutils.errors import CCompilerError from pathlib import Path import re +import importlib +import tempfile import os import sys @@ -21,6 +25,20 @@ def get_package_list(): 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(): with open("requirements.txt") as f: requirements = f.read().splitlines() @@ -31,6 +49,9 @@ def get_requirements(): except ValueError: 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): requirements.append("discord.py>=1.0.0a0") if sys.platform.startswith("linux"):