diff --git a/bin/project_rename.py b/bin/project_rename.py index f687a52484..6dc04069d4 100644 --- a/bin/project_rename.py +++ b/bin/project_rename.py @@ -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) diff --git a/docs/pylib/auto_link_remapper.py b/docs/pylib/auto_link_remapper.py index dd7df35958..4ea81e065e 100644 --- a/docs/pylib/auto_link_remapper.py +++ b/docs/pylib/auto_link_remapper.py @@ -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[\w -\[\]\`]+?)\]\((?P.+?)\)", re.I + re.S + re.U + re.M) + ref_regex = re.compile( + r"\[(?P[\w -\[\]\`]+?)\]\((?P.+?)\)", re.I + re.S + re.U + re.M + ) # in document references - ref_doc_regex = re.compile(r"\[(?P[\w -\`]+?)\]:\s+?(?P.+?)(?=$|\n)", re.I + re.S + re.U + re.M) + ref_doc_regex = re.compile( + r"\[(?P[\w -\`]+?)\]:\s+?(?P.+?)(?=$|\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() diff --git a/docs/pylib/build_search_index.py b/docs/pylib/build_search_index.py index 4c9497cf08..15d4d56b0a 100644 --- a/docs/pylib/build_search_index.py +++ b/docs/pylib/build_search_index.py @@ -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() diff --git a/docs/pylib/copy_from_wiki.py b/docs/pylib/copy_from_wiki.py index 1c8dc85fe3..07ab6dd8e1 100644 --- a/docs/pylib/copy_from_wiki.py +++ b/docs/pylib/copy_from_wiki.py @@ -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) diff --git a/docs/pylib/fmtwidth.py b/docs/pylib/fmtwidth.py index d6de0a7900..4c30c5a185 100644 --- a/docs/pylib/fmtwidth.py +++ b/docs/pylib/fmtwidth.py @@ -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 diff --git a/docs/source/Howto/Starting/Part3/A-Sittable-Object.md b/docs/source/Howto/Starting/Part3/A-Sittable-Object.md new file mode 100644 index 0000000000..fed953e33d --- /dev/null +++ b/docs/source/Howto/Starting/Part3/A-Sittable-Object.md @@ -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 + 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 + + """ + 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 `` \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index d05d3a8be5..3de7270eb6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -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#{1,2})\s*?(?P.*?)$", _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 diff --git a/evennia/__init__.py b/evennia/__init__.py index 20ce85e7c3..4bb82ebaf2 100644 --- a/evennia/__init__.py +++ b/evennia/__init__.py @@ -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",) + ) +) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index ef6dba043b..4045c33662 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -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) diff --git a/evennia/accounts/admin.py b/evennia/accounts/admin.py index 327d2b4eb2..a00169eb26 100644 --- a/evennia/accounts/admin.py +++ b/evennia/accounts/admin.py @@ -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, ) diff --git a/evennia/accounts/models.py b/evennia/accounts/models.py index 18f2dd9bf0..734bab0276 100644 --- a/evennia/accounts/models.py +++ b/evennia/accounts/models.py @@ -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 diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index c992d18a66..f013e79c60 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -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 diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index ca8800b3d8..87c6acd73b 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -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 diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index cfe7b8e098..12ec9acf62 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -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) diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index d2ab33fe5a..ea6e1db9b0 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -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, diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index e2c4d591ec..dda315c8e3 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -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(), "") diff --git a/evennia/contrib/tutorial_examples/mirror.py b/evennia/contrib/tutorial_examples/mirror.py index 94c0798195..6f51e1b045 100644 --- a/evennia/contrib/tutorial_examples/mirror.py +++ b/evennia/contrib/tutorial_examples/mirror.py @@ -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.") diff --git a/evennia/locks/lockhandler.py b/evennia/locks/lockhandler.py index 0c5132d796..25b2f2be45 100644 --- a/evennia/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -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: diff --git a/evennia/objects/manager.py b/evennia/objects/manager.py index a3db831149..d29ca9a739 100644 --- a/evennia/objects/manager.py +++ b/evennia/objects/manager.py @@ -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): diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 437e9c80f2..ea24b916b0 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -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): diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 223edb92a3..fe1f844896 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -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) diff --git a/evennia/server/tests/testrunner.py b/evennia/server/tests/testrunner.py index c2eb334a7d..cf940eaa22 100644 --- a/evennia/server/tests/testrunner.py +++ b/evennia/server/tests/testrunner.py @@ -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 ) - diff --git a/evennia/typeclasses/models.py b/evennia/typeclasses/models.py index ff3c15c886..833b359c69 100644 --- a/evennia/typeclasses/models.py +++ b/evennia/typeclasses/models.py @@ -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 diff --git a/evennia/typeclasses/tests.py b/evennia/typeclasses/tests.py index 6d14f97c8e..c6986541f0 100644 --- a/evennia/typeclasses/tests.py +++ b/evennia/typeclasses/tests.py @@ -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) diff --git a/evennia/utils/create.py b/evennia/utils/create.py index 1853f91a5f..8f773b497f 100644 --- a/evennia/utils/create.py +++ b/evennia/utils/create.py @@ -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 diff --git a/evennia/utils/dbserialize.py b/evennia/utils/dbserialize.py index 7b514a2c08..b89b4f30b5 100644 --- a/evennia/utils/dbserialize.py +++ b/evennia/utils/dbserialize.py @@ -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)) diff --git a/evennia/utils/inlinefuncs.py b/evennia/utils/inlinefuncs.py index 84b45aa0eb..8acf9b3812 100644 --- a/evennia/utils/inlinefuncs.py +++ b/evennia/utils/inlinefuncs.py @@ -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 # diff --git a/evennia/utils/picklefield.py b/evennia/utils/picklefield.py index e76825086c..bc1dad7b69 100644 --- a/evennia/utils/picklefield.py +++ b/evennia/utils/picklefield.py @@ -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 diff --git a/evennia/utils/tests/test_create_functions.py b/evennia/utils/tests/test_create_functions.py index f996880af3..392d62a9ee 100644 --- a/evennia/utils/tests/test_create_functions.py +++ b/evennia/utils/tests/test_create_functions.py @@ -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()) diff --git a/evennia/utils/tests/test_dbserialize.py b/evennia/utils/tests/test_dbserialize.py index 9d00abfae4..b9c2cc3d9b 100644 --- a/evennia/utils/tests/test_dbserialize.py +++ b/evennia/utils/tests/test_dbserialize.py @@ -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}]) diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 79a32276eb..175e7b11a3 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -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 # ------------------------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..f68f4353a7 --- /dev/null +++ b/pyproject.toml @@ -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 + + ) +'''