diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index 3d6899987e..1e4786e1ee 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -6,8 +6,6 @@ set. The normal, database-tied help system is used for collaborative creation of other help topics such as RP help or game-world aides. """ -from lunr import lunr -from lunr.exceptions import QueryParseError from django.conf import settings from collections import defaultdict from evennia.utils.utils import fill, dedent @@ -16,7 +14,12 @@ from evennia.help.models import HelpEntry from evennia.utils import create, evmore from evennia.utils.ansi import ANSIString from evennia.utils.eveditor import EvEditor -from evennia.utils.utils import string_suggestions, class_from_module, inherits_from, format_grid +from evennia.utils.utils import ( + string_suggestions, + class_from_module, + inherits_from, + format_grid, +) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) HELP_MORE = settings.HELP_MORE @@ -27,6 +30,9 @@ __all__ = ("CmdHelp", "CmdSetHelp") _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH _SEP = "|C" + "-" * _DEFAULT_WIDTH + "|n" +_LUNR = None +_LUNR_EXCEPTION = None + class HelpCategory: def __init__(self, key): @@ -34,7 +40,13 @@ class HelpCategory: @property def search_index_entry(self): - return {"key": str(self), "aliases": "", "category": self.key, "tags": "", "text": ""} + return { + "key": str(self), + "aliases": "", + "category": self.key, + "tags": "", + "text": "", + } def __str__(self): return f"Category: {self.key}" @@ -47,30 +59,42 @@ class HelpCategory: def help_search_with_index(query, candidate_entries, suggestion_maxnum=5): + """ + Lunr-powered fast index search and suggestion wrapper + """ + global _LUNR, _LUNR_EXCEPTION + if not _LUNR: + # we have to delay-load lunr because it messes with logging if it's imported + # before twisted's logging has been set up + from lunr import lunr as _LUNR + from lunr.exceptions import QueryParseError as _LUNR_EXCEPTION + indx = [cnd.search_index_entry for cnd in candidate_entries] mapping = {indx[ix]["key"]: cand for ix, cand in enumerate(candidate_entries)} - search_index = lunr( + search_index = _LUNR( ref="key", fields=[ - {"field_name": "key", "boost": 10,}, - {"field_name": "aliases", "boost": 9,}, - {"field_name": "category", "boost": 8,}, + {"field_name": "key", "boost": 10}, + {"field_name": "aliases", "boost": 9}, + {"field_name": "category", "boost": 8}, {"field_name": "tags", "boost": 5}, - {"field_name": "text", "boost": 1,}, + {"field_name": "text", "boost": 1}, ], documents=indx, ) try: matches = search_index.search(query)[:suggestion_maxnum] - except QueryParseError: + except _LUNR_EXCEPTION: # this is a user-input problem matches = [] # matches (objs), suggestions (strs) return ( [mapping[match["ref"]] for match in matches], - [str(match["ref"]) for match in matches], # + f" (score {match['score']})") # good debug + [ + str(match["ref"]) for match in matches + ], # + f" (score {match['score']})") # good debug ) @@ -116,7 +140,10 @@ class CmdHelp(Command): if type(self).help_more: usemore = True - if self.session and self.session.protocol_key in ("websocket", "ajax/comet"): + if self.session and self.session.protocol_key in ( + "websocket", + "ajax/comet", + ): try: options = self.account.db._saved_webclient_options if options and options["helppopup"]: @@ -150,7 +177,9 @@ class CmdHelp(Command): if title: string += "|CHelp for |w%s|n" % title if aliases: - string += " |C(aliases: %s|C)|n" % ("|C,|n ".join("|w%s|n" % ali for ali in aliases)) + string += " |C(aliases: %s|C)|n" % ( + "|C,|n ".join("|w%s|n" % ali for ali in aliases) + ) if help_text: string += "\n%s" % dedent(help_text.rstrip()) if suggested: @@ -177,15 +206,22 @@ class CmdHelp(Command): category_str = f"-- {category.title()} " grid.append( ANSIString( - category_clr + category_str + "-" * (width - len(category_str)) + topic_clr + category_clr + + category_str + + "-" * (width - len(category_str)) + + topic_clr ) ) verbatim_elements.append(len(grid) - 1) - entries = sorted(set(hdict_cmds.get(category, []) + hdict_db.get(category, []))) + entries = sorted( + set(hdict_cmds.get(category, []) + hdict_db.get(category, [])) + ) grid.extend(entries) - gridrows = format_grid(grid, width, sep=" ", verbatim_elements=verbatim_elements) + gridrows = format_grid( + grid, width, sep=" ", verbatim_elements=verbatim_elements + ) gridrows = ANSIString("\n").join(gridrows) return gridrows @@ -257,7 +293,9 @@ class CmdHelp(Command): # retrieve all available commands and database topics all_cmds = [cmd for cmd in cmdset if self.check_show_help(cmd, caller)] all_topics = [ - topic for topic in HelpEntry.objects.all() if topic.access(caller, "view", default=True) + topic + for topic in HelpEntry.objects.all() + if topic.access(caller, "view", default=True) ] all_categories = list( set( @@ -282,7 +320,11 @@ class CmdHelp(Command): return # Try to access a particular help entry or category - entries = [cmd for cmd in all_cmds if cmd] + list(HelpEntry.objects.all()) + all_categories + entries = ( + [cmd for cmd in all_cmds if cmd] + + list(HelpEntry.objects.all()) + + all_categories + ) for match_query in [f"{query}~1", f"{query}*"]: # We first do an exact word-match followed by a start-by query @@ -407,14 +449,19 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS): self.msg("You have to define a topic!") return topicstrlist = topicstr.split(";") - topicstr, aliases = (topicstrlist[0], topicstrlist[1:] if len(topicstr) > 1 else []) + topicstr, aliases = ( + topicstrlist[0], + topicstrlist[1:] if len(topicstr) > 1 else [], + ) aliastxt = ("(aliases: %s)" % ", ".join(aliases)) if aliases else "" old_entry = None # check if we have an old entry with the same name try: for querystr in topicstrlist: - old_entry = HelpEntry.objects.find_topicmatch(querystr) # also search by alias + old_entry = HelpEntry.objects.find_topicmatch( + querystr + ) # also search by alias if old_entry: old_entry = list(old_entry)[0] break @@ -436,7 +483,11 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS): helpentry = old_entry else: helpentry = create.create_help_entry( - topicstr, self.rhs, category=category, locks=lockstring, aliases=aliases + topicstr, + self.rhs, + category=category, + locks=lockstring, + aliases=aliases, ) self.caller.db._editing_help = helpentry @@ -453,7 +504,9 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS): if "append" in switches or "merge" in switches or "extend" in switches: # merge/append operations if not old_entry: - self.msg("Could not find topic '%s'. You must give an exact name." % topicstr) + self.msg( + "Could not find topic '%s'. You must give an exact name." % topicstr + ) return if not self.rhs: self.msg("You must supply text to append/merge.") @@ -500,7 +553,9 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS): topicstr, self.rhs, category=category, locks=lockstring, aliases=aliases ) if new_entry: - self.msg("Topic '%s'%s was successfully created." % (topicstr, aliastxt)) + self.msg( + "Topic '%s'%s was successfully created." % (topicstr, aliastxt) + ) if "edit" in switches: # open the line editor to edit the helptext self.caller.db._editing_help = new_entry @@ -515,5 +570,6 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS): return else: self.msg( - "Error when creating topic '%s'%s! Contact an admin." % (topicstr, aliastxt) + "Error when creating topic '%s'%s! Contact an admin." + % (topicstr, aliastxt) ) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 6969829871..c6eff24809 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -265,6 +265,18 @@ class TestGeneral(CommandTest): class TestHelp(CommandTest): + + def setUp(self): + super().setUp() + # we need to set up a logger here since lunr takes over the logger otherwise + import logging + logging.basicConfig(level=logging.ERROR) + + def tearDown(self): + super().tearDown() + import logging + logging.disable(level=logging.ERROR) + def test_help(self): self.call(help.CmdHelp(), "", "Admin", cmdset=CharacterCmdSet())