diff --git a/.gitignore b/.gitignore index a872de84a7..c69bb5c293 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,10 @@ docs/build # Obsidian .obsidian + +# Virtual environments +.venv/ +.env/ + +# Testing folder +.test_game_dir diff --git a/Makefile b/Makefile index b8b4857155..d0e8c86a8c 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ TESTS ?= evennia default: @echo " Usage: " @echo " make install - install evennia (recommended to activate virtualenv first)" + @echo " make installextra - install evennia with extra-requirements (activate virtualenv first)" @echo " make fmt/format - run the black autoformatter on the source code" @echo " make lint - run black in --check mode" @echo " make test - run evennia test suite with all default values." @@ -17,6 +18,10 @@ default: install: pip install -e . +installextra: + pip install -e . + pip install -r requirements_extra.txt + format: black $(BLACK_FORMAT_CONFIGS) evennia diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index c0002987ac..f8030108df 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -748,7 +748,9 @@ def cmdhandler( ) if suggestions: sysarg += _(" Maybe you meant {command}?").format( - command=utils.list_to_string(suggestions, endsep=_("or"), addquote=True) + command=utils.list_to_string( + suggestions, endsep=_("or"), addquote=True + ) ) else: sysarg += _(' Type "help" for help.') diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index 9754d069fa..6a5489b5e5 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -668,7 +668,9 @@ class CmdOption(COMMAND_DEFAULT_CLASS): self.msg(f"Option |w{new_name}|n was kept as '|w{old_val}|n'.") else: flags[new_name] = new_val - self.msg(f"Option |w{new_name}|n was changed from '|w{old_val}|n' to '|w{new_val}|n'.") + self.msg( + f"Option |w{new_name}|n was changed from '|w{old_val}|n' to '|w{new_val}|n'." + ) return {new_name: new_val} except Exception as err: self.msg(f"|rCould not set option |w{new_name}|r:|n {err}") @@ -759,7 +761,9 @@ class CmdPassword(COMMAND_DEFAULT_CLASS): account.set_password(newpass) account.save() self.msg("Password changed.") - logger.log_sec(f"Password Changed: {account} (Caller: {account}, IP: {self.session.address}).") + logger.log_sec( + f"Password Changed: {account} (Caller: {account}, IP: {self.session.address})." + ) class CmdQuit(COMMAND_DEFAULT_CLASS): diff --git a/evennia/commands/default/admin.py b/evennia/commands/default/admin.py index 987722cbb2..4b5606a9a0 100644 --- a/evennia/commands/default/admin.py +++ b/evennia/commands/default/admin.py @@ -480,7 +480,7 @@ class CmdPerm(COMMAND_DEFAULT_CLASS): # we supplied an argument on the form obj = perm locktype = "edit" if accountmode else "control" if not obj.access(caller, locktype): - accountstr = 'account' if accountmode else 'object' + accountstr = "account" if accountmode else "object" caller.msg(f"You are not allowed to edit this {accountstr}'s permissions.") return @@ -521,9 +521,7 @@ class CmdPerm(COMMAND_DEFAULT_CLASS): return if perm in permissions: - caller_result.append( - f"\nPermission '{perm}' is already defined on {obj.name}." - ) + caller_result.append(f"\nPermission '{perm}' is already defined on {obj.name}.") else: obj.permissions.add(perm) plystring = "the Account" if accountmode else "the Object/Character" diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index 35367820f3..ff9d3fbe45 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -1392,7 +1392,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS): header = f"|wAccount|n |c{caller.key}|n |wpages:|n" if message.startswith(":"): - message = f"{caller.key} {message.strip(':').strip()}" + message = f"{caller.key} {message.strip(':').strip()}" # create the persistent message object create.create_message(caller, message, receivers=targets) @@ -1565,7 +1565,7 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS): return if "disconnect" in self.switches or "remove" in self.switches or "delete" in self.switches: - botname = f"ircbot-{self.lhs}" + botname = f"ircbot-{self.lhs}" matches = AccountDB.objects.filter(db_is_bot=True, username=botname) dbref = utils.dbref(self.lhs) if not matches and dbref: @@ -1870,7 +1870,7 @@ class CmdGrapevine2Chan(COMMAND_DEFAULT_CLASS): return if "disconnect" in self.switches or "remove" in self.switches or "delete" in self.switches: - botname = f"grapevinebot-{self.lhs}" + botname = f"grapevinebot-{self.lhs}" matches = AccountDB.objects.filter(db_is_bot=True, db_key=botname) if not matches: diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 46138564d3..2c5e58fa01 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -221,7 +221,8 @@ class CmdNick(COMMAND_DEFAULT_CLASS): _, _, old_nickstring, old_replstring = oldnick.value caller.nicks.remove(old_nickstring, category=nicktype) caller.msg( - f"{nicktypestr} removed: '|w{old_nickstring}|n' -> |w{old_replstring}|n.") + f"{nicktypestr} removed: '|w{old_nickstring}|n' -> |w{old_replstring}|n." + ) else: caller.msg("No matching nicks to remove.") return @@ -242,9 +243,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS): for nick in nicks: _, _, nick, repl = nick.value if nick.startswith(self.lhs): - strings.append( - f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'" - ) + strings.append(f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'") if strings: caller.msg("\n".join(strings)) else: @@ -265,9 +264,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS): for nick in nicks: _, _, nick, repl = nick.value if nick.startswith(self.lhs): - strings.append( - f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'" - ) + strings.append(f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'") if strings: caller.msg("\n".join(strings)) else: @@ -288,9 +285,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS): for nick in nicks: _, _, nick, repl = nick.value if nick.startswith(self.lhs): - strings.append( - f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'" - ) + strings.append(f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'") if strings: caller.msg("\n".join(strings)) else: @@ -724,4 +719,4 @@ class CmdAccess(COMMAND_DEFAULT_CLASS): string += f"\nCharacter |c{caller.key}|n: {cperms}" if hasattr(caller, "account"): string += f"\nAccount |c{caller.account.key}|n: {pperms}" - caller.msg(string) \ No newline at end of file + caller.msg(string) diff --git a/evennia/contrib/game_systems/crafting/crafting.py b/evennia/contrib/game_systems/crafting/crafting.py index 95f0bb09fb..0b383854b3 100644 --- a/evennia/contrib/game_systems/crafting/crafting.py +++ b/evennia/contrib/game_systems/crafting/crafting.py @@ -343,6 +343,7 @@ class CraftingRecipeBase: class NonExistentRecipe(CraftingRecipeBase): """A recipe that does not exist and never produces anything.""" + allow_craft = True allow_reuse = True diff --git a/evennia/contrib/grid/ingame_map_display/ingame_map_display.py b/evennia/contrib/grid/ingame_map_display/ingame_map_display.py index b3ef013b5f..e8f5158080 100644 --- a/evennia/contrib/grid/ingame_map_display/ingame_map_display.py +++ b/evennia/contrib/grid/ingame_map_display/ingame_map_display.py @@ -61,21 +61,21 @@ from django.conf import settings from evennia import CmdSet from evennia.commands.default.muxcommand import MuxCommand -_BASIC_MAP_SIZE = settings.BASIC_MAP_SIZE if hasattr(settings, 'BASIC_MAP_SIZE') else 2 -_MAX_MAP_SIZE = settings.BASIC_MAP_SIZE if hasattr(settings, 'MAX_MAP_SIZE') else 10 +_BASIC_MAP_SIZE = settings.BASIC_MAP_SIZE if hasattr(settings, "BASIC_MAP_SIZE") else 2 +_MAX_MAP_SIZE = settings.BASIC_MAP_SIZE if hasattr(settings, "MAX_MAP_SIZE") else 10 # _COMPASS_DIRECTIONS specifies which way to move the pointer on the x/y axes and what characters to use to depict the exits on the map. _COMPASS_DIRECTIONS = { - 'north': (0, -3, ' | '), - 'south': (0, 3, ' | '), - 'east': (3, 0, '-'), - 'west': (-3, 0, '-'), - 'northeast': (3, -3, '/'), - 'northwest': (-3, -3, '\\'), - 'southeast': (3, 3, '\\'), - 'southwest': (-3, 3, '/'), - 'up': (0, 0, '^'), - 'down': (0, 0, 'v') + "north": (0, -3, " | "), + "south": (0, 3, " | "), + "east": (3, 0, "-"), + "west": (-3, 0, "-"), + "northeast": (3, -3, "/"), + "northwest": (-3, -3, "\\"), + "southeast": (3, 3, "\\"), + "southwest": (-3, 3, "/"), + "up": (0, 0, "^"), + "down": (0, 0, "v"), } @@ -91,7 +91,7 @@ class Map(object): """ self.start_time = time.time() self.caller = caller - self.max_width = int(size * 2 + 1) * 5 # This must be an odd number + self.max_width = int(size * 2 + 1) * 5 # This must be an odd number self.max_length = int(size * 2 + 1) * 3 # This must be an odd number self.has_mapped = {} self.curX = None @@ -109,8 +109,8 @@ class Map(object): board = [] for row in range(self.max_length): board.append([]) - for column in range(int(self.max_width/5)): - board[row].extend([' ', ' ', ' ']) + for column in range(int(self.max_width / 5)): + board[row].extend([" ", " ", " "]) return board def exit_name_as_ordinal(self, ex): @@ -124,11 +124,13 @@ class Map(object): """ exit_name = ex.name if exit_name not in _COMPASS_DIRECTIONS: - compass_aliases = [direction in ex.aliases.all() for direction in _COMPASS_DIRECTIONS.keys()] + compass_aliases = [ + direction in ex.aliases.all() for direction in _COMPASS_DIRECTIONS.keys() + ] if compass_aliases[0]: exit_name = compass_aliases[0] if exit_name not in _COMPASS_DIRECTIONS: - return '' + return "" return exit_name def update_pos(self, room, exit_name): @@ -179,7 +181,7 @@ class Map(object): # Additionally, if the name of the exit is not ordinal but an alias of it is, use that. for ex in [x for x in room.exits if x.access(self.caller, "traverse")]: ex_name = self.exit_name_as_ordinal(ex) - if not ex_name or ex_name in ['up', 'down']: + if not ex_name or ex_name in ["up", "down"]: continue if self.has_drawn(ex.destination): continue @@ -201,20 +203,20 @@ class Map(object): continue ex_character = _COMPASS_DIRECTIONS[ex_name][2] - delta_x = int(_COMPASS_DIRECTIONS[ex_name][1]/3) - delta_y = int(_COMPASS_DIRECTIONS[ex_name][0]/3) + delta_x = int(_COMPASS_DIRECTIONS[ex_name][1] / 3) + delta_y = int(_COMPASS_DIRECTIONS[ex_name][0] / 3) # Make modifications if the exit has BOTH up and down exits - if ex_name == 'up': - if 'v' in self.grid[x][y]: - self.render_room(room, x, y, p1='^', p2='v') + if ex_name == "up": + if "v" in self.grid[x][y]: + self.render_room(room, x, y, p1="^", p2="v") else: - self.render_room(room, x, y, here='^') - elif ex_name == 'down': - if '^' in self.grid[x][y]: - self.render_room(room, x, y, p1='^', p2='v') + self.render_room(room, x, y, here="^") + elif ex_name == "down": + if "^" in self.grid[x][y]: + self.render_room(room, x, y, p1="^", p2="v") else: - self.render_room(room, x, y, here='v') + self.render_room(room, x, y, here="v") else: self.grid[x + delta_x][y + delta_y] = ex_character @@ -234,7 +236,7 @@ class Map(object): self.has_mapped[room] = [self.curX, self.curY] self.render_room(room, self.curX, self.curY) - def render_room(self, room, x, y, p1='[', p2=']', here=None): + def render_room(self, room, x, y, p1="[", p2="]", here=None): """ Draw a given room with ascii characters @@ -253,7 +255,7 @@ class Map(object): you[0] = f"{p1}|n" you[1] = f"{here if here else you[1]}" if room == self.caller.location: - you[1] = '|[x|co|n' # Highlight the location you are currently in + you[1] = "|[x|co|n" # Highlight the location you are currently in you[2] = f"{p2}|n" self.grid[x][y] = "".join(you) @@ -300,6 +302,7 @@ class CmdMap(MuxCommand): Usage: map (optional size) """ + key = "map" def func(self): diff --git a/evennia/contrib/grid/ingame_map_display/tests.py b/evennia/contrib/grid/ingame_map_display/tests.py index 9d505575c5..555629f503 100644 --- a/evennia/contrib/grid/ingame_map_display/tests.py +++ b/evennia/contrib/grid/ingame_map_display/tests.py @@ -17,19 +17,32 @@ class TestIngameMap(BaseEvenniaCommandTest): Expected output: [ ]--[ ] """ + def setUp(self): super().setUp() self.west_room = create_object(rooms.Room, key="Room 1") self.east_room = create_object(rooms.Room, key="Room 2") - create_object(exits.Exit, key="east", aliases=["e"], location=self.west_room, destination=self.east_room) - create_object(exits.Exit, key="west", aliases=["w"], location=self.east_room, destination=self.west_room) + create_object( + exits.Exit, + key="east", + aliases=["e"], + location=self.west_room, + destination=self.east_room, + ) + create_object( + exits.Exit, + key="west", + aliases=["w"], + location=self.east_room, + destination=self.west_room, + ) def test_west_room_map_room(self): self.char1.location = self.west_room map_here = ingame_map_display.Map(self.char1).show_map() - self.assertEqual(map_here.strip(), '[|n|[x|co|n]|n--[|n ]|n') + self.assertEqual(map_here.strip(), "[|n|[x|co|n]|n--[|n ]|n") def test_east_room_map_room(self): self.char1.location = self.east_room map_here = ingame_map_display.Map(self.char1).show_map() - self.assertEqual(map_here.strip(), '[|n ]|n--[|n|[x|co|n]|n') \ No newline at end of file + self.assertEqual(map_here.strip(), "[|n ]|n--[|n|[x|co|n]|n") diff --git a/evennia/contrib/grid/xyzgrid/tests.py b/evennia/contrib/grid/xyzgrid/tests.py index 8abda9c0fa..f0a6030a45 100644 --- a/evennia/contrib/grid/xyzgrid/tests.py +++ b/evennia/contrib/grid/xyzgrid/tests.py @@ -1421,16 +1421,19 @@ class TestBuildExampleGrid(BaseEvenniaTest): mock_room_callbacks = mock.MagicMock() mock_exit_callbacks = mock.MagicMock() + class TestXyzRoom(xyzroom.XYZRoom): - def at_object_creation(self): - mock_room_callbacks.at_object_creation() + def at_object_creation(self): + mock_room_callbacks.at_object_creation() + class TestXyzExit(xyzroom.XYZExit): - def at_object_creation(self): - mock_exit_callbacks.at_object_creation() + def at_object_creation(self): + mock_exit_callbacks.at_object_creation() + MAP_DATA = { - "map": """ + "map": """ + 0 1 @@ -1439,35 +1442,37 @@ MAP_DATA = { + 0 1 """, - "zcoord": "map1", - "prototypes": { - ("*", "*"): { - "key": "room", - "desc": "A room.", - "prototype_parent": "xyz_room", - }, - ("*", "*", "*"): { - "desc": "A passage.", - "prototype_parent": "xyz_exit", - } - }, - "options": { - "map_visual_range": 1, - "map_mode": "scan", - } + "zcoord": "map1", + "prototypes": { + ("*", "*"): { + "key": "room", + "desc": "A room.", + "prototype_parent": "xyz_room", + }, + ("*", "*", "*"): { + "desc": "A passage.", + "prototype_parent": "xyz_exit", + }, + }, + "options": { + "map_visual_range": 1, + "map_mode": "scan", + }, } + class TestCallbacks(BaseEvenniaTest): def setUp(self): super().setUp() mock_room_callbacks.reset_mock() mock_exit_callbacks.reset_mock() - + def setup_grid(self, map_data): self.grid, err = xyzgrid.XYZGrid.create("testgrid") def _log(msg): - print(msg) + print(msg) + self.grid.log = _log self.map_data = map_data @@ -1489,5 +1494,9 @@ class TestCallbacks(BaseEvenniaTest): self.grid.spawn() # Two rooms and 2 exits, Each one should have gotten one `at_object_creation` callback. - self.assertEqual(mock_room_callbacks.at_object_creation.mock_calls, [mock.call(), mock.call()]) - self.assertEqual(mock_exit_callbacks.at_object_creation.mock_calls, [mock.call(), mock.call()]) + self.assertEqual( + mock_room_callbacks.at_object_creation.mock_calls, [mock.call(), mock.call()] + ) + self.assertEqual( + mock_exit_callbacks.at_object_creation.mock_calls, [mock.call(), mock.call()] + ) diff --git a/evennia/contrib/grid/xyzgrid/xymap_legend.py b/evennia/contrib/grid/xyzgrid/xymap_legend.py index 21f820f0c4..99d158d9ff 100644 --- a/evennia/contrib/grid/xyzgrid/xymap_legend.py +++ b/evennia/contrib/grid/xyzgrid/xymap_legend.py @@ -321,7 +321,9 @@ class MapNode: # with proper coordinates etc typeclass = self.prototype.get("typeclass") if typeclass is None: - raise MapError(f"The prototype {self.prototype} for this node has no 'typeclass' key.", self) + raise MapError( + f"The prototype {self.prototype} for this node has no 'typeclass' key.", self + ) self.log(f" spawning room at xyz={xyz} ({typeclass})") Typeclass = class_from_module(typeclass) nodeobj, err = Typeclass.create(self.prototype.get("key", "An empty room"), xyz=xyz) @@ -405,7 +407,10 @@ class MapNode: prot = maplinks[key.lower()][3].prototype typeclass = prot.get("typeclass") if typeclass is None: - raise MapError(f"The prototype {self.prototype} for this node has no 'typeclass' key.", self) + raise MapError( + f"The prototype {self.prototype} for this node has no 'typeclass' key.", + self, + ) self.log(f" spawning/updating exit xyz={xyz}, direction={key} ({typeclass})") Typeclass = class_from_module(typeclass) diff --git a/evennia/contrib/utils/git_integration/git_integration.py b/evennia/contrib/utils/git_integration/git_integration.py index 17bdd0abd2..770837e586 100644 --- a/evennia/contrib/utils/git_integration/git_integration.py +++ b/evennia/contrib/utils/git_integration/git_integration.py @@ -7,6 +7,7 @@ from evennia.server.sessionhandler import SESSIONS import git import datetime + class GitCommand(MuxCommand): """ The shared functionality between git/git evennia @@ -17,31 +18,35 @@ class GitCommand(MuxCommand): Parse the arguments, set default arg to 'status' and check for existence of currently targeted repo """ super().parse() - + if self.args: split_args = self.args.strip().split(" ", 1) self.action = split_args[0] if len(split_args) > 1: - self.args = ''.join(split_args[1:]) + self.args = "".join(split_args[1:]) else: - self.args = '' + self.args = "" else: self.action = "status" self.args = "" - - self.err_msgs = ["|rInvalid Git Repository|n:", + + self.err_msgs = [ + "|rInvalid Git Repository|n:", "The {repo_type} repository is not recognized as a git directory.", "In order to initialize it as a git directory, you will need to access your terminal and run the following commands from within your directory:", " git init", - " git remote add origin {remote_link}"] - + " git remote add origin {remote_link}", + ] + try: self.repo = git.Repo(self.directory, search_parent_directories=True) except git.exc.InvalidGitRepositoryError: - err_msg = '\n'.join(self.err_msgs).format(repo_type=self.repo_type, remote_link=self.remote_link) + err_msg = "\n".join(self.err_msgs).format( + repo_type=self.repo_type, remote_link=self.remote_link + ) self.caller.msg(err_msg) raise InterruptCommand - + self.commit = self.repo.head.commit try: @@ -56,16 +61,20 @@ class GitCommand(MuxCommand): """ short_sha = repo.git.rev_parse(hexsha, short=True) return short_sha - + def get_status(self): """ Retrieves the status of the active git repository, displaying unstaged changes/untracked files. """ time_of_commit = datetime.datetime.fromtimestamp(self.commit.committed_date) - status_msg = '\n'.join([f"Branch: |w{self.branch}|n ({self.repo.git.rev_parse(self.commit.hexsha, short=True)}) ({time_of_commit})", - f"By {self.commit.author.email}: {self.commit.message}"]) + status_msg = "\n".join( + [ + f"Branch: |w{self.branch}|n ({self.repo.git.rev_parse(self.commit.hexsha, short=True)}) ({time_of_commit})", + f"By {self.commit.author.email}: {self.commit.message}", + ] + ) - changedFiles = { item.a_path for item in self.repo.index.diff(None) } + changedFiles = {item.a_path for item in self.repo.index.diff(None)} if changedFiles: status_msg += f"Unstaged/uncommitted changes:|/ |g{'|/ '.join(changedFiles)}|n|/" if len(self.repo.untracked_files) > 0: @@ -77,7 +86,9 @@ class GitCommand(MuxCommand): Display current and available branches. """ remote_refs = self.repo.remote().refs - branch_msg = f"Current branch: |w{self.branch}|n. Branches available: {list_to_string(remote_refs)}" + branch_msg = ( + f"Current branch: |w{self.branch}|n. Branches available: {list_to_string(remote_refs)}" + ) return branch_msg def checkout(self): @@ -85,7 +96,9 @@ class GitCommand(MuxCommand): Check out a specific branch. """ remote_refs = self.repo.remote().refs - to_branch = self.args.strip().removeprefix('origin/') # Slightly hacky, but git tacks on the origin/ + to_branch = self.args.strip().removeprefix( + "origin/" + ) # Slightly hacky, but git tacks on the origin/ if to_branch not in remote_refs: self.caller.msg(f"Branch '{to_branch}' not available.") @@ -101,7 +114,7 @@ class GitCommand(MuxCommand): return False self.msg(f"Checked out |w{to_branch}|n successfully. Server restart initiated.") return True - + def pull(self): """ Attempt to pull new code. @@ -116,7 +129,9 @@ class GitCommand(MuxCommand): self.caller.msg("No new code to pull, no need to reset.\n") return False else: - self.caller.msg(f"You have pulled new code. Server restart initiated.|/Head now at {self.repo.git.rev_parse(self.repo.head.commit.hexsha, short=True)}.|/Author: {self.repo.head.commit.author.name} ({self.repo.head.commit.author.email})|/{self.repo.head.commit.message.strip()}") + self.caller.msg( + f"You have pulled new code. Server restart initiated.|/Head now at {self.repo.git.rev_parse(self.repo.head.commit.hexsha, short=True)}.|/Author: {self.repo.head.commit.author.name} ({self.repo.head.commit.author.email})|/{self.repo.head.commit.message.strip()}" + ) return True def func(self): @@ -139,18 +154,19 @@ class GitCommand(MuxCommand): caller.msg("You can only git status, git branch, git checkout, or git pull.") return + class CmdGitEvennia(GitCommand): """ Pull the latest code from the evennia core or checkout a different branch. - + Usage: git evennia status - View an overview of the evennia repository status. git evennia branch - View available branches in evennia. git evennia checkout - Checkout a different branch in evennia. git evennia pull - Pull the latest evennia code. - + For updating your local mygame repository, the same commands are available with 'git'. - + If there are any conflicts encountered, the command will abort. The command will reload your game after pulling new code automatically, but for some changes involving persistent scripts etc, you may need to manually restart. """ @@ -173,7 +189,7 @@ class CmdGit(GitCommand): git pull - Pull the latest code from your current branch. For updating evennia code, the same commands are available with 'git evennia'. - + If there are any conflicts encountered, the command will abort. The command will reload your game after pulling new code automatically, but for changes involving persistent scripts etc, you may need to manually restart. """ diff --git a/evennia/contrib/utils/git_integration/tests.py b/evennia/contrib/utils/git_integration/tests.py index c454373a92..5e99af6b0e 100644 --- a/evennia/contrib/utils/git_integration/tests.py +++ b/evennia/contrib/utils/git_integration/tests.py @@ -11,6 +11,7 @@ import git import mock import datetime + class TestGitIntegration(EvenniaTest): @mock.patch("git.Repo") @mock.patch("git.Git") @@ -45,11 +46,15 @@ class TestGitIntegration(EvenniaTest): test_cmd_git.caller = self.char1 test_cmd_git.args = "nonexistent_branch" self.test_cmd_git = test_cmd_git - + def test_git_status(self): time_of_commit = datetime.datetime.fromtimestamp(self.test_cmd_git.commit.committed_date) - status_msg = '\n'.join([f"Branch: |w{self.test_cmd_git.branch}|n ({self.test_cmd_git.repo.git.rev_parse(self.test_cmd_git.commit.hexsha, short=True)}) ({time_of_commit})", - f"By {self.test_cmd_git.commit.author.email}: {self.test_cmd_git.commit.message}"]) + status_msg = "\n".join( + [ + f"Branch: |w{self.test_cmd_git.branch}|n ({self.test_cmd_git.repo.git.rev_parse(self.test_cmd_git.commit.hexsha, short=True)}) ({time_of_commit})", + f"By {self.test_cmd_git.commit.author.email}: {self.test_cmd_git.commit.message}", + ] + ) self.assertEqual(status_msg, self.test_cmd_git.get_status()) def test_git_branch(self): @@ -62,8 +67,9 @@ class TestGitIntegration(EvenniaTest): # Checkout no branch self.test_cmd_git.checkout() self.char1.msg.assert_called_with("Branch 'nonexistent_branch' not available.") - + def test_git_pull(self): self.test_cmd_git.pull() - self.char1.msg.assert_called_with(f"You have pulled new code. Server restart initiated.|/Head now at {self.repo.git.rev_parse(self.repo.head.commit.hexsha, short=True)}.|/Author: {self.repo.head.commit.author.name} ({self.repo.head.commit.author.email})|/{self.repo.head.commit.message.strip()}") - \ No newline at end of file + self.char1.msg.assert_called_with( + f"You have pulled new code. Server restart initiated.|/Head now at {self.repo.git.rev_parse(self.repo.head.commit.hexsha, short=True)}.|/Author: {self.repo.head.commit.author.name} ({self.repo.head.commit.author.email})|/{self.repo.head.commit.message.strip()}" + ) diff --git a/evennia/server/server.py b/evennia/server/server.py index f7df084986..13411a4c90 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -98,18 +98,17 @@ except ImportError: "copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf." ) - # Maintenance function - this is called repeatedly by the server +_IDMAPPER_CACHE_MAXSIZE = settings.IDMAPPER_CACHE_MAXSIZE +_IDLE_TIMEOUT = settings.IDLE_TIMEOUT +_LAST_SERVER_TIME_SNAPSHOT = 0 + _MAINTENANCE_COUNT = 0 _FLUSH_CACHE = None -_IDMAPPER_CACHE_MAXSIZE = settings.IDMAPPER_CACHE_MAXSIZE _GAMETIME_MODULE = None _DEFAULTOBJECT = None -_IDLE_TIMEOUT = settings.IDLE_TIMEOUT -_LAST_SERVER_TIME_SNAPSHOT = 0 - def _server_maintenance(): """ @@ -120,9 +119,11 @@ def _server_maintenance(): global _LAST_SERVER_TIME_SNAPSHOT global _OBJECTDB - if not _FLUSH_CACHE: + if not _OBJECTDB: from evennia.objects.models import ObjectDB as _OBJECTDB + if not _GAMETIME_MODULE: from evennia.utils import gametime as _GAMETIME_MODULE + if not _FLUSH_CACHE: from evennia.utils.idmapper.models import conditional_flush as _FLUSH_CACHE _MAINTENANCE_COUNT += 1 diff --git a/evennia/utils/containers.py b/evennia/utils/containers.py index d0ed248dd1..331f026610 100644 --- a/evennia/utils/containers.py +++ b/evennia/utils/containers.py @@ -20,6 +20,7 @@ from evennia.utils.utils import callables_from_module, class_from_module SCRIPTDB = None + class Container: """ Base container class. A container is simply a storage object whose @@ -203,7 +204,9 @@ class GlobalScriptContainer(Container): self.typeclass_storage = {} for key, data in list(self.loaded_data.items()): typeclass = data.get("typeclass", settings.BASE_SCRIPT_TYPECLASS) - self.typeclass_storage[key] = class_from_module(typeclass, fallback=settings.BASE_SCRIPT_TYPECLASS) + self.typeclass_storage[key] = class_from_module( + typeclass, fallback=settings.BASE_SCRIPT_TYPECLASS + ) def get(self, key, default=None): """ diff --git a/evennia/utils/funcparser.py b/evennia/utils/funcparser.py index 911b6c39d1..086cfd2bd9 100644 --- a/evennia/utils/funcparser.py +++ b/evennia/utils/funcparser.py @@ -352,7 +352,7 @@ class FuncParser: if curr_func: # we are starting a nested funcdef - if len(callstack) > _MAX_NESTING: + if len(callstack) >= _MAX_NESTING - 1: # stack full - ignore this function if raise_errors: raise ParsingError( diff --git a/evennia/utils/tests/data/broken_script.py b/evennia/utils/tests/data/broken_script.py index 76d3e7a4c7..f27483c5f0 100644 --- a/evennia/utils/tests/data/broken_script.py +++ b/evennia/utils/tests/data/broken_script.py @@ -6,5 +6,6 @@ dependencies. from evennia import nonexistent_module, DefaultScript + class BrokenScript(DefaultScript): pass diff --git a/evennia/utils/tests/test_containers.py b/evennia/utils/tests/test_containers.py index 91cba86179..27f9ba5090 100644 --- a/evennia/utils/tests/test_containers.py +++ b/evennia/utils/tests/test_containers.py @@ -8,14 +8,16 @@ from evennia import DefaultScript _BASE_TYPECLASS = class_from_module(settings.BASE_SCRIPT_TYPECLASS) + class GoodScript(DefaultScript): pass + class InvalidScript: pass -class TestGlobalScriptContainer(unittest.TestCase): +class TestGlobalScriptContainer(unittest.TestCase): def test_init_with_no_scripts(self): gsc = containers.GlobalScriptContainer() @@ -29,7 +31,7 @@ class TestGlobalScriptContainer(unittest.TestCase): self.assertEqual(len(gsc.typeclass_storage), 0) - @override_settings(GLOBAL_SCRIPTS={'script_name': {}}) + @override_settings(GLOBAL_SCRIPTS={"script_name": {}}) def test_start_with_typeclassless_script(self): """No specified typeclass should fallback to base""" gsc = containers.GlobalScriptContainer() @@ -37,10 +39,14 @@ class TestGlobalScriptContainer(unittest.TestCase): gsc.start() self.assertEqual(len(gsc.typeclass_storage), 1) - self.assertIn('script_name', gsc.typeclass_storage) - self.assertEqual(gsc.typeclass_storage['script_name'], _BASE_TYPECLASS) + self.assertIn("script_name", gsc.typeclass_storage) + self.assertEqual(gsc.typeclass_storage["script_name"], _BASE_TYPECLASS) - @override_settings(GLOBAL_SCRIPTS={'script_name': {'typeclass': 'evennia.utils.tests.test_containers.NoScript'}}) + @override_settings( + GLOBAL_SCRIPTS={ + "script_name": {"typeclass": "evennia.utils.tests.test_containers.NoScript"} + } + ) def test_start_with_nonexistent_script(self): """Missing script class should fall back to base""" gsc = containers.GlobalScriptContainer() @@ -48,35 +54,53 @@ class TestGlobalScriptContainer(unittest.TestCase): gsc.start() self.assertEqual(len(gsc.typeclass_storage), 1) - self.assertIn('script_name', gsc.typeclass_storage) - self.assertEqual(gsc.typeclass_storage['script_name'], _BASE_TYPECLASS) + self.assertIn("script_name", gsc.typeclass_storage) + self.assertEqual(gsc.typeclass_storage["script_name"], _BASE_TYPECLASS) - @override_settings(GLOBAL_SCRIPTS={'script_name': {'typeclass': 'evennia.utils.tests.test_containers.GoodScript'}}) + @override_settings( + GLOBAL_SCRIPTS={ + "script_name": {"typeclass": "evennia.utils.tests.test_containers.GoodScript"} + } + ) def test_start_with_valid_script(self): gsc = containers.GlobalScriptContainer() gsc.start() self.assertEqual(len(gsc.typeclass_storage), 1) - self.assertIn('script_name', gsc.typeclass_storage) - self.assertEqual(gsc.typeclass_storage['script_name'], GoodScript) + self.assertIn("script_name", gsc.typeclass_storage) + self.assertEqual(gsc.typeclass_storage["script_name"], GoodScript) - @override_settings(GLOBAL_SCRIPTS={'script_name': {'typeclass': 'evennia.utils.tests.test_containers.InvalidScript'}}) + @override_settings( + GLOBAL_SCRIPTS={ + "script_name": {"typeclass": "evennia.utils.tests.test_containers.InvalidScript"} + } + ) def test_start_with_invalid_script(self): """Script class doesn't implement required methods methods""" gsc = containers.GlobalScriptContainer() - with self.assertRaises(AttributeError) as err: + with self.assertRaises(AttributeError) as err: gsc.start() # check for general attribute failure on the invalid class to preserve against future code-rder changes - self.assertTrue(str(err.exception).startswith("type object 'InvalidScript' has no attribute"), err.exception) + self.assertTrue( + str(err.exception).startswith("type object 'InvalidScript' has no attribute"), + err.exception, + ) - @override_settings(GLOBAL_SCRIPTS={'script_name': {'typeclass': 'evennia.utils.tests.data.broken_script.BrokenScript'}}) + @override_settings( + GLOBAL_SCRIPTS={ + "script_name": {"typeclass": "evennia.utils.tests.data.broken_script.BrokenScript"} + } + ) def test_start_with_broken_script(self): """Un-importable script should traceback""" gsc = containers.GlobalScriptContainer() - with self.assertRaises(Exception) as err: + with self.assertRaises(Exception) as err: gsc.start() # exception raised by imported module - self.assertTrue(str(err.exception).startswith("cannot import name 'nonexistent_module' from 'evennia'"), err.exception) + self.assertTrue( + str(err.exception).startswith("cannot import name 'nonexistent_module' from 'evennia'"), + err.exception, + ) diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index 35d7be155f..c1acf3ed73 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -252,25 +252,37 @@ class TestFuncParser(TestCase): with self.assertRaises(funcparser.ParsingError): self.parser.parse(unparseable, raise_errors=True) - @patch("evennia.utils.funcparser._MAX_NESTING", 2) - def test_parse_max_nesting(self): + @parameterized.expand( + [ + # max_nest, cause error for 4 nested funcs? + (0, False), + (1, False), + (2, False), + (3, False), + (4, True), + (5, True), + (6, True), + ] + ) + def test_parse_max_nesting(self, max_nest, ok): """ - Make sure it is an error if the max nesting value is reached. + Make sure it is an error if the max nesting value is reached. We test + four nested functions against differnt MAX_NESTING values. TODO: Does this make sense? When it sees the first function, len(callstack) is 0. It doesn't raise until the stack length is greater than the _MAX_NESTING value, which means you can nest 4 values with a value of 2, as demonstrated by this test. """ - string = "$add(1, $add(1, $add(1, $toint(42))))" - ret = self.parser.parse(string) + string = "$add(1, $add(1, $add(1, $eval(42))))" - # TODO: Does this return value actually make sense? - # It removed the spaces from the calls. - self.assertEqual("$add(1,$add(1,$add(1,$toint(42))))", ret) - - with self.assertRaises(funcparser.ParsingError): - self.parser.parse(string, raise_errors=True) + with patch("evennia.utils.funcparser._MAX_NESTING", max_nest): + if ok: + ret = self.parser.parse(string, raise_errors=True) + self.assertEqual(ret, "45") + else: + with self.assertRaises(funcparser.ParsingError): + self.parser.parse(string, raise_errors=True) def test_parse_underlying_exception(self): string = "test $add(1, 1) $raise()" diff --git a/evennia/utils/tests/test_tagparsing.py b/evennia/utils/tests/test_tagparsing.py index 76cc9dd801..5110604cc0 100644 --- a/evennia/utils/tests/test_tagparsing.py +++ b/evennia/utils/tests/test_tagparsing.py @@ -353,12 +353,9 @@ class TestTextToHTMLparser(TestCase): def test_non_url_with_www(self): self.assertEqual( - self.parser.convert_urls('Awwww.this should not be highlighted'), - 'Awwww.this should not be highlighted' + self.parser.convert_urls("Awwww.this should not be highlighted"), + "Awwww.this should not be highlighted", ) def test_invalid_www_url(self): - self.assertEqual( - self.parser.convert_urls('www.t'), - 'www.t' - ) + self.assertEqual(self.parser.convert_urls("www.t"), "www.t") diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 40007547b7..50339104b3 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -721,10 +721,10 @@ class TestIntConversions(TestCase): # basic mapped numbers self.assertEqual(3, utils.str2int("three")) self.assertEqual(20, utils.str2int("twenty")) - + # multi-place numbers self.assertEqual(2345, utils.str2int("two thousand, three hundred and forty-five")) - + # ordinal numbers self.assertEqual(1, utils.str2int("1st")) self.assertEqual(1, utils.str2int("first")) @@ -734,4 +734,4 @@ class TestIntConversions(TestCase): self.assertEqual(20, utils.str2int("twentieth")) with self.assertRaises(ValueError): - utils.str2int("not a number") \ No newline at end of file + utils.str2int("not a number") diff --git a/evennia/utils/text2html.py b/evennia/utils/text2html.py index 9ea74bf269..0e0eea41ef 100644 --- a/evennia/utils/text2html.py +++ b/evennia/utils/text2html.py @@ -90,8 +90,10 @@ class TextToHTMLparser(object): re_url = re.compile( r'(?\[\]\s])+)(\.(?:\s|$)|&\w+;|)' ) - re_protocol = re.compile(r'^(?:ftp|https?)://') - re_valid_no_protocol = re.compile(r'^(?:www|ftp)\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b[-a-zA-Z0-9@:%_\+.~#?&//=]*') + re_protocol = re.compile(r"^(?:ftp|https?)://") + re_valid_no_protocol = re.compile( + r"^(?:www|ftp)\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b[-a-zA-Z0-9@:%_\+.~#?&//=]*" + ) re_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL) re_mxpurl = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL) @@ -151,20 +153,24 @@ class TextToHTMLparser(object): """ m = self.re_url.search(text) if m: - href = m.group(1) - label = href - # if there is no protocol (i.e. starts with www or ftp) - # prefix with http:// so the link isn't treated as relative - if not self.re_protocol.match(href): - if not self.re_valid_no_protocol.match(href): - return text - href = "http://" + href - rest = m.group(2) - # -> added target to output prevent the web browser from attempting to - # change pages (and losing our webclient session). - return text[:m.start()] + f'{label}{rest}' + text[m.end():] + href = m.group(1) + label = href + # if there is no protocol (i.e. starts with www or ftp) + # prefix with http:// so the link isn't treated as relative + if not self.re_protocol.match(href): + if not self.re_valid_no_protocol.match(href): + return text + href = "http://" + href + rest = m.group(2) + # -> added target to output prevent the web browser from attempting to + # change pages (and losing our webclient session). + return ( + text[: m.start()] + + f'{label}{rest}' + + text[m.end() :] + ) else: - return text + return text def sub_mxp_links(self, match): """ diff --git a/evennia/utils/verb_conjugation/pronouns.py b/evennia/utils/verb_conjugation/pronouns.py index 95d9d1c753..c676216c97 100644 --- a/evennia/utils/verb_conjugation/pronouns.py +++ b/evennia/utils/verb_conjugation/pronouns.py @@ -59,10 +59,7 @@ PRONOUN_MAPPING = { "neutral": "mine", "plural": "ours", }, - "reflexive pronoun": { - "neutral": "myself", - "plural": "ourselves" - } + "reflexive pronoun": {"neutral": "myself", "plural": "ourselves"}, }, "2nd person": { "subject pronoun": { @@ -80,26 +77,16 @@ PRONOUN_MAPPING = { "reflexive pronoun": { "neutral": "yourself", "plural": "yourselves", - } + }, }, "3rd person": { - "subject pronoun": { - "male": "he", - "female": "she", - "neutral": "it", - "plural": "they" - }, - "object pronoun": { - "male": "him", - "female": "her", - "neutral": "it", - "plural": "them" - }, + "subject pronoun": {"male": "he", "female": "she", "neutral": "it", "plural": "they"}, + "object pronoun": {"male": "him", "female": "her", "neutral": "it", "plural": "them"}, "possessive adjective": { "male": "his", "female": "her", "neutral": "its", - "plural": "their" + "plural": "their", }, "possessive pronoun": { "male": "his", @@ -113,166 +100,61 @@ PRONOUN_MAPPING = { "neutral": "itself", "plural": "themselves", }, - } + }, } PRONOUN_TABLE = { - "I": ( - "1st person", - ("neutral", "male", "female"), - "subject pronoun" - ), - "me": ( - "1st person", - ("neutral", "male", "female"), - "object pronoun" - ), - "my": ( - "1st person", - ("neutral", "male", "female"), - "possessive adjective" - ), - "mine": ( - "1st person", - ("neutral", "male", "female"), - "possessive pronoun" - ), - "myself": ( - "1st person", - ("neutral", "male", "female"), - "reflexive pronoun" - ), - - "we": ( - "1st person", - "plural", - "subject pronoun" - ), - "us": ( - "1st person", - "plural", - "object pronoun" - ), - "our": ( - "1st person", - "plural", - "possessive adjective" - ), - "ours": ( - "1st person", - "plural", - "possessive pronoun" - ), - "ourselves": ( - "1st person", - "plural", - "reflexive pronoun" - ), + "I": ("1st person", ("neutral", "male", "female"), "subject pronoun"), + "me": ("1st person", ("neutral", "male", "female"), "object pronoun"), + "my": ("1st person", ("neutral", "male", "female"), "possessive adjective"), + "mine": ("1st person", ("neutral", "male", "female"), "possessive pronoun"), + "myself": ("1st person", ("neutral", "male", "female"), "reflexive pronoun"), + "we": ("1st person", "plural", "subject pronoun"), + "us": ("1st person", "plural", "object pronoun"), + "our": ("1st person", "plural", "possessive adjective"), + "ours": ("1st person", "plural", "possessive pronoun"), + "ourselves": ("1st person", "plural", "reflexive pronoun"), "you": ( - "2nd person", - ("neutral", "male", "female", "plural"), - ("subject pronoun", "object pronoun") - ), - "your": ( - "2nd person", - ("neutral", "male", "female", "plural"), - "possessive adjective" - ), - "yours": ( - "2nd person", - ("neutral", "male", "female", "plural"), - "possessive pronoun" - ), - "yourself": ( - "2nd person", - ("neutral", "male", "female"), - "reflexive pronoun" - ), - "yourselves": ( - "2nd person", - "plural", - "reflexive pronoun" - ), - "he": ( - "3rd person", - "male", - "subject pronoun" - ), - "him": ( - "3rd person", - "male", - "object pronoun" - ), - "his":( - "3rd person", - "male", - ("possessive pronoun","possessive adjective"), - ), - "himself": ( - "3rd person", - "male", - "reflexive pronoun" - ), - "she": ( - "3rd person", - "female", - "subject pronoun" - ), + "2nd person", + ("neutral", "male", "female", "plural"), + ("subject pronoun", "object pronoun"), + ), + "your": ("2nd person", ("neutral", "male", "female", "plural"), "possessive adjective"), + "yours": ("2nd person", ("neutral", "male", "female", "plural"), "possessive pronoun"), + "yourself": ("2nd person", ("neutral", "male", "female"), "reflexive pronoun"), + "yourselves": ("2nd person", "plural", "reflexive pronoun"), + "he": ("3rd person", "male", "subject pronoun"), + "him": ("3rd person", "male", "object pronoun"), + "his": ( + "3rd person", + "male", + ("possessive pronoun", "possessive adjective"), + ), + "himself": ("3rd person", "male", "reflexive pronoun"), + "she": ("3rd person", "female", "subject pronoun"), "her": ( - "3rd person", - "female", - ("object pronoun", "possessive adjective"), - ), - "hers": ( - "3rd person", - "female", - "possessive pronoun" - ), - "herself": ( - "3rd person", - "female", - "reflexive pronoun" - ), + "3rd person", + "female", + ("object pronoun", "possessive adjective"), + ), + "hers": ("3rd person", "female", "possessive pronoun"), + "herself": ("3rd person", "female", "reflexive pronoun"), "it": ( - "3rd person", - "neutral", - ("subject pronoun", "object pronoun"), - ), + "3rd person", + "neutral", + ("subject pronoun", "object pronoun"), + ), "its": ( - "3rd person", - "neutral", - ("possessive pronoun", "possessive adjective"), - ), - "itself": ( - "3rd person", - "neutral", - "reflexive pronoun" - ), - "they": ( - "3rd person", - "plural", - "subject pronoun" - ), - "them": ( - "3rd person", - "plural", - "object pronoun" - ), - "their": ( - "3rd person", - "plural", - "possessive adjective" - ), - "theirs": ( - "3rd person", - "plural", - "possessive pronoun" - ), - "themselves": ( - "3rd person", - "plural", - "reflexive pronoun" - ), + "3rd person", + "neutral", + ("possessive pronoun", "possessive adjective"), + ), + "itself": ("3rd person", "neutral", "reflexive pronoun"), + "they": ("3rd person", "plural", "subject pronoun"), + "them": ("3rd person", "plural", "object pronoun"), + "their": ("3rd person", "plural", "possessive adjective"), + "theirs": ("3rd person", "plural", "possessive pronoun"), + "themselves": ("3rd person", "plural", "reflexive pronoun"), } # define the default viewpoint conversions @@ -304,7 +186,11 @@ ALIASES = { def pronoun_to_viewpoints( - pronoun, options=None, pronoun_type=DEFAULT_PRONOUN_TYPE, gender=DEFAULT_GENDER, viewpoint=DEFAULT_VIEWPOINT + pronoun, + options=None, + pronoun_type=DEFAULT_PRONOUN_TYPE, + gender=DEFAULT_GENDER, + viewpoint=DEFAULT_VIEWPOINT, ): """ Access function for determining the forms of a pronount from different viewpoints. @@ -365,7 +251,7 @@ def pronoun_to_viewpoints( viewpoint = DEFAULT_VIEWPOINT if gender not in GENDERS: gender = DEFAULT_GENDER - + if options: # option string/list will override the kwargs differentiators given if isinstance(options, str): @@ -395,9 +281,9 @@ def pronoun_to_viewpoints( # special handling for the royal "we" if is_iter(source_gender): - gender_opts = list(source_gender) + gender_opts = list(source_gender) else: - gender_opts = [source_gender] + gender_opts = [source_gender] if viewpoint == "1st person": # make sure plural is always an option when converting to 1st person # it doesn't matter if it's in the list twice, so don't bother checking @@ -409,7 +295,7 @@ def pronoun_to_viewpoints( viewpoint_map = PRONOUN_MAPPING[viewpoint] pronouns = viewpoint_map.get(pronoun_type, viewpoint_map[DEFAULT_PRONOUN_TYPE]) mapped_pronoun = pronouns.get(gender, pronouns[DEFAULT_GENDER]) - + # keep the same capitalization as the original if pronoun != "I": # don't remap I, since this is always capitalized. diff --git a/evennia/utils/verb_conjugation/tests.py b/evennia/utils/verb_conjugation/tests.py index 18863e200c..6eb96d40a7 100644 --- a/evennia/utils/verb_conjugation/tests.py +++ b/evennia/utils/verb_conjugation/tests.py @@ -279,7 +279,7 @@ class TestPronounMapping(TestCase): ("you", "m", "you", "he"), ("you", "f op", "you", "her"), ("I", "", "I", "it"), - ("I", "p", "I", "it"), # plural is invalid + ("I", "p", "I", "it"), # plural is invalid ("I", "m", "I", "he"), ("Me", "n", "Me", "It"), ("your", "p", "your", "their"),