diff --git a/CHANGELOG.md b/CHANGELOG.md index b1f6f252fa..7b00e777af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ - `utils.evmenu.ask_yes_no` is a helper function that makes it easy to ask a yes/no question to the user and respond to their input. This complements the existing `get_input` helper. - Allow sending messages with `page/tell` without a `=` if target name contains no spaces. +- New FileHelpStorage system allows adding help entries via external files. ### Evennia 0.9.5 (2019-2020) diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index 4379405498..baf30bc161 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -874,7 +874,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): caller, f"Are you sure you want to delete channel '{channel.key}'" "(make sure name is correct!)? This will disconnect and " - "remove all users' aliases. {yesno}?", + "remove all users' aliases. {options}?", _perform_delete, "Aborted." ) @@ -969,7 +969,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): caller, f"Are you sure you want to boot user {target.key} from " f"channel(s) {channames} (make sure name/channels are correct{reasonwarn}). " - "{yesno}?", + "{options}?", _boot_user, "Aborted.", default="Y" @@ -1022,7 +1022,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): caller, f"Are you sure you want to ban user {target.key} from " f"channel(s) {channames} (make sure name/channels are correct{reasonwarn}) " - "{yesno}?", + "{options}?", _ban_user, "Aborted.", ) diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index 0b09c051ec..3646214c6f 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -8,229 +8,53 @@ creation of other help topics such as RP help or game-world aides. """ import re +from dataclasses import dataclass from django.conf import settings from collections import defaultdict from evennia.utils.utils import fill, dedent from evennia.help.models import HelpEntry from evennia.utils import create, evmore from evennia.utils.ansi import ANSIString +from evennia.help.filehelp import FILE_HELP_ENTRIES from evennia.utils.eveditor import EvEditor from evennia.utils.utils import ( class_from_module, inherits_from, format_grid, pad ) +from evennia.help.utils import help_search_with_index, parse_entry_for_subcategories COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) HELP_MORE = settings.HELP_MORE CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES -_RE_HELP_SUBTOPICS_START = re.compile( - r"^\s*?#\s*?subtopics\s*?$", re.I + re.M) -_RE_HELP_SUBTOPIC_SPLIT = re.compile(r"^\s*?(\#{2,6}\s*?\w+?[a-z0-9 \-\?!,\.]*?)$", re.M + re.I) -_RE_HELP_SUBTOPIC_PARSE = re.compile( - r"^(?P\#{2,6})\s*?(?P.*?)$", re.I + re.M) -MAX_SUBTOPIC_NESTING = 5 - - # limit symbol import for API __all__ = ("CmdHelp", "CmdSetHelp") _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH _SEP = "|C" + "-" * _DEFAULT_WIDTH + "|n" -_LUNR = None -_LUNR_EXCEPTION = None +@dataclass class HelpCategory: - def __init__(self, key): - self.key = key + """ + Mock 'help entry' to search categories with the same code. + + """ + key: str @property def search_index_entry(self): return { - "key": str(self), + "key": self.key, "aliases": "", "category": self.key, "tags": "", "text": "", } - def __str__(self): - return f"Category: {self.key}" - - def __eq__(self, other): - return str(self).lower() == str(other).lower() - def __hash__(self): - return id(self) - - -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( - ref="key", - fields=[ - {"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}, - ], - documents=indx, - ) - try: - matches = search_index.search(query)[:suggestion_maxnum] - 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 - ) - - -def parse_entry_for_subcategories(entry): - """ - Parse a command docstring for special sub-category blocks: - - Args: - entry (str): A help entry to parse - - Returns: - dict: The dict is a mapping that splits the entry into subcategories. This - will always hold a key `None` for the main help entry and - zero or more keys holding the subcategories. Each is itself - a dict with a key `None` for the main text of that subcategory - followed by any sub-sub-categories down to a max-depth of 5. - - Example: - :: - - ''' - Main topic text - - # SUBTOPICS - - ## foo - - A subcategory of the main entry, accessible as `help topic foo` - (or using /, like `help topic/foo`) - - ## bar - - Another subcategory, accessed as `help topic bar` - (or `help topic/bar`) - - ### moo - - A subcategory of bar, accessed as `help bar moo` - (or `help bar/moo`) - - #### dum - - A subcategory of moo, accessed `help bar moo dum` - (or `help bar/moo/dum`) - - ''' - - This will result in this returned entry structure: - :: - - { - None: "Main topic text": - "foo": { - None: "main topic/foo text" - }, - "bar": { - None: "Main topic/bar text", - "moo": { - None: "topic/bar/moo text" - "dum": { - None: "topic/bar/moo/dum text" - } - } - } - } - - - Apart from making - sub-categories at the bottom of the entry. - - This will be applied both to command docstrings and database-based help - entries. - - """ - topic, *subtopics = _RE_HELP_SUBTOPICS_START.split(entry, maxsplit=1) - structure = {None: topic.strip()} - - if subtopics: - subtopics = subtopics[0] - else: - return structure - - keypath = [] - current_nesting = 0 - subtopic = None - - # from evennia import set_trace;set_trace() - for part in _RE_HELP_SUBTOPIC_SPLIT.split(subtopics.strip()): - - subtopic_match = _RE_HELP_SUBTOPIC_PARSE.match(part.strip()) - if subtopic_match: - # a new sub(-sub..) category starts. - mdict = subtopic_match.groupdict() - subtopic = mdict['name'].lower().strip() - new_nesting = len(mdict['nesting']) - 1 - - if new_nesting > MAX_SUBTOPIC_NESTING: - raise RuntimeError( - f"Can have max {MAX_SUBTOPIC_NESTING} levels of nested help subtopics.") - - nestdiff = new_nesting - current_nesting - if nestdiff < 0: - # jumping back up in nesting - for _ in range(abs(nestdiff) + 1): - try: - keypath.pop() - except IndexError: - pass - elif nestdiff == 0: - # don't add a deeper nesting but replace the current - try: - keypath.pop() - except IndexError: - pass - keypath.append(subtopic) - current_nesting = new_nesting - else: - # an entry belonging to a subtopic - find the nested location - dct = structure - if not keypath and subtopic is not None: - structure[subtopic] = dedent(part.strip()) - else: - for key in keypath: - if key in dct: - dct = dct[key] - else: - dct[key] = { - None: dedent(part.strip()) - } - return structure + return hash(id(self)) class CmdHelp(COMMAND_DEFAULT_CLASS): @@ -329,7 +153,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS): else: aliases = '' - help_text = f"\n\n{dedent(' ' + help_text.strip())}\n" if help_text else "" + help_text = "\n\n" + dedent(help_text.strip(), indent=0) + "\n" if help_text else "" if subtopics: subtopics = ( @@ -503,11 +327,17 @@ class CmdHelp(COMMAND_DEFAULT_CLASS): # having to allow doublet commands to manage exits etc. cmdset.make_unique(caller) - # retrieve all available commands and database topics + # retrieve all available commands and database / file-help topics all_cmds = [cmd for cmd in cmdset if self.check_show_help(cmd, caller)] - all_db_topics = [ - topic for topic in HelpEntry.objects.all() if topic.access(caller, "view", default=True) - ] + + # we group the file-help topics with the db ones, giving the db ones priority + file_help_topics = FILE_HELP_ENTRIES.all(return_dict=True) + db_topics = { + topic.key.lower().strip(): topic for topic in HelpEntry.objects.all() + if topic.access(caller, "view", default=True) + } + all_db_topics = list({**file_help_topics, **db_topics}.values()) + all_categories = list(set( [HelpCategory(cmd.help_category) for cmd in all_cmds] + [HelpCategory(topic.help_category) for topic in all_db_topics] @@ -539,7 +369,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS): # Lunr search engine # all available options - entries = [cmd for cmd in all_cmds if cmd] + list(HelpEntry.objects.all()) + all_categories + entries = [cmd for cmd in all_cmds if cmd] + all_db_topics + all_categories match, suggestions = None, None for match_query in [f"{query}~1", f"{query}*"]: @@ -590,10 +420,10 @@ class CmdHelp(COMMAND_DEFAULT_CLASS): aliases = match.aliases suggested = suggestions[1:] else: - # a database match + # a database (or file-help) match topic = match.key help_text = match.entrytext - aliases = match.aliases.all() + aliases = match.aliases if isinstance(match.aliases, list) else match.aliases.all() suggested = suggestions[1:] # parse for subtopics. The subtopic_map is a dict with the current topic/subtopic diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 053cebf7ba..4cfaaf4336 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -424,7 +424,7 @@ class TestHelp(CommandTest): logging.disable(level=logging.ERROR) def test_help(self): - self.call(help_module.CmdHelp(), "", "Admin", cmdset=CharacterCmdSet()) + self.call(help_module.CmdHelp(), "", "Commands", cmdset=CharacterCmdSet()) def test_set_help(self): self.call( @@ -434,85 +434,6 @@ class TestHelp(CommandTest): ) self.call(help_module.CmdHelp(), "testhelp", "Help for testhelp", cmdset=CharacterCmdSet()) - def test_parse_entry(self): - """ - Test for subcategories - - """ - entry = """ - Main topic text - - # subtopics - - ## foo - - Foo sub-category - - ### moo - - Foo/Moo subsub-category - - #### dum - - Foo/Moo/Dum subsubsub-category - - ## bar - - Bar subcategory - - ### moo - - Bar/Moo subcategory - - """ - expected = { - None: "Main topic text", - "foo": { - None: "Foo sub-category", - "moo": { - None: "Foo/Moo subsub-category", - "dum": { - None: "Foo/Moo/Dum subsubsub-category", - } - }, - }, - "bar": { - None: "Bar subcategory", - "moo": { - None: "Bar/Moo subcategory" - } - } - } - - actual_result = help_module.parse_entry_for_subcategories(entry) - self.assertEqual(expected, actual_result) - - def test_parse_single_entry(self): - """ - Test parsing single subcategory - - """ - - entry = """ - Main topic text - - # SUBTOPICS - - ## creating extra stuff - - Help on creating extra stuff. - - """ - expected = { - None: "Main topic text", - "creating extra stuff": { - None: "Help on creating extra stuff." - } - } - - actual_result = help_module.parse_entry_for_subcategories(entry) - self.assertEqual(expected, actual_result) - @parameterized.expand([ ("test", # main help entry "Help for test\n\n" diff --git a/evennia/game_template/world/help_entries.py b/evennia/game_template/world/help_entries.py new file mode 100644 index 0000000000..70c795ecab --- /dev/null +++ b/evennia/game_template/world/help_entries.py @@ -0,0 +1,60 @@ +""" +File-based help entries. These complements command-based help and help entries +added in the database using the `sethelp` command in-game. + +Control where Evennia reads these entries with `settings.FILE_HELP_ENTRY_MODULES`, +which is a list of python-paths to modules to read. + +A module like this should hold a global `HELP_ENTRY_DICTS` list, containing +dicts that each represent a help entry. If no `HELP_ENTRY_DICTS` variable is +given, all top-level variables that are dicts in the module are read as help +entries. + +Each dict is on the form +:: + + {'key': , + 'category': , # optional, otherwise settings.FILE_DEFAULT_HELP_CATEGORY + 'aliases': , # optional + 'text': }`` # the actual help text. Can contain # subtopic sections + +""" + +HELP_ENTRY_DICTS = [ + { + "key": "evennia", + "aliases": ['ev'], + "category": "General", + "text": """ + Evennia is a MUD game server in Python. + + # subtopics + + ## Installation + + You'll find installation instructions on https:evennia.com + + ## Community + + There are many ways to get help and communicate with other devs! + + ### IRC + + The irc channel is #evennia on irc.freenode.net + + ### Discord + + There is also a discord channel you can find from the sidebard on evennia.com. + + """ + }, + { + "key": "building", + "category": "building", + "text": """ + Evennia comes with a bunch of default building commands. You can + find a building tutorial in the evennia documentation. + + """ + } +] diff --git a/evennia/help/filehelp.py b/evennia/help/filehelp.py new file mode 100644 index 0000000000..ae7fc61dbd --- /dev/null +++ b/evennia/help/filehelp.py @@ -0,0 +1,178 @@ +""" +The filehelp-system allows for defining help files outside of the game. These +will be treated as non-command help entries and displayed in the same way as +help entries created using the `sethelp` default command. After changing an +entry on-disk you need to reload the server to have the change show in-game. + +An filehelp file is a regular python modules with dicts representing each help +entry. If a list `HELP_ENTRY_DICTS` is found in the module, this should be a list of +dicts. Otherwise *all* top-level dicts in the module will be assumed to be a +help-entry dict. + +Each help-entry dict is on the form +:: + + {'key': , + 'category': , # optional, otherwise settings.FILE_DEFAULT_HELP_CATEGORY + 'aliases': , # optional + 'text': }`` + +where the ``category`` is optional and the ``text`` should be formatted on the +same form as other help entry-texts and contain ``# subtopics`` as normal. + +New help-entry modules are added to the system by providing the python-path to +the module to `settings.FILE_HELP_ENTRY_MODULES`. Note that if same-key entries are +added, entries in latter modules will override that of earlier ones. Use +``settings.FILE_DEFAULT_HELP_CATEGORY`` to customize what category is used if +not set explicitly. + +An example of the contents of a module: +:: + + help_entry1 = { + "key": "The Gods", # case-insensitive, can be searched by 'gods' as well + "aliases": ['pantheon', 'religion'] + "category": "Lore", + "text": ''' + The gods formed the world ... + + # Subtopics + + ## Pantheon + + ... + + ### God of love + + ... + + ### God of war + + ... + + ''' + } + + + HELP_ENTRY_DICTS = [ + help_entry1, + ... + ] + +---- + +""" + +from dataclasses import dataclass +from django.conf import settings +from evennia.utils.utils import ( + variable_from_module, make_iter, all_from_module) +from evennia.utils import logger + + +@dataclass +class FileHelpEntry: + """ + Represents a help entry read from file. This mimics the api of the + database-bound HelpEntry so that they can be used interchangeably in the + help command. + + """ + key: str + aliases: list + help_category: str + entrytext: str + + @property + def search_index_entry(self): + """ + Property for easily retaining a search index entry for this object. + + """ + return { + "key": self.key, + "aliases": " ".join(self.aliases), + "category": self.help_category, + "tags": "", + "text": self.entrytext, + } + + + +class FileHelpStorageHandler: + """ + This reads and stores help entries for quick access. By default + it reads modules from `settings.FILE_HELP_ENTRY_MODULES`. + + Note that this is not meant to any searching/lookup - that is all handled + by the help command. + """ + + def __init__(self, help_file_modules=settings.FILE_HELP_ENTRY_MODULES): + """ + Initialize the storage. + """ + self.help_file_modules = [str(part).strip() + for part in make_iter(help_file_modules)] + self.help_entries = [] + self.help_entries_dict = {} + self.load() + + def load(self): + """ + Load/reload file-based help-entries from file. + + """ + loaded_help_dicts = [] + + for module_or_path in self.help_file_modules: + help_dict_list = variable_from_module( + module_or_path, variable="HELP_ENTRY_DICTS" + ) + if not help_dict_list: + help_dict_list = [ + dct for dct in all_from_module(module_or_path).values() + if isinstance(dct, dict)] + if help_dict_list: + loaded_help_dicts.extend(help_dict_list) + else: + logger.log_err(f"Could not find file-help module {module_or_path} (skipping).") + + # validate and parse dicts into FileEntryHelp objects and make sure they are unique-by-key + # by letting latter added ones override earlier ones. + unique_help_entries = {} + + for dct in loaded_help_dicts: + key = dct.get('key').lower().strip() + category = dct.get('category', settings.FILE_DEFAULT_HELP_CATEGORY).strip() + aliases = list(dct.get('aliases', [])) + entrytext = dct.get('text') + + if not key and entrytext: + logger.error(f"Cannot load file-help-entry (missing key or text): {dct}") + continue + + unique_help_entries[key] = FileHelpEntry( + key=key, help_category=category, aliases=aliases, + entrytext=entrytext) + + self.help_entries_dict = unique_help_entries + self.help_entries = list(unique_help_entries.values()) + + def all(self, return_dict=False): + """ + Get all help entries. + + Args: + return_dict (bool): Return a dict ``{key: FileHelpEntry,...}``. Otherwise, + return a list of ``FileHelpEntry`. + + Returns: + dict or list: Depending on the setting of ``return_dict``. + + """ + return self.help_entries_dict if return_dict else self.help_entries + + +# singleton to hold the loaded help entries +FILE_HELP_ENTRIES = FileHelpStorageHandler() diff --git a/evennia/help/tests.py b/evennia/help/tests.py new file mode 100644 index 0000000000..0ebd1b0553 --- /dev/null +++ b/evennia/help/tests.py @@ -0,0 +1,145 @@ +""" +Unittests for help code (The default help-command is tested as part of default +command test-suite). + +""" + +from unittest import mock +from evennia.utils.test_resources import TestCase +from evennia.utils.utils import dedent +from evennia.help import filehelp, utils as help_utils + + +class TestParseSubtopics(TestCase): + """ + Test the subtopic parser. + + """ + + def test_parse_entry(self): + """ + Test for subcategories + + """ + self.maxDiff = None + + entry = dedent(""" + Main topic text + # subtopics + ## foo + Foo sub-category + ### moo + Foo/Moo subsub-category + #### dum + Foo/Moo/Dum subsubsub-category + ## bar + Bar subcategory + ### moo + Bar/Moo subcategory + """, indent=0) + expected = { + None: "Main topic text", + "foo": { + None: "\nFoo sub-category\n", + "moo": { + None: "\nFoo/Moo subsub-category\n", + "dum": { + None: "\nFoo/Moo/Dum subsubsub-category\n", + } + }, + }, + "bar": { + None: "\nBar subcategory\n", + "moo": { + None: "\nBar/Moo subcategory" + } + } + } + + actual_result = help_utils.parse_entry_for_subcategories(entry) + self.assertEqual(expected, actual_result) + + def test_parse_single_entry(self): + """ + Test parsing single subcategory + + """ + + entry = dedent(""" + Main topic text + # SUBTOPICS + ## creating extra stuff + Help on creating extra stuff. + """, indent=0) + expected = { + None: "Main topic text", + "creating extra stuff": { + None: "\nHelp on creating extra stuff." + } + } + + actual_result = help_utils.parse_entry_for_subcategories(entry) + self.assertEqual(expected, actual_result) + +# test filehelp system + +HELP_ENTRY_DICTS = [ + { + "key": "evennia", + "aliases": ['ev'], + "category": "General", + "text": """ + Evennia is a MUD game server in Python. + + # subtopics + + ## Installation + + You'll find installation instructions on https:evennia.com + + ## Community + + There are many ways to get help and communicate with other devs! + + ### IRC + + The irc channel is #evennia on irc.freenode.net + + ### Discord + + There is also a discord channel you can find from the sidebard on evennia.com. + + """ + }, + { + "key": "building", + "category": "building", + "text": """ + Evennia comes with a bunch of default building commands. You can + find a building tutorial in the evennia documentation. + + """ + } +] + + + +class TestFileHelp(TestCase): + """ + Test the File-help system + + """ + + @mock.patch("evennia.help.filehelp.variable_from_module") + def test_file_help(self, mock_variable_from_module): + mock_variable_from_module.return_value = HELP_ENTRY_DICTS + + # we just need anything here since we mock the load anyway + storage = filehelp.FileHelpStorageHandler(help_file_modules=["dummypath"]) + result = storage.all() + + for inum, helpentry in enumerate(result): + self.assertEqual(HELP_ENTRY_DICTS[inum]['key'], helpentry.key) + self.assertEqual(HELP_ENTRY_DICTS[inum].get('aliases', []), helpentry.aliases) + self.assertEqual(HELP_ENTRY_DICTS[inum]['category'], helpentry.help_category) + self.assertEqual(HELP_ENTRY_DICTS[inum]['text'], helpentry.entrytext) diff --git a/evennia/server/server.py b/evennia/server/server.py index 011a9f7be0..e064d941db 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -143,9 +143,6 @@ def _server_maintenance(): if _MAINTENANCE_COUNT % 5 == 0: # check cache size every 5 minutes _FLUSH_CACHE(_IDMAPPER_CACHE_MAXSIZE) - if _MAINTENANCE_COUNT % 60 == 0: - # validate scripts every hour - evennia.ScriptDB.objects.validate() if _MAINTENANCE_COUNT % 61 == 0: # validate channels off-sync with scripts evennia.CHANNEL_HANDLER.update() diff --git a/evennia/settings_default.py b/evennia/settings_default.py index d848cb5796..05aa66cbce 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -448,8 +448,6 @@ COMMAND_DEFAULT_ARG_REGEX = None # calling the Command. This may be more intuitive for users in certain # multisession modes. COMMAND_DEFAULT_MSG_ALL_SESSIONS = False -# The help category of a command if not otherwise specified. -COMMAND_DEFAULT_HELP_CATEGORY = "general" # The default lockstring of a command. COMMAND_DEFAULT_LOCKS = "" # The Channel Handler is responsible for managing all available channels. By @@ -597,6 +595,21 @@ TIME_GAME_EPOCH = None # the real time (add a different epoch to shift time) TIME_IGNORE_DOWNTIMES = False +###################################################################### +# Help system +###################################################################### +# Help output from CmdHelp are wrapped in an EvMore call +# (excluding webclient with separate help popups). If continuous scroll +# is preferred, change 'HELP_MORE' to False. EvMORE uses CLIENT_DEFAULT_HEIGHT +HELP_MORE = True +# The help category of a command if not specified. +COMMAND_DEFAULT_HELP_CATEGORY = "general" +# The help category of a file-based help entry if not specified +FILE_DEFAULT_HELP_CATEGORY = "general" +# File-based help entries. These are modules containing dicts defining help +# entries. They can be used together with in-database entries created in-game. +FILE_HELP_ENTRY_MODULES = ["world.help_entries"] + ###################################################################### # FuncParser # @@ -679,10 +692,6 @@ PERMISSION_ACCOUNT_DEFAULT = "Player" CLIENT_DEFAULT_WIDTH = 78 # telnet standard height is 24; does anyone use such low-res displays anymore? CLIENT_DEFAULT_HEIGHT = 45 -# Help output from CmdHelp are wrapped in an EvMore call -# (excluding webclient with separate help popups). If continuous scroll -# is preferred, change 'HELP_MORE' to False. EvMORE uses CLIENT_DEFAULT_HEIGHT -HELP_MORE = True # Set rate limits per-IP on account creations and login attempts CREATION_THROTTLE_LIMIT = 2 CREATION_THROTTLE_TIMEOUT = 10 * 60 diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index c637734ccf..c389b2c328 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -1335,18 +1335,19 @@ def all_from_module(module): already imported module object (e.g. `models`) Returns: - variables (dict): A dict of {variablename: variable} for all + dict: A dict of {variablename: variable} for all variables in the given module. Notes: - Ignores modules and variable names starting with an underscore. + Ignores modules and variable names starting with an underscore, as well + as variables imported into the module from other modules. """ mod = mod_import(module) if not mod: return {} # make sure to only return variables actually defined in this - # module if available (try to avoid not imports) + # module if available (try to avoid imports) members = getmembers(mod, predicate=lambda obj: getmodule(obj) in (mod, None)) return dict((key, val) for key, val in members if not key.startswith("_"))