mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 12:56:30 +01:00
Ran black on sources, add black config
This commit is contained in:
parent
abe4b1e4ee
commit
0df87037e7
32 changed files with 1031 additions and 232 deletions
|
|
@ -108,13 +108,7 @@ def _case_sensitive_replace(string, old, new):
|
|||
result.append(new_word[ind + 1 :].lower())
|
||||
out.append("".join(result))
|
||||
# if we have more new words than old ones, just add them verbatim
|
||||
out.extend(
|
||||
[
|
||||
new_word
|
||||
for ind, new_word in enumerate(new_words)
|
||||
if ind >= len(old_words)
|
||||
]
|
||||
)
|
||||
out.extend([new_word for ind, new_word in enumerate(new_words) if ind >= len(old_words)])
|
||||
return " ".join(out)
|
||||
|
||||
regex = re.compile(re.escape(old), re.I)
|
||||
|
|
@ -154,9 +148,7 @@ def rename_in_tree(path, in_list, out_list, excl_list, fileend_list, is_interact
|
|||
print("%s skipped (excluded)." % full_path)
|
||||
continue
|
||||
|
||||
if not fileend_list or any(
|
||||
file.endswith(ending) for ending in fileend_list
|
||||
):
|
||||
if not fileend_list or any(file.endswith(ending) for ending in fileend_list):
|
||||
rename_in_file(full_path, in_list, out_list, is_interactive)
|
||||
|
||||
# rename file - always ask
|
||||
|
|
@ -164,9 +156,7 @@ def rename_in_tree(path, in_list, out_list, excl_list, fileend_list, is_interact
|
|||
for src, dst in repl_mapping:
|
||||
new_file = _case_sensitive_replace(new_file, src, dst)
|
||||
if new_file != file:
|
||||
inp = input(
|
||||
_green("Rename %s\n -> %s\n Y/[N]? > " % (file, new_file))
|
||||
)
|
||||
inp = input(_green("Rename %s\n -> %s\n Y/[N]? > " % (file, new_file)))
|
||||
if inp.upper() == "Y":
|
||||
new_full_path = os.path.join(root, new_file)
|
||||
try:
|
||||
|
|
@ -182,9 +172,7 @@ def rename_in_tree(path, in_list, out_list, excl_list, fileend_list, is_interact
|
|||
for src, dst in repl_mapping:
|
||||
new_root = _case_sensitive_replace(new_root, src, dst)
|
||||
if new_root != root:
|
||||
inp = input(
|
||||
_green("Dir Rename %s\n -> %s\n Y/[N]? > " % (root, new_root))
|
||||
)
|
||||
inp = input(_green("Dir Rename %s\n -> %s\n Y/[N]? > " % (root, new_root)))
|
||||
if inp.upper() == "Y":
|
||||
try:
|
||||
os.rename(root, new_root)
|
||||
|
|
@ -252,9 +240,7 @@ def rename_in_file(path, in_list, out_list, is_interactive):
|
|||
|
||||
while True:
|
||||
|
||||
for iline, renamed_line in sorted(
|
||||
list(renamed.items()), key=lambda tup: tup[0]
|
||||
):
|
||||
for iline, renamed_line in sorted(list(renamed.items()), key=lambda tup: tup[0]):
|
||||
print("%3i orig: %s" % (iline + 1, org_lines[iline]))
|
||||
print(" new : %s" % (_yellow(renamed_line)))
|
||||
print(_green("%s (%i lines changed)" % (path, len(renamed))))
|
||||
|
|
@ -297,11 +283,7 @@ def rename_in_file(path, in_list, out_list, is_interactive):
|
|||
input(_HELP_TEXT.format(sources=in_list, targets=out_list))
|
||||
elif ret.startswith("i"):
|
||||
# ignore one or more lines
|
||||
ignores = [
|
||||
int(ind) - 1
|
||||
for ind in ret[1:].split(",")
|
||||
if ind.strip().isdigit()
|
||||
]
|
||||
ignores = [int(ind) - 1 for ind in ret[1:].split(",") if ind.strip().isdigit()]
|
||||
if not ignores:
|
||||
input("Ignore example: i 2,7,34,133\n (return to continue)")
|
||||
continue
|
||||
|
|
@ -313,9 +295,7 @@ def rename_in_file(path, in_list, out_list, is_interactive):
|
|||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Rename text in a source tree, or a single file"
|
||||
)
|
||||
parser = argparse.ArgumentParser(description="Rename text in a source tree, or a single file")
|
||||
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
|
|
@ -326,27 +306,19 @@ if __name__ == "__main__":
|
|||
parser.add_argument(
|
||||
"-o", "--output", action="append", help="Word to rename a matching src-word to"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-x", "--exc", action="append", help="File path patterns to exclude"
|
||||
)
|
||||
parser.add_argument("-x", "--exc", action="append", help="File path patterns to exclude")
|
||||
parser.add_argument(
|
||||
"-a", "--auto", action="store_true", help="Automatic mode, don't ask to rename"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--recursive", action="store_true", help="Recurse subdirs"
|
||||
)
|
||||
parser.add_argument("-r", "--recursive", action="store_true", help="Recurse subdirs")
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--fileending",
|
||||
action="append",
|
||||
help="Change which file endings to allow (default .py and .html)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nocolor", action="store_true", help="Turn off in-program color"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fake", action="store_true", help="Simulate run but don't actually save"
|
||||
)
|
||||
parser.add_argument("--nocolor", action="store_true", help="Turn off in-program color")
|
||||
parser.add_argument("--fake", action="store_true", help="Simulate run but don't actually save")
|
||||
parser.add_argument("path", help="File or directory in which to rename text")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
|
@ -362,9 +334,7 @@ if __name__ == "__main__":
|
|||
print("At least one source- and destination word must be given.")
|
||||
sys.exit()
|
||||
if len(in_list) != len(out_list):
|
||||
print(
|
||||
"Number of sources must be identical to the number of destination arguments."
|
||||
)
|
||||
print("Number of sources must be identical to the number of destination arguments.")
|
||||
sys.exit()
|
||||
|
||||
exc_list = exc_list or []
|
||||
|
|
@ -376,8 +346,6 @@ if __name__ == "__main__":
|
|||
FAKE_MODE = args.fake
|
||||
|
||||
if is_recursive:
|
||||
rename_in_tree(
|
||||
args.path, in_list, out_list, exc_list, fileend_list, is_interactive
|
||||
)
|
||||
rename_in_tree(args.path, in_list, out_list, exc_list, fileend_list, is_interactive)
|
||||
else:
|
||||
rename_in_file(args.path, in_list, out_list, is_interactive)
|
||||
|
|
|
|||
|
|
@ -15,8 +15,16 @@ _IGNORE_FILES = []
|
|||
_SOURCEDIR_NAME = "source"
|
||||
_SOURCE_DIR = pathjoin(dirname(dirname(abspath(__file__))), _SOURCEDIR_NAME)
|
||||
_TOC_FILE = pathjoin(_SOURCE_DIR, "toc.md")
|
||||
_NO_REMAP_STARTSWITH = ["http://", "https://", "github:", "api:",
|
||||
"feature-request", "report-bug", "issue", "bug-report"]
|
||||
_NO_REMAP_STARTSWITH = [
|
||||
"http://",
|
||||
"https://",
|
||||
"github:",
|
||||
"api:",
|
||||
"feature-request",
|
||||
"report-bug",
|
||||
"issue",
|
||||
"bug-report",
|
||||
]
|
||||
|
||||
TXT_REMAPS = {
|
||||
"Developer Central": "Evennia Components overview",
|
||||
|
|
@ -52,6 +60,7 @@ _USED_REFS = {}
|
|||
|
||||
_CURRFILE = None
|
||||
|
||||
|
||||
def auto_link_remapper():
|
||||
"""
|
||||
- Auto-Remaps links to fit with the actual document file structure. Requires
|
||||
|
|
@ -69,7 +78,7 @@ def auto_link_remapper():
|
|||
# we allow a max of 4 levels of nesting in the source dir
|
||||
ind = pathparts[-5:].index(_SOURCEDIR_NAME)
|
||||
# get the part after source/
|
||||
pathparts = pathparts[-5 + 1 + ind:]
|
||||
pathparts = pathparts[-5 + 1 + ind :]
|
||||
url = "/".join(pathparts)
|
||||
# get the reference, without .md
|
||||
url = url.rsplit(".", 1)[0]
|
||||
|
|
@ -96,7 +105,8 @@ def auto_link_remapper():
|
|||
raise DocumentError(
|
||||
f" Tried to add {src_url}.md, but a file {duplicate_src_url}.md already exists.\n"
|
||||
" Evennia's auto-link-corrector does not accept doc-files with the same \n"
|
||||
" name, even in different folders. Rename one.\n")
|
||||
" name, even in different folders. Rename one.\n"
|
||||
)
|
||||
toc_map[fname] = src_url
|
||||
|
||||
# find relative links to all other files
|
||||
|
|
@ -111,17 +121,20 @@ def auto_link_remapper():
|
|||
url = "./" + url
|
||||
docref_map[sourcepath][targetname] = url.rsplit(".", 1)[0]
|
||||
|
||||
|
||||
# normal reference-links [txt](urls)
|
||||
ref_regex = re.compile(r"\[(?P<txt>[\w -\[\]\`]+?)\]\((?P<url>.+?)\)", re.I + re.S + re.U + re.M)
|
||||
ref_regex = re.compile(
|
||||
r"\[(?P<txt>[\w -\[\]\`]+?)\]\((?P<url>.+?)\)", re.I + re.S + re.U + re.M
|
||||
)
|
||||
# in document references
|
||||
ref_doc_regex = re.compile(r"\[(?P<txt>[\w -\`]+?)\]:\s+?(?P<url>.+?)(?=$|\n)", re.I + re.S + re.U + re.M)
|
||||
ref_doc_regex = re.compile(
|
||||
r"\[(?P<txt>[\w -\`]+?)\]:\s+?(?P<url>.+?)(?=$|\n)", re.I + re.S + re.U + re.M
|
||||
)
|
||||
|
||||
def _sub(match):
|
||||
# inline reference links
|
||||
global _USED_REFS
|
||||
grpdict = match.groupdict()
|
||||
txt, url = grpdict['txt'], grpdict['url']
|
||||
txt, url = grpdict["txt"], grpdict["url"]
|
||||
|
||||
txt = TXT_REMAPS.get(txt, txt)
|
||||
url = URL_REMAPS.get(url, url)
|
||||
|
|
@ -142,7 +155,7 @@ def auto_link_remapper():
|
|||
|
||||
if _CURRFILE in docref_map and fname in docref_map[_CURRFILE]:
|
||||
cfilename = _CURRFILE.rsplit("/", 1)[-1]
|
||||
urlout = docref_map[_CURRFILE][fname] + ('#' + anchor[0] if anchor else '')
|
||||
urlout = docref_map[_CURRFILE][fname] + ("#" + anchor[0] if anchor else "")
|
||||
if urlout != url:
|
||||
print(f" {cfilename}: [{txt}]({url}) -> [{txt}]({urlout})")
|
||||
else:
|
||||
|
|
@ -154,7 +167,7 @@ def auto_link_remapper():
|
|||
# reference links set at the bottom of the page
|
||||
global _USED_REFS
|
||||
grpdict = match.groupdict()
|
||||
txt, url = grpdict['txt'], grpdict['url']
|
||||
txt, url = grpdict["txt"], grpdict["url"]
|
||||
|
||||
txt = TXT_REMAPS.get(txt, txt)
|
||||
url = URL_REMAPS.get(url, url)
|
||||
|
|
@ -175,7 +188,7 @@ def auto_link_remapper():
|
|||
|
||||
if _CURRFILE in docref_map and fname in docref_map[_CURRFILE]:
|
||||
cfilename = _CURRFILE.rsplit("/", 1)[-1]
|
||||
urlout = docref_map[_CURRFILE][fname] + ('#' + anchor[0] if anchor else '')
|
||||
urlout = docref_map[_CURRFILE][fname] + ("#" + anchor[0] if anchor else "")
|
||||
if urlout != url:
|
||||
print(f" {cfilename}: [{txt}]: {url} -> [{txt}]: {urlout}")
|
||||
else:
|
||||
|
|
@ -190,12 +203,12 @@ def auto_link_remapper():
|
|||
# from pudb import debugger;debugger.Debugger().set_trace()
|
||||
_CURRFILE = path.as_posix()
|
||||
|
||||
with open(path, 'r') as fil:
|
||||
with open(path, "r") as fil:
|
||||
intxt = fil.read()
|
||||
outtxt = ref_regex.sub(_sub, intxt)
|
||||
outtxt = ref_doc_regex.sub(_sub_doc, outtxt)
|
||||
if intxt != outtxt:
|
||||
with open(path, 'w') as fil:
|
||||
with open(path, "w") as fil:
|
||||
fil.write(outtxt)
|
||||
count += 1
|
||||
print(f" -- Auto-relinked links in {path.name}")
|
||||
|
|
@ -232,5 +245,6 @@ def auto_link_remapper():
|
|||
|
||||
print(" -- Auto-Remapper finished.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
auto_link_remapper()
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ def create_search_index(sourcedir, outfile):
|
|||
print(f"Building Search index from {len(filepaths)} files ... ", end="")
|
||||
|
||||
for filepath in filepaths:
|
||||
with open(filepath, 'r') as fil:
|
||||
with open(filepath, "r") as fil:
|
||||
filename = filepath.rsplit(sep, 1)[1].split(".", 1)[0]
|
||||
url = f"{URL_BASE}{sep}{filename}.html".strip()
|
||||
title = filename.replace("-", " ").strip()
|
||||
|
|
@ -61,16 +61,7 @@ def create_search_index(sourcedir, outfile):
|
|||
idx = lunr(
|
||||
ref="url",
|
||||
documents=outlist,
|
||||
fields=[
|
||||
{
|
||||
"field_name": "title",
|
||||
"boost": 10
|
||||
},
|
||||
{
|
||||
"field_name": "text",
|
||||
"boost": 1
|
||||
}
|
||||
],
|
||||
fields=[{"field_name": "title", "boost": 10}, {"field_name": "text", "boost": 1}],
|
||||
)
|
||||
|
||||
with open(outfile, "w") as fil:
|
||||
|
|
@ -83,10 +74,18 @@ if __name__ == "__main__":
|
|||
|
||||
parser = ArgumentParser(description="Build a static search index.")
|
||||
|
||||
parser.add_argument("-i", dest="sourcedir", default=DEFAULT_SOURCE_DIR,
|
||||
help="Absolute path to the documentation source dir")
|
||||
parser.add_argument("-o", dest="outfile", default=DEFAULT_OUTFILE,
|
||||
help="Absolute path to the index file to output.")
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
dest="sourcedir",
|
||||
default=DEFAULT_SOURCE_DIR,
|
||||
help="Absolute path to the documentation source dir",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
dest="outfile",
|
||||
default=DEFAULT_OUTFILE,
|
||||
help="Absolute path to the index file to output.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
|
|
|||
|
|
@ -43,8 +43,11 @@ _INDEX_PREFIX = f"""
|
|||
"""
|
||||
|
||||
_WIKI_DIR = "../../../evennia.wiki/"
|
||||
_INFILES = [path for path in sorted(glob.glob(_WIKI_DIR + "/*.md"))
|
||||
if path.rsplit('/', 1)[-1] not in _IGNORE_FILES]
|
||||
_INFILES = [
|
||||
path
|
||||
for path in sorted(glob.glob(_WIKI_DIR + "/*.md"))
|
||||
if path.rsplit("/", 1)[-1] not in _IGNORE_FILES
|
||||
]
|
||||
_FILENAMES = [path.rsplit("/", 1)[-1] for path in _INFILES]
|
||||
_FILENAMES = [path.split(".", 1)[0] for path in _FILENAMES]
|
||||
_FILENAMESLOW = [path.lower() for path in _FILENAMES]
|
||||
|
|
@ -95,8 +98,17 @@ _ABSOLUTE_LINK_SKIP = (
|
|||
# specific references tokens that should be ignored. Should be given
|
||||
# without any #anchor.
|
||||
_REF_SKIP = (
|
||||
"[5](Win)", "[6](Win)", "[7](Win)", "[10](Win)", "[11](Mac)", "[13](Win)",
|
||||
"[14](IOS)", "[15](IOS)", "[16](Andr)", "[17](Andr)", "[18](Unix)",
|
||||
"[5](Win)",
|
||||
"[6](Win)",
|
||||
"[7](Win)",
|
||||
"[10](Win)",
|
||||
"[11](Mac)",
|
||||
"[13](Win)",
|
||||
"[14](IOS)",
|
||||
"[15](IOS)",
|
||||
"[16](Andr)",
|
||||
"[17](Andr)",
|
||||
"[18](Unix)",
|
||||
"[21](Chrome)",
|
||||
# these should be checked
|
||||
"[EvTable](EvTable)",
|
||||
|
|
@ -126,20 +138,19 @@ def _sub_remap(match):
|
|||
def _sub_link(match):
|
||||
|
||||
mdict = match.groupdict()
|
||||
txt, url_orig = mdict['txt'], mdict['url']
|
||||
txt, url_orig = mdict["txt"], mdict["url"]
|
||||
url = url_orig
|
||||
# if not txt:
|
||||
# # the 'comment' is not supported by Mkdocs
|
||||
# return ""
|
||||
print(f" [{txt}]({url})")
|
||||
|
||||
|
||||
url = _CUSTOM_LINK_REMAP.get(url, url)
|
||||
|
||||
url, *anchor = url.rsplit("#", 1)
|
||||
|
||||
if url in _ABSOLUTE_LINK_SKIP:
|
||||
url += (("#" + anchor[0]) if anchor else "")
|
||||
url += ("#" + anchor[0]) if anchor else ""
|
||||
return f"[{txt}]({url})"
|
||||
|
||||
if url.startswith("evennia"):
|
||||
|
|
@ -166,11 +177,10 @@ def _sub_link(match):
|
|||
# this happens on same-file #labels in wiki
|
||||
url = _CURRENT_TITLE
|
||||
|
||||
if (url not in _FILENAMES and
|
||||
not url.startswith("http") and not url.startswith(_CODE_PREFIX)):
|
||||
if url not in _FILENAMES and not url.startswith("http") and not url.startswith(_CODE_PREFIX):
|
||||
|
||||
url_cap = url.capitalize()
|
||||
url_plur = url[:-3] + 's' + ".md"
|
||||
url_plur = url[:-3] + "s" + ".md"
|
||||
url_cap_plur = url_plur.capitalize()
|
||||
|
||||
link = f"[{txt}]({url})"
|
||||
|
|
@ -201,6 +211,7 @@ def _sub_link(match):
|
|||
|
||||
return f"[{txt}]({url})"
|
||||
|
||||
|
||||
def create_toctree(files):
|
||||
with open("../source/toc.md", "w") as fil:
|
||||
fil.write("# Toc\n")
|
||||
|
|
@ -215,13 +226,14 @@ def create_toctree(files):
|
|||
|
||||
fil.write(f"\n* [{linkname}]({ref}.md)")
|
||||
|
||||
|
||||
def convert_links(files, outdir):
|
||||
global _CURRENT_TITLE
|
||||
|
||||
for inpath in files:
|
||||
|
||||
is_index = False
|
||||
outfile = inpath.rsplit('/', 1)[-1]
|
||||
outfile = inpath.rsplit("/", 1)[-1]
|
||||
if outfile == "Home.md":
|
||||
outfile = "index.md"
|
||||
is_index = True
|
||||
|
|
@ -236,24 +248,31 @@ def convert_links(files, outdir):
|
|||
if is_index:
|
||||
text = _INDEX_PREFIX + text
|
||||
lines = text.split("\n")
|
||||
lines = (lines[:-11]
|
||||
+ [" - The [TOC](toc) lists all regular documentation pages.\n\n"]
|
||||
+ lines[-11:])
|
||||
lines = (
|
||||
lines[:-11]
|
||||
+ [" - The [TOC](toc) lists all regular documentation pages.\n\n"]
|
||||
+ lines[-11:]
|
||||
)
|
||||
text = "\n".join(lines)
|
||||
|
||||
_CURRENT_TITLE = title.replace(" ", "-")
|
||||
text = _RE_CLEAN.sub("", text)
|
||||
text = _RE_REF_LINK.sub(_sub_remap, text)
|
||||
text = _RE_MD_LINK.sub(_sub_link, text)
|
||||
text = text.split('\n')[1:] if text.split('\n')[0].strip().startswith('[]') else text.split('\n')
|
||||
text = (
|
||||
text.split("\n")[1:]
|
||||
if text.split("\n")[0].strip().startswith("[]")
|
||||
else text.split("\n")
|
||||
)
|
||||
text = "\n".join(text)
|
||||
|
||||
if not is_index:
|
||||
text = f"# {title}\n\n{text}"
|
||||
|
||||
with open(outfile, 'w') as fil:
|
||||
with open(outfile, "w") as fil:
|
||||
fil.write(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("This should not be run on develop files, it would overwrite changes.")
|
||||
# create_toctree(_INFILES)
|
||||
|
|
|
|||
|
|
@ -19,30 +19,26 @@ if __name__ == "__main__":
|
|||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument("files")
|
||||
parser.add_argument("-w", '--width', dest="width", type=int, default=_DEFAULT_WIDTH)
|
||||
parser.add_argument("-w", "--width", dest="width", type=int, default=_DEFAULT_WIDTH)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
filepaths = glob.glob(args.files, recursive=True)
|
||||
width = args.width
|
||||
|
||||
wrapper = textwrap.TextWrapper(
|
||||
width=width,
|
||||
break_long_words=False,
|
||||
expand_tabs=True,
|
||||
)
|
||||
wrapper = textwrap.TextWrapper(width=width, break_long_words=False, expand_tabs=True,)
|
||||
|
||||
count = 0
|
||||
for filepath in filepaths:
|
||||
with open(filepath, 'r') as fil:
|
||||
with open(filepath, "r") as fil:
|
||||
lines = fil.readlines()
|
||||
|
||||
outlines = [
|
||||
"\n".join(wrapper.wrap(line)) if len(line) > width else line.strip('\n')
|
||||
"\n".join(wrapper.wrap(line)) if len(line) > width else line.strip("\n")
|
||||
for line in lines
|
||||
]
|
||||
txt = "\n".join(outlines)
|
||||
with open(filepath, 'w') as fil:
|
||||
with open(filepath, "w") as fil:
|
||||
fil.write(txt)
|
||||
count += 1
|
||||
|
||||
|
|
|
|||
729
docs/source/Howto/Starting/Part3/A-Sittable-Object.md
Normal file
729
docs/source/Howto/Starting/Part3/A-Sittable-Object.md
Normal file
|
|
@ -0,0 +1,729 @@
|
|||
# Making a sittable object
|
||||
|
||||
In this lesson we will go through how to make a chair you can sit on. Sounds easy, right?
|
||||
Well it is. But in the process of making the chair we will need to consider the various ways
|
||||
to do it depending on how we want our game to work.
|
||||
|
||||
The goals of this lesson are as follows:
|
||||
|
||||
- We want a new 'sittable' object, a Chair in particular".
|
||||
- We want to be able to use a command to sit in the chair.
|
||||
- Once we are sitting in the chair it should affect us somehow. To demonstrate this we'll
|
||||
set a flag "Resting" on the Character sitting in the Chair.
|
||||
- When you sit down you should not be able to walk to another room without first standing up.
|
||||
- A character should be able to stand up and move away from the chair.
|
||||
|
||||
## Don't move us when resting
|
||||
|
||||
This requires a change to our Character typeclass. Open `mygame/characters.py`:
|
||||
|
||||
```python
|
||||
|
||||
# ...
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
# ...
|
||||
|
||||
def at_before_move(self, destination):
|
||||
"""
|
||||
Called by self.move_to when trying to move somewhere. If this returns
|
||||
False, the move is immediately cancelled.
|
||||
"""
|
||||
if self.db.is_resting:
|
||||
self.msg("You can't go anywhere while resting.")
|
||||
return False
|
||||
return True
|
||||
|
||||
```
|
||||
|
||||
When moving somewhere, [character.move_to](api.objects.objects#Object.move_to) is called. This in turn
|
||||
will call `character.at_before_move`. Here we look for an Attribute `is_resting` (which we will assign below)
|
||||
to determine if we are stuck on the chair or not.
|
||||
|
||||
## Making the Chair itself
|
||||
|
||||
First we need the Chair itself. Since we want this to do some extra coded actions when
|
||||
you sit in it, we can't just use a default Object. We need a new, custom Typeclass.
|
||||
Create a new module `mygame/typeclasses/sittables.py` with the following content:
|
||||
|
||||
```python
|
||||
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Sittable(DefaultObject):
|
||||
|
||||
def at_object_creation(self):
|
||||
self.db.sitter = None
|
||||
|
||||
def do_sit(self, sitter):
|
||||
"""
|
||||
Called when trying to sit on/in this object.
|
||||
|
||||
Args:
|
||||
sitter (Object): The one trying to sit down.
|
||||
|
||||
"""
|
||||
current = self.db.sitter
|
||||
if current:
|
||||
if current == sitter:
|
||||
sitter.msg("You are already sitting on {self.key}.")
|
||||
else:
|
||||
sitter.msg(f"You can't sit on {self.key} "
|
||||
f"- {current.key} is already sitting there!")
|
||||
return
|
||||
self.db.sitting = sitter
|
||||
sitter.db.is_resting = True
|
||||
sitter.msg(f"You sit on {self.key}")
|
||||
|
||||
def do_stand(self, stander):
|
||||
"""
|
||||
Called when trying to stand from this object.
|
||||
|
||||
Args:
|
||||
stander (Object): The one trying to stand up.
|
||||
|
||||
"""
|
||||
current = self.db.sitter
|
||||
if not stander == current:
|
||||
stander.msg(f"You are not sitting on {self.key}.")
|
||||
else:
|
||||
self.db.sitting = None
|
||||
stander.db.is_resting = False
|
||||
stander.msg(f"You stand up from {self.key}")
|
||||
```
|
||||
|
||||
Here we have a small Typeclass that handles someone trying to sit on it. It has two methods that we can simply
|
||||
call from a Command later. We set the `is_resting` Attribute on the one sitting down.
|
||||
|
||||
One could imagine that one could have the future `sit` command check if someone is already sitting in the
|
||||
chair instead. This would work too, but letting the `Sittable` class handle the logic around who can sit on it makes
|
||||
logical sense.
|
||||
|
||||
We let the typeclass handle the logic, we also let it do all the return messaging. This makes it easy to churn out
|
||||
a bunch of chairs for people to sit on. But it's not perfect. The `Sittable` class is general. What if you want to
|
||||
make an armchair. You sit "in" an armchair rather than "on" it. We _could_ make a child class of `Sittable` named
|
||||
`SittableIn` that makes this change, but that feels excessive. Instead we will make it so that Sittables can
|
||||
modify this per-instance:
|
||||
|
||||
|
||||
```python
|
||||
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Sittable(DefaultObject):
|
||||
|
||||
def at_object_creation(self):
|
||||
self.db.sitter = None
|
||||
# do you sit "on" or "in" this object?
|
||||
self.db.adjective = "on"
|
||||
|
||||
def do_sit(self, sitter):
|
||||
"""
|
||||
Called when trying to sit on/in this object.
|
||||
|
||||
Args:
|
||||
sitter (Object): The one trying to sit down.
|
||||
|
||||
"""
|
||||
adjective = self.db.adjective
|
||||
current = self.db.sitter
|
||||
if current:
|
||||
if current == sitter:
|
||||
sitter.msg(f"You are already sitting {adjective} {self.key}.")
|
||||
else:
|
||||
sitter.msg(
|
||||
f"You can't sit {adjective} {self.key} "
|
||||
f"- {current.key} is already sitting there!")
|
||||
return
|
||||
self.db.sitting = sitter
|
||||
sitter.db.is_resting = True
|
||||
sitter.msg(f"You sit {adjective} {self.key}")
|
||||
|
||||
def do_stand(self, stander):
|
||||
"""
|
||||
Called when trying to stand from this object.
|
||||
|
||||
Args:
|
||||
stander (Object): The one trying to stand up.
|
||||
|
||||
"""
|
||||
current = self.db.sitter
|
||||
if not stander == current:
|
||||
stander.msg(f"You are not sitting {self.db.adjective} {self.key}.")
|
||||
else:
|
||||
self.db.sitting = None
|
||||
stander.db.is_resting = False
|
||||
stander.msg(f"You stand up from {self.key}")
|
||||
```
|
||||
|
||||
We added a new Attribute `adjective` which will probably usually be `in` or `on` but could also be `at` if you
|
||||
want to be able to sit at a `desk` for example. A regular builder would use it like this:
|
||||
|
||||
> create/drop armchair : sittables.Sittable
|
||||
> set armchair/adjective = in
|
||||
|
||||
This is probably enough. But all those strings are hard-coded. What if we want some more dramatic flair when you
|
||||
sit down?
|
||||
|
||||
You sit down and a whoopie cushion makes a loud fart noise!
|
||||
|
||||
For this we needs to allow some further customization. Let's let the current strings be defaults that
|
||||
we can replace.
|
||||
|
||||
```python
|
||||
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Sittable(DefaultObject):
|
||||
"""
|
||||
An object one can sit on
|
||||
|
||||
Customizable Attributes:
|
||||
adjective: How to sit (on, in, at etc)
|
||||
Return messages (set as Attributes):
|
||||
msg_already_sitting: Already sitting here
|
||||
format tokens {adjective} and {key}
|
||||
msg_other_sitting: Someone else is sitting here.
|
||||
format tokens {adjective}, {key} and {other}
|
||||
msg_sitting_down: Successfully sit down
|
||||
format tokens {adjective}, {key}
|
||||
msg_standing_fail: Fail to stand because not sitting.
|
||||
format tokens {adjective}, {key}
|
||||
msg_standing_up: Successfully stand up
|
||||
format tokens {adjective}, {key}
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
self.db.sitter = None
|
||||
# do you sit "on" or "in" this object?
|
||||
self.db.adjective = "on"
|
||||
|
||||
def do_sit(self, sitter):
|
||||
"""
|
||||
Called when trying to sit on/in this object.
|
||||
|
||||
Args:
|
||||
sitter (Object): The one trying to sit down.
|
||||
|
||||
"""
|
||||
adjective = self.db.adjective
|
||||
current = self.db.sitter
|
||||
if current:
|
||||
if current == sitter:
|
||||
if self.db.msg_already_sitting:
|
||||
sitter.msg(
|
||||
self.db.msg_already_sitting.format(
|
||||
adjective=self.db.adjective, key=self.key))
|
||||
else:
|
||||
sitter.msg(f"You are already sitting {adjective} {self.key}.")
|
||||
else:
|
||||
if self.db.msg_other_sitting:
|
||||
sitter.msg(self.db.msg_already_sitting.format(
|
||||
other=current.key, adjective=self.db.adjective, key=self.key))
|
||||
else:
|
||||
sitter.msg(f"You can't sit {adjective} {self.key} "
|
||||
f"- {current.key} is already sitting there!")
|
||||
return
|
||||
self.db.sitting = sitter
|
||||
sitter.db.is_resting = True
|
||||
if self.db.msg_sitting_down:
|
||||
sitter.msg(self.db.msg_sitting_down.format(adjective=adjective, key=self.key))
|
||||
else:
|
||||
sitter.msg(f"You sit {adjective} {self.key}")
|
||||
|
||||
def do_stand(self, stander):
|
||||
"""
|
||||
Called when trying to stand from this object.
|
||||
|
||||
Args:
|
||||
stander (Object): The one trying to stand up.
|
||||
|
||||
"""
|
||||
current = self.db.sitter
|
||||
if not stander == current:
|
||||
if self.db.msg_standing_fail:
|
||||
stander.msg(self.db.msg_standing_fail.format(
|
||||
adjective=self.db.adjective, key=self.key))
|
||||
else:
|
||||
stander.msg(f"You are not sitting {self.db.adjective} {self.key}")
|
||||
else:
|
||||
self.db.sitting = None
|
||||
stander.db.is_resting = False
|
||||
if self.db.msg_standing_up:
|
||||
stander.msg(self.db.msg_standing_up.format(
|
||||
adjective=self.db.adjective, key=self.key))
|
||||
else:
|
||||
stander.msg(f"You stand up from {self.key}")
|
||||
```
|
||||
|
||||
Here we really went all out with flexibility. We added a bunch of optional Attributes to
|
||||
hold alternative versions of all the messages. There are some things to note:
|
||||
|
||||
- We don't actually initiate those Attributes in `at_object_creation`. This is a simple
|
||||
optimization. The assumption is that _most_ chairs will probably not be this customized.
|
||||
So initiating to, say, empty strings would be a lot of useless database calls. The drawback
|
||||
is that the available Attributes become less visible when reading the code. So we add a long
|
||||
describing docstring to the end to explain all you can use.
|
||||
- We use `.format` to inject formatting-tokens in the text. The good thing about such formatting
|
||||
markers is that they are _optional_. They are there if you want them, but Python will not complain
|
||||
if you don't include some or any of them.
|
||||
|
||||
Let's actually create the chair now so we can test it later.
|
||||
|
||||
> reload # if you have new code
|
||||
> create/drop armchair : sittables.Sittable
|
||||
> set armchair/adjective = in
|
||||
> set armchair/msg_sitting_down = As you sit down {adjective} {key}, life feels easier.
|
||||
> set armchair/msg_standing_up = You stand up from {key}. Life resumes.
|
||||
|
||||
We could have skipped `{key}` and `{adjective}` if we wanted. Whenever the message is returned, the format-tokens
|
||||
within weill be replaced with `armchair` and `in` respectively. Should we rename the chair later, this will
|
||||
show in the messages automatically (since `{key}` will change).
|
||||
|
||||
We have no Command to use this chair yet. But we can try it out with `py`:
|
||||
|
||||
> py self.search("armchair").do_sit(self)
|
||||
As you sit down in armchair, life feels easier.
|
||||
> self.db.resting
|
||||
True
|
||||
> py self.search("armchair").do_stand(self)
|
||||
You stand up from armchair. Life resumes
|
||||
> self.db.resting
|
||||
False
|
||||
|
||||
If you follow along and get a result like this, all seems to be working well!
|
||||
|
||||
## Deciding on a sitting command
|
||||
|
||||
We already know what our `sit` command must do:
|
||||
|
||||
- It needs to somehow figure out what object to sit on
|
||||
- Once it finds a suitable sittable, it should call `sittable.do_sit(caller)`. The
|
||||
object does the rest from there.
|
||||
|
||||
There are a few ways to do a Command like this. We'll explore the two main ones.
|
||||
|
||||
### Command variant 1: Command on the Chair
|
||||
|
||||
We can put the `sit` command in a command-set _on_ the chair. As we've learned before, commands on
|
||||
objects are made available to others in the room. This makes the command easy. In combination with our
|
||||
new `armchair`:
|
||||
|
||||
> sit
|
||||
As you sit down in armchair, life feels easier.
|
||||
|
||||
What happens if there are also a sittable `sofa` and `barstool` in the room? Evennia will automatically
|
||||
handle this for us and allow us to specify which one we want:
|
||||
|
||||
> sit
|
||||
More than one match for 'sit' (please narrow target):
|
||||
sit-1 (armchair)
|
||||
sit-2 (sofa)
|
||||
sit-3 (barstool)
|
||||
> sit-1
|
||||
As you sit down in armchair, life feels easier.
|
||||
|
||||
This is how we'd implement this type of command. To keep things separate we'll make a new module
|
||||
`mygame/commands/sittables.py`
|
||||
|
||||
```sidebar:: Separate Commands and Typeclasses?
|
||||
|
||||
You can organize these things as you like. If you wanted you could also put the sit-command + cmdset
|
||||
together with the `Sittable` typeclass in `mygame/typeclasses/sittables.py`. That has the advantage of
|
||||
keeping everything related to sitting in one place.
|
||||
|
||||
```
|
||||
|
||||
```python
|
||||
from evennia import Command, CmdSet
|
||||
|
||||
class CmdSit(Command):
|
||||
"""
|
||||
Sit down.
|
||||
"""
|
||||
key = "sit"
|
||||
|
||||
def func(self):
|
||||
self.obj.do_sit(self.caller)
|
||||
|
||||
class CmdStand(Command):
|
||||
"""
|
||||
Stand up.
|
||||
"""
|
||||
key = "stand"
|
||||
def func(self):
|
||||
self.obj.do_stand(self.caller)
|
||||
|
||||
|
||||
class CmdSetSit(CmdSet):
|
||||
priority = 1
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdSit)
|
||||
self.add(CmdStand)
|
||||
|
||||
```
|
||||
|
||||
As seen, the commands are nearly trivial. `self.obj` is the object to which we added the cmdset with this
|
||||
Command (so for example a chair). We just call the `do_sit/stand` on that object and the `Sittable` will
|
||||
do the rest.
|
||||
|
||||
Why that `priority = 1` on `CmdSetSit`? This makes same-named Commands from this cmdset merge with a bit higher
|
||||
priority than Commands from the Character-cmdset. Why this is a good idea will become clear shortly.
|
||||
|
||||
We also need to make a change to our `Sittable` typeclass. Open `mygame/typeclasses/sittables.py`:
|
||||
|
||||
```python
|
||||
from evennia import DefaultObject
|
||||
from commands.sittables import CmdSetSit # <- new
|
||||
|
||||
class Sittable(DefaultObject):
|
||||
"""
|
||||
(docstring)
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
|
||||
self.db.sitter = None
|
||||
# do you sit "on" or "in" this object?
|
||||
self.db.adjective = "on"
|
||||
self.cmdset.add_default(CmdSetSit) # <- new
|
||||
```
|
||||
|
||||
Any _new_ Sittables will now have your `sit` Command. Your existing `armchair` will not,
|
||||
since `at_object_creation` will not re-run for already existing objects. We can update it manually:
|
||||
|
||||
> reload
|
||||
> update armchair
|
||||
|
||||
We could also update all existing sittables (all on one line):
|
||||
|
||||
> py from typeclasses.sittables import Sittable ;
|
||||
[sittable.at_object_creation() for sittable in Sittable.objects.all()]
|
||||
|
||||
We should now be able to use `sit` while in the room with the armchair.
|
||||
|
||||
One issue with placing the `sit` (or `stand`) Command "on" the chair is that it will not be available when in a
|
||||
room without a Sittable object:
|
||||
|
||||
> sit
|
||||
Command 'sit' is not available. ...
|
||||
|
||||
This is practical but not so good-looking; it makes it harder for the user to know a `sit` action is at all possible.
|
||||
Here is a trick for fixing this. Let's add another Command to the bottom of `mygame/commands/sittables.py`:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdNoSitStand(Command):
|
||||
"""
|
||||
Sit down or Stand up
|
||||
"""
|
||||
key = "sit"
|
||||
aliases = ["stand"]
|
||||
|
||||
def func(self):
|
||||
if self.cmdname == "sit":
|
||||
self.msg("You have nothing to sit on.")
|
||||
else:
|
||||
self.msg("You are not sitting down.")
|
||||
|
||||
```
|
||||
|
||||
Here we have a Command that is actually two - it will answer to both `sit` and `stand` since we
|
||||
added `stand` to its `aliases`. In the command we look at `self.cmdname`, which is the string
|
||||
_actually used_ to call this command. We use this to return different messages.
|
||||
|
||||
We don't need a separate CmdSet for this, instead we will add this
|
||||
to the default Character cmdset. Open `mygame/commands/default_cmdsets`:
|
||||
|
||||
```python
|
||||
# ...
|
||||
from commands.sittables import CmdNoSitStand
|
||||
|
||||
class CharacterCmdSet(CmdSet):
|
||||
"""
|
||||
(docstring)
|
||||
"""
|
||||
def at_cmdset_creation(self):
|
||||
# ...
|
||||
self.add(CmdNoSitStand)
|
||||
|
||||
```
|
||||
|
||||
To test we'll build a new location without any comfy armchairs and go there:
|
||||
|
||||
> reload
|
||||
> tunnel n = kitchen
|
||||
north
|
||||
> sit
|
||||
You have nothing to sit on.
|
||||
> south
|
||||
sit
|
||||
As you sit down in armchair, life feels easier.
|
||||
|
||||
We now have a fully functioning `sit` action that is contained with the chair itself. When no chair is around, a
|
||||
default error message is shown.
|
||||
|
||||
How does this work? There are two cmdsets at play, both of which have a `sit` Command. As you may remember we
|
||||
set the chair's cmdset to `priority = 1`. This is where that matters. The default Character cmdset has a
|
||||
priority of 0. This means that whenever we enter a room with a Sittable thing, _its_ cmdset will take
|
||||
_precedence_ over the Character cmdset. So we are actually picking different `sit` commands
|
||||
depending on circumstance. The user will never be the wiser.
|
||||
|
||||
So this handles `sit`. What about `stand`? That will work just fine:
|
||||
|
||||
> stand
|
||||
You stand up from armchair.
|
||||
> north
|
||||
> stand
|
||||
You are not sitting down.
|
||||
|
||||
We have one remaining problem with `stand` though - what happens when you are sitting down and try to
|
||||
`stand` in a room with more than one chair:
|
||||
|
||||
> stand
|
||||
More than one match for 'stand' (please narrow target):
|
||||
stand-1 (armchair)
|
||||
stand-2 (sofa)
|
||||
stand-3 (barstool)
|
||||
|
||||
Since all the sittables have the `stand` Command on them, you'll get a multi-match error. This _works_ ... but
|
||||
you could pick _any_ of those sittables to "stand up from". That's really weird and non-intuitive. With `sit` it
|
||||
was okay to get a choice - Evennia can't know which chair we intended to sit on. But we know which chair we
|
||||
sit on so we should only get _its_ `stand` command.
|
||||
|
||||
We will fix this with a `lock` and a custom `lock function`. We want a lock on the `stand` Command that only
|
||||
makes it available when the caller is actually sitting on the chair the `stand` command is on.
|
||||
|
||||
First let's add the lock so we see what we want. Open `mygame/commands/sittables.py`:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdStand(Command):
|
||||
"""
|
||||
Stand up.
|
||||
"""
|
||||
key = "stand"
|
||||
lock = "cmd:sitsonthis()" # < this is new
|
||||
|
||||
def func(self):
|
||||
self.obj.do_stand(self.caller)
|
||||
# ...
|
||||
```
|
||||
|
||||
We define a [Lock](../../../Components/Locks) on the command. The `cmd:` is in what situation Evennia will check the lock. The `cmd`
|
||||
means that it will check the lock when determining if a user has access to this command or not. What will be
|
||||
checked is the `sitsonthis` _lock function_ which doesn't exist yet.
|
||||
|
||||
Open `mygame/server/conf/lockfuncs.py` to add it!
|
||||
|
||||
```python
|
||||
"""
|
||||
(module lockstring)
|
||||
"""
|
||||
|
||||
def sitsonthis(accessing_obj, accessed_obj, *args, **kwargs):
|
||||
return accessed_obj.db.sitting == accessing_obj
|
||||
|
||||
```
|
||||
|
||||
Evennia knows that all functions in `mygame/server/conf/lockfuncs` should be possible to use in a lock definition.
|
||||
The arguments are required and Evennia will pass all relevant objects to them:
|
||||
|
||||
- `accessing_obj` is the one trying to access the lock. So us, in this case.
|
||||
- `accessed_obj` is the entity we are trying to gain a particular type of access to. So the chair.
|
||||
- `args` is a tuple holding any arguments passed to the lockfunc. Since we use `sitsondthis()` this will
|
||||
be empty (and if we add anything, it will be ignored).
|
||||
- `kwargs` is a tuple of keyword arguments passed to the lockfuncs. This will be empty as well in our example.
|
||||
|
||||
If you are superuser, it's important that you `quell` yourself before trying this out. This is because the superuser
|
||||
bypasses all locks - it can never get locked out, but it means it will also not see the effects of a lock like this.
|
||||
|
||||
> reload
|
||||
> quell
|
||||
> stand
|
||||
You stand up from armchair
|
||||
|
||||
None of the other sittables' `stand` commands passed the lock and only the one we are actually sitting on did.
|
||||
|
||||
Adding a Command to the chair object like this is powerful and a good technique to know. It does come with some
|
||||
caveats though that one needs to keep in mind.
|
||||
|
||||
We'll now try another way to add the `sit/stand` commands.
|
||||
|
||||
### Command variant 2: Command on Character
|
||||
|
||||
Before we start with this, delete the chairs you've created (`del armchair` etc) and then do the following
|
||||
changes:
|
||||
|
||||
- In `mygame/typeclasses/sittables.py`, comment out the line `self.cmdset.add_default(CmdSetSit)`.
|
||||
- In `mygame/commands/default_cmdsets.py`, comment out the line `self.add(CmdNoSitStand)`.
|
||||
|
||||
This disables the on-object command solution so we can try an alternative. Make sure to `reload` so the
|
||||
changes are known to Evennia.
|
||||
|
||||
In this variation we will put the `sit` and `stand` commands on the `Character` instead of on the chair. This
|
||||
makes some things easier, but makes the Commands themselves more complex because they will not know which
|
||||
chair to sit on. We can't just do `sit` anymore. We need this:
|
||||
|
||||
> sit <chair>
|
||||
You sit on chair.
|
||||
> stand
|
||||
You stand up from chair.
|
||||
|
||||
Open `mygame/commands.sittables.py` again. We'll add a new sit-command. We name the class `CmdSit2` since
|
||||
we already have `CmdSit` from the previous example. We put everything at the end of the module to
|
||||
keep it separate.
|
||||
|
||||
```python
|
||||
from evennia import Command, CmdSet
|
||||
from evennia import InterruptCommand # <- this is new
|
||||
|
||||
class CmdSit(Command):
|
||||
# ...
|
||||
|
||||
# ...
|
||||
|
||||
# new from here
|
||||
|
||||
class CmdSit2(Command):
|
||||
"""
|
||||
Sit down.
|
||||
|
||||
Usage:
|
||||
sit <sittable>
|
||||
|
||||
"""
|
||||
key = "sit"
|
||||
|
||||
def parse(self):
|
||||
self.args = self.args.strip()
|
||||
if not self.args:
|
||||
self.caller.msg("Sit on what?")
|
||||
raise InterruptCommand
|
||||
|
||||
def func(self):
|
||||
|
||||
# self.search handles all error messages etc.
|
||||
sittable = self.caller.search(self.args)
|
||||
if not sittable:
|
||||
return
|
||||
try:
|
||||
sittable.do_sit(self.caller)
|
||||
except AttributeError:
|
||||
self.caller.msg("You can't sit on that!")
|
||||
|
||||
```
|
||||
|
||||
With this Command-variation we need to search for the sittable. A series of methods on the Command
|
||||
are run in sequence:
|
||||
|
||||
1. `Command.at_pre_command` - this is not used by default
|
||||
2. `Command.parse` - this should parse the input
|
||||
3. `Command.func` - this should implement the actual Command functionality
|
||||
4. `Command.at_post_func` - this is not used by default
|
||||
|
||||
So if we just `return` in `.parse`, `.func` will still run, which is not what we want. To immediately
|
||||
abort this sequence we need to `raise InterruptCommand`.
|
||||
|
||||
```sidebar:: Raising exceptions
|
||||
|
||||
Raising an exception allows for immediately interrupting the current program flow. Python
|
||||
automatically raises error-exceptions when detecting problems with the code. It will be
|
||||
raised up through the sequence of called code (the 'stack') until it's either `caught` with
|
||||
a `try ... except` or reaches the outermost scope where it'll be logged or displayed.
|
||||
|
||||
```
|
||||
|
||||
`InterruptCommand` is an _exception_ that the Command-system catches with the understanding that we want
|
||||
to do a clean abort. In the `.parse` method we strip any whitespaces from the argument and
|
||||
sure there actuall _is_ an argument. We abort immediately if there isn't.
|
||||
|
||||
We we get to `.func` at all, we know that we have an argument. We search for this and abort if we there was
|
||||
a problem finding the target.
|
||||
|
||||
> We could have done `raise InterruptCommand` in `.func` as well, but `return` is a little shorter to write
|
||||
> and there is no harm done if `at_post_func` runs since it's empty.
|
||||
|
||||
Next we call the found sittable's `do_sit` method. Note that we wrap this call like this:
|
||||
|
||||
```python
|
||||
|
||||
try:
|
||||
# code
|
||||
except AttributeError:
|
||||
# stuff to do if AttributeError exception was raised
|
||||
```
|
||||
|
||||
The reason is that `caller.search` has no idea we are looking for a Sittable. The user could have tried
|
||||
`sit wall` or `sit sword`. These don't have a `do_sit` method _but we call it anyway and handle the error_.
|
||||
This is a very "Pythonic" thing to do. The concept is often called "leap before you look" or "it's easier to
|
||||
ask for forgiveness than for permission". If `sittable.do_sit` does not exist, Python will raise an `AttributeError`.
|
||||
We catch this with `try ... except AttributeError` and convert it to a proper error message.
|
||||
|
||||
While it's useful to learn about `try ... except`, there is also a way to leverage Evennia to do this without
|
||||
`try ... except`:
|
||||
|
||||
```python
|
||||
|
||||
# ...
|
||||
|
||||
def func(self):
|
||||
|
||||
# self.search handles all error messages etc.
|
||||
sittable = self.caller.search(
|
||||
self.args,
|
||||
typeclass="typeclasses.sittables.Sittable")
|
||||
if not sittable:
|
||||
return
|
||||
sittable.do_sit(self.caller)
|
||||
```
|
||||
|
||||
```sidebar:: Continuing across multiple lines
|
||||
|
||||
Note how the `.search()` method's arguments are spread out over multiple
|
||||
lines. This works for all lists, tuples and other listings and is
|
||||
a good way to avoid very long and hard-to-read lines.
|
||||
|
||||
```
|
||||
|
||||
The `caller.search` method has an keyword argument `typeclass` that can take either a python-path to a
|
||||
typeclass, the typeclass itself, or a list of either to widen the allowed options. In this case we know
|
||||
for sure that the `sittable` we get is actually a `Sittable` class and we can call `sittable.do_sit` without
|
||||
needing to worry about catching errors.
|
||||
|
||||
Let's do the `stand` command while we are at it. Again, since the Command is external to the chair we don't
|
||||
know which object we are sitting in and have to search for it.
|
||||
|
||||
```python
|
||||
|
||||
class CmdStand2(Command):
|
||||
"""
|
||||
Stand up.
|
||||
|
||||
Usage:
|
||||
stand
|
||||
|
||||
"""
|
||||
key = "stand"
|
||||
|
||||
def func(self):
|
||||
|
||||
caller = self.caller
|
||||
# find the thing we are sitting on/in, by finding the object
|
||||
# in the current location that as an Attribute "sitter" set
|
||||
# to the caller
|
||||
sittable = self.caller.search(
|
||||
caller,
|
||||
candidates=caller.location.contents,
|
||||
attribute_name="sitter",
|
||||
typeclass="typeclasses.sittables.Sittable")
|
||||
# if this is None, the error was already reported to user
|
||||
if not sittable:
|
||||
return
|
||||
|
||||
sittable.do_stand(caller)
|
||||
|
||||
```
|
||||
|
||||
This forced us to to use the full power of the ``
|
||||
|
|
@ -128,7 +128,7 @@ def url_resolver(url):
|
|||
if url.endswith(choose_issue):
|
||||
return _github_issue_choose
|
||||
elif githubstart in url:
|
||||
urlpath = url[url.index(githubstart) + len(githubstart):]
|
||||
urlpath = url[url.index(githubstart) + len(githubstart) :]
|
||||
if not (urlpath.startswith("develop/") or urlpath.startswith("master")):
|
||||
urlpath = "master/" + urlpath
|
||||
return _github_code_root + urlpath
|
||||
|
|
@ -137,14 +137,14 @@ def url_resolver(url):
|
|||
ind = url.index(apistart)
|
||||
depth = url[:ind].count("/") + 1
|
||||
path = "../".join("" for _ in range(depth))
|
||||
urlpath = path + "api/" + url[ind + len(apistart):] + ".html"
|
||||
urlpath = path + "api/" + url[ind + len(apistart) :] + ".html"
|
||||
return urlpath
|
||||
elif sourcestart in url:
|
||||
ind = url.index(sourcestart)
|
||||
depth = url[:ind].count("/") + 1
|
||||
path = "../".join("" for _ in range(depth))
|
||||
|
||||
modpath, *inmodule = url[ind + len(sourcestart):].rsplit("#", 1)
|
||||
modpath, *inmodule = url[ind + len(sourcestart) :].rsplit("#", 1)
|
||||
modpath = "/".join(modpath.split("."))
|
||||
inmodule = "#" + inmodule[0] if inmodule else ""
|
||||
modpath = modpath + ".html" + inmodule
|
||||
|
|
@ -252,13 +252,12 @@ def autodoc_post_process_docstring(app, what, name, obj, options, lines):
|
|||
|
||||
def _sub_codeblock(match):
|
||||
code = match.group(1)
|
||||
return "::\n\n {}".format(
|
||||
"\n ".join(lne for lne in code.split("\n")))
|
||||
return "::\n\n {}".format("\n ".join(lne for lne in code.split("\n")))
|
||||
|
||||
underline_map = {
|
||||
1: "-",
|
||||
2: "=",
|
||||
3: '^',
|
||||
3: "^",
|
||||
4: '"',
|
||||
}
|
||||
|
||||
|
|
@ -271,11 +270,14 @@ def autodoc_post_process_docstring(app, what, name, obj, options, lines):
|
|||
return f"{title}\n" + (underline_map[lvl] * len(title))
|
||||
|
||||
doc = "\n".join(lines)
|
||||
doc = re.sub(r"```python\s*\n+(.*?)```", _sub_codeblock, doc,
|
||||
flags=re.MULTILINE + re.DOTALL)
|
||||
doc = re.sub(
|
||||
r"```python\s*\n+(.*?)```", _sub_codeblock, doc, flags=re.MULTILINE + re.DOTALL
|
||||
)
|
||||
doc = re.sub(r"```", "", doc, flags=re.MULTILINE)
|
||||
doc = re.sub(r"`{1}", "**", doc, flags=re.MULTILINE)
|
||||
doc = re.sub(r"^(?P<hashes>#{1,2})\s*?(?P<title>.*?)$", _sub_header, doc, flags=re.MULTILINE)
|
||||
doc = re.sub(
|
||||
r"^(?P<hashes>#{1,2})\s*?(?P<title>.*?)$", _sub_header, doc, flags=re.MULTILINE
|
||||
)
|
||||
|
||||
newlines = doc.split("\n")
|
||||
# we must modify lines in-place
|
||||
|
|
|
|||
|
|
@ -450,10 +450,14 @@ def set_trace(term_size=(140, 80), debugger="auto"):
|
|||
# Stopped at breakpoint. Press 'n' to continue into the code.
|
||||
dbg.set_trace()
|
||||
|
||||
|
||||
# initialize the doc string
|
||||
global __doc__
|
||||
__doc__ = DOCSTRING.format(
|
||||
"\n- " + "\n- ".join(
|
||||
f"evennia.{key}" for key in sorted(globals())
|
||||
if not key.startswith("_")
|
||||
and key not in ("DOCSTRING", )))
|
||||
"\n- "
|
||||
+ "\n- ".join(
|
||||
f"evennia.{key}"
|
||||
for key in sorted(globals())
|
||||
if not key.startswith("_") and key not in ("DOCSTRING",)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -675,7 +675,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
|
||||
# Load the appropriate Character class
|
||||
character_typeclass = kwargs.pop("typeclass", None)
|
||||
character_typeclass = character_typeclass if character_typeclass else settings.BASE_CHARACTER_TYPECLASS
|
||||
character_typeclass = (
|
||||
character_typeclass if character_typeclass else settings.BASE_CHARACTER_TYPECLASS
|
||||
)
|
||||
Character = class_from_module(character_typeclass)
|
||||
|
||||
# Create the character
|
||||
|
|
@ -685,7 +687,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
ip=character_ip,
|
||||
typeclass=character_typeclass,
|
||||
permissions=character_permissions,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
)
|
||||
if character:
|
||||
# Update playable character list
|
||||
|
|
@ -806,7 +808,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
if account and settings.MULTISESSION_MODE < 2:
|
||||
# Auto-create a character to go with this account
|
||||
|
||||
character, errs = account.create_character(typeclass=kwargs.get("character_typeclass"))
|
||||
character, errs = account.create_character(
|
||||
typeclass=kwargs.get("character_typeclass")
|
||||
)
|
||||
if errs:
|
||||
errors.extend(errs)
|
||||
|
||||
|
|
@ -994,9 +998,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
searchdata, categories=("account",), include_account=False
|
||||
)
|
||||
if search_object:
|
||||
matches = ObjectDB.objects.object_search(
|
||||
searchdata, typeclass=typeclass
|
||||
)
|
||||
matches = ObjectDB.objects.object_search(searchdata, typeclass=typeclass)
|
||||
else:
|
||||
matches = AccountDB.objects.account_search(searchdata, typeclass=typeclass)
|
||||
|
||||
|
|
|
|||
|
|
@ -274,27 +274,28 @@ class AccountDBAdmin(BaseUserAdmin):
|
|||
)
|
||||
|
||||
@sensitive_post_parameters_m
|
||||
def user_change_password(self, request, id, form_url=''):
|
||||
def user_change_password(self, request, id, form_url=""):
|
||||
user = self.get_object(request, unquote(id))
|
||||
if not self.has_change_permission(request, user):
|
||||
raise PermissionDenied
|
||||
if user is None:
|
||||
raise Http404('%(name)s object with primary key %(key)r does not exist.') % {
|
||||
'name': self.model._meta.verbose_name,
|
||||
'key': escape(id),
|
||||
raise Http404("%(name)s object with primary key %(key)r does not exist.") % {
|
||||
"name": self.model._meta.verbose_name,
|
||||
"key": escape(id),
|
||||
}
|
||||
if request.method == 'POST':
|
||||
if request.method == "POST":
|
||||
form = self.change_password_form(user, request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
change_message = self.construct_change_message(request, form, None)
|
||||
self.log_change(request, user, change_message)
|
||||
msg = 'Password changed successfully.'
|
||||
msg = "Password changed successfully."
|
||||
messages.success(request, msg)
|
||||
update_session_auth_hash(request, form.user)
|
||||
return HttpResponseRedirect(
|
||||
reverse(
|
||||
'%s:%s_%s_change' % (
|
||||
"%s:%s_%s_change"
|
||||
% (
|
||||
self.admin_site.name,
|
||||
user._meta.app_label,
|
||||
# the model_name is something we need to hardcode
|
||||
|
|
@ -307,25 +308,24 @@ class AccountDBAdmin(BaseUserAdmin):
|
|||
else:
|
||||
form = self.change_password_form(user)
|
||||
|
||||
fieldsets = [(None, {'fields': list(form.base_fields)})]
|
||||
fieldsets = [(None, {"fields": list(form.base_fields)})]
|
||||
adminForm = admin.helpers.AdminForm(form, fieldsets, {})
|
||||
|
||||
context = {
|
||||
'title': 'Change password: %s' % escape(user.get_username()),
|
||||
'adminForm': adminForm,
|
||||
'form_url': form_url,
|
||||
'form': form,
|
||||
'is_popup': (IS_POPUP_VAR in request.POST or
|
||||
IS_POPUP_VAR in request.GET),
|
||||
'add': True,
|
||||
'change': False,
|
||||
'has_delete_permission': False,
|
||||
'has_change_permission': True,
|
||||
'has_absolute_url': False,
|
||||
'opts': self.model._meta,
|
||||
'original': user,
|
||||
'save_as': False,
|
||||
'show_save': True,
|
||||
"title": "Change password: %s" % escape(user.get_username()),
|
||||
"adminForm": adminForm,
|
||||
"form_url": form_url,
|
||||
"form": form,
|
||||
"is_popup": (IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET),
|
||||
"add": True,
|
||||
"change": False,
|
||||
"has_delete_permission": False,
|
||||
"has_change_permission": True,
|
||||
"has_absolute_url": False,
|
||||
"opts": self.model._meta,
|
||||
"original": user,
|
||||
"save_as": False,
|
||||
"show_save": True,
|
||||
**self.admin_site.each_context(request),
|
||||
}
|
||||
|
||||
|
|
@ -333,8 +333,7 @@ class AccountDBAdmin(BaseUserAdmin):
|
|||
|
||||
return TemplateResponse(
|
||||
request,
|
||||
self.change_user_password_template or
|
||||
'admin/auth/user/change_password.html',
|
||||
self.change_user_password_template or "admin/auth/user/change_password.html",
|
||||
context,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -109,8 +109,8 @@ class AccountDB(TypedObject, AbstractUser):
|
|||
__applabel__ = "accounts"
|
||||
__settingsclasspath__ = settings.BASE_SCRIPT_TYPECLASS
|
||||
|
||||
# class Meta:
|
||||
# verbose_name = "Account"
|
||||
# class Meta:
|
||||
# verbose_name = "Account"
|
||||
|
||||
# cmdset_storage property
|
||||
# This seems very sensitive to caching, so leaving it be for now /Griatch
|
||||
|
|
|
|||
|
|
@ -314,8 +314,12 @@ class CmdIC(COMMAND_DEFAULT_CLASS):
|
|||
if account.db._playable_characters:
|
||||
# look at the playable_characters list first
|
||||
character_candidates.extend(
|
||||
account.search(self.args, candidates=account.db._playable_characters,
|
||||
search_object=True, quiet=True)
|
||||
account.search(
|
||||
self.args,
|
||||
candidates=account.db._playable_characters,
|
||||
search_object=True,
|
||||
quiet=True,
|
||||
)
|
||||
)
|
||||
|
||||
if account.locks.check_lockstring(account, "perm(Builder)"):
|
||||
|
|
@ -337,8 +341,12 @@ class CmdIC(COMMAND_DEFAULT_CLASS):
|
|||
# fall back to global search only if Builder+ has no
|
||||
# playable_characers in list and is not standing in a room
|
||||
# with a matching char.
|
||||
character_candidates.extend([
|
||||
char for char in search.object_search(self.args) if char.access(account, "puppet")]
|
||||
character_candidates.extend(
|
||||
[
|
||||
char
|
||||
for char in search.object_search(self.args)
|
||||
if char.access(account, "puppet")
|
||||
]
|
||||
)
|
||||
|
||||
# handle possible candidates
|
||||
|
|
|
|||
|
|
@ -1459,7 +1459,12 @@ class CmdOpen(ObjManipCommand):
|
|||
if not typeclass:
|
||||
typeclass = settings.BASE_EXIT_TYPECLASS
|
||||
exit_obj = create.create_object(
|
||||
typeclass, key=exit_name, location=location, aliases=exit_aliases, locks=lockstring, report_to=caller
|
||||
typeclass,
|
||||
key=exit_name,
|
||||
location=location,
|
||||
aliases=exit_aliases,
|
||||
locks=lockstring,
|
||||
report_to=caller,
|
||||
)
|
||||
if exit_obj:
|
||||
# storing a destination is what makes it an exit!
|
||||
|
|
@ -2375,7 +2380,7 @@ class CmdExamine(ObjManipCommand):
|
|||
value (any): Attribute value.
|
||||
Returns:
|
||||
"""
|
||||
if attr is None:
|
||||
if attr is None:
|
||||
return "No such attribute was found."
|
||||
value = utils.to_str(value)
|
||||
if crop:
|
||||
|
|
@ -2413,13 +2418,14 @@ class CmdExamine(ObjManipCommand):
|
|||
output = {}
|
||||
if db_attr and db_attr[0]:
|
||||
output["Persistent attribute(s)"] = "\n " + "\n ".join(
|
||||
sorted(self.list_attribute(crop, attr, category, value)
|
||||
for attr, value, category in db_attr)
|
||||
sorted(
|
||||
self.list_attribute(crop, attr, category, value)
|
||||
for attr, value, category in db_attr
|
||||
)
|
||||
)
|
||||
if ndb_attr and ndb_attr[0]:
|
||||
output["Non-Persistent attribute(s)"] = " \n" + " \n".join(
|
||||
sorted(self.list_attribute(crop, attr, None, value)
|
||||
for attr, value in ndb_attr)
|
||||
sorted(self.list_attribute(crop, attr, None, value) for attr, value in ndb_attr)
|
||||
)
|
||||
return output
|
||||
|
||||
|
|
@ -2449,7 +2455,7 @@ class CmdExamine(ObjManipCommand):
|
|||
output["Typeclass"] = f"{obj.typename} ({obj.typeclass_path})"
|
||||
# sessions
|
||||
if hasattr(obj, "sessions") and obj.sessions.all():
|
||||
output["Session id(s)"] = ", ".join(f"#{sess.sessid}" for sess in obj.sessions.all())
|
||||
output["Session id(s)"] = ", ".join(f"#{sess.sessid}" for sess in obj.sessions.all())
|
||||
# email, if any
|
||||
if hasattr(obj, "email") and obj.email:
|
||||
output["Email"] = f"{dclr}{obj.email}|n"
|
||||
|
|
@ -2499,20 +2505,19 @@ class CmdExamine(ObjManipCommand):
|
|||
locks = str(obj.locks)
|
||||
if locks:
|
||||
locks_string = "\n" + utils.fill(
|
||||
"; ".join([lock for lock in locks.split(";")]), indent=2)
|
||||
"; ".join([lock for lock in locks.split(";")]), indent=2
|
||||
)
|
||||
else:
|
||||
locks_string = " Default"
|
||||
output["Locks"] = locks_string
|
||||
# cmdsets
|
||||
if not (len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "_EMPTY_CMDSET"):
|
||||
# all() returns a 'stack', so make a copy to sort.
|
||||
stored_cmdsets = sorted(obj.cmdset.all(), key=lambda x: x.priority,
|
||||
reverse=True)
|
||||
output["Stored Cmdset(s)"] = (
|
||||
"\n " + "\n ".join(
|
||||
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority})"
|
||||
for cmdset in stored_cmdsets if cmdset.key != "_EMPTY_CMDSET"
|
||||
)
|
||||
stored_cmdsets = sorted(obj.cmdset.all(), key=lambda x: x.priority, reverse=True)
|
||||
output["Stored Cmdset(s)"] = "\n " + "\n ".join(
|
||||
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority})"
|
||||
for cmdset in stored_cmdsets
|
||||
if cmdset.key != "_EMPTY_CMDSET"
|
||||
)
|
||||
|
||||
# this gets all components of the currently merged set
|
||||
|
|
@ -2546,11 +2551,9 @@ class CmdExamine(ObjManipCommand):
|
|||
pass
|
||||
all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()]
|
||||
all_cmdsets.sort(key=lambda x: x.priority, reverse=True)
|
||||
output["Merged Cmdset(s)"] = (
|
||||
"\n " + "\n ".join(
|
||||
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype} prio {cmdset.priority})"
|
||||
for cmdset in all_cmdsets
|
||||
)
|
||||
output["Merged Cmdset(s)"] = "\n " + "\n ".join(
|
||||
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype} prio {cmdset.priority})"
|
||||
for cmdset in all_cmdsets
|
||||
)
|
||||
# list the commands available to this object
|
||||
avail_cmdset = sorted([cmd.key for cmd in avail_cmdset if cmd.access(obj, "cmd")])
|
||||
|
|
@ -2565,9 +2568,7 @@ class CmdExamine(ObjManipCommand):
|
|||
# Tags
|
||||
tags = obj.tags.all(return_key_and_category=True)
|
||||
tags_string = "\n" + utils.fill(
|
||||
", ".join(
|
||||
sorted(f"{tag}[{category}]" for tag, category in tags )),
|
||||
indent=2,
|
||||
", ".join(sorted(f"{tag}[{category}]" for tag, category in tags)), indent=2,
|
||||
)
|
||||
if tags:
|
||||
output["Tags[category]"] = tags_string
|
||||
|
|
@ -2584,9 +2585,13 @@ class CmdExamine(ObjManipCommand):
|
|||
else:
|
||||
things.append(content)
|
||||
if exits:
|
||||
output["Exits (has .destination)"] = ", ".join(f"{exit.name}({exit.dbref})" for exit in exits)
|
||||
output["Exits (has .destination)"] = ", ".join(
|
||||
f"{exit.name}({exit.dbref})" for exit in exits
|
||||
)
|
||||
if pobjs:
|
||||
output["Characters"] = ", ".join(f"{dclr}{pobj.name}|n({pobj.dbref})" for pobj in pobjs)
|
||||
output["Characters"] = ", ".join(
|
||||
f"{dclr}{pobj.name}|n({pobj.dbref})" for pobj in pobjs
|
||||
)
|
||||
if things:
|
||||
output["Contents"] = ", ".join(
|
||||
f"{cont.name}({cont.dbref})"
|
||||
|
|
@ -2601,7 +2606,6 @@ class CmdExamine(ObjManipCommand):
|
|||
mainstr = "\n".join(f"{hclr}{header}|n: {block}" for (header, block) in output.items())
|
||||
return f"{sep}\n{mainstr}\n{sep}"
|
||||
|
||||
|
||||
def func(self):
|
||||
"""Process command"""
|
||||
caller = self.caller
|
||||
|
|
@ -2671,7 +2675,10 @@ class CmdExamine(ObjManipCommand):
|
|||
# we are only interested in specific attributes
|
||||
ret = "\n".join(
|
||||
f"{self.header_color}{header}|n:{value}"
|
||||
for header, value in self.format_attributes(obj, attrname, crop=False).items())
|
||||
for header, value in self.format_attributes(
|
||||
obj, attrname, crop=False
|
||||
).items()
|
||||
)
|
||||
self.caller.msg(ret)
|
||||
else:
|
||||
session = None
|
||||
|
|
|
|||
|
|
@ -431,7 +431,9 @@ class CmdGet(COMMAND_DEFAULT_CLASS):
|
|||
caller.msg("This can't be picked up.")
|
||||
else:
|
||||
caller.msg("You pick up %s." % obj.name)
|
||||
caller.location.msg_contents("%s picks up %s." % (caller.name, obj.name), exclude=caller)
|
||||
caller.location.msg_contents(
|
||||
"%s picks up %s." % (caller.name, obj.name), exclude=caller
|
||||
)
|
||||
# calling at_get hook method
|
||||
obj.at_get(caller)
|
||||
|
||||
|
|
|
|||
|
|
@ -448,7 +448,9 @@ def format_script_list(scripts):
|
|||
|
||||
table.add_row(
|
||||
script.id,
|
||||
f"{script.obj.key}({script.obj.dbref})" if (hasattr(script, "obj") and script.obj) else "<Global>",
|
||||
f"{script.obj.key}({script.obj.dbref})"
|
||||
if (hasattr(script, "obj") and script.obj)
|
||||
else "<Global>",
|
||||
script.key,
|
||||
script.interval if script.interval > 0 else "--",
|
||||
nextrep,
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ class CommandTest(EvenniaTest):
|
|||
prt = ""
|
||||
for ic, char in enumerate(msg):
|
||||
import re
|
||||
|
||||
prt += char
|
||||
|
||||
sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n"
|
||||
|
|
@ -382,11 +383,13 @@ class TestAccount(CommandTest):
|
|||
def test_ic__nonaccess(self):
|
||||
self.account.unpuppet_object(self.session)
|
||||
self.call(
|
||||
account.CmdIC(), "Nonexistent", "That is not a valid character choice.",
|
||||
caller=self.account, receiver=self.account
|
||||
account.CmdIC(),
|
||||
"Nonexistent",
|
||||
"That is not a valid character choice.",
|
||||
caller=self.account,
|
||||
receiver=self.account,
|
||||
)
|
||||
|
||||
|
||||
def test_password(self):
|
||||
self.call(
|
||||
account.CmdPassword(),
|
||||
|
|
@ -498,7 +501,11 @@ class TestBuilding(CommandTest):
|
|||
|
||||
# escape inlinefuncs
|
||||
self.char1.db.test2 = "this is a $random() value."
|
||||
self.call(building.CmdExamine(), "self/test2", "Persistent attribute(s):\n test2 = this is a \$random() value.")
|
||||
self.call(
|
||||
building.CmdExamine(),
|
||||
"self/test2",
|
||||
"Persistent attribute(s):\n test2 = this is a \$random() value.",
|
||||
)
|
||||
|
||||
self.room1.scripts.add(self.script.__class__)
|
||||
self.call(building.CmdExamine(), "")
|
||||
|
|
|
|||
|
|
@ -54,9 +54,9 @@ class TutorialMirror(DefaultObject):
|
|||
text = text[0] if is_iter(text) else text
|
||||
if from_obj:
|
||||
for obj in make_iter(from_obj):
|
||||
obj.msg(f"{self.key} echoes back to you:\n\"{text}\".")
|
||||
obj.msg(f'{self.key} echoes back to you:\n"{text}".')
|
||||
elif self.location:
|
||||
self.location.msg_contents(f"{self.key} echoes back:\n\"{text}\".", exclude=[self])
|
||||
self.location.msg_contents(f'{self.key} echoes back:\n"{text}".', exclude=[self])
|
||||
else:
|
||||
# no from_obj and no location, just log
|
||||
logger.log_msg(f"{self.key}.msg was called without from_obj and .location is None.")
|
||||
|
|
|
|||
|
|
@ -236,7 +236,13 @@ class LockHandler(object):
|
|||
elist.append(_("Lock: lock-function '%s' is not available.") % funcstring)
|
||||
continue
|
||||
args = list(arg.strip() for arg in rest.split(",") if arg and "=" not in arg)
|
||||
kwargs = dict([(part.strip() for part in arg.split("=", 1)) for arg in rest.split(",") if arg and "=" in arg])
|
||||
kwargs = dict(
|
||||
[
|
||||
(part.strip() for part in arg.split("=", 1))
|
||||
for arg in rest.split(",")
|
||||
if arg and "=" in arg
|
||||
]
|
||||
)
|
||||
lock_funcs.append((func, args, kwargs))
|
||||
evalstring = evalstring.replace(funcstring, "%s")
|
||||
if len(lock_funcs) < nfuncs:
|
||||
|
|
|
|||
|
|
@ -175,15 +175,12 @@ class ObjectDBManager(TypedObjectManager):
|
|||
)
|
||||
type_restriction = typeclasses and Q(db_typeclass_path__in=make_iter(typeclasses)) or Q()
|
||||
|
||||
results = (
|
||||
self
|
||||
.filter(
|
||||
cand_restriction
|
||||
& type_restriction
|
||||
& Q(db_attributes__db_key=attribute_name)
|
||||
& Q(db_attributes__db_value=attribute_value))
|
||||
.order_by("id")
|
||||
)
|
||||
results = self.filter(
|
||||
cand_restriction
|
||||
& type_restriction
|
||||
& Q(db_attributes__db_key=attribute_name)
|
||||
& Q(db_attributes__db_value=attribute_value)
|
||||
).order_by("id")
|
||||
return results
|
||||
|
||||
def get_objs_with_db_property(self, property_name, candidates=None):
|
||||
|
|
|
|||
|
|
@ -2041,8 +2041,10 @@ class DefaultCharacter(DefaultObject):
|
|||
_content_types = ("character",)
|
||||
# lockstring of newly created rooms, for easy overloading.
|
||||
# Will be formatted with the appropriate attributes.
|
||||
lockstring = ("puppet:id({character_id}) or pid({account_id}) or perm(Developer) or pperm(Developer);"
|
||||
"delete:id({account_id}) or perm(Admin)")
|
||||
lockstring = (
|
||||
"puppet:id({character_id}) or pid({account_id}) or perm(Developer) or pperm(Developer);"
|
||||
"delete:id({account_id}) or perm(Admin)"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(cls, key, account=None, **kwargs):
|
||||
|
|
|
|||
|
|
@ -203,9 +203,7 @@ class SessionHandler(dict):
|
|||
elif isinstance(data, (str, bytes)):
|
||||
data = _utf8(data)
|
||||
|
||||
if (_INLINEFUNC_ENABLED
|
||||
and not raw
|
||||
and isinstance(self, ServerSessionHandler)):
|
||||
if _INLINEFUNC_ENABLED and not raw and isinstance(self, ServerSessionHandler):
|
||||
# only parse inlinefuncs on the outgoing path (sessionhandler->)
|
||||
data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class EvenniaTestSuiteRunner(DiscoverRunner):
|
|||
# can't mock it - instead we stop it before starting the test - otherwise
|
||||
# we'd get unclean reactor errors across test boundaries.
|
||||
from evennia.server.portal.portal import PORTAL
|
||||
|
||||
PORTAL.maintenance_task.stop()
|
||||
|
||||
import evennia
|
||||
|
|
@ -34,4 +35,3 @@ class EvenniaTestSuiteRunner(DiscoverRunner):
|
|||
return super(EvenniaTestSuiteRunner, self).build_suite(
|
||||
test_labels, extra_tests=extra_tests, **kwargs
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -132,7 +132,6 @@ class TypeclassBase(SharedMemoryModelBase):
|
|||
if not dbmodel:
|
||||
raise TypeError(f"{name} does not appear to inherit from a database model.")
|
||||
|
||||
|
||||
# typeclass proxy setup
|
||||
# first check explicit __applabel__ on the typeclass, then figure
|
||||
# it out from the dbmodel
|
||||
|
|
@ -141,6 +140,7 @@ class TypeclassBase(SharedMemoryModelBase):
|
|||
attrs["__applabel__"] = dbmodel._meta.app_label
|
||||
|
||||
if "Meta" not in attrs:
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
app_label = attrs.get("__applabel__", "typeclasses")
|
||||
|
|
@ -162,8 +162,7 @@ class TypeclassBase(SharedMemoryModelBase):
|
|||
|
||||
# attach signals
|
||||
signals.post_save.connect(call_at_first_save, sender=new_class)
|
||||
signals.pre_delete.connect(
|
||||
remove_attributes_on_delete, sender=new_class)
|
||||
signals.pre_delete.connect(remove_attributes_on_delete, sender=new_class)
|
||||
return new_class
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -43,10 +43,12 @@ class TestAttributes(EvenniaTest):
|
|||
self.assertEqual(self.obj1.attributes.get(key), value)
|
||||
|
||||
def test_batch_add(self):
|
||||
attrs = [("key1", "value1"),
|
||||
("key2", "value2", "category2"),
|
||||
("key3", "value3"),
|
||||
("key4", "value4", "category4", "attrread:id(1)", False)]
|
||||
attrs = [
|
||||
("key1", "value1"),
|
||||
("key2", "value2", "category2"),
|
||||
("key3", "value3"),
|
||||
("key4", "value4", "category4", "attrread:id(1)", False),
|
||||
]
|
||||
new_attrs = self.obj1.attributes.batch_add(*attrs)
|
||||
attrobj = self.obj1.attributes.get(key="key4", category="category4", return_obj=True)
|
||||
self.assertEqual(attrobj.value, "value4")
|
||||
|
|
@ -136,11 +138,7 @@ class TestTypedObjectManager(EvenniaTest):
|
|||
)
|
||||
|
||||
def test_batch_add(self):
|
||||
tags = ["tag1",
|
||||
("tag2", "category2"),
|
||||
"tag3",
|
||||
("tag4", "category4", "data4")
|
||||
]
|
||||
tags = ["tag1", ("tag2", "category2"), "tag3", ("tag4", "category4", "data4")]
|
||||
self.obj1.tags.batch_add(*tags)
|
||||
self.assertEqual(self.obj1.tags.get("tag1"), "tag1")
|
||||
tagobj = self.obj1.tags.get("tag4", category="category4", return_tagobj=True)
|
||||
|
|
|
|||
|
|
@ -360,8 +360,9 @@ help_entry = create_help_entry
|
|||
# Comm system methods
|
||||
|
||||
|
||||
def create_message(senderobj, message, channels=None, receivers=None,
|
||||
locks=None, tags=None, header=None):
|
||||
def create_message(
|
||||
senderobj, message, channels=None, receivers=None, locks=None, tags=None, header=None
|
||||
):
|
||||
"""
|
||||
Create a new communication Msg. Msgs represent a unit of
|
||||
database-persistent communication between entites.
|
||||
|
|
@ -415,7 +416,9 @@ message = create_message
|
|||
create_msg = create_message
|
||||
|
||||
|
||||
def create_channel(key, aliases=None, desc=None, locks=None, keep_log=True, typeclass=None, tags=None):
|
||||
def create_channel(
|
||||
key, aliases=None, desc=None, locks=None, keep_log=True, typeclass=None, tags=None
|
||||
):
|
||||
"""
|
||||
Create A communication Channel. A Channel serves as a central hub
|
||||
for distributing Msgs to groups of people without specifying the
|
||||
|
|
|
|||
|
|
@ -235,7 +235,6 @@ class _SaverMutable(object):
|
|||
def __gt__(self, other):
|
||||
return self._data > other
|
||||
|
||||
|
||||
@_save
|
||||
def __setitem__(self, key, value):
|
||||
self._data.__setitem__(key, self._convert_mutables(value))
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ _STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE
|
|||
|
||||
# example/testing inline functions
|
||||
|
||||
|
||||
def random(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Returns a random number between
|
||||
|
|
@ -102,11 +103,11 @@ def random(*args, **kwargs):
|
|||
nargs = len(args)
|
||||
if nargs == 1:
|
||||
# only maxval given
|
||||
minval, maxval = '0', args[0]
|
||||
minval, maxval = "0", args[0]
|
||||
elif nargs > 1:
|
||||
minval, maxval = args[:2]
|
||||
else:
|
||||
minval, maxval = ('0', '1')
|
||||
minval, maxval = ("0", "1")
|
||||
|
||||
if "." in minval or "." in maxval:
|
||||
# float mode
|
||||
|
|
@ -521,10 +522,13 @@ def raw(string):
|
|||
Args:
|
||||
string (str): String with inlinefuncs to escape.
|
||||
"""
|
||||
|
||||
def _escape(match):
|
||||
return "\\" + match.group(0)
|
||||
|
||||
return _RE_STARTTOKEN.sub(_escape, string)
|
||||
|
||||
|
||||
#
|
||||
# Nick templating
|
||||
#
|
||||
|
|
|
|||
|
|
@ -102,7 +102,6 @@ def dbsafe_encode(value, compress_object=False, pickle_protocol=DEFAULT_PROTOCOL
|
|||
|
||||
value = dumps(value, protocol=pickle_protocol)
|
||||
|
||||
|
||||
if compress_object:
|
||||
value = compress(value)
|
||||
value = b64encode(value).decode() # decode bytes to str
|
||||
|
|
|
|||
|
|
@ -109,11 +109,17 @@ class TestCreateHelpEntry(TestCase):
|
|||
|
||||
def test_create_help_entry__complex(self):
|
||||
locks = "foo:false();bar:true()"
|
||||
aliases = ['foo', 'bar', 'tst']
|
||||
aliases = ["foo", "bar", "tst"]
|
||||
tags = [("tag1", "help"), ("tag2", "help"), ("tag3", "help")]
|
||||
|
||||
entry = create.create_help_entry("testentry", self.help_entry, category="Testing",
|
||||
locks=locks, aliases=aliases, tags=tags)
|
||||
entry = create.create_help_entry(
|
||||
"testentry",
|
||||
self.help_entry,
|
||||
category="Testing",
|
||||
locks=locks,
|
||||
aliases=aliases,
|
||||
tags=tags,
|
||||
)
|
||||
self.assertTrue(all(lock in entry.locks.all() for lock in locks.split(";")))
|
||||
self.assertEqual(list(entry.aliases.all()).sort(), aliases.sort())
|
||||
self.assertEqual(entry.tags.all(return_key_and_category=True), tags)
|
||||
|
|
@ -137,21 +143,28 @@ class TestCreateMessage(EvenniaTest):
|
|||
def test_create_msg__channel(self):
|
||||
chan1 = create.create_channel("DummyChannel1")
|
||||
chan2 = create.create_channel("DummyChannel2")
|
||||
msg = create.create_message(self.char1, self.msgtext, channels=[chan1, chan2], header="TestHeader")
|
||||
msg = create.create_message(
|
||||
self.char1, self.msgtext, channels=[chan1, chan2], header="TestHeader"
|
||||
)
|
||||
self.assertEqual(list(msg.channels), [chan1, chan2])
|
||||
|
||||
def test_create_msg__custom(self):
|
||||
locks = "foo:false();bar:true()"
|
||||
tags = ["tag1", "tag2", "tag3"]
|
||||
msg = create.create_message(self.char1, self.msgtext, header="TestHeader",
|
||||
receivers=[self.char1, self.char2], locks=locks, tags=tags)
|
||||
msg = create.create_message(
|
||||
self.char1,
|
||||
self.msgtext,
|
||||
header="TestHeader",
|
||||
receivers=[self.char1, self.char2],
|
||||
locks=locks,
|
||||
tags=tags,
|
||||
)
|
||||
self.assertEqual(msg.receivers, [self.char1, self.char2])
|
||||
self.assertTrue(all(lock in msg.locks.all() for lock in locks.split(";")))
|
||||
self.assertEqual(msg.tags.all(), tags)
|
||||
|
||||
|
||||
class TestCreateChannel(TestCase):
|
||||
|
||||
def test_create_channel__simple(self):
|
||||
chan = create.create_channel("TestChannel1", desc="Testing channel")
|
||||
self.assertEqual(chan.key, "TestChannel1")
|
||||
|
|
@ -160,10 +173,11 @@ class TestCreateChannel(TestCase):
|
|||
def test_create_channel__complex(self):
|
||||
locks = "foo:false();bar:true()"
|
||||
tags = ["tag1", "tag2", "tag3"]
|
||||
aliases = ['foo', 'bar', 'tst']
|
||||
aliases = ["foo", "bar", "tst"]
|
||||
|
||||
chan = create.create_channel("TestChannel2", desc="Testing channel",
|
||||
aliases=aliases, locks=locks, tags=tags)
|
||||
chan = create.create_channel(
|
||||
"TestChannel2", desc="Testing channel", aliases=aliases, locks=locks, tags=tags
|
||||
)
|
||||
self.assertTrue(all(lock in chan.locks.all() for lock in locks.split(";")))
|
||||
self.assertEqual(chan.tags.all(), tags)
|
||||
self.assertEqual(list(chan.aliases.all()).sort(), aliases.sort())
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ class TestDbSerialize(TestCase):
|
|||
"""
|
||||
Database serialization operations.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.obj = DefaultObject(db_key="Tester", )
|
||||
self.obj = DefaultObject(db_key="Tester",)
|
||||
self.obj.save()
|
||||
|
||||
def test_constants(self):
|
||||
|
|
@ -54,5 +55,5 @@ class TestDbSerialize(TestCase):
|
|||
self.assertEqual(self.obj.db.test, [[1, 2, 3], [4, 5, 6]])
|
||||
self.obj.db.test = [{1: 0}, {0: 1}]
|
||||
self.assertEqual(self.obj.db.test, [{1: 0}, {0: 1}])
|
||||
self.obj.db.test.sort(key=lambda d: str(d))
|
||||
self.obj.db.test.sort(key=lambda d: str(d))
|
||||
self.assertEqual(self.obj.db.test, [{0: 1}, {1: 0}])
|
||||
|
|
|
|||
|
|
@ -390,6 +390,7 @@ def iter_to_string(initer, endsep="and", addquote=False):
|
|||
return str(initer[0])
|
||||
return ", ".join(str(v) for v in initer[:-1]) + "%s %s" % (endsep, initer[-1])
|
||||
|
||||
|
||||
# legacy alias
|
||||
list_to_string = iter_to_string
|
||||
|
||||
|
|
@ -2045,8 +2046,6 @@ def display_len(target):
|
|||
return len(target)
|
||||
|
||||
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Search handler function
|
||||
# -------------------------------------------------------------------
|
||||
|
|
|
|||
22
pyproject.toml
Normal file
22
pyproject.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ['py37', 'py38']
|
||||
exclude = '''
|
||||
|
||||
(
|
||||
/(
|
||||
\.eggs # exclude a few common directories in the
|
||||
| \.git # root of the project
|
||||
| \.hg
|
||||
| \.mypy_cache
|
||||
| \.tox
|
||||
| \.venv
|
||||
| _build
|
||||
| buck-out
|
||||
| build
|
||||
| dist
|
||||
)/
|
||||
| migrations
|
||||
|
||||
)
|
||||
'''
|
||||
Loading…
Add table
Add a link
Reference in a new issue