Merge branch 'evennia:develop' into develop

This commit is contained in:
owllex 2022-10-31 13:40:40 -07:00 committed by GitHub
commit 7878b600be
25 changed files with 339 additions and 345 deletions

7
.gitignore vendored
View file

@ -59,3 +59,10 @@ docs/build
# Obsidian # Obsidian
.obsidian .obsidian
# Virtual environments
.venv/
.env/
# Testing folder
.test_game_dir

View file

@ -8,6 +8,7 @@ TESTS ?= evennia
default: default:
@echo " Usage: " @echo " Usage: "
@echo " make install - install evennia (recommended to activate virtualenv first)" @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 fmt/format - run the black autoformatter on the source code"
@echo " make lint - run black in --check mode" @echo " make lint - run black in --check mode"
@echo " make test - run evennia test suite with all default values." @echo " make test - run evennia test suite with all default values."
@ -17,6 +18,10 @@ default:
install: install:
pip install -e . pip install -e .
installextra:
pip install -e .
pip install -r requirements_extra.txt
format: format:
black $(BLACK_FORMAT_CONFIGS) evennia black $(BLACK_FORMAT_CONFIGS) evennia

View file

@ -748,7 +748,9 @@ def cmdhandler(
) )
if suggestions: if suggestions:
sysarg += _(" Maybe you meant {command}?").format( 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: else:
sysarg += _(' Type "help" for help.') sysarg += _(' Type "help" for help.')

View file

@ -668,7 +668,9 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
self.msg(f"Option |w{new_name}|n was kept as '|w{old_val}|n'.") self.msg(f"Option |w{new_name}|n was kept as '|w{old_val}|n'.")
else: else:
flags[new_name] = new_val 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} return {new_name: new_val}
except Exception as err: except Exception as err:
self.msg(f"|rCould not set option |w{new_name}|r:|n {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.set_password(newpass)
account.save() account.save()
self.msg("Password changed.") 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): class CmdQuit(COMMAND_DEFAULT_CLASS):

View file

@ -480,7 +480,7 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
# we supplied an argument on the form obj = perm # we supplied an argument on the form obj = perm
locktype = "edit" if accountmode else "control" locktype = "edit" if accountmode else "control"
if not obj.access(caller, locktype): 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.") caller.msg(f"You are not allowed to edit this {accountstr}'s permissions.")
return return
@ -521,9 +521,7 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
return return
if perm in permissions: if perm in permissions:
caller_result.append( caller_result.append(f"\nPermission '{perm}' is already defined on {obj.name}.")
f"\nPermission '{perm}' is already defined on {obj.name}."
)
else: else:
obj.permissions.add(perm) obj.permissions.add(perm)
plystring = "the Account" if accountmode else "the Object/Character" plystring = "the Account" if accountmode else "the Object/Character"

View file

@ -1392,7 +1392,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
header = f"|wAccount|n |c{caller.key}|n |wpages:|n" header = f"|wAccount|n |c{caller.key}|n |wpages:|n"
if message.startswith(":"): if message.startswith(":"):
message = f"{caller.key} {message.strip(':').strip()}" message = f"{caller.key} {message.strip(':').strip()}"
# create the persistent message object # create the persistent message object
create.create_message(caller, message, receivers=targets) create.create_message(caller, message, receivers=targets)
@ -1565,7 +1565,7 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
return return
if "disconnect" in self.switches or "remove" in self.switches or "delete" in self.switches: 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) matches = AccountDB.objects.filter(db_is_bot=True, username=botname)
dbref = utils.dbref(self.lhs) dbref = utils.dbref(self.lhs)
if not matches and dbref: if not matches and dbref:
@ -1870,7 +1870,7 @@ class CmdGrapevine2Chan(COMMAND_DEFAULT_CLASS):
return return
if "disconnect" in self.switches or "remove" in self.switches or "delete" in self.switches: 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) matches = AccountDB.objects.filter(db_is_bot=True, db_key=botname)
if not matches: if not matches:

View file

@ -221,7 +221,8 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
_, _, old_nickstring, old_replstring = oldnick.value _, _, old_nickstring, old_replstring = oldnick.value
caller.nicks.remove(old_nickstring, category=nicktype) caller.nicks.remove(old_nickstring, category=nicktype)
caller.msg( 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: else:
caller.msg("No matching nicks to remove.") caller.msg("No matching nicks to remove.")
return return
@ -242,9 +243,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
for nick in nicks: for nick in nicks:
_, _, nick, repl = nick.value _, _, nick, repl = nick.value
if nick.startswith(self.lhs): if nick.startswith(self.lhs):
strings.append( strings.append(f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'")
f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'"
)
if strings: if strings:
caller.msg("\n".join(strings)) caller.msg("\n".join(strings))
else: else:
@ -265,9 +264,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
for nick in nicks: for nick in nicks:
_, _, nick, repl = nick.value _, _, nick, repl = nick.value
if nick.startswith(self.lhs): if nick.startswith(self.lhs):
strings.append( strings.append(f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'")
f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'"
)
if strings: if strings:
caller.msg("\n".join(strings)) caller.msg("\n".join(strings))
else: else:
@ -288,9 +285,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
for nick in nicks: for nick in nicks:
_, _, nick, repl = nick.value _, _, nick, repl = nick.value
if nick.startswith(self.lhs): if nick.startswith(self.lhs):
strings.append( strings.append(f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'")
f"{nicktype.capitalize()}-nick: '{nick}' -> '{repl}'"
)
if strings: if strings:
caller.msg("\n".join(strings)) caller.msg("\n".join(strings))
else: else:
@ -724,4 +719,4 @@ class CmdAccess(COMMAND_DEFAULT_CLASS):
string += f"\nCharacter |c{caller.key}|n: {cperms}" string += f"\nCharacter |c{caller.key}|n: {cperms}"
if hasattr(caller, "account"): if hasattr(caller, "account"):
string += f"\nAccount |c{caller.account.key}|n: {pperms}" string += f"\nAccount |c{caller.account.key}|n: {pperms}"
caller.msg(string) caller.msg(string)

View file

@ -343,6 +343,7 @@ class CraftingRecipeBase:
class NonExistentRecipe(CraftingRecipeBase): class NonExistentRecipe(CraftingRecipeBase):
"""A recipe that does not exist and never produces anything.""" """A recipe that does not exist and never produces anything."""
allow_craft = True allow_craft = True
allow_reuse = True allow_reuse = True

View file

@ -61,21 +61,21 @@ from django.conf import settings
from evennia import CmdSet from evennia import CmdSet
from evennia.commands.default.muxcommand import MuxCommand from evennia.commands.default.muxcommand import MuxCommand
_BASIC_MAP_SIZE = settings.BASIC_MAP_SIZE if hasattr(settings, 'BASIC_MAP_SIZE') else 2 _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 _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 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 = { _COMPASS_DIRECTIONS = {
'north': (0, -3, ' | '), "north": (0, -3, " | "),
'south': (0, 3, ' | '), "south": (0, 3, " | "),
'east': (3, 0, '-'), "east": (3, 0, "-"),
'west': (-3, 0, '-'), "west": (-3, 0, "-"),
'northeast': (3, -3, '/'), "northeast": (3, -3, "/"),
'northwest': (-3, -3, '\\'), "northwest": (-3, -3, "\\"),
'southeast': (3, 3, '\\'), "southeast": (3, 3, "\\"),
'southwest': (-3, 3, '/'), "southwest": (-3, 3, "/"),
'up': (0, 0, '^'), "up": (0, 0, "^"),
'down': (0, 0, 'v') "down": (0, 0, "v"),
} }
@ -91,7 +91,7 @@ class Map(object):
""" """
self.start_time = time.time() self.start_time = time.time()
self.caller = caller 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.max_length = int(size * 2 + 1) * 3 # This must be an odd number
self.has_mapped = {} self.has_mapped = {}
self.curX = None self.curX = None
@ -109,8 +109,8 @@ class Map(object):
board = [] board = []
for row in range(self.max_length): for row in range(self.max_length):
board.append([]) board.append([])
for column in range(int(self.max_width/5)): for column in range(int(self.max_width / 5)):
board[row].extend([' ', ' ', ' ']) board[row].extend([" ", " ", " "])
return board return board
def exit_name_as_ordinal(self, ex): def exit_name_as_ordinal(self, ex):
@ -124,11 +124,13 @@ class Map(object):
""" """
exit_name = ex.name exit_name = ex.name
if exit_name not in _COMPASS_DIRECTIONS: 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]: if compass_aliases[0]:
exit_name = compass_aliases[0] exit_name = compass_aliases[0]
if exit_name not in _COMPASS_DIRECTIONS: if exit_name not in _COMPASS_DIRECTIONS:
return '' return ""
return exit_name return exit_name
def update_pos(self, room, 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. # 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")]: for ex in [x for x in room.exits if x.access(self.caller, "traverse")]:
ex_name = self.exit_name_as_ordinal(ex) 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 continue
if self.has_drawn(ex.destination): if self.has_drawn(ex.destination):
continue continue
@ -201,20 +203,20 @@ class Map(object):
continue continue
ex_character = _COMPASS_DIRECTIONS[ex_name][2] ex_character = _COMPASS_DIRECTIONS[ex_name][2]
delta_x = int(_COMPASS_DIRECTIONS[ex_name][1]/3) delta_x = int(_COMPASS_DIRECTIONS[ex_name][1] / 3)
delta_y = int(_COMPASS_DIRECTIONS[ex_name][0]/3) delta_y = int(_COMPASS_DIRECTIONS[ex_name][0] / 3)
# Make modifications if the exit has BOTH up and down exits # Make modifications if the exit has BOTH up and down exits
if ex_name == 'up': if ex_name == "up":
if 'v' in self.grid[x][y]: if "v" in self.grid[x][y]:
self.render_room(room, x, y, p1='^', p2='v') self.render_room(room, x, y, p1="^", p2="v")
else: else:
self.render_room(room, x, y, here='^') self.render_room(room, x, y, here="^")
elif ex_name == 'down': elif ex_name == "down":
if '^' in self.grid[x][y]: if "^" in self.grid[x][y]:
self.render_room(room, x, y, p1='^', p2='v') self.render_room(room, x, y, p1="^", p2="v")
else: else:
self.render_room(room, x, y, here='v') self.render_room(room, x, y, here="v")
else: else:
self.grid[x + delta_x][y + delta_y] = ex_character 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.has_mapped[room] = [self.curX, self.curY]
self.render_room(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 Draw a given room with ascii characters
@ -253,7 +255,7 @@ class Map(object):
you[0] = f"{p1}|n" you[0] = f"{p1}|n"
you[1] = f"{here if here else you[1]}" you[1] = f"{here if here else you[1]}"
if room == self.caller.location: 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" you[2] = f"{p2}|n"
self.grid[x][y] = "".join(you) self.grid[x][y] = "".join(you)
@ -300,6 +302,7 @@ class CmdMap(MuxCommand):
Usage: map (optional size) Usage: map (optional size)
""" """
key = "map" key = "map"
def func(self): def func(self):

View file

@ -17,19 +17,32 @@ class TestIngameMap(BaseEvenniaCommandTest):
Expected output: Expected output:
[ ]--[ ] [ ]--[ ]
""" """
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.west_room = create_object(rooms.Room, key="Room 1") self.west_room = create_object(rooms.Room, key="Room 1")
self.east_room = create_object(rooms.Room, key="Room 2") 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(
create_object(exits.Exit, key="west", aliases=["w"], location=self.east_room, destination=self.west_room) 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): def test_west_room_map_room(self):
self.char1.location = self.west_room self.char1.location = self.west_room
map_here = ingame_map_display.Map(self.char1).show_map() 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): def test_east_room_map_room(self):
self.char1.location = self.east_room self.char1.location = self.east_room
map_here = ingame_map_display.Map(self.char1).show_map() map_here = ingame_map_display.Map(self.char1).show_map()
self.assertEqual(map_here.strip(), '[|n ]|n--[|n|[x|co|n]|n') self.assertEqual(map_here.strip(), "[|n ]|n--[|n|[x|co|n]|n")

View file

@ -1421,16 +1421,19 @@ class TestBuildExampleGrid(BaseEvenniaTest):
mock_room_callbacks = mock.MagicMock() mock_room_callbacks = mock.MagicMock()
mock_exit_callbacks = mock.MagicMock() mock_exit_callbacks = mock.MagicMock()
class TestXyzRoom(xyzroom.XYZRoom): class TestXyzRoom(xyzroom.XYZRoom):
def at_object_creation(self): def at_object_creation(self):
mock_room_callbacks.at_object_creation() mock_room_callbacks.at_object_creation()
class TestXyzExit(xyzroom.XYZExit): class TestXyzExit(xyzroom.XYZExit):
def at_object_creation(self): def at_object_creation(self):
mock_exit_callbacks.at_object_creation() mock_exit_callbacks.at_object_creation()
MAP_DATA = { MAP_DATA = {
"map": """ "map": """
+ 0 1 + 0 1
@ -1439,35 +1442,37 @@ MAP_DATA = {
+ 0 1 + 0 1
""", """,
"zcoord": "map1", "zcoord": "map1",
"prototypes": { "prototypes": {
("*", "*"): { ("*", "*"): {
"key": "room", "key": "room",
"desc": "A room.", "desc": "A room.",
"prototype_parent": "xyz_room", "prototype_parent": "xyz_room",
}, },
("*", "*", "*"): { ("*", "*", "*"): {
"desc": "A passage.", "desc": "A passage.",
"prototype_parent": "xyz_exit", "prototype_parent": "xyz_exit",
} },
}, },
"options": { "options": {
"map_visual_range": 1, "map_visual_range": 1,
"map_mode": "scan", "map_mode": "scan",
} },
} }
class TestCallbacks(BaseEvenniaTest): class TestCallbacks(BaseEvenniaTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
mock_room_callbacks.reset_mock() mock_room_callbacks.reset_mock()
mock_exit_callbacks.reset_mock() mock_exit_callbacks.reset_mock()
def setup_grid(self, map_data): def setup_grid(self, map_data):
self.grid, err = xyzgrid.XYZGrid.create("testgrid") self.grid, err = xyzgrid.XYZGrid.create("testgrid")
def _log(msg): def _log(msg):
print(msg) print(msg)
self.grid.log = _log self.grid.log = _log
self.map_data = map_data self.map_data = map_data
@ -1489,5 +1494,9 @@ class TestCallbacks(BaseEvenniaTest):
self.grid.spawn() self.grid.spawn()
# Two rooms and 2 exits, Each one should have gotten one `at_object_creation` callback. # 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(
self.assertEqual(mock_exit_callbacks.at_object_creation.mock_calls, [mock.call(), mock.call()]) 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()]
)

View file

@ -321,7 +321,9 @@ class MapNode:
# with proper coordinates etc # with proper coordinates etc
typeclass = self.prototype.get("typeclass") typeclass = self.prototype.get("typeclass")
if typeclass is None: 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})") self.log(f" spawning room at xyz={xyz} ({typeclass})")
Typeclass = class_from_module(typeclass) Typeclass = class_from_module(typeclass)
nodeobj, err = Typeclass.create(self.prototype.get("key", "An empty room"), xyz=xyz) 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 prot = maplinks[key.lower()][3].prototype
typeclass = prot.get("typeclass") typeclass = prot.get("typeclass")
if typeclass is None: 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})") self.log(f" spawning/updating exit xyz={xyz}, direction={key} ({typeclass})")
Typeclass = class_from_module(typeclass) Typeclass = class_from_module(typeclass)

View file

@ -7,6 +7,7 @@ from evennia.server.sessionhandler import SESSIONS
import git import git
import datetime import datetime
class GitCommand(MuxCommand): class GitCommand(MuxCommand):
""" """
The shared functionality between git/git evennia 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 Parse the arguments, set default arg to 'status' and check for existence of currently targeted repo
""" """
super().parse() super().parse()
if self.args: if self.args:
split_args = self.args.strip().split(" ", 1) split_args = self.args.strip().split(" ", 1)
self.action = split_args[0] self.action = split_args[0]
if len(split_args) > 1: if len(split_args) > 1:
self.args = ''.join(split_args[1:]) self.args = "".join(split_args[1:])
else: else:
self.args = '' self.args = ""
else: else:
self.action = "status" self.action = "status"
self.args = "" 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.", "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:", "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 init",
" git remote add origin {remote_link}"] " git remote add origin {remote_link}",
]
try: try:
self.repo = git.Repo(self.directory, search_parent_directories=True) self.repo = git.Repo(self.directory, search_parent_directories=True)
except git.exc.InvalidGitRepositoryError: 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) self.caller.msg(err_msg)
raise InterruptCommand raise InterruptCommand
self.commit = self.repo.head.commit self.commit = self.repo.head.commit
try: try:
@ -56,16 +61,20 @@ class GitCommand(MuxCommand):
""" """
short_sha = repo.git.rev_parse(hexsha, short=True) short_sha = repo.git.rev_parse(hexsha, short=True)
return short_sha return short_sha
def get_status(self): def get_status(self):
""" """
Retrieves the status of the active git repository, displaying unstaged changes/untracked files. Retrieves the status of the active git repository, displaying unstaged changes/untracked files.
""" """
time_of_commit = datetime.datetime.fromtimestamp(self.commit.committed_date) 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})", status_msg = "\n".join(
f"By {self.commit.author.email}: {self.commit.message}"]) [
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: if changedFiles:
status_msg += f"Unstaged/uncommitted changes:|/ |g{'|/ '.join(changedFiles)}|n|/" status_msg += f"Unstaged/uncommitted changes:|/ |g{'|/ '.join(changedFiles)}|n|/"
if len(self.repo.untracked_files) > 0: if len(self.repo.untracked_files) > 0:
@ -77,7 +86,9 @@ class GitCommand(MuxCommand):
Display current and available branches. Display current and available branches.
""" """
remote_refs = self.repo.remote().refs 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 return branch_msg
def checkout(self): def checkout(self):
@ -85,7 +96,9 @@ class GitCommand(MuxCommand):
Check out a specific branch. Check out a specific branch.
""" """
remote_refs = self.repo.remote().refs 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: if to_branch not in remote_refs:
self.caller.msg(f"Branch '{to_branch}' not available.") self.caller.msg(f"Branch '{to_branch}' not available.")
@ -101,7 +114,7 @@ class GitCommand(MuxCommand):
return False return False
self.msg(f"Checked out |w{to_branch}|n successfully. Server restart initiated.") self.msg(f"Checked out |w{to_branch}|n successfully. Server restart initiated.")
return True return True
def pull(self): def pull(self):
""" """
Attempt to pull new code. 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") self.caller.msg("No new code to pull, no need to reset.\n")
return False return False
else: 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 return True
def func(self): def func(self):
@ -139,18 +154,19 @@ class GitCommand(MuxCommand):
caller.msg("You can only git status, git branch, git checkout, or git pull.") caller.msg("You can only git status, git branch, git checkout, or git pull.")
return return
class CmdGitEvennia(GitCommand): class CmdGitEvennia(GitCommand):
""" """
Pull the latest code from the evennia core or checkout a different branch. Pull the latest code from the evennia core or checkout a different branch.
Usage: Usage:
git evennia status - View an overview of the evennia repository status. git evennia status - View an overview of the evennia repository status.
git evennia branch - View available branches in evennia. git evennia branch - View available branches in evennia.
git evennia checkout <branch> - Checkout a different branch in evennia. git evennia checkout <branch> - Checkout a different branch in evennia.
git evennia pull - Pull the latest evennia code. git evennia pull - Pull the latest evennia code.
For updating your local mygame repository, the same commands are available with 'git'. 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. 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. git pull - Pull the latest code from your current branch.
For updating evennia code, the same commands are available with 'git evennia'. 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. 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.
""" """

View file

@ -11,6 +11,7 @@ import git
import mock import mock
import datetime import datetime
class TestGitIntegration(EvenniaTest): class TestGitIntegration(EvenniaTest):
@mock.patch("git.Repo") @mock.patch("git.Repo")
@mock.patch("git.Git") @mock.patch("git.Git")
@ -45,11 +46,15 @@ class TestGitIntegration(EvenniaTest):
test_cmd_git.caller = self.char1 test_cmd_git.caller = self.char1
test_cmd_git.args = "nonexistent_branch" test_cmd_git.args = "nonexistent_branch"
self.test_cmd_git = test_cmd_git self.test_cmd_git = test_cmd_git
def test_git_status(self): def test_git_status(self):
time_of_commit = datetime.datetime.fromtimestamp(self.test_cmd_git.commit.committed_date) 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})", status_msg = "\n".join(
f"By {self.test_cmd_git.commit.author.email}: {self.test_cmd_git.commit.message}"]) [
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()) self.assertEqual(status_msg, self.test_cmd_git.get_status())
def test_git_branch(self): def test_git_branch(self):
@ -62,8 +67,9 @@ class TestGitIntegration(EvenniaTest):
# Checkout no branch # Checkout no branch
self.test_cmd_git.checkout() self.test_cmd_git.checkout()
self.char1.msg.assert_called_with("Branch 'nonexistent_branch' not available.") self.char1.msg.assert_called_with("Branch 'nonexistent_branch' not available.")
def test_git_pull(self): def test_git_pull(self):
self.test_cmd_git.pull() 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()}") 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()}"
)

View file

@ -98,18 +98,17 @@ except ImportError:
"copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf." "copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf."
) )
# Maintenance function - this is called repeatedly by the server # 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 _MAINTENANCE_COUNT = 0
_FLUSH_CACHE = None _FLUSH_CACHE = None
_IDMAPPER_CACHE_MAXSIZE = settings.IDMAPPER_CACHE_MAXSIZE
_GAMETIME_MODULE = None _GAMETIME_MODULE = None
_DEFAULTOBJECT = None _DEFAULTOBJECT = None
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT
_LAST_SERVER_TIME_SNAPSHOT = 0
def _server_maintenance(): def _server_maintenance():
""" """
@ -120,9 +119,11 @@ def _server_maintenance():
global _LAST_SERVER_TIME_SNAPSHOT global _LAST_SERVER_TIME_SNAPSHOT
global _OBJECTDB global _OBJECTDB
if not _FLUSH_CACHE: if not _OBJECTDB:
from evennia.objects.models import ObjectDB as _OBJECTDB from evennia.objects.models import ObjectDB as _OBJECTDB
if not _GAMETIME_MODULE:
from evennia.utils import gametime as _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 from evennia.utils.idmapper.models import conditional_flush as _FLUSH_CACHE
_MAINTENANCE_COUNT += 1 _MAINTENANCE_COUNT += 1

View file

@ -20,6 +20,7 @@ from evennia.utils.utils import callables_from_module, class_from_module
SCRIPTDB = None SCRIPTDB = None
class Container: class Container:
""" """
Base container class. A container is simply a storage object whose Base container class. A container is simply a storage object whose
@ -203,7 +204,9 @@ class GlobalScriptContainer(Container):
self.typeclass_storage = {} self.typeclass_storage = {}
for key, data in list(self.loaded_data.items()): for key, data in list(self.loaded_data.items()):
typeclass = data.get("typeclass", settings.BASE_SCRIPT_TYPECLASS) 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): def get(self, key, default=None):
""" """

View file

@ -352,7 +352,7 @@ class FuncParser:
if curr_func: if curr_func:
# we are starting a nested funcdef # we are starting a nested funcdef
if len(callstack) > _MAX_NESTING: if len(callstack) >= _MAX_NESTING - 1:
# stack full - ignore this function # stack full - ignore this function
if raise_errors: if raise_errors:
raise ParsingError( raise ParsingError(

View file

@ -6,5 +6,6 @@ dependencies.
from evennia import nonexistent_module, DefaultScript from evennia import nonexistent_module, DefaultScript
class BrokenScript(DefaultScript): class BrokenScript(DefaultScript):
pass pass

View file

@ -8,14 +8,16 @@ from evennia import DefaultScript
_BASE_TYPECLASS = class_from_module(settings.BASE_SCRIPT_TYPECLASS) _BASE_TYPECLASS = class_from_module(settings.BASE_SCRIPT_TYPECLASS)
class GoodScript(DefaultScript): class GoodScript(DefaultScript):
pass pass
class InvalidScript: class InvalidScript:
pass pass
class TestGlobalScriptContainer(unittest.TestCase):
class TestGlobalScriptContainer(unittest.TestCase):
def test_init_with_no_scripts(self): def test_init_with_no_scripts(self):
gsc = containers.GlobalScriptContainer() gsc = containers.GlobalScriptContainer()
@ -29,7 +31,7 @@ class TestGlobalScriptContainer(unittest.TestCase):
self.assertEqual(len(gsc.typeclass_storage), 0) 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): def test_start_with_typeclassless_script(self):
"""No specified typeclass should fallback to base""" """No specified typeclass should fallback to base"""
gsc = containers.GlobalScriptContainer() gsc = containers.GlobalScriptContainer()
@ -37,10 +39,14 @@ class TestGlobalScriptContainer(unittest.TestCase):
gsc.start() gsc.start()
self.assertEqual(len(gsc.typeclass_storage), 1) self.assertEqual(len(gsc.typeclass_storage), 1)
self.assertIn('script_name', gsc.typeclass_storage) self.assertIn("script_name", gsc.typeclass_storage)
self.assertEqual(gsc.typeclass_storage['script_name'], _BASE_TYPECLASS) 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): def test_start_with_nonexistent_script(self):
"""Missing script class should fall back to base""" """Missing script class should fall back to base"""
gsc = containers.GlobalScriptContainer() gsc = containers.GlobalScriptContainer()
@ -48,35 +54,53 @@ class TestGlobalScriptContainer(unittest.TestCase):
gsc.start() gsc.start()
self.assertEqual(len(gsc.typeclass_storage), 1) self.assertEqual(len(gsc.typeclass_storage), 1)
self.assertIn('script_name', gsc.typeclass_storage) self.assertIn("script_name", gsc.typeclass_storage)
self.assertEqual(gsc.typeclass_storage['script_name'], _BASE_TYPECLASS) 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): def test_start_with_valid_script(self):
gsc = containers.GlobalScriptContainer() gsc = containers.GlobalScriptContainer()
gsc.start() gsc.start()
self.assertEqual(len(gsc.typeclass_storage), 1) self.assertEqual(len(gsc.typeclass_storage), 1)
self.assertIn('script_name', gsc.typeclass_storage) self.assertIn("script_name", gsc.typeclass_storage)
self.assertEqual(gsc.typeclass_storage['script_name'], GoodScript) 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): def test_start_with_invalid_script(self):
"""Script class doesn't implement required methods methods""" """Script class doesn't implement required methods methods"""
gsc = containers.GlobalScriptContainer() gsc = containers.GlobalScriptContainer()
with self.assertRaises(AttributeError) as err: with self.assertRaises(AttributeError) as err:
gsc.start() gsc.start()
# check for general attribute failure on the invalid class to preserve against future code-rder changes # 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): def test_start_with_broken_script(self):
"""Un-importable script should traceback""" """Un-importable script should traceback"""
gsc = containers.GlobalScriptContainer() gsc = containers.GlobalScriptContainer()
with self.assertRaises(Exception) as err: with self.assertRaises(Exception) as err:
gsc.start() gsc.start()
# exception raised by imported module # 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,
)

View file

@ -252,25 +252,37 @@ class TestFuncParser(TestCase):
with self.assertRaises(funcparser.ParsingError): with self.assertRaises(funcparser.ParsingError):
self.parser.parse(unparseable, raise_errors=True) self.parser.parse(unparseable, raise_errors=True)
@patch("evennia.utils.funcparser._MAX_NESTING", 2) @parameterized.expand(
def test_parse_max_nesting(self): [
# 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) 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 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 _MAX_NESTING value, which means you can nest 4 values with a value of
2, as demonstrated by this test. 2, as demonstrated by this test.
""" """
string = "$add(1, $add(1, $add(1, $toint(42))))" string = "$add(1, $add(1, $add(1, $eval(42))))"
ret = self.parser.parse(string)
# TODO: Does this return value actually make sense? with patch("evennia.utils.funcparser._MAX_NESTING", max_nest):
# It removed the spaces from the calls. if ok:
self.assertEqual("$add(1,$add(1,$add(1,$toint(42))))", ret) ret = self.parser.parse(string, raise_errors=True)
self.assertEqual(ret, "45")
with self.assertRaises(funcparser.ParsingError): else:
self.parser.parse(string, raise_errors=True) with self.assertRaises(funcparser.ParsingError):
self.parser.parse(string, raise_errors=True)
def test_parse_underlying_exception(self): def test_parse_underlying_exception(self):
string = "test $add(1, 1) $raise()" string = "test $add(1, 1) $raise()"

View file

@ -353,12 +353,9 @@ class TestTextToHTMLparser(TestCase):
def test_non_url_with_www(self): def test_non_url_with_www(self):
self.assertEqual( self.assertEqual(
self.parser.convert_urls('Awwww.this should not be highlighted'), self.parser.convert_urls("Awwww.this should not be highlighted"),
'Awwww.this should not be highlighted' "Awwww.this should not be highlighted",
) )
def test_invalid_www_url(self): def test_invalid_www_url(self):
self.assertEqual( self.assertEqual(self.parser.convert_urls("www.t"), "www.t")
self.parser.convert_urls('www.t'),
'www.t'
)

View file

@ -721,10 +721,10 @@ class TestIntConversions(TestCase):
# basic mapped numbers # basic mapped numbers
self.assertEqual(3, utils.str2int("three")) self.assertEqual(3, utils.str2int("three"))
self.assertEqual(20, utils.str2int("twenty")) self.assertEqual(20, utils.str2int("twenty"))
# multi-place numbers # multi-place numbers
self.assertEqual(2345, utils.str2int("two thousand, three hundred and forty-five")) self.assertEqual(2345, utils.str2int("two thousand, three hundred and forty-five"))
# ordinal numbers # ordinal numbers
self.assertEqual(1, utils.str2int("1st")) self.assertEqual(1, utils.str2int("1st"))
self.assertEqual(1, utils.str2int("first")) self.assertEqual(1, utils.str2int("first"))
@ -734,4 +734,4 @@ class TestIntConversions(TestCase):
self.assertEqual(20, utils.str2int("twentieth")) self.assertEqual(20, utils.str2int("twentieth"))
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
utils.str2int("not a number") utils.str2int("not a number")

View file

@ -90,8 +90,10 @@ class TextToHTMLparser(object):
re_url = re.compile( re_url = re.compile(
r'(?<!=")(\b(?:ftp|www|https?)\W+(?:(?!\.(?:\s|$)|&\w+;)[^"\',;$*^\\(){}<>\[\]\s])+)(\.(?:\s|$)|&\w+;|)' r'(?<!=")(\b(?:ftp|www|https?)\W+(?:(?!\.(?:\s|$)|&\w+;)[^"\',;$*^\\(){}<>\[\]\s])+)(\.(?:\s|$)|&\w+;|)'
) )
re_protocol = re.compile(r'^(?:ftp|https?)://') 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_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_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
re_mxpurl = re.compile(r"\|lu(.*?)\|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) m = self.re_url.search(text)
if m: if m:
href = m.group(1) href = m.group(1)
label = href label = href
# if there is no protocol (i.e. starts with www or ftp) # if there is no protocol (i.e. starts with www or ftp)
# prefix with http:// so the link isn't treated as relative # prefix with http:// so the link isn't treated as relative
if not self.re_protocol.match(href): if not self.re_protocol.match(href):
if not self.re_valid_no_protocol.match(href): if not self.re_valid_no_protocol.match(href):
return text return text
href = "http://" + href href = "http://" + href
rest = m.group(2) rest = m.group(2)
# -> added target to output prevent the web browser from attempting to # -> added target to output prevent the web browser from attempting to
# change pages (and losing our webclient session). # change pages (and losing our webclient session).
return text[:m.start()] + f'<a href="{href}" target="_blank">{label}</a>{rest}' + text[m.end():] return (
text[: m.start()]
+ f'<a href="{href}" target="_blank">{label}</a>{rest}'
+ text[m.end() :]
)
else: else:
return text return text
def sub_mxp_links(self, match): def sub_mxp_links(self, match):
""" """

View file

@ -59,10 +59,7 @@ PRONOUN_MAPPING = {
"neutral": "mine", "neutral": "mine",
"plural": "ours", "plural": "ours",
}, },
"reflexive pronoun": { "reflexive pronoun": {"neutral": "myself", "plural": "ourselves"},
"neutral": "myself",
"plural": "ourselves"
}
}, },
"2nd person": { "2nd person": {
"subject pronoun": { "subject pronoun": {
@ -80,26 +77,16 @@ PRONOUN_MAPPING = {
"reflexive pronoun": { "reflexive pronoun": {
"neutral": "yourself", "neutral": "yourself",
"plural": "yourselves", "plural": "yourselves",
} },
}, },
"3rd person": { "3rd person": {
"subject pronoun": { "subject pronoun": {"male": "he", "female": "she", "neutral": "it", "plural": "they"},
"male": "he", "object pronoun": {"male": "him", "female": "her", "neutral": "it", "plural": "them"},
"female": "she",
"neutral": "it",
"plural": "they"
},
"object pronoun": {
"male": "him",
"female": "her",
"neutral": "it",
"plural": "them"
},
"possessive adjective": { "possessive adjective": {
"male": "his", "male": "his",
"female": "her", "female": "her",
"neutral": "its", "neutral": "its",
"plural": "their" "plural": "their",
}, },
"possessive pronoun": { "possessive pronoun": {
"male": "his", "male": "his",
@ -113,166 +100,61 @@ PRONOUN_MAPPING = {
"neutral": "itself", "neutral": "itself",
"plural": "themselves", "plural": "themselves",
}, },
} },
} }
PRONOUN_TABLE = { PRONOUN_TABLE = {
"I": ( "I": ("1st person", ("neutral", "male", "female"), "subject pronoun"),
"1st person", "me": ("1st person", ("neutral", "male", "female"), "object pronoun"),
("neutral", "male", "female"), "my": ("1st person", ("neutral", "male", "female"), "possessive adjective"),
"subject pronoun" "mine": ("1st person", ("neutral", "male", "female"), "possessive pronoun"),
), "myself": ("1st person", ("neutral", "male", "female"), "reflexive pronoun"),
"me": ( "we": ("1st person", "plural", "subject pronoun"),
"1st person", "us": ("1st person", "plural", "object pronoun"),
("neutral", "male", "female"), "our": ("1st person", "plural", "possessive adjective"),
"object pronoun" "ours": ("1st person", "plural", "possessive pronoun"),
), "ourselves": ("1st person", "plural", "reflexive 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": ( "you": (
"2nd person", "2nd person",
("neutral", "male", "female", "plural"), ("neutral", "male", "female", "plural"),
("subject pronoun", "object pronoun") ("subject pronoun", "object pronoun"),
), ),
"your": ( "your": ("2nd person", ("neutral", "male", "female", "plural"), "possessive adjective"),
"2nd person", "yours": ("2nd person", ("neutral", "male", "female", "plural"), "possessive pronoun"),
("neutral", "male", "female", "plural"), "yourself": ("2nd person", ("neutral", "male", "female"), "reflexive pronoun"),
"possessive adjective" "yourselves": ("2nd person", "plural", "reflexive pronoun"),
), "he": ("3rd person", "male", "subject pronoun"),
"yours": ( "him": ("3rd person", "male", "object pronoun"),
"2nd person", "his": (
("neutral", "male", "female", "plural"), "3rd person",
"possessive pronoun" "male",
), ("possessive pronoun", "possessive adjective"),
"yourself": ( ),
"2nd person", "himself": ("3rd person", "male", "reflexive pronoun"),
("neutral", "male", "female"), "she": ("3rd person", "female", "subject pronoun"),
"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": ( "her": (
"3rd person", "3rd person",
"female", "female",
("object pronoun", "possessive adjective"), ("object pronoun", "possessive adjective"),
), ),
"hers": ( "hers": ("3rd person", "female", "possessive pronoun"),
"3rd person", "herself": ("3rd person", "female", "reflexive pronoun"),
"female",
"possessive pronoun"
),
"herself": (
"3rd person",
"female",
"reflexive pronoun"
),
"it": ( "it": (
"3rd person", "3rd person",
"neutral", "neutral",
("subject pronoun", "object pronoun"), ("subject pronoun", "object pronoun"),
), ),
"its": ( "its": (
"3rd person", "3rd person",
"neutral", "neutral",
("possessive pronoun", "possessive adjective"), ("possessive pronoun", "possessive adjective"),
), ),
"itself": ( "itself": ("3rd person", "neutral", "reflexive pronoun"),
"3rd person", "they": ("3rd person", "plural", "subject pronoun"),
"neutral", "them": ("3rd person", "plural", "object pronoun"),
"reflexive pronoun" "their": ("3rd person", "plural", "possessive adjective"),
), "theirs": ("3rd person", "plural", "possessive pronoun"),
"they": ( "themselves": ("3rd person", "plural", "reflexive pronoun"),
"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 # define the default viewpoint conversions
@ -304,7 +186,11 @@ ALIASES = {
def pronoun_to_viewpoints( 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. Access function for determining the forms of a pronount from different viewpoints.
@ -365,7 +251,7 @@ def pronoun_to_viewpoints(
viewpoint = DEFAULT_VIEWPOINT viewpoint = DEFAULT_VIEWPOINT
if gender not in GENDERS: if gender not in GENDERS:
gender = DEFAULT_GENDER gender = DEFAULT_GENDER
if options: if options:
# option string/list will override the kwargs differentiators given # option string/list will override the kwargs differentiators given
if isinstance(options, str): if isinstance(options, str):
@ -395,9 +281,9 @@ def pronoun_to_viewpoints(
# special handling for the royal "we" # special handling for the royal "we"
if is_iter(source_gender): if is_iter(source_gender):
gender_opts = list(source_gender) gender_opts = list(source_gender)
else: else:
gender_opts = [source_gender] gender_opts = [source_gender]
if viewpoint == "1st person": if viewpoint == "1st person":
# make sure plural is always an option when converting to 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 # 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] viewpoint_map = PRONOUN_MAPPING[viewpoint]
pronouns = viewpoint_map.get(pronoun_type, viewpoint_map[DEFAULT_PRONOUN_TYPE]) pronouns = viewpoint_map.get(pronoun_type, viewpoint_map[DEFAULT_PRONOUN_TYPE])
mapped_pronoun = pronouns.get(gender, pronouns[DEFAULT_GENDER]) mapped_pronoun = pronouns.get(gender, pronouns[DEFAULT_GENDER])
# keep the same capitalization as the original # keep the same capitalization as the original
if pronoun != "I": if pronoun != "I":
# don't remap I, since this is always capitalized. # don't remap I, since this is always capitalized.

View file

@ -279,7 +279,7 @@ class TestPronounMapping(TestCase):
("you", "m", "you", "he"), ("you", "m", "you", "he"),
("you", "f op", "you", "her"), ("you", "f op", "you", "her"),
("I", "", "I", "it"), ("I", "", "I", "it"),
("I", "p", "I", "it"), # plural is invalid ("I", "p", "I", "it"), # plural is invalid
("I", "m", "I", "he"), ("I", "m", "I", "he"),
("Me", "n", "Me", "It"), ("Me", "n", "Me", "It"),
("your", "p", "your", "their"), ("your", "p", "your", "their"),