New FileHelp system to create help entries from external files
This commit is contained in:
parent
8a7e19db16
commit
f5fd398480
10 changed files with 432 additions and 290 deletions
178
evennia/help/filehelp.py
Normal file
178
evennia/help/filehelp.py
Normal file
|
|
@ -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': <str>,
|
||||
'category': <str>, # optional, otherwise settings.FILE_DEFAULT_HELP_CATEGORY
|
||||
'aliases': <list>, # optional
|
||||
'text': <str>}``
|
||||
|
||||
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()
|
||||
145
evennia/help/tests.py
Normal file
145
evennia/help/tests.py
Normal file
|
|
@ -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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue