mirror of
https://github.com/evennia/evennia.git
synced 2025-10-30 03:44:47 +00:00
Cleaned up tests to use newly-renamed Account hooks for add/remove characters.
This commit is contained in:
parent
f782cd8fc8
commit
4b80b200d8
@ -622,7 +622,7 @@ node_apply_character(caller, raw_string, **kwargs):
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
new_character = tmp_character.apply(caller)
|
||||
|
||||
caller.account.db._playable_characters = [new_character]
|
||||
caller.account.add_character_to_playable_list(new_character)
|
||||
|
||||
text = "Character created!"
|
||||
|
||||
|
||||
@ -311,12 +311,12 @@ Our rock-paper-scissor setup works like this:
|
||||
- `defend` does nothing but has a chance to beat `hit`.
|
||||
- `flee/disengage` must succeed two times in a row (i.e. not beaten by a `hit` once during the turn). If so the character leaves combat.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/world/rules.py
|
||||
|
||||
import random
|
||||
|
||||
|
||||
# messages
|
||||
|
||||
def resolve_combat(combat_handler, actiondict):
|
||||
@ -326,7 +326,7 @@ def resolve_combat(combat_handler, actiondict):
|
||||
for each character:
|
||||
{char.id:[(action1, char, target), (action2, char, target)], ...}
|
||||
"""
|
||||
flee = {} # track number of flee commands per character
|
||||
flee = {} # track number of flee commands per character
|
||||
for isub in range(2):
|
||||
# loop over sub-turns
|
||||
messages = []
|
||||
@ -389,7 +389,7 @@ def resolve_combat(combat_handler, actiondict):
|
||||
for (char, fleevalue) in flee.items():
|
||||
if fleevalue == 2:
|
||||
combat_handler.msg_all(f"{char} withdraws from combat.")
|
||||
combat_handler.remove_character(char)
|
||||
combat_handler.remove_character_from_playable_list(char)
|
||||
```
|
||||
|
||||
To make it simple (and to save space), this example rule module actually resolves each interchange twice - first when it gets to each character and then again when handling the target. Also, since we use the combat handler's `msg_all` method here, the system will get pretty spammy. To clean it up, one could imagine tracking all the possible interactions to make sure each pair is only handled and reported once.
|
||||
@ -403,6 +403,7 @@ This is the last component we need, a command to initiate combat. This will tie
|
||||
|
||||
from evennia import create_script
|
||||
|
||||
|
||||
class CmdAttack(Command):
|
||||
"""
|
||||
initiates combat
|
||||
@ -419,7 +420,7 @@ class CmdAttack(Command):
|
||||
def func(self):
|
||||
"Handle command"
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: attack <target>")
|
||||
self.caller.msg("Usage: attack <target>")
|
||||
return
|
||||
target = self.caller.search(self.args)
|
||||
if not target:
|
||||
@ -427,13 +428,13 @@ class CmdAttack(Command):
|
||||
# set up combat
|
||||
if target.ndb.combat_handler:
|
||||
# target is already in combat - join it
|
||||
target.ndb.combat_handler.add_character(self.caller)
|
||||
target.ndb.combat_handler.add_character_to_playable_list(self.caller)
|
||||
target.ndb.combat_handler.msg_all(f"{self.caller} joins combat!")
|
||||
else:
|
||||
# create a new combat handler
|
||||
chandler = create_script("combat_handler.CombatHandler")
|
||||
chandler.add_character(self.caller)
|
||||
chandler.add_character(target)
|
||||
chandler.add_character_to_playable_list(self.caller)
|
||||
chandler.add_character_to_playable_list(target)
|
||||
self.caller.msg(f"You attack {target}! You are in combat.")
|
||||
target.msg(f"{self.caller} attacks you! You are in combat.")
|
||||
```
|
||||
|
||||
@ -206,7 +206,7 @@ def creating(request):
|
||||
# create the character
|
||||
char = create.create_object(typeclass=typeclass, key=name,
|
||||
home=home, permissions=perms)
|
||||
user.db._playable_characters.append(char)
|
||||
user.add_character_to_playable_list(char)
|
||||
# add the right locks for the character so the account can
|
||||
# puppet it
|
||||
char.locks.add(" or ".join([
|
||||
@ -290,7 +290,7 @@ def creating(request):
|
||||
# create the character
|
||||
char = create.create_object(typeclass=typeclass, key=name,
|
||||
home=home, permissions=perms)
|
||||
user.db._playable_characters.append(char)
|
||||
user.add_character_to_playable_list(char)
|
||||
# add the right locks for the character so the account can
|
||||
# puppet it
|
||||
char.locks.add(" or ".join([
|
||||
|
||||
@ -198,8 +198,8 @@ def index(request):
|
||||
def index(request):
|
||||
"""The 'index' view."""
|
||||
user = request.user
|
||||
if not user.is_anonymous() and user.db._playable_characters:
|
||||
character = user.db._playable_characters[0]
|
||||
if not user.is_anonymous() and user.characters:
|
||||
character = user.characters[0]
|
||||
```
|
||||
|
||||
In this second case, it will select the first character of the account.
|
||||
|
||||
@ -186,6 +186,7 @@ def _init(portal_mode=False):
|
||||
from .typeclasses.tags import TagCategoryProperty, TagProperty
|
||||
from .utils import ansi, gametime, logger
|
||||
from .utils.ansi import ANSIString
|
||||
from .utils.evrich import install as install_evrich
|
||||
|
||||
# containers
|
||||
from .utils.containers import GLOBAL_SCRIPTS, OPTION_CLASSES
|
||||
@ -375,6 +376,9 @@ def _init(portal_mode=False):
|
||||
del SystemCmds
|
||||
del _EvContainer
|
||||
|
||||
# Trigger EvRich to monkey-patch Rich in-memory.
|
||||
install_evrich()
|
||||
|
||||
# delayed starts - important so as to not back-access evennia before it has
|
||||
# finished initializing
|
||||
if not portal_mode:
|
||||
|
||||
@ -236,15 +236,15 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
||||
|
||||
return objs
|
||||
|
||||
def add_character(self, character: "DefaultCharacter"):
|
||||
def add_character_to_playable_list(self, character: "DefaultCharacter"):
|
||||
"""
|
||||
Add a character to this account's list of playable characters.
|
||||
"""
|
||||
if character not in self.db._playable_characters:
|
||||
self.db._playable_characters.append(character)
|
||||
self.at_post_add_character(character)
|
||||
self.at_post_add_character_to_playable_list(character)
|
||||
|
||||
def at_post_add_character(self, character: "DefaultCharacter"):
|
||||
def at_post_add_character_to_playable_list(self, character: "DefaultCharacter"):
|
||||
"""
|
||||
Called after a character is added to this account's list of playable characters.
|
||||
|
||||
@ -252,15 +252,15 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
||||
"""
|
||||
pass
|
||||
|
||||
def remove_character(self, character):
|
||||
def remove_character_from_playable_list(self, character):
|
||||
"""
|
||||
Remove a character from this account's list of playable characters.
|
||||
"""
|
||||
if character in self.db._playable_characters:
|
||||
self.db._playable_characters.remove(character)
|
||||
self.at_post_remove_character(character)
|
||||
self.at_post_remove_character_from_playable_list(character)
|
||||
|
||||
def at_post_remove_character(self, character):
|
||||
def at_post_remove_character_from_playable_list(self, character):
|
||||
"""
|
||||
Called after a character is removed from this account's list of playable characters.
|
||||
|
||||
@ -776,7 +776,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
||||
)
|
||||
if character:
|
||||
# Update playable character list
|
||||
self.add_character(character)
|
||||
self.add_character_to_playable_list(character)
|
||||
|
||||
# We need to set this to have @ic auto-connect to this character
|
||||
self.db._last_puppet = character
|
||||
|
||||
@ -105,14 +105,14 @@ class TestDefaultGuest(BaseEvenniaTest):
|
||||
def test_at_server_shutdown(self):
|
||||
account, errors = DefaultGuest.create(ip=self.ip)
|
||||
self.char1.delete = MagicMock()
|
||||
account.db._playable_characters = [self.char1]
|
||||
account.add_character_to_playable_list(self.char1)
|
||||
account.at_server_shutdown()
|
||||
self.char1.delete.assert_called()
|
||||
|
||||
def test_at_post_disconnect(self):
|
||||
account, errors = DefaultGuest.create(ip=self.ip)
|
||||
self.char1.delete = MagicMock()
|
||||
account.db._playable_characters = [self.char1]
|
||||
account.add_character_to_playable_list(self.char1)
|
||||
account.at_post_disconnect()
|
||||
self.char1.delete.assert_called()
|
||||
|
||||
@ -358,19 +358,19 @@ class TestAccountPuppetDeletion(BaseEvenniaTest):
|
||||
def test_puppet_deletion(self):
|
||||
# Check for existing chars
|
||||
self.assertFalse(
|
||||
self.account.db._playable_characters, "Account should not have any chars by default."
|
||||
self.account.characters, "Account should not have any chars by default."
|
||||
)
|
||||
|
||||
# Add char1 to account's playable characters
|
||||
self.account.db._playable_characters.append(self.char1)
|
||||
self.assertTrue(self.account.db._playable_characters, "Char was not added to account.")
|
||||
self.account.add_character_to_playable_list(self.char1)
|
||||
self.assertTrue(self.account.characters, "Char was not added to account.")
|
||||
|
||||
# See what happens when we delete char1.
|
||||
self.char1.delete()
|
||||
# Playable char list should be empty.
|
||||
self.assertFalse(
|
||||
self.account.db._playable_characters,
|
||||
f"Playable character list is not empty! {self.account.db._playable_characters}",
|
||||
self.account.characters,
|
||||
f"Playable character list is not empty! {self.account.characters}",
|
||||
)
|
||||
|
||||
|
||||
@ -387,6 +387,17 @@ class TestDefaultAccountEv(BaseEvenniaTest):
|
||||
self.assertEqual(chars, [self.char1])
|
||||
self.assertEqual(self.account.db._playable_characters, [self.char1])
|
||||
|
||||
def test_add_character_to_playable_list(self):
|
||||
self.assertEqual(self.account.characters, [])
|
||||
self.account.add_character_to_playable_list(self.char1)
|
||||
self.assertEqual(self.account.characters, [self.char1])
|
||||
|
||||
def test_remove_character_from_playable_list(self):
|
||||
self.account.add_character_to_playable_list(self.char1)
|
||||
self.assertEqual(self.account.characters, [self.char1])
|
||||
self.account.remove_character_from_playable_list(self.char1)
|
||||
self.assertEqual(self.account.characters, [])
|
||||
|
||||
def test_puppet_success(self):
|
||||
self.account.msg = MagicMock()
|
||||
with patch("evennia.accounts.accounts._MULTISESSION_MODE", 2):
|
||||
|
||||
@ -179,7 +179,7 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
|
||||
"puppet:id(%i) or pid(%i) or perm(Developer) or pperm(Developer);delete:id(%i) or"
|
||||
" perm(Admin)" % (new_character.id, account.id, account.id)
|
||||
)
|
||||
account.add_character(new_character)
|
||||
account.add_character_to_playable_list(new_character)
|
||||
if desc:
|
||||
new_character.db.desc = desc
|
||||
elif not new_character.db.desc:
|
||||
@ -238,7 +238,7 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS):
|
||||
# only take action
|
||||
delobj = caller.ndb._char_to_delete
|
||||
key = delobj.key
|
||||
caller.remove_character(delobj)
|
||||
caller.remove_character_from_playable_list(delobj)
|
||||
delobj.delete()
|
||||
self.msg(f"Character '{key}' was permanently deleted.")
|
||||
logger.log_sec(
|
||||
|
||||
@ -589,7 +589,7 @@ class TestAccount(BaseEvenniaCommandTest):
|
||||
]
|
||||
)
|
||||
def test_ooc_look(self, multisession_mode, auto_puppet, max_nr_chars, expected_result):
|
||||
self.account.db._playable_characters = [self.char1]
|
||||
self.account.add_character_to_playable_list(self.char1)
|
||||
self.account.unpuppet_all()
|
||||
|
||||
with self.settings(MULTISESSION=multisession_mode):
|
||||
@ -609,14 +609,14 @@ class TestAccount(BaseEvenniaCommandTest):
|
||||
self.call(account.CmdOOC(), "", "You go OOC.", caller=self.account)
|
||||
|
||||
def test_ic(self):
|
||||
self.account.db._playable_characters = [self.char1]
|
||||
self.account.add_character_to_playable_list(self.char1)
|
||||
self.account.unpuppet_object(self.session)
|
||||
self.call(
|
||||
account.CmdIC(), "Char", "You become Char.", caller=self.account, receiver=self.char1
|
||||
)
|
||||
|
||||
def test_ic__other_object(self):
|
||||
self.account.db._playable_characters = [self.obj1]
|
||||
self.account.add_character_to_playable_list(self.obj1)
|
||||
self.account.unpuppet_object(self.session)
|
||||
self.call(
|
||||
account.CmdIC(), "Obj", "You become Obj.", caller=self.account, receiver=self.obj1
|
||||
@ -670,7 +670,7 @@ class TestAccount(BaseEvenniaCommandTest):
|
||||
# whether permissions are being checked
|
||||
|
||||
# Add char to account playable characters
|
||||
self.account.db._playable_characters.append(self.char1)
|
||||
self.account.add_character_to_playable_list(self.char1)
|
||||
|
||||
# Try deleting as Developer
|
||||
self.call(
|
||||
|
||||
@ -507,7 +507,7 @@ def _create_character(session, new_account, typeclass, home, permissions):
|
||||
typeclass, key=new_account.key, home=home, permissions=permissions
|
||||
)
|
||||
# set playable character list
|
||||
new_account.add_character(new_character)
|
||||
new_account.add_character_to_playable_list(new_character)
|
||||
|
||||
# allow only the character itself and the account to puppet this character (and Developers).
|
||||
new_character.locks.add(
|
||||
|
||||
@ -90,7 +90,7 @@ class ContribCmdCharCreate(MuxAccountCommand):
|
||||
)
|
||||
# initalize the new character to the beginning of the chargen menu
|
||||
new_character.db.chargen_step = "menunode_welcome"
|
||||
account.add_character(new_character)
|
||||
account.add_character_to_playable_list(new_character)
|
||||
|
||||
# set the menu node to start at to the character's last saved step
|
||||
startnode = new_character.db.chargen_step
|
||||
|
||||
@ -17,7 +17,7 @@ class TestCharacterCreator(BaseEvenniaCommandTest):
|
||||
self.account.swap_typeclass(character_creator.ContribChargenAccount)
|
||||
|
||||
def test_ooc_look(self):
|
||||
self.account.db._playable_characters = [self.char1]
|
||||
self.account.add_character_to_playable_list(self.char1)
|
||||
self.account.unpuppet_all()
|
||||
|
||||
self.char1.db.chargen_step = "start"
|
||||
|
||||
@ -316,7 +316,7 @@ def node_apply_character(caller, raw_string, **kwargs):
|
||||
"""
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
new_character = tmp_character.apply(caller)
|
||||
caller.add_character(new_character)
|
||||
caller.add_character_to_playable_list(new_character)
|
||||
|
||||
text = "Character created!"
|
||||
|
||||
|
||||
@ -1149,7 +1149,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
||||
# sever the connection (important!)
|
||||
if self.account:
|
||||
# Remove the object from playable characters list
|
||||
self.account.remove_character(self)
|
||||
self.account.remove_character_from_playable_list(self)
|
||||
for session in self.sessions.all():
|
||||
self.account.unpuppet_object(session)
|
||||
|
||||
@ -2559,7 +2559,7 @@ class DefaultCharacter(DefaultObject):
|
||||
obj.db.creator_ip = ip
|
||||
if account:
|
||||
obj.db.creator_id = account.id
|
||||
account.add_character(obj)
|
||||
account.add_character_to_playable_list(obj)
|
||||
|
||||
# Add locks
|
||||
if not locks and account:
|
||||
|
||||
@ -16,6 +16,8 @@ from evennia.scripts.monitorhandler import MONITOR_HANDLER
|
||||
from evennia.typeclasses.attributes import AttributeHandler, DbHolder, InMemoryAttributeBackend
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.utils import class_from_module, lazy_property, make_iter
|
||||
from evennia.utils.evrich import MudConsole, MudConsoleOptions
|
||||
from rich.color import ColorSystem
|
||||
|
||||
_GA = object.__getattribute__
|
||||
_SA = object.__setattr__
|
||||
@ -51,6 +53,57 @@ class ServerSession(_BASE_SESSION_CLASS):
|
||||
self.cmdset_storage_string = ""
|
||||
self.cmdset = CmdSetHandler(self, True)
|
||||
|
||||
@lazy_property
|
||||
def console(self):
|
||||
from mudrich import MudConsole
|
||||
if "SCREENWIDTH" in self.protocol_flags:
|
||||
width = self.protocol_flags["SCREENWIDTH"][0]
|
||||
else:
|
||||
width = 78
|
||||
return MudConsole(color_system=self.rich_color_system(), width=width,
|
||||
file=self, record=True)
|
||||
|
||||
def rich_color_system(self):
|
||||
if self.protocol_flags.get("NOCOLOR", False):
|
||||
return None
|
||||
if self.protocol_flags.get("XTERM256", False):
|
||||
return "256"
|
||||
if self.protocol_flags.get("ANSI", False):
|
||||
return "standard"
|
||||
return None
|
||||
|
||||
def update_rich(self):
|
||||
check = self.console
|
||||
if "SCREENWIDTH" in self.protocol_flags:
|
||||
self.console._width = self.protocol_flags["SCREENWIDTH"][0]
|
||||
else:
|
||||
self.console._width = 80
|
||||
if self.protocol_flags.get("NOCOLOR", False):
|
||||
self.console._color_system = None
|
||||
elif self.protocol_flags.get("XTERM256", False):
|
||||
self.console._color_system = ColorSystem.EIGHT_BIT
|
||||
elif self.protocol_flags.get("ANSI", False):
|
||||
self.console._color_system = ColorSystem.STANDARD
|
||||
|
||||
def write(self, b: str):
|
||||
"""
|
||||
When self.console.print() is called, it writes output to here.
|
||||
Not necessarily useful, but it ensures console print doesn't end up sent out stdout or etc.
|
||||
"""
|
||||
|
||||
def flush(self):
|
||||
"""
|
||||
Do not remove this method. It's needed to trick Console into treating this object
|
||||
as a file.
|
||||
"""
|
||||
|
||||
def print(self, *args, **kwargs) -> str:
|
||||
"""
|
||||
A thin wrapper around Rich.Console's print. Returns the exported data.
|
||||
"""
|
||||
self.console.print(*args, highlight=False, **kwargs)
|
||||
return self.console.export_text(clear=True, styles=True)
|
||||
|
||||
def __cmdset_storage_get(self):
|
||||
return [path.strip() for path in self.cmdset_storage_string.split(",")]
|
||||
|
||||
@ -257,6 +310,9 @@ class ServerSession(_BASE_SESSION_CLASS):
|
||||
for the protocol(s).
|
||||
|
||||
"""
|
||||
if (t := kwargs.get("text", None)):
|
||||
if hasattr(t, "__rich_console__"):
|
||||
kwargs["text"] = self.print(t)
|
||||
self.sessionhandler.data_out(self, **kwargs)
|
||||
|
||||
def data_in(self, **kwargs):
|
||||
@ -293,6 +349,8 @@ class ServerSession(_BASE_SESSION_CLASS):
|
||||
kwargs.pop("session", None)
|
||||
kwargs.pop("from_obj", None)
|
||||
if text is not None:
|
||||
if hasattr(text, "__rich_console__"):
|
||||
text = self.print(text)
|
||||
self.data_out(text=text, **kwargs)
|
||||
else:
|
||||
self.data_out(**kwargs)
|
||||
@ -444,3 +502,7 @@ class ServerSession(_BASE_SESSION_CLASS):
|
||||
return self.account.get_display_name(*args, **kwargs)
|
||||
else:
|
||||
return f"{self.protocol_key}({self.address})"
|
||||
|
||||
def load_sync_data(self, sessdata):
|
||||
super().load_sync_data(sessdata)
|
||||
self.update_rich()
|
||||
|
||||
@ -72,6 +72,9 @@ from evennia.utils.utils import to_str
|
||||
|
||||
MXP_ENABLED = settings.MXP_ENABLED
|
||||
|
||||
from rich.ansi import AnsiDecoder
|
||||
from .evrich import MudText
|
||||
|
||||
|
||||
# ANSI definitions
|
||||
|
||||
@ -1054,6 +1057,13 @@ class ANSIString(str, metaclass=ANSIMeta):
|
||||
result += self._raw_string[index]
|
||||
return ANSIString(result + clean + append_tail, decoded=True)
|
||||
|
||||
def __rich_console__(self, console, options):
|
||||
"""
|
||||
Implements the Rich console API, allowing AnsiStrings to be
|
||||
converted to MudText instances.
|
||||
"""
|
||||
yield MudText("\n").join(AnsiDecoder().decode(self))
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Return a string object *without* the ANSI escapes.
|
||||
|
||||
635
evennia/utils/evrich.py
Normal file
635
evennia/utils/evrich.py
Normal file
@ -0,0 +1,635 @@
|
||||
"""
|
||||
This module installs monkey patches to Rich, allowing it to support MXP.
|
||||
|
||||
MudRich system, by Volund, ported the hard way to Evennia.
|
||||
"""
|
||||
import html
|
||||
from dataclasses import dataclass
|
||||
import random
|
||||
import re
|
||||
from marshal import loads, dumps
|
||||
|
||||
from typing import Any, Dict, Iterable, List, Optional, Type, Union, Tuple
|
||||
|
||||
from rich.color import Color, ColorSystem
|
||||
|
||||
from rich.style import Style as OLD_STYLE
|
||||
from rich.text import Text as OLD_TEXT, Segment, Span
|
||||
from rich.console import Console as OLD_CONSOLE, ConsoleOptions as OLD_CONSOLE_OPTIONS, NoChange, NO_CHANGE
|
||||
from rich.console import JustifyMethod, OverflowMethod
|
||||
|
||||
|
||||
_RE_SQUISH = re.compile("\S+")
|
||||
_RE_NOTSPACE = re.compile("[^ ]+")
|
||||
|
||||
|
||||
class MudStyle(OLD_STYLE):
|
||||
_tag: str
|
||||
|
||||
__slots__ = [
|
||||
"_tag",
|
||||
"_xml_attr",
|
||||
"_xml_attr_data"
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
color: Optional[Union[Color, str]] = None,
|
||||
bgcolor: Optional[Union[Color, str]] = None,
|
||||
bold: Optional[bool] = None,
|
||||
dim: Optional[bool] = None,
|
||||
italic: Optional[bool] = None,
|
||||
underline: Optional[bool] = None,
|
||||
blink: Optional[bool] = None,
|
||||
blink2: Optional[bool] = None,
|
||||
reverse: Optional[bool] = None,
|
||||
conceal: Optional[bool] = None,
|
||||
strike: Optional[bool] = None,
|
||||
underline2: Optional[bool] = None,
|
||||
frame: Optional[bool] = None,
|
||||
encircle: Optional[bool] = None,
|
||||
overline: Optional[bool] = None,
|
||||
link: Optional[str] = None,
|
||||
meta: Optional[Dict[str, Any]] = None,
|
||||
tag: Optional[str] = None,
|
||||
xml_attr: Optional[Dict] = None,
|
||||
):
|
||||
super().__init__(color=color, bgcolor=bgcolor, bold=bold, dim=dim, italic=italic,
|
||||
underline=underline, blink=blink, blink2=blink2, reverse=reverse,
|
||||
conceal=conceal, strike=strike, underline2=underline2, frame=frame,
|
||||
encircle=encircle, overline=overline, link=link, meta=meta)
|
||||
|
||||
self._tag = tag
|
||||
self._xml_attr = xml_attr
|
||||
if self._xml_attr:
|
||||
self._xml_attr_data = (
|
||||
" ".join(f'{k}="{html.escape(v)}"' for k, v in xml_attr.items())
|
||||
if xml_attr
|
||||
else ""
|
||||
)
|
||||
else:
|
||||
self._xml_attr_data = ""
|
||||
|
||||
self._hash = hash(
|
||||
(
|
||||
self._color,
|
||||
self._bgcolor,
|
||||
self._attributes,
|
||||
self._set_attributes,
|
||||
link,
|
||||
self._meta,
|
||||
tag,
|
||||
self._xml_attr_data
|
||||
)
|
||||
)
|
||||
|
||||
self._null = not (self._set_attributes or color or bgcolor or link or meta or tag)
|
||||
|
||||
@classmethod
|
||||
def upgrade(cls, old):
|
||||
return cls.parse(str(old))
|
||||
|
||||
def render(
|
||||
self,
|
||||
text: str = "",
|
||||
*,
|
||||
color_system: Optional[ColorSystem] = ColorSystem.TRUECOLOR,
|
||||
legacy_windows: bool = False,
|
||||
mxp: bool = False,
|
||||
pueblo: bool = False,
|
||||
links: bool = True,
|
||||
) -> str:
|
||||
"""Render the ANSI codes for the style.
|
||||
|
||||
Args:
|
||||
text (str, optional): A string to style. Defaults to "".
|
||||
color_system (Optional[ColorSystem], optional): Color system to render to. Defaults to ColorSystem.TRUECOLOR.
|
||||
|
||||
Returns:
|
||||
str: A string containing ANSI style codes.
|
||||
"""
|
||||
out_text = text
|
||||
if mxp:
|
||||
out_text = html.escape(out_text)
|
||||
if not out_text:
|
||||
return out_text
|
||||
if color_system is not None:
|
||||
attrs = self._make_ansi_codes(color_system)
|
||||
rendered = f"\x1b[{attrs}m{out_text}\x1b[0m" if attrs else out_text
|
||||
else:
|
||||
rendered = out_text
|
||||
if links and self._link and not legacy_windows:
|
||||
rendered = (
|
||||
f"\x1b]8;id={self._link_id};{self._link}\x1b\\{rendered}\x1b]8;;\x1b\\"
|
||||
)
|
||||
if (pueblo or mxp) and self._tag:
|
||||
if mxp:
|
||||
if self._xml_attr:
|
||||
rendered = f"\x1b[4z<{self._tag} {self._xml_attr_data}>{rendered}\x1b[4z</{self._tag}>"
|
||||
else:
|
||||
rendered = f"\x1b[4z<{self._tag}>{rendered}\x1b[4z</{self._tag}>"
|
||||
else:
|
||||
if self._xml_attr:
|
||||
rendered = (
|
||||
f"{self._tag} {self._xml_attr_data}>{rendered}</{self._tag}>"
|
||||
)
|
||||
else:
|
||||
rendered = f"<{self._tag}>{rendered}</{self._tag}>"
|
||||
return rendered
|
||||
|
||||
def __add__(self, style: Union["Style", str]) -> "Style":
|
||||
if isinstance(style, str):
|
||||
style = self.__class__.parse(style)
|
||||
if not (isinstance(style, MudStyle) or style is None):
|
||||
return NotImplemented
|
||||
if style is None or style._null:
|
||||
return self
|
||||
if self._null:
|
||||
return style
|
||||
new_style: MudStyle = self.__new__(MudStyle)
|
||||
new_style._ansi = None
|
||||
new_style._style_definition = None
|
||||
new_style._color = style._color or self._color
|
||||
new_style._bgcolor = style._bgcolor or self._bgcolor
|
||||
new_style._attributes = (self._attributes & ~style._set_attributes) | (
|
||||
style._attributes & style._set_attributes
|
||||
)
|
||||
new_style._set_attributes = self._set_attributes | style._set_attributes
|
||||
new_style._link = style._link or self._link
|
||||
new_style._link_id = style._link_id or self._link_id
|
||||
|
||||
new_style._tag = None
|
||||
if hasattr(style, "_tag") and hasattr(self, "_tag"):
|
||||
new_style._tag = style._tag or self._tag
|
||||
|
||||
new_style._xml_attr = None
|
||||
if hasattr(style, "_xml_attr") and hasattr(self, "_xml_attr"):
|
||||
new_style._xml_attr = style._xml_attr or self._xml_attr
|
||||
|
||||
new_style._xml_attr_data = ""
|
||||
if hasattr(style, "_xml_attr_data") and hasattr(self, "_xml_attr_data"):
|
||||
new_style._xml_attr_data = style._xml_attr_data or self._xml_attr_data
|
||||
|
||||
new_style._hash = style._hash
|
||||
new_style._null = self._null or style._null
|
||||
if self._meta and style._meta:
|
||||
new_style._meta = dumps({**self.meta, **style.meta})
|
||||
else:
|
||||
new_style._meta = self._meta or style._meta
|
||||
|
||||
return new_style
|
||||
|
||||
def __radd__(self, other):
|
||||
if isinstance(other, str):
|
||||
other = self.__class__.parse(other)
|
||||
return other + self
|
||||
return NotImplemented
|
||||
|
||||
|
||||
@dataclass
|
||||
class MudConsoleOptions(OLD_CONSOLE_OPTIONS):
|
||||
mxp: Optional[bool] = False
|
||||
"""Enable MXP/MUD HTML when printing. For MUDs only."""
|
||||
pueblo: Optional[bool] = False
|
||||
"""Enable Pueblo/MUD HTML when printing. For MUDs only."""
|
||||
links: Optional[bool] = True
|
||||
"""Enable ANSI Links when printing. Turn off if MXP/Pueblo is on."""
|
||||
|
||||
def update(
|
||||
self,
|
||||
*,
|
||||
width: Union[int, NoChange] = NO_CHANGE,
|
||||
min_width: Union[int, NoChange] = NO_CHANGE,
|
||||
max_width: Union[int, NoChange] = NO_CHANGE,
|
||||
justify: Union[Optional[JustifyMethod], NoChange] = NO_CHANGE,
|
||||
overflow: Union[Optional[OverflowMethod], NoChange] = NO_CHANGE,
|
||||
no_wrap: Union[Optional[bool], NoChange] = NO_CHANGE,
|
||||
highlight: Union[Optional[bool], NoChange] = NO_CHANGE,
|
||||
markup: Union[Optional[bool], NoChange] = NO_CHANGE,
|
||||
height: Union[Optional[int], NoChange] = NO_CHANGE,
|
||||
mxp: Union[Optional[bool], NoChange] = NO_CHANGE,
|
||||
pueblo: Union[Optional[bool], NoChange] = NO_CHANGE,
|
||||
links: Union[Optional[bool], NoChange] = NO_CHANGE,
|
||||
) -> "ConsoleOptions":
|
||||
"""Update values, return a copy."""
|
||||
options = self.copy()
|
||||
if not isinstance(width, NoChange):
|
||||
options.min_width = options.max_width = max(0, width)
|
||||
if not isinstance(min_width, NoChange):
|
||||
options.min_width = min_width
|
||||
if not isinstance(max_width, NoChange):
|
||||
options.max_width = max_width
|
||||
if not isinstance(justify, NoChange):
|
||||
options.justify = justify
|
||||
if not isinstance(overflow, NoChange):
|
||||
options.overflow = overflow
|
||||
if not isinstance(no_wrap, NoChange):
|
||||
options.no_wrap = no_wrap
|
||||
if not isinstance(highlight, NoChange):
|
||||
options.highlight = highlight
|
||||
if not isinstance(markup, NoChange):
|
||||
options.markup = markup
|
||||
if not isinstance(height, NoChange):
|
||||
options.height = None if height is None else max(0, height)
|
||||
if not isinstance(mxp, NoChange):
|
||||
options.mxp = mxp
|
||||
if not isinstance(pueblo, NoChange):
|
||||
options.pueblo = pueblo
|
||||
if not isinstance(links, NoChange):
|
||||
options.links = links
|
||||
return options
|
||||
|
||||
|
||||
class MudConsole(OLD_CONSOLE):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
mxp = kwargs.pop("mxp", False)
|
||||
pueblo = kwargs.pop("pueblo", False)
|
||||
links = kwargs.pop("links", False)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._mxp = mxp
|
||||
self._pueblo = pueblo
|
||||
self._links = links
|
||||
|
||||
def export_text(self, *, clear: bool = True, styles: bool = False) -> str:
|
||||
"""Generate text from console contents (requires record=True argument in constructor).
|
||||
Args:
|
||||
clear (bool, optional): Clear record buffer after exporting. Defaults to ``True``.
|
||||
styles (bool, optional): If ``True``, ansi escape codes will be included. ``False`` for plain text.
|
||||
Defaults to ``False``.
|
||||
Returns:
|
||||
str: String containing console contents.
|
||||
"""
|
||||
assert (
|
||||
self.record
|
||||
), "To export console contents set record=True in the constructor or instance"
|
||||
|
||||
with self._record_buffer_lock:
|
||||
if styles:
|
||||
text = "".join(
|
||||
(style.render(
|
||||
text,
|
||||
color_system=self.color_system,
|
||||
legacy_windows=self.legacy_windows,
|
||||
mxp=self._mxp,
|
||||
pueblo=self._pueblo,
|
||||
links=self._links,
|
||||
) if style else text)
|
||||
for text, style, _ in self._record_buffer
|
||||
)
|
||||
else:
|
||||
text = "".join(
|
||||
segment.text
|
||||
for segment in self._record_buffer
|
||||
if not segment.control
|
||||
)
|
||||
if clear:
|
||||
del self._record_buffer[:]
|
||||
return text
|
||||
|
||||
def _render_buffer(self, buffer: Iterable[Segment]) -> str:
|
||||
"""Render buffered output, and clear buffer."""
|
||||
output: List[str] = []
|
||||
append = output.append
|
||||
color_system = self._color_system
|
||||
legacy_windows = self.legacy_windows
|
||||
not_terminal = not self.is_terminal
|
||||
if self.no_color and color_system:
|
||||
buffer = Segment.remove_color(buffer)
|
||||
for text, style, control in buffer:
|
||||
if style:
|
||||
append(
|
||||
style.render(
|
||||
text,
|
||||
color_system=color_system,
|
||||
legacy_windows=legacy_windows,
|
||||
mxp=self._mxp,
|
||||
pueblo=self._pueblo,
|
||||
links=self._links,
|
||||
)
|
||||
)
|
||||
elif not (not_terminal and control):
|
||||
append(text)
|
||||
|
||||
rendered = "".join(output)
|
||||
return rendered
|
||||
|
||||
|
||||
class MudText(OLD_TEXT):
|
||||
|
||||
def __radd__(self, other):
|
||||
if isinstance(other, str):
|
||||
other = self.__class__(text=other)
|
||||
return other + self
|
||||
return NotImplemented
|
||||
|
||||
def __iadd__(self, other: Any) -> "Text":
|
||||
if isinstance(other, (str, OLD_TEXT)):
|
||||
self.append(other)
|
||||
return self
|
||||
return NotImplemented
|
||||
|
||||
def __mul__(self, other):
|
||||
if not isinstance(other, int):
|
||||
return self
|
||||
if other <= 0:
|
||||
return self.__class__()
|
||||
if other == 1:
|
||||
return self.copy()
|
||||
if other > 1:
|
||||
out = self.copy()
|
||||
for i in range(other - 1):
|
||||
out.append(self)
|
||||
return out
|
||||
|
||||
def __rmul__(self, other):
|
||||
if not isinstance(other, int):
|
||||
return self
|
||||
return self * other
|
||||
|
||||
def __format__(self, format_spec):
|
||||
"""
|
||||
Allows use of f-strings, although styling is not preserved.
|
||||
"""
|
||||
return self.plain.__format__(format_spec)
|
||||
|
||||
# Begin implementing Python String Api below...
|
||||
|
||||
def capitalize(self):
|
||||
return self.__class__(text=self.plain.capitalize(), style=self.style, spans=list(self.spans))
|
||||
|
||||
def count(self, *args, **kwargs):
|
||||
return self.plain.count(*args, **kwargs)
|
||||
|
||||
def startswith(self, *args, **kwargs):
|
||||
return self.plain.startswith(*args, **kwargs)
|
||||
|
||||
def endswith(self, *args, **kwargs):
|
||||
return self.plain.endswith(*args, **kwargs)
|
||||
|
||||
def find(self, *args, **kwargs):
|
||||
return self.plain.find(*args, **kwargs)
|
||||
|
||||
def index(self, *args, **kwargs):
|
||||
return self.plain.index(*args, **kwargs)
|
||||
|
||||
def isalnum(self):
|
||||
return self.plain.isalnum()
|
||||
|
||||
def isalpha(self):
|
||||
return self.plain.isalpha()
|
||||
|
||||
def isdecimal(self):
|
||||
return self.plain.isdecimal()
|
||||
|
||||
def isdigit(self):
|
||||
return self.plain.isdigit()
|
||||
|
||||
def isidentifier(self):
|
||||
return self.plain.isidentifier()
|
||||
|
||||
def islower(self):
|
||||
return self.plain.islower()
|
||||
|
||||
def isnumeric(self):
|
||||
return self.plain.isnumeric()
|
||||
|
||||
def isprintable(self):
|
||||
return self.plain.isprintable()
|
||||
|
||||
def isspace(self):
|
||||
return self.plain.isspace()
|
||||
|
||||
def istitle(self):
|
||||
return self.plain.istitle()
|
||||
|
||||
def isupper(self):
|
||||
return self.plain.isupper()
|
||||
|
||||
def center(self, width, fillchar=" "):
|
||||
changed = self.plain.center(width, fillchar)
|
||||
start = changed.find(self.plain)
|
||||
lside = changed[:start]
|
||||
rside = changed[len(lside) + len(self.plain):]
|
||||
idx = self.disassemble_bits()
|
||||
new_idx = list()
|
||||
for c in lside:
|
||||
new_idx.append((None, c))
|
||||
new_idx.extend(idx)
|
||||
for c in rside:
|
||||
new_idx.append((None, c))
|
||||
return self.__class__.assemble_bits(new_idx)
|
||||
|
||||
def ljust(self, width: int, fillchar: Union[str, "MudText"] = " "):
|
||||
diff = width - len(self)
|
||||
out = self.copy()
|
||||
if diff <= 0:
|
||||
return out
|
||||
else:
|
||||
if isinstance(fillchar, str):
|
||||
fillchar = self.__class__(fillchar)
|
||||
out.append(fillchar * diff)
|
||||
return out
|
||||
|
||||
def rjust(self, width: int, fillchar: Union[str, "MudText"] = " "):
|
||||
diff = width - len(self)
|
||||
if diff <= 0:
|
||||
return self.copy()
|
||||
else:
|
||||
if isinstance(fillchar, str):
|
||||
fillchar = self.__class__(fillchar)
|
||||
out = fillchar * diff
|
||||
out.append(self)
|
||||
return out
|
||||
|
||||
def lstrip(self, chars: str = None):
|
||||
lstripped = self.plain.lstrip(chars)
|
||||
strip_count = len(self.plain) - len(lstripped)
|
||||
return self[strip_count:]
|
||||
|
||||
def strip(self, chars: str = " "):
|
||||
out_map = self.disassemble_bits()
|
||||
for i, e in enumerate(out_map):
|
||||
if e[1] != chars:
|
||||
out_map = out_map[i:]
|
||||
break
|
||||
out_map.reverse()
|
||||
for i, e in enumerate(out_map):
|
||||
if e[1] != chars:
|
||||
out_map = out_map[i:]
|
||||
break
|
||||
out_map.reverse()
|
||||
return self.__class__.assemble_bits(out_map)
|
||||
|
||||
def replace(self, old: str, new: Union[str, "Text"], count=None) -> "Text":
|
||||
if not (indexes := self.find_all(old)):
|
||||
return self.clone()
|
||||
if count and count > 0:
|
||||
indexes = indexes[:count]
|
||||
old_len = len(old)
|
||||
new_len = len(new)
|
||||
other = self.clone()
|
||||
markup_idx_map = self.disassemble_bits()
|
||||
other_map = other.disassemble_bits()
|
||||
|
||||
for idx in reversed(indexes):
|
||||
final_markup = markup_idx_map[idx + old_len][0]
|
||||
diff = abs(old_len - new_len)
|
||||
replace_chars = min(new_len, old_len)
|
||||
# First, replace any characters that overlap.
|
||||
for i in range(replace_chars):
|
||||
other_map[idx + i] = (markup_idx_map[idx + i][0], new[i])
|
||||
if old_len == new_len:
|
||||
pass # the nicest case. nothing else needs doing.
|
||||
elif old_len > new_len:
|
||||
# slightly complex. pop off remaining characters.
|
||||
for i in range(diff):
|
||||
deleted = other_map.pop(idx + new_len)
|
||||
elif new_len > old_len:
|
||||
# slightly complex. insert new characters.
|
||||
for i in range(diff):
|
||||
other_map.insert(
|
||||
idx + old_len + i, (final_markup, new[old_len + i])
|
||||
)
|
||||
|
||||
return self.__class__.assemble_bits(other_map)
|
||||
|
||||
def find_all(self, sub: str):
|
||||
indexes = list()
|
||||
start = 0
|
||||
while True:
|
||||
start = self.plain.find(sub, start)
|
||||
if start == -1:
|
||||
return indexes
|
||||
indexes.append(start)
|
||||
start += len(sub)
|
||||
|
||||
def scramble(self):
|
||||
idx = self.disassemble_bits()
|
||||
random.shuffle(idx)
|
||||
return self.__class__.assemble_bits(idx)
|
||||
|
||||
def reverse(self):
|
||||
idx = self.disassemble_bits()
|
||||
idx.reverse()
|
||||
return self.__class__.assemble_bits(idx)
|
||||
|
||||
@classmethod
|
||||
def assemble_bits(cls, idx: List[Tuple[Optional[Union[str, MudStyle, None]], str]]):
|
||||
out = cls()
|
||||
for i, t in enumerate(idx):
|
||||
s = [Span(0, 1, t[0])]
|
||||
out.append_text(cls(text=t[1], spans=s))
|
||||
return out
|
||||
|
||||
def style_at_index(self, offset: int) -> MudStyle:
|
||||
if offset < 0:
|
||||
offset = len(self) + offset
|
||||
style = MudStyle.null()
|
||||
for start, end, span_style in self._spans:
|
||||
if end > offset >= start:
|
||||
style = style + span_style
|
||||
return style
|
||||
|
||||
def disassemble_bits(self) -> List[Tuple[Optional[Union[str, MudStyle, None]], str]]:
|
||||
idx = list()
|
||||
for i, c in enumerate(self.plain):
|
||||
idx.append((self.style_at_index(i), c))
|
||||
return idx
|
||||
|
||||
def squish(self) -> "MudText":
|
||||
"""
|
||||
Removes leading and trailing whitespace, and coerces all internal whitespace sequences
|
||||
into at most a single space. Returns the results.
|
||||
"""
|
||||
out = list()
|
||||
matches = _RE_SQUISH.finditer(self.plain)
|
||||
for match in matches:
|
||||
out.append(self[match.start(): match.end()])
|
||||
return self.__class__(" ").join(out)
|
||||
|
||||
def squish_spaces(self) -> "MudText":
|
||||
"""
|
||||
Like squish, but retains newlines and tabs. Just squishes spaces.
|
||||
"""
|
||||
out = list()
|
||||
matches = _RE_NOTSPACE.finditer(self.plain)
|
||||
for match in matches:
|
||||
out.append(self[match.start(): match.end()])
|
||||
return self.__class__(" ").join(out)
|
||||
|
||||
def serialize(self) -> dict:
|
||||
def ser_style(style):
|
||||
if isinstance(style, str):
|
||||
style = MudStyle.parse(style)
|
||||
if not isinstance(style, MudStyle):
|
||||
style = MudStyle.upgrade(style)
|
||||
return style.serialize()
|
||||
|
||||
def ser_span(span):
|
||||
if not span.style:
|
||||
return None
|
||||
return {
|
||||
"start": span.start,
|
||||
"end": span.end,
|
||||
"style": ser_style(span.style),
|
||||
}
|
||||
|
||||
out = {"text": self.plain}
|
||||
|
||||
if self.style:
|
||||
out["style"] = ser_style(self.style)
|
||||
|
||||
out_spans = [s for span in self.spans if (s := ser_span(span))]
|
||||
|
||||
if out_spans:
|
||||
out["spans"] = out_spans
|
||||
|
||||
return out
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, data) -> "Text":
|
||||
text = data.get("text", None)
|
||||
if text is None:
|
||||
return cls("")
|
||||
style = data.get("style", None)
|
||||
if style:
|
||||
style = MudStyle(**style)
|
||||
|
||||
spans = data.get("spans", None)
|
||||
|
||||
if spans:
|
||||
spans = [Span(s["start"], s["end"], MudStyle(**s["style"])) for s in spans]
|
||||
|
||||
return cls(text=text, style=style, spans=spans)
|
||||
|
||||
|
||||
DEFAULT_STYLES = dict()
|
||||
|
||||
|
||||
def install():
|
||||
from rich import style, text, console, default_styles, themes, syntax, traceback
|
||||
global DEFAULT_STYLES
|
||||
style.Style = MudStyle
|
||||
style.NULL_STYLE = MudStyle()
|
||||
text.Text = MudText
|
||||
console.Console = MudConsole
|
||||
console.ConsoleOptions = MudConsoleOptions
|
||||
|
||||
traceback.Style = MudStyle
|
||||
syntax.Style = MudStyle
|
||||
traceback.Text = MudText
|
||||
syntax.Text = MudText
|
||||
|
||||
for k, v in default_styles.DEFAULT_STYLES.items():
|
||||
DEFAULT_STYLES[k] = MudStyle.upgrade(v)
|
||||
|
||||
for theme in syntax.RICH_SYNTAX_THEMES.values():
|
||||
for k, v in theme.items():
|
||||
if isinstance(v, OLD_STYLE):
|
||||
theme[k] = MudStyle.upgrade(v)
|
||||
|
||||
default_styles.DEFAULT_STYLES = DEFAULT_STYLES
|
||||
themes.DEFAULT = themes.Theme(DEFAULT_STYLES)
|
||||
@ -319,7 +319,7 @@ class ObjectAdmin(admin.ModelAdmin):
|
||||
|
||||
if account:
|
||||
account.db._last_puppet = obj
|
||||
account.add_character(obj)
|
||||
account.add_character_to_playable_list(obj)
|
||||
if not obj.access(account, "puppet"):
|
||||
lock = obj.locks.get("puppet")
|
||||
lock += f" or pid({account.id})"
|
||||
|
||||
@ -35,8 +35,8 @@ class EvenniaWebTest(BaseEvenniaTest):
|
||||
super().setUp()
|
||||
|
||||
# Add chars to account rosters
|
||||
self.account.db._playable_characters = [self.char1]
|
||||
self.account2.db._playable_characters = [self.char2]
|
||||
self.account.add_character_to_playable_list(self.char1)
|
||||
self.account2.add_character_to_playable_list(self.char2)
|
||||
|
||||
for account in (self.account, self.account2):
|
||||
# Demote accounts to Player permissions
|
||||
@ -44,15 +44,15 @@ class EvenniaWebTest(BaseEvenniaTest):
|
||||
account.permissions.remove("Developer")
|
||||
|
||||
# Grant permissions to chars
|
||||
for char in account.db._playable_characters:
|
||||
for char in account.characters:
|
||||
char.locks.add("edit:id(%s) or perm(Admin)" % account.pk)
|
||||
char.locks.add("delete:id(%s) or perm(Admin)" % account.pk)
|
||||
char.locks.add("view:all()")
|
||||
|
||||
def test_valid_chars(self):
|
||||
"Make sure account has playable characters"
|
||||
self.assertTrue(self.char1 in self.account.db._playable_characters)
|
||||
self.assertTrue(self.char2 in self.account2.db._playable_characters)
|
||||
self.assertTrue(self.char1 in self.account.characters)
|
||||
self.assertTrue(self.char2 in self.account2.characters)
|
||||
|
||||
def get_kwargs(self):
|
||||
return {}
|
||||
@ -220,7 +220,7 @@ class CharacterCreateView(EvenniaWebTest):
|
||||
@override_settings(MAX_NR_CHARACTERS=1)
|
||||
def test_valid_access_multisession_0(self):
|
||||
"Account1 with no characters should be able to create a new one"
|
||||
self.account.db._playable_characters = []
|
||||
self.assertFalse(self.account.characters, "Account1 has characters but shouldn't!")
|
||||
|
||||
# Login account
|
||||
self.login()
|
||||
@ -233,9 +233,9 @@ class CharacterCreateView(EvenniaWebTest):
|
||||
|
||||
# Make sure the character was actually created
|
||||
self.assertTrue(
|
||||
len(self.account.db._playable_characters) == 1,
|
||||
len(self.account.characters) == 1,
|
||||
"Account only has the following characters attributed to it: %s"
|
||||
% self.account.db._playable_characters,
|
||||
% self.account.characters,
|
||||
)
|
||||
|
||||
@override_settings(MAX_NR_CHARACTERS=5)
|
||||
@ -252,9 +252,9 @@ class CharacterCreateView(EvenniaWebTest):
|
||||
|
||||
# Make sure the character was actually created
|
||||
self.assertTrue(
|
||||
len(self.account.db._playable_characters) > 1,
|
||||
len(self.account.characters) > 1,
|
||||
"Account only has the following characters attributed to it: %s"
|
||||
% self.account.db._playable_characters,
|
||||
% self.account.characters,
|
||||
)
|
||||
|
||||
|
||||
@ -352,7 +352,7 @@ class CharacterDeleteView(EvenniaWebTest):
|
||||
|
||||
# Make sure it deleted
|
||||
self.assertFalse(
|
||||
self.char1 in self.account.db._playable_characters,
|
||||
self.char1 in self.account.characters,
|
||||
"Char1 is still in Account playable characters list.",
|
||||
)
|
||||
|
||||
|
||||
@ -84,6 +84,7 @@ dependencies = [
|
||||
"black >= 22.6",
|
||||
"isort >= 5.10",
|
||||
"parameterized ==0.8.1",
|
||||
"rich >= 13.3.5,
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user