Ran black on sources, add black config

This commit is contained in:
Griatch 2020-07-27 21:09:13 +02:00
parent abe4b1e4ee
commit 0df87037e7
32 changed files with 1031 additions and 232 deletions

View file

@ -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)

View file

@ -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()

View file

@ -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()

View file

@ -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)

View file

@ -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

View 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 ``

View file

@ -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

View file

@ -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",)
)
)

View file

@ -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)

View file

@ -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,
)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -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(), "")

View file

@ -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.")

View file

@ -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:

View file

@ -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):

View file

@ -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):

View file

@ -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)

View file

@ -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
)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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))

View file

@ -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
#

View file

@ -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

View file

@ -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())

View file

@ -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}])

View file

@ -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
View 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
)
'''