Merge branch 'develop' of github.com:Tegiminis/evennia into develop

# Conflicts:
#	evennia/typeclasses/attributes.py
This commit is contained in:
Tegiminis 2022-07-10 02:34:13 -07:00
commit da0e380fa5
73 changed files with 2774 additions and 1099 deletions

View file

@ -678,7 +678,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
typeclass (str, optional): Typeclass to use for this character. If
not given, use settings.BASE_CHARACTER_TYPECLASS.
permissions (list, optional): If not given, use the account's permissions.
ip (str, optiona): The client IP creating this character. Will fall back to the
ip (str, optional): The client IP creating this character. Will fall back to the
one stored for the account if not given.
kwargs (any): Other kwargs will be used in the create_call.
Returns:
@ -955,7 +955,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
kwargs (any): Other keyword arguments will be added to the
found command object instance as variables before it
executes. This is unused by default Evennia but may be
used to set flags and change operating paramaters for
used to set flags and change operating parameters for
commands at run-time.
"""
@ -1433,7 +1433,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key))
if _MULTISESSION_MODE == 0:
# in this mode we should have only one character available. We
# try to auto-connect to our last conneted object, if any
# try to auto-connect to our last connected object, if any
try:
self.puppet_object(session, self.db._last_puppet)
except RuntimeError:
@ -1460,7 +1460,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
"""
Called by the login process if a user account is targeted correctly
but provided with an invalid password. By default it does nothing,
but exists to be overriden.
but exists to be overridden.
Args:
session (session): Session logging in.
@ -1703,7 +1703,7 @@ class DefaultGuest(DefaultAccount):
Gets or creates a Guest account object.
Keyword Args:
ip (str, optional): IP address of requestor; used for ban checking,
ip (str, optional): IP address of requester; used for ban checking,
throttling and logging
Returns:

View file

@ -450,9 +450,7 @@ class CmdSetHandler(object):
"""
if "permanent" in kwargs:
logger.log_dep(
"obj.cmdset.add() kwarg 'permanent' has changed name to 'persistent'."
)
logger.log_dep("obj.cmdset.add() kwarg 'permanent' has changed name to 'persistent'.")
persistent = kwargs["permanent"] if persistent is False else persistent
if not (isinstance(cmdset, str) or utils.inherits_from(cmdset, CmdSet)):

View file

@ -1071,7 +1071,7 @@ class CmdTunnel(COMMAND_DEFAULT_CLASS):
exitname, backshort = self.directions[exitshort]
backname = self.directions[backshort][0]
# if we recieved a typeclass for the exit, add it to the alias(short name)
# if we received a typeclass for the exit, add it to the alias(short name)
if ":" in self.lhs:
# limit to only the first : character
exit_typeclass = ":" + self.lhs.split(":", 1)[-1]
@ -1665,7 +1665,7 @@ class CmdSetAttribute(ObjManipCommand):
def split_nested_attr(self, attr):
"""
Yields tuples of (possible attr name, nested keys on that attr).
For performance, this is biased to the deepest match, but allows compatability
For performance, this is biased to the deepest match, but allows compatibility
with older attrs that might have been named with `[]`'s.
> list(split_nested_attr("nested['asdf'][0]"))
@ -2219,11 +2219,13 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
old_typeclass_path = obj.typeclass_path
if reset:
answer = yield("|yNote that this will reset the object back to its typeclass' default state, "
"removing any custom locks/perms/attributes etc that may have been added "
"by an explicit create_object call. Use `update` or type/force instead in order "
"to keep such data. "
"Continue [Y]/N?|n")
answer = yield (
"|yNote that this will reset the object back to its typeclass' default state, "
"removing any custom locks/perms/attributes etc that may have been added "
"by an explicit create_object call. Use `update` or type/force instead in order "
"to keep such data. "
"Continue [Y]/N?|n"
)
if answer.upper() in ("N", "NO"):
caller.msg("Aborted.")
return
@ -2732,7 +2734,7 @@ class CmdExamine(ObjManipCommand):
return
if ndb_attr and ndb_attr[0]:
return "\n " + " \n".join(
return "\n " + "\n ".join(
sorted(self.format_single_attribute(attr) for attr in ndb_attr)
)
@ -2830,7 +2832,7 @@ class CmdExamine(ObjManipCommand):
objdata["Stored Cmdset(s)"] = self.format_stored_cmdsets(obj)
objdata["Merged Cmdset(s)"] = self.format_merged_cmdsets(obj, current_cmdset)
objdata[
f"Commands vailable to {obj.key} (result of Merged Cmdset(s))"
f"Commands available to {obj.key} (result of Merged Cmdset(s))"
] = self.format_current_cmds(obj, current_cmdset)
if self.object_type == "script":
objdata["Description"] = self.format_script_desc(obj)
@ -3473,7 +3475,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
caller.msg("\n".join(msgs))
if "delete" not in self.switches:
if script and script.pk:
ScriptEvMore(caller, [script], session=self.session)
ScriptEvMore(caller, [script], session=self.session)
else:
caller.msg("Script was deleted automatically.")
else:
@ -4029,7 +4031,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
)
return
try:
# we homogenize the protoype first, to be more lenient with free-form
# we homogenize the prototype first, to be more lenient with free-form
protlib.validate_prototype(protlib.homogenize_prototype(prototype))
except RuntimeError as err:
self.caller.msg(str(err))

View file

@ -1818,7 +1818,7 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
class CmdGrapevine2Chan(COMMAND_DEFAULT_CLASS):
"""
Link an Evennia channel to an exteral Grapevine channel
Link an Evennia channel to an external Grapevine channel
Usage:
grapevine2chan[/switches] <evennia_channel> = <grapevine_channel>

View file

@ -67,7 +67,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
help <topic>/<subtopic>/<subsubtopic> ...
Use the 'help' command alone to see an index of all help topics, organized
by category.eSome big topics may offer additional sub-topics.
by category. Some big topics may offer additional sub-topics.
"""
@ -138,7 +138,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
click_topics=True,
):
"""This visually formats the help entry.
This method can be overriden to customize the way a help
This method can be overridden to customize the way a help
entry is displayed.
Args:

View file

@ -107,8 +107,7 @@ class TestGeneral(BaseEvenniaCommandTest):
def test_nick_list(self):
self.call(general.CmdNick(), "/list", "No nicks defined.")
self.call(general.CmdNick(), "test1 = Hello",
"Inputline-nick 'test1' mapped to 'Hello'.")
self.call(general.CmdNick(), "test1 = Hello", "Inputline-nick 'test1' mapped to 'Hello'.")
self.call(general.CmdNick(), "/list", "Defined Nicks:")
def test_get_and_drop(self):
@ -1295,7 +1294,8 @@ class TestBuilding(BaseEvenniaCommandTest):
"Obj2 = evennia.objects.objects.DefaultExit",
"Obj2 changed typeclass from evennia.objects.objects.DefaultObject "
"to evennia.objects.objects.DefaultExit.",
cmdstring="swap", inputs=["yes"],
cmdstring="swap",
inputs=["yes"],
)
self.call(building.CmdTypeclass(), "/list Obj", "Core typeclasses")
self.call(
@ -1332,7 +1332,7 @@ class TestBuilding(BaseEvenniaCommandTest):
"/reset/force Obj=evennia.objects.objects.DefaultObject",
"Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\n"
"All object creation hooks were run. All old attributes where deleted before the swap.",
inputs=["yes"]
inputs=["yes"],
)
from evennia.prototypes.prototypes import homogenize_prototype
@ -1359,7 +1359,7 @@ class TestBuilding(BaseEvenniaCommandTest):
"typeclasses.objects.Object.\nOnly the at_object_creation hook was run "
"(update mode). Attributes set before swap were not removed\n"
"(use `swap` or `type/reset` to clear all). Prototype 'replaced_obj' was "
"successfully applied over the object type."
"successfully applied over the object type.",
)
assert self.obj1.db.desc == "protdesc"

View file

@ -17,8 +17,10 @@ def get_component_class(component_name):
subclasses = Component.__subclasses__()
component_class = next((sc for sc in subclasses if sc.name == component_name), None)
if component_class is None:
message = f"Component named {component_name} has not been found. " \
f"Make sure it has been imported before being used."
message = (
f"Component named {component_name} has not been found. "
f"Make sure it has been imported before being used."
)
raise Exception(message)
return component_class

View file

@ -13,6 +13,7 @@ class Component:
Each Component must supply the name, it is used as a slot name but also part of the attribute key.
"""
name = ""
def __init__(self, host=None):

View file

@ -26,7 +26,7 @@ class DBField(AttributeProperty):
db_fields = getattr(owner, "_db_fields", None)
if db_fields is None:
db_fields = {}
setattr(owner, '_db_fields', db_fields)
setattr(owner, "_db_fields", db_fields)
db_fields[name] = self
@ -50,7 +50,7 @@ class NDBField(NAttributeProperty):
ndb_fields = getattr(owner, "_ndb_fields", None)
if ndb_fields is None:
ndb_fields = {}
setattr(owner, '_ndb_fields', ndb_fields)
setattr(owner, "_ndb_fields", ndb_fields)
ndb_fields[name] = self
@ -64,6 +64,7 @@ class TagField:
Default value of a tag is added when the component is registered.
Tags are removed if the component itself is removed.
"""
def __init__(self, default=None, enforce_single=False):
self._category_key = None
self._default = default
@ -78,7 +79,7 @@ class TagField:
tag_fields = getattr(owner, "_tag_fields", None)
if tag_fields is None:
tag_fields = {}
setattr(owner, '_tag_fields', tag_fields)
setattr(owner, "_tag_fields", tag_fields)
tag_fields[name] = self
def __get__(self, instance, owner):

View file

@ -16,6 +16,7 @@ class ComponentProperty:
Defaults can be overridden for this typeclass by passing kwargs
"""
def __init__(self, component_name, **kwargs):
"""
Initializes the descriptor
@ -49,6 +50,7 @@ class ComponentHandler:
It lets you add or remove components and will load components as needed.
It stores the list of registered components on the host .db with component_names as key.
"""
def __init__(self, host):
self.host = host
self._loaded_components = {}
@ -124,7 +126,9 @@ class ComponentHandler:
self.host.signals.remove_object_listeners_and_responders(component)
del self._loaded_components[component_name]
else:
message = f"Cannot remove {component_name} from {self.host.name} as it is not registered."
message = (
f"Cannot remove {component_name} from {self.host.name} as it is not registered."
)
raise ComponentIsNotRegistered(message)
def remove_by_name(self, name):
@ -199,7 +203,9 @@ class ComponentHandler:
self._set_component(component_instance)
self.host.signals.add_object_listeners_and_responders(component_instance)
else:
message = f"Could not initialize runtime component {component_name} of {self.host.name}"
message = (
f"Could not initialize runtime component {component_name} of {self.host.name}"
)
raise ComponentDoesNotExist(message)
def _set_component(self, component):

View file

@ -15,9 +15,11 @@ def as_listener(func=None, signal_name=None):
signal_name (str): The name of the signal to listen to, defaults to function name.
"""
if not func and signal_name:
def wrapper(func):
func._listener_signal_name = signal_name
return func
return wrapper
signal_name = func.__name__
@ -35,9 +37,11 @@ def as_responder(func=None, signal_name=None):
signal_name (str): The name of the signal to respond to, defaults to function name.
"""
if not func and signal_name:
def wrapper(func):
func._responder_signal_name = signal_name
return func
return wrapper
signal_name = func.__name__
@ -177,12 +181,12 @@ class SignalsHandler(object):
"""
type_host = type(obj)
for att_name, att_obj in type_host.__dict__.items():
listener_signal_name = getattr(att_obj, '_listener_signal_name', None)
listener_signal_name = getattr(att_obj, "_listener_signal_name", None)
if listener_signal_name:
callback = getattr(obj, att_name)
self.add_listener(signal_name=listener_signal_name, callback=callback)
responder_signal_name = getattr(att_obj, '_responder_signal_name', None)
responder_signal_name = getattr(att_obj, "_responder_signal_name", None)
if responder_signal_name:
callback = getattr(obj, att_name)
self.add_responder(signal_name=responder_signal_name, callback=callback)
@ -196,12 +200,12 @@ class SignalsHandler(object):
"""
type_host = type(obj)
for att_name, att_obj in type_host.__dict__.items():
listener_signal_name = getattr(att_obj, '_listener_signal_name', None)
listener_signal_name = getattr(att_obj, "_listener_signal_name", None)
if listener_signal_name:
callback = getattr(obj, att_name)
self.remove_listener(signal_name=listener_signal_name, callback=callback)
responder_signal_name = getattr(att_obj, '_responder_signal_name', None)
responder_signal_name = getattr(att_obj, "_responder_signal_name", None)
if responder_signal_name:
callback = getattr(obj, att_name)
self.remove_responder(signal_name=responder_signal_name, callback=callback)

View file

@ -56,7 +56,7 @@ class TestComponents(EvenniaTest):
def test_character_can_register_runtime_component(self):
rct = RuntimeComponentTestC.create(self.char1)
self.char1.components.add(rct)
test_c = self.char1.components.get('test_c')
test_c = self.char1.components.get("test_c")
assert test_c
assert test_c.my_int == 6
@ -110,7 +110,7 @@ class TestComponents(EvenniaTest):
assert handler.get("test_c") is rct
def test_can_access_component_regular_get(self):
assert self.char1.cmp.test_a is self.char1.components.get('test_a')
assert self.char1.cmp.test_a is self.char1.components.get("test_a")
def test_returns_none_with_regular_get_when_no_attribute(self):
assert self.char1.cmp.does_not_exist is None
@ -127,7 +127,7 @@ class TestComponents(EvenniaTest):
def test_host_has_added_component_tags(self):
rct = RuntimeComponentTestC.create(self.char1)
self.char1.components.add(rct)
test_c = self.char1.components.get('test_c')
test_c = self.char1.components.get("test_c")
assert self.char1.tags.has(key="test_c", category="components")
assert self.char1.tags.has(key="added_value", category="test_c::added_tag")
@ -162,7 +162,7 @@ class TestComponents(EvenniaTest):
assert not self.char1.tags.has(key="added_value", category="test_c::added_tag")
def test_component_tags_only_hold_one_value_when_enforce_single(self):
test_b = self.char1.components.get('test_b')
test_b = self.char1.components.get("test_b")
test_b.single_tag = "first_value"
test_b.single_tag = "second value"
@ -171,7 +171,7 @@ class TestComponents(EvenniaTest):
assert not self.char1.tags.has(key="first_value", category="test_b::single_tag")
def test_component_tags_default_value_is_overridden_when_enforce_single(self):
test_b = self.char1.components.get('test_b')
test_b = self.char1.components.get("test_b")
test_b.default_single_tag = "second value"
assert self.char1.tags.has(key="second value", category="test_b::default_single_tag")
@ -179,12 +179,14 @@ class TestComponents(EvenniaTest):
assert not self.char1.tags.has(key="first_value", category="test_b::default_single_tag")
def test_component_tags_support_multiple_values_by_default(self):
test_b = self.char1.components.get('test_b')
test_b = self.char1.components.get("test_b")
test_b.multiple_tags = "first value"
test_b.multiple_tags = "second value"
test_b.multiple_tags = "third value"
assert all(val in test_b.multiple_tags for val in ("first value", "second value", "third value"))
assert all(
val in test_b.multiple_tags for val in ("first value", "second value", "third value")
)
assert self.char1.tags.has(key="first value", category="test_b::multiple_tags")
assert self.char1.tags.has(key="second value", category="test_b::multiple_tags")
assert self.char1.tags.has(key="third value", category="test_b::multiple_tags")
@ -193,11 +195,11 @@ class TestComponents(EvenniaTest):
class CharWithSignal(ComponentHolderMixin, DefaultCharacter):
@signals.as_listener
def my_signal(self):
setattr(self, 'my_signal_is_called', True)
setattr(self, "my_signal_is_called", True)
@signals.as_listener
def my_other_signal(self):
setattr(self, 'my_other_signal_is_called', True)
setattr(self, "my_other_signal_is_called", True)
@signals.as_responder
def my_response(self):
@ -213,11 +215,11 @@ class ComponentWithSignal(Component):
@signals.as_listener
def my_signal(self):
setattr(self, 'my_signal_is_called', True)
setattr(self, "my_signal_is_called", True)
@signals.as_listener
def my_other_signal(self):
setattr(self, 'my_other_signal_is_called', True)
setattr(self, "my_other_signal_is_called", True)
@signals.as_responder
def my_response(self):
@ -236,14 +238,15 @@ class TestComponentSignals(BaseEvenniaTest):
def setUp(self):
super().setUp()
self.char1 = create.create_object(
CharWithSignal, key="Char",
CharWithSignal,
key="Char",
)
def test_host_can_register_as_listener(self):
self.char1.signals.trigger("my_signal")
assert self.char1.my_signal_is_called
assert not getattr(self.char1, 'my_other_signal_is_called', None)
assert not getattr(self.char1, "my_other_signal_is_called", None)
def test_host_can_register_as_responder(self):
responses = self.char1.signals.query("my_response")
@ -258,7 +261,7 @@ class TestComponentSignals(BaseEvenniaTest):
component = char.cmp.test_signal_a
assert component.my_signal_is_called
assert not getattr(component, 'my_other_signal_is_called', None)
assert not getattr(component, "my_other_signal_is_called", None)
def test_component_can_register_as_responder(self):
char = self.char1

View file

@ -328,4 +328,4 @@ class GametimeScript(DefaultScript):
callback()
seconds = real_seconds_until(**self.db.gametime)
self.restart(interval=seconds)
self.start(interval=seconds, force_restart=True)

View file

@ -4,7 +4,6 @@ Roleplaying emotes and language - Griatch, 2015
"""
from .rpsystem import EmoteError, SdescError, RecogError, LanguageError # noqa
from .rpsystem import ordered_permutation_regex, regex_tuple_from_key_alias # noqa
from .rpsystem import parse_language, parse_sdescs_and_recogs, send_emote # noqa
from .rpsystem import SdescHandler, RecogHandler # noqa
from .rpsystem import RPCommand, CmdEmote, CmdSay, CmdSdesc, CmdPose, CmdRecog, CmdMask # noqa

File diff suppressed because it is too large Load diff

View file

@ -96,7 +96,7 @@ recog01 = "Mr Receiver"
recog02 = "Mr Receiver2"
recog10 = "Mr Sender"
emote = 'With a flair, /me looks at /first and /colliding sdesc-guy. She says "This is a test."'
case_emote = "/me looks at /first, then /FIRST, /First and /Colliding twice."
case_emote = "/Me looks at /first. Then, /me looks at /FIRST, /First and /Colliding twice."
class TestRPSystem(BaseEvenniaTest):
@ -113,41 +113,11 @@ class TestRPSystem(BaseEvenniaTest):
rpsystem.ContribRPCharacter, key="Receiver2", location=self.room
)
def test_ordered_permutation_regex(self):
self.assertEqual(
rpsystem.ordered_permutation_regex(sdesc0),
"/[0-9]*-*A\\ nice\\ sender\\ of\\ emotes(?=\\W|$)+|"
"/[0-9]*-*nice\\ sender\\ of\\ emotes(?=\\W|$)+|"
"/[0-9]*-*A\\ nice\\ sender\\ of(?=\\W|$)+|"
"/[0-9]*-*sender\\ of\\ emotes(?=\\W|$)+|"
"/[0-9]*-*nice\\ sender\\ of(?=\\W|$)+|"
"/[0-9]*-*A\\ nice\\ sender(?=\\W|$)+|"
"/[0-9]*-*nice\\ sender(?=\\W|$)+|"
"/[0-9]*-*of\\ emotes(?=\\W|$)+|"
"/[0-9]*-*sender\\ of(?=\\W|$)+|"
"/[0-9]*-*A\\ nice(?=\\W|$)+|"
"/[0-9]*-*emotes(?=\\W|$)+|"
"/[0-9]*-*sender(?=\\W|$)+|"
"/[0-9]*-*nice(?=\\W|$)+|"
"/[0-9]*-*of(?=\\W|$)+|"
"/[0-9]*-*A(?=\\W|$)+",
)
def test_sdesc_handler(self):
self.speaker.sdesc.add(sdesc0)
self.assertEqual(self.speaker.sdesc.get(), sdesc0)
self.speaker.sdesc.add("This is {#324} ignored")
self.assertEqual(self.speaker.sdesc.get(), "This is 324 ignored")
self.speaker.sdesc.add("Testing three words")
self.assertEqual(
self.speaker.sdesc.get_regex_tuple()[0].pattern,
"/[0-9]*-*Testing\ three\ words(?=\W|$)+|"
"/[0-9]*-*Testing\ three(?=\W|$)+|"
"/[0-9]*-*three\ words(?=\W|$)+|"
"/[0-9]*-*Testing(?=\W|$)+|"
"/[0-9]*-*three(?=\W|$)+|"
"/[0-9]*-*words(?=\W|$)+",
)
def test_recog_handler(self):
self.speaker.sdesc.add(sdesc0)
@ -156,12 +126,8 @@ class TestRPSystem(BaseEvenniaTest):
self.speaker.recog.add(self.receiver2, recog02)
self.assertEqual(self.speaker.recog.get(self.receiver1), recog01)
self.assertEqual(self.speaker.recog.get(self.receiver2), recog02)
self.assertEqual(
self.speaker.recog.get_regex_tuple(self.receiver1)[0].pattern,
"/[0-9]*-*Mr\\ Receiver(?=\\W|$)+|/[0-9]*-*Receiver(?=\\W|$)+|/[0-9]*-*Mr(?=\\W|$)+",
)
self.speaker.recog.remove(self.receiver1)
self.assertEqual(self.speaker.recog.get(self.receiver1), sdesc1)
self.assertEqual(self.speaker.recog.get(self.receiver1), None)
self.assertEqual(self.speaker.recog.all(), {"Mr Receiver2": self.receiver2})
@ -198,6 +164,26 @@ class TestRPSystem(BaseEvenniaTest):
result,
)
def test_get_sdesc(self):
looker = self.speaker # Sender
target = self.receiver1 # Receiver1
looker.sdesc.add(sdesc0) # A nice sender of emotes
target.sdesc.add(sdesc1) # The first receiver of emotes.
# sdesc with no processing
self.assertEqual(looker.get_sdesc(target), "The first receiver of emotes.")
# sdesc with processing
self.assertEqual(
looker.get_sdesc(target, process=True), "|bThe first receiver of emotes.|n"
)
looker.recog.add(target, recog01) # Mr Receiver
# recog with no processing
self.assertEqual(looker.get_sdesc(target), "Mr Receiver")
# recog with processing
self.assertEqual(looker.get_sdesc(target, process=True), "|mMr Receiver|n")
def test_send_emote(self):
speaker = self.speaker
receiver1 = self.receiver1
@ -212,18 +198,18 @@ class TestRPSystem(BaseEvenniaTest):
rpsystem.send_emote(speaker, receivers, emote, case_sensitive=False)
self.assertEqual(
self.out0,
"With a flair, |bSender|n looks at |bThe first receiver of emotes.|n "
"With a flair, |mSender|n looks at |bThe first receiver of emotes.|n "
'and |bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
)
self.assertEqual(
self.out1,
"With a flair, |bA nice sender of emotes|n looks at |bReceiver1|n and "
"With a flair, |bA nice sender of emotes|n looks at |mReceiver1|n and "
'|bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
)
self.assertEqual(
self.out2,
"With a flair, |bA nice sender of emotes|n looks at |bThe first "
'receiver of emotes.|n and |bReceiver2|n. She says |w"This is a test."|n',
'receiver of emotes.|n and |mReceiver2|n. She says |w"This is a test."|n',
)
def test_send_case_sensitive_emote(self):
@ -241,20 +227,21 @@ class TestRPSystem(BaseEvenniaTest):
rpsystem.send_emote(speaker, receivers, case_emote)
self.assertEqual(
self.out0,
"|bSender|n looks at |bthe first receiver of emotes.|n, then "
"|bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n and "
"|bAnother nice colliding sdesc-guy for tests|n twice.",
"|mSender|n looks at |bthe first receiver of emotes.|n. Then, |mSender|n "
"looks at |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n "
"and |bAnother nice colliding sdesc-guy for tests|n twice.",
)
self.assertEqual(
self.out1,
"|bA nice sender of emotes|n looks at |bReceiver1|n, then |bReceiver1|n, "
"|bReceiver1|n and |bAnother nice colliding sdesc-guy for tests|n twice.",
"|bA nice sender of emotes|n looks at |mReceiver1|n. Then, "
"|ba nice sender of emotes|n looks at |mReceiver1|n, |mReceiver1|n "
"and |bAnother nice colliding sdesc-guy for tests|n twice.",
)
self.assertEqual(
self.out2,
"|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n, "
"then |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of "
"emotes.|n and |bReceiver2|n twice.",
"|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n. "
"Then, |ba nice sender of emotes|n looks at |bTHE FIRST RECEIVER OF EMOTES.|n, "
"|bThe first receiver of emotes.|n and |mReceiver2|n twice.",
)
def test_rpsearch(self):
@ -265,18 +252,6 @@ class TestRPSystem(BaseEvenniaTest):
self.assertEqual(self.speaker.search("receiver of emotes"), self.receiver1)
self.assertEqual(self.speaker.search("colliding"), self.receiver2)
def test_regex_tuple_from_key_alias(self):
self.speaker.aliases.add("foo bar")
self.speaker.aliases.add("this thing is a long thing")
t0 = time.time()
result = rpsystem.regex_tuple_from_key_alias(self.speaker)
t1 = time.time()
result = rpsystem.regex_tuple_from_key_alias(self.speaker)
t2 = time.time()
# print(f"t1: {t1 - t0}, t2: {t2 - t1}")
self.assertLess(t2 - t1, 10**-4)
self.assertEqual(result, (Anything, self.speaker, self.speaker.key))
class TestRPSystemCommands(BaseEvenniaCommandTest):
def setUp(self):
@ -305,7 +280,7 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
self.call(
rpsystem.CmdRecog(),
"barfoo as friend",
"Char will now remember BarFoo Character as friend.",
"You will now remember BarFoo Character as friend.",
)
self.call(
rpsystem.CmdRecog(),
@ -316,6 +291,6 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
self.call(
rpsystem.CmdRecog(),
"friend",
"Char will now know them only as 'BarFoo Character'",
"You will now know them only as 'BarFoo Character'",
cmdstring="forget",
)

View file

@ -321,7 +321,6 @@ class TestTraitStatic(_TraitHandlerBase):
self.trait.mult = 0.75
self.assertEqual(self._get_values(), (5, 1, 0.75, 4.5))
def test_delete(self):
"""Deleting resets to default."""
self.trait.mult = 2.0
@ -362,7 +361,14 @@ class TestTraitCounter(_TraitHandlerBase):
def _get_values(self):
"""Get (base, mod, mult, value, min, max)."""
return (self.trait.base, self.trait.mod, self.trait.mult, self.trait.value, self.trait.min, self.trait.max)
return (
self.trait.base,
self.trait.mod,
self.trait.mult,
self.trait.value,
self.trait.min,
self.trait.max,
)
def test_init(self):
self.assertEqual(
@ -634,7 +640,14 @@ class TestTraitGauge(_TraitHandlerBase):
def _get_values(self):
"""Get (base, mod, mult, value, min, max)."""
return (self.trait.base, self.trait.mod, self.trait.mult, self.trait.value, self.trait.min, self.trait.max)
return (
self.trait.base,
self.trait.mod,
self.trait.mult,
self.trait.value,
self.trait.min,
self.trait.max,
)
def test_init(self):
self.assertEqual(

View file

@ -1148,7 +1148,7 @@ class Trait:
class StaticTrait(Trait):
"""
Static Trait. This is a single value with a modifier,
Static Trait. This is a single value with a modifier,
multiplier, and no concept of a 'current' value or min/max etc.
value = (base + mod) * mult
@ -1161,7 +1161,9 @@ class StaticTrait(Trait):
def __str__(self):
status = "{value:11}".format(value=self.value)
return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(name=self.name, status=status, mod=self.mod, mult=self.mult)
return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(
name=self.name, status=status, mod=self.mod, mult=self.mult
)
# Helpers
@property
@ -1189,7 +1191,7 @@ class StaticTrait(Trait):
def mult(self):
"""The trait's multiplier."""
return self._data["mult"]
@mult.setter
def mult(self, amount):
if type(amount) in (int, float):
@ -1322,16 +1324,16 @@ class CounterTrait(Trait):
now = time()
tdiff = now - self._data["last_update"]
current += rate * tdiff
value = (current + self.mod)
value = current + self.mod
# we must make sure so we don't overstep our bounds
# even if .mod is included
if self._passed_ratetarget(value):
current = (self._data["ratetarget"] - self.mod)
current = self._data["ratetarget"] - self.mod
self._stop_timer()
elif not self._within_boundaries(value):
current = (self._enforce_boundaries(value) - self.mod)
current = self._enforce_boundaries(value) - self.mod
self._stop_timer()
else:
self._data["last_update"] = now
@ -1378,7 +1380,7 @@ class CounterTrait(Trait):
@property
def mult(self):
return self._data["mult"]
@mult.setter
def mult(self, amount):
if type(amount) in (int, float):
@ -1571,7 +1573,9 @@ class GaugeTrait(CounterTrait):
def __str__(self):
status = "{value:4} / {base:4}".format(value=self.value, base=self.base)
return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(name=self.name, status=status, mod=self.mod, mult=self.mult)
return "{name:12} {status} ({mod:+3}) (* {mult:.2f})".format(
name=self.name, status=status, mod=self.mod, mult=self.mult
)
@property
def base(self):
@ -1596,11 +1600,11 @@ class GaugeTrait(CounterTrait):
if value + self.base < self.min:
value = self.min - self.base
self._data["mod"] = value
@property
def mult(self):
return self._data["mult"]
@mult.setter
def mult(self, amount):
if type(amount) in (int, float):
@ -1621,7 +1625,7 @@ class GaugeTrait(CounterTrait):
if value is None:
self._data["min"] = self.default_keys["min"]
elif type(value) in (int, float):
self._data["min"] = min(value, (self.base + self.mod) * self.mult)
self._data["min"] = min(value, (self.base + self.mod) * self.mult)
@property
def max(self):
@ -1644,7 +1648,7 @@ class GaugeTrait(CounterTrait):
def current(self):
"""The `current` value of the gauge."""
return self._update_current(
self._enforce_boundaries(self._data.get("current", (self.base + self.mod) * self.mult))
self._enforce_boundaries(self._data.get("current", (self.base + self.mod) * self.mult))
)
@current.setter
@ -1655,7 +1659,7 @@ class GaugeTrait(CounterTrait):
@current.deleter
def current(self):
"Resets current back to 'full'"
self._data["current"] = (self.base + self.mod) * self.mult
self._data["current"] = (self.base + self.mod) * self.mult
@property
def value(self):

View file

@ -1,6 +1,6 @@
# world/
This folder is meant as a miscellanous folder for all that other stuff
This folder is meant as a miscellaneous folder for all that other stuff
related to the game. Code which are not commands or typeclasses go
here, like custom economy systems, combat code, batch-files etc.

View file

@ -28,7 +28,7 @@ Possible keywords are:
- `prototype_key` - the name of the prototype. This is required for db-prototypes,
for module-prototypes, the global variable name of the dict is used instead
- `prototype_parent` - string pointing to parent prototype if any. Prototype inherits
in a similar way as classes, with children overriding values in their partents.
in a similar way as classes, with children overriding values in their parents.
- `key` - string, the main object identifier.
- `typeclass` - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`.
- `location` - this should be a valid object or #dbref.
@ -42,7 +42,7 @@ Possible keywords are:
of the shorter forms, defaults are used for the rest.
- `tags` - Tags, as a list of tuples `(tag,)`, `(tag, category)` or `(tag, category, data)`.
- Any other keywords are interpreted as Attributes with no category or lock.
These will internally be added to `attrs` (eqivalent to `(attrname, value)`.
These will internally be added to `attrs` (equivalent to `(attrname, value)`.
See the `spawn` command and `evennia.prototypes.spawner.spawn` for more info.

View file

@ -12,9 +12,13 @@ import re
# since we use them (e.g. as command names).
# Lunr's default ignore-word list is found here:
# https://github.com/yeraydiazdiaz/lunr.py/blob/master/lunr/stop_word_filter.py
_LUNR_STOP_WORD_FILTER_EXCEPTIONS = (
["about", "might", "get", "who", "say"] + settings.LUNR_STOP_WORD_FILTER_EXCEPTIONS
)
_LUNR_STOP_WORD_FILTER_EXCEPTIONS = [
"about",
"might",
"get",
"who",
"say",
] + settings.LUNR_STOP_WORD_FILTER_EXCEPTIONS
_LUNR = None

View file

@ -473,6 +473,7 @@ def tag(accessing_obj, accessed_obj, *args, **kwargs):
category = args[1] if len(args) > 1 else None
return bool(accessing_obj.tags.get(tagkey, category=category))
def is_ooc(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
@ -489,13 +490,14 @@ def is_ooc(accessing_obj, accessed_obj, *args, **kwargs):
session = accessed_obj.session
except AttributeError:
session = account.sessions.get()[0] # note-this doesn't work well
# for high multisession mode. We may need
# to change to sessiondb to resolve this
# for high multisession mode. We may need
# to change to sessiondb to resolve this
try:
return not account.get_puppet(session)
except TypeError:
return not session.get_puppet()
def objtag(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:

View file

@ -528,10 +528,10 @@ def search_prototype(
"""
# This will load the prototypes the first time they are searched
loaded = getattr(load_module_prototypes, '_LOADED', False)
loaded = getattr(load_module_prototypes, "_LOADED", False)
if not loaded:
load_module_prototypes()
setattr(load_module_prototypes, '_LOADED', True)
setattr(load_module_prototypes, "_LOADED", True)
# prototype keys are always in lowecase
if key:

View file

@ -2316,9 +2316,11 @@ def main():
if option in ("makemessages", "compilemessages"):
# some commands don't require the presence of a game directory to work
need_gamedir = False
if CURRENT_DIR != EVENNIA_LIB:
print("You must stand in the evennia/evennia/ folder (where the 'locale/' "
"folder is located) to run this command.")
if CURRENT_DIR != EVENNIA_LIB:
print(
"You must stand in the evennia/evennia/ folder (where the 'locale/' "
"folder is located) to run this command."
)
sys.exit()
if option in ("shell", "check", "makemigrations", "createsuperuser", "shell_plus"):

View file

@ -226,6 +226,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
or option == naws.NAWS
or option == MCCP
or option == mssp.MSSP
or option == ECHO
or option == suppress_ga.SUPPRESS_GA
)
@ -236,6 +237,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, _BASE_SESSION_CLASS):
or option == naws.NAWS
or option == MCCP
or option == mssp.MSSP
or option == ECHO
or option == suppress_ga.SUPPRESS_GA
)

View file

@ -423,6 +423,7 @@ class Evennia:
logger.log_msg("Evennia Server successfully restarted in 'reset' mode.")
elif mode == "shutdown":
from evennia.objects.models import ObjectDB
self.at_server_cold_start()
# clear eventual lingering session storages
ObjectDB.objects.clear_all_sessids()

View file

@ -683,7 +683,7 @@ class ServerSessionHandler(SessionHandler):
Get a unique list of connected and logged-in Accounts.
Returns:
accounts (list): All conected Accounts (which may be fewer than the
accounts (list): All connected Accounts (which may be fewer than the
amount of Sessions due to multi-playing).
"""

View file

@ -197,7 +197,6 @@ class TestServer(TestCase):
class TestInitHooks(TestCase):
def setUp(self):
from evennia.utils import create

View file

@ -16,15 +16,18 @@ class EvenniaTestSuiteRunner(DiscoverRunner):
avoid running the large number of tests defined by Django
"""
def setup_test_environment(self, **kwargs):
# the portal looping call starts before the unit-test suite so we
# can't mock it - instead we stop it before starting the test - otherwise
# we'd get unclean reactor errors across test boundaries.
from evennia.server.portal.portal import PORTAL
PORTAL.maintenance_task.stop()
# initialize evennia itself
import evennia
evennia._init()
from django.conf import settings
@ -37,6 +40,7 @@ class EvenniaTestSuiteRunner(DiscoverRunner):
# remove testing flag after suite has run
from django.conf import settings
settings._TEST_ENVIRONMENT = False
super().teardown_test_environment(**kwargs)

View file

@ -218,6 +218,7 @@ class AttributeProperty:
"""
value = self._default
try:
<<<<<<< HEAD
value = self.at_get(getattr(instance, self.attrhandler_name).get(
key=self._key,
default=self._default,
@ -225,6 +226,17 @@ class AttributeProperty:
strattr=self._strattr,
raise_exception=self._autocreate,
), instance)
=======
value = self.at_get(
getattr(instance, self.attrhandler_name).get(
key=self._key,
default=self._default,
category=self._category,
strattr=self._strattr,
raise_exception=self._autocreate,
)
)
>>>>>>> ce3992f999a164881462d8f878d71a47a8f946cc
except AttributeError:
if self._autocreate:
# attribute didn't exist and autocreate is set

View file

@ -286,7 +286,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
categories = make_iter(category) if category else []
n_keys = len(keys)
n_categories = len(categories)
unique_categories = sorted(set(categories))
unique_categories = set(categories)
n_unique_categories = len(unique_categories)
dbmodel = self.model.__dbclass__.__name__.lower()

View file

@ -96,6 +96,7 @@ class Tag(models.Model):
# Handlers making use of the Tags model
#
class TagProperty:
"""
Tag property descriptor. Allows for setting tags on an object as Django-like 'fields'
@ -112,6 +113,7 @@ class TagProperty:
mytag2 = TagProperty(category="tagcategory")
"""
taghandler_name = "tags"
def __init__(self, category=None, data=None):
@ -134,10 +136,7 @@ class TagProperty:
"""
try:
return getattr(instance, self.taghandler_name).get(
key=self._key,
category=self._category,
return_list=False,
raise_exception=True
key=self._key, category=self._category, return_list=False, raise_exception=True
)
except AttributeError:
self.__set__(instance, self._category)
@ -150,9 +149,7 @@ class TagProperty:
self._category = category
(
getattr(instance, self.taghandler_name).add(
key=self._key,
category=self._category,
data=self._data
key=self._key, category=self._category, data=self._data
)
)
@ -430,8 +427,15 @@ class TagHandler(object):
return ret[0] if len(ret) == 1 else ret
def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False,
raise_exception=False):
def get(
self,
key=None,
default=None,
category=None,
return_tagobj=False,
return_list=False,
raise_exception=False,
):
"""
Get the tag for the given key, category or combination of the two.
@ -613,6 +617,7 @@ class AliasProperty(TagProperty):
bob = AliasProperty()
"""
taghandler_name = "aliases"
@ -636,6 +641,7 @@ class PermissionProperty(TagProperty):
myperm = PermissionProperty()
"""
taghandler_name = "permissions"

View file

@ -142,6 +142,15 @@ class TestTypedObjectManager(BaseEvenniaTest):
[self.obj1],
)
def test_get_tag_with_any_including_nones(self):
self.obj1.tags.add("tagA", "categoryA")
self.assertEqual(
self._manager(
"get_by_tag", ["tagA", "tagB"], ["categoryA", "categoryB", None], match="any"
),
[self.obj1],
)
def test_get_tag_withnomatch(self):
self.obj1.tags.add("tagC", "categoryC")
self.assertEqual(

View file

@ -12,6 +12,7 @@ evennia.OPTION_CLASSES
from pickle import dumps
from django.db.utils import OperationalError, ProgrammingError
from django.conf import settings
from evennia.utils.utils import class_from_module, callables_from_module
from evennia.utils import logger
@ -167,7 +168,6 @@ class GlobalScriptContainer(Container):
# store a hash representation of the setup
script.attributes.add("_global_script_settings", compare_hash, category="settings_hash")
script.start()
return script
@ -183,9 +183,16 @@ class GlobalScriptContainer(Container):
# populate self.typeclass_storage
self.load_data()
# start registered scripts
# make sure settings-defined scripts are loaded
for key in self.loaded_data:
self._load_script(key)
# start all global scripts
try:
for script in self._get_scripts():
script.start()
except (OperationalError, ProgrammingError):
# this can happen if db is not loaded yet (such as when building docs)
pass
def load_data(self):
"""

View file

@ -23,7 +23,7 @@ from collections import deque, OrderedDict, defaultdict
from collections.abc import MutableSequence, MutableSet, MutableMapping
try:
from pickle import dumps, loads
from pickle import dumps, loads, UnpicklingError
except ImportError:
from pickle import dumps, loads
from django.core.exceptions import ObjectDoesNotExist
@ -239,6 +239,9 @@ class _SaverMutable(object):
def __gt__(self, other):
return self._data > other
def __or__(self, other):
return self._data | other
@_save
def __setitem__(self, key, value):
self._data.__setitem__(key, self._convert_mutables(value))
@ -450,7 +453,9 @@ def deserialize(obj):
elif tname in ("_SaverOrderedDict", "OrderedDict"):
return OrderedDict([(_iter(key), _iter(val)) for key, val in obj.items()])
elif tname in ("_SaverDefaultDict", "defaultdict"):
return defaultdict(obj.default_factory, {_iter(key): _iter(val) for key, val in obj.items()})
return defaultdict(
obj.default_factory, {_iter(key): _iter(val) for key, val in obj.items()}
)
elif tname in _DESERIALIZE_MAPPING:
return _DESERIALIZE_MAPPING[tname](_iter(val) for val in obj)
elif is_iter(obj):
@ -602,7 +607,9 @@ def to_pickle(data):
def process_item(item):
"""Recursive processor and identification of data"""
dtype = type(item)
if dtype in (str, int, float, bool, bytes, SafeString):
return item
elif dtype == tuple:
@ -612,7 +619,10 @@ def to_pickle(data):
elif dtype in (dict, _SaverDict):
return dict((process_item(key), process_item(val)) for key, val in item.items())
elif dtype in (defaultdict, _SaverDefaultDict):
return defaultdict(item.default_factory, ((process_item(key), process_item(val)) for key, val in item.items()))
return defaultdict(
item.default_factory,
((process_item(key), process_item(val)) for key, val in item.items()),
)
elif dtype in (set, _SaverSet):
return set(process_item(val) for val in item)
elif dtype in (OrderedDict, _SaverOrderedDict):
@ -620,7 +630,20 @@ def to_pickle(data):
elif dtype in (deque, _SaverDeque):
return deque(process_item(val) for val in item)
elif hasattr(item, "__iter__"):
# not one of the base types
if hasattr(item, "__serialize_dbobjs__"):
# Allows custom serialization of any dbobjects embedded in
# the item that Evennia will otherwise not find (these would
# otherwise lead to an error). Use the dbserialize helper from
# this method.
try:
item.__serialize_dbobjs__()
except TypeError as err:
# we catch typerrors so we can handle both classes (requiring
# classmethods) and instances
pass
if hasattr(item, "__iter__"):
# we try to conserve the iterable class, if not convert to list
try:
return item.__class__([process_item(val) for val in item])
@ -678,7 +701,10 @@ def from_pickle(data, db_obj=None):
elif dtype == dict:
return dict((process_item(key), process_item(val)) for key, val in item.items())
elif dtype == defaultdict:
return defaultdict(item.default_factory, ((process_item(key), process_item(val)) for key, val in item.items()))
return defaultdict(
item.default_factory,
((process_item(key), process_item(val)) for key, val in item.items()),
)
elif dtype == set:
return set(process_item(val) for val in item)
elif dtype == OrderedDict:
@ -692,6 +718,22 @@ def from_pickle(data, db_obj=None):
return item.__class__(process_item(val) for val in item)
except (AttributeError, TypeError):
return [process_item(val) for val in item]
if hasattr(item, "__deserialize_dbobjs__"):
# this allows the object to custom-deserialize any embedded dbobjs
# that we previously serialized with __serialize_dbobjs__.
# use the dbunserialize helper in this module.
try:
item.__deserialize_dbobjs__()
except (TypeError, UnpicklingError):
# handle recoveries both of classes (requiring classmethods
# or instances. Unpickling errors can happen when re-loading the
# data from cache (because the hidden entity was already
# deserialized and stored back on the object, unpickling it
# again fails). TODO: Maybe one could avoid this retry in a
# more graceful way?
pass
return item
def process_tree(item, parent):

View file

@ -274,12 +274,13 @@ import inspect
from ast import literal_eval
from fnmatch import fnmatch
from math import ceil
from inspect import isfunction, getargspec
from django.conf import settings
from evennia import Command, CmdSet
from evennia.utils import logger
from evennia.utils.evtable import EvTable
from evennia.utils.evtable import EvTable, EvColumn
from evennia.utils.ansi import strip_ansi
from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
from evennia.commands import cmdhandler
@ -1210,7 +1211,6 @@ class EvMenu:
Args:
optionlist (list): List of (key, description) tuples for every
option related to this node.
caller (Object, Account or None, optional): The caller of the node.
Returns:
options (str): The formatted option display.
@ -1229,7 +1229,7 @@ class EvMenu:
table = []
for key, desc in optionlist:
if key or desc:
desc_string = ": %s" % desc if desc else ""
desc_string = f": {desc}" if desc else ""
table_width_max = max(
table_width_max,
max(m_len(p) for p in key.split("\n"))
@ -1239,42 +1239,31 @@ class EvMenu:
raw_key = strip_ansi(key)
if raw_key != key:
# already decorations in key definition
table.append(" |lc%s|lt%s|le%s" % (raw_key, key, desc_string))
table.append(f" |lc{raw_key}|lt{key}|le{desc_string}")
else:
# add a default white color to key
table.append(" |lc%s|lt|w%s|n|le%s" % (raw_key, raw_key, desc_string))
ncols = _MAX_TEXT_WIDTH // table_width_max # number of ncols
table.append(f" |lc{raw_key}|lt|w{key}|n|le{desc_string}")
ncols = _MAX_TEXT_WIDTH // table_width_max # number of columns
if ncols < 0:
# no visible option at all
# no visible options at all
return ""
ncols = ncols + 1 if ncols == 0 else ncols
# get the amount of rows needed (start with 4 rows)
nrows = 4
while nrows * ncols < nlist:
nrows += 1
ncols = nlist // nrows # number of full columns
nlastcol = nlist % nrows # number of elements in last column
ncols = 1 if ncols == 0 else ncols
# get the final column count
ncols = ncols + 1 if nlastcol > 0 else ncols
if ncols > 1:
# only extend if longer than one column
table.extend([" " for i in range(nrows - nlastcol)])
# minimum number of rows in a column
min_rows = 4
# build the actual table grid
table = [table[icol * nrows : (icol * nrows) + nrows] for icol in range(0, ncols)]
# split the items into columns
split = max(min_rows, ceil(len(table) / ncols))
max_end = len(table)
cols_list = []
for icol in range(ncols):
start = icol * split
end = min(start + split, max_end)
cols_list.append(EvColumn(*table[start:end]))
# adjust the width of each column
for icol in range(len(table)):
col_width = (
max(max(m_len(p) for p in part.split("\n")) for part in table[icol]) + colsep
)
table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]]
# format the table into columns
return str(EvTable(table=table, border="none"))
return str(EvTable(table=cols_list, border="none"))
def node_formatter(self, nodetext, optionstext):
"""

View file

@ -181,7 +181,7 @@ class EvMore(object):
justify (bool, optional): If set, auto-justify long lines. This must be turned
off for fixed-width or formatted output, like tables. It's force-disabled
if `inp` is an EvTable.
justify_kwargs (dict, optional): Keywords for the justifiy function. Used only
justify_kwargs (dict, optional): Keywords for the justify function. Used only
if `justify` is True. If this is not set, default arguments will be used.
exit_on_lastpage (bool, optional): If reaching the last page without the
page being completely filled, exit pager immediately. If unset,
@ -507,7 +507,7 @@ class EvMore(object):
def page_formatter(self, page):
"""
Page formatter. Every page passes through this method. Override
it to customize behvaior per-page. A common use is to generate a new
it to customize behavior per-page. A common use is to generate a new
EvTable for every page (this is more efficient than to generate one huge
EvTable across many pages and feed it into EvMore all at once).

View file

@ -84,8 +84,8 @@ class _ParsedFunc:
# state storage
fullstr: str = ""
infuncstr: str = ""
single_quoted: bool = False
double_quoted: bool = False
single_quoted: int = -1
double_quoted: int = -1
current_kwarg: str = ""
open_lparens: int = 0
open_lsquate: int = 0
@ -318,8 +318,8 @@ class FuncParser:
# parsing state
callstack = []
single_quoted = False
double_quoted = False
single_quoted = -1
double_quoted = -1
open_lparens = 0 # open (
open_lsquare = 0 # open [
open_lcurly = 0 # open {
@ -330,6 +330,7 @@ class FuncParser:
curr_func = None
fullstr = "" # final string
infuncstr = "" # string parts inside the current level of $funcdef (including $)
literal_infuncstr = False
for char in string:
@ -373,12 +374,13 @@ class FuncParser:
curr_func.open_lcurly = open_lcurly
current_kwarg = ""
infuncstr = ""
single_quoted = False
double_quoted = False
single_quoted = -1
double_quoted = -1
open_lparens = 0
open_lsquare = 0
open_lcurly = 0
exec_return = ""
literal_infuncstr = False
callstack.append(curr_func)
# start a new func
@ -401,19 +403,41 @@ class FuncParser:
infuncstr += str(exec_return)
exec_return = ""
if char == "'": # note that this is the same as "\'"
if char == "'" and double_quoted < 0: # note that this is the same as "\'"
# a single quote - flip status
single_quoted = not single_quoted
infuncstr += char
if single_quoted == 0:
infuncstr = infuncstr[1:]
single_quoted = -1
elif single_quoted > 0:
prefix = infuncstr[0:single_quoted]
infuncstr = prefix + infuncstr[single_quoted + 1 :]
single_quoted = -1
else:
infuncstr += char
infuncstr = infuncstr.strip()
single_quoted = len(infuncstr) - 1
literal_infuncstr = True
continue
if char == '"': # note that this is the same as '\"'
if char == '"' and single_quoted < 0: # note that this is the same as '\"'
# a double quote = flip status
double_quoted = not double_quoted
infuncstr += char
if double_quoted == 0:
infuncstr = infuncstr[1:]
double_quoted = -1
elif double_quoted > 0:
prefix = infuncstr[0:double_quoted]
infuncstr = prefix + infuncstr[double_quoted + 1 :]
double_quoted = -1
else:
infuncstr += char
infuncstr = infuncstr.strip()
double_quoted = len(infuncstr) - 1
literal_infuncstr = True
continue
if double_quoted or single_quoted:
if double_quoted >= 0 or single_quoted >= 0:
# inside a string definition - this escapes everything else
infuncstr += char
continue
@ -477,12 +501,15 @@ class FuncParser:
else:
curr_func.args.append(exec_return)
else:
if not literal_infuncstr:
infuncstr = infuncstr.strip()
# store a string instead
if current_kwarg:
curr_func.kwargs[current_kwarg] = infuncstr.strip()
elif infuncstr.strip():
curr_func.kwargs[current_kwarg] = infuncstr
elif literal_infuncstr or infuncstr.strip():
# don't store the empty string
curr_func.args.append(infuncstr.strip())
curr_func.args.append(infuncstr)
# note that at this point either exec_return or infuncstr will
# be empty. We need to store the full string so we can print
@ -493,6 +520,7 @@ class FuncParser:
current_kwarg = ""
exec_return = ""
infuncstr = ""
literal_infuncstr = False
if char == ")":
# closing the function list - this means we have a
@ -536,6 +564,7 @@ class FuncParser:
if return_str:
exec_return = ""
infuncstr = ""
literal_infuncstr = False
continue
infuncstr += char

View file

@ -67,7 +67,7 @@ class TimeScript(DefaultScript):
callback(*args, **kwargs)
seconds = real_seconds_until(**self.db.gametime)
self.restart(interval=seconds)
self.start(interval=seconds, force_restart=True)
# Access functions

View file

@ -50,6 +50,7 @@ def _log(msg, logfunc, prefix="", **kwargs):
# log call functions (each has legacy aliases)
def log_info(msg, **kwargs):
"""
Logs any generic debugging/informative info that should appear in the log.
@ -62,6 +63,7 @@ def log_info(msg, **kwargs):
"""
_log(msg, log.info, **kwargs)
info = log_info
log_infomsg = log_info
log_msg = log_info
@ -79,6 +81,7 @@ def log_warn(msg, **kwargs):
"""
_log(msg, log.warn, **kwargs)
warn = log_warn
warning = log_warn
log_warnmsg = log_warn
@ -120,6 +123,7 @@ def log_trace(msg=None, **kwargs):
if msg:
_log(msg, log.error, prefix="!!", **kwargs)
log_tracemsg = log_trace
exception = log_trace
critical = log_trace
@ -156,6 +160,7 @@ def log_sec(msg, **kwargs):
"""
_log(msg, log.info, prefix="SS", **kwargs)
sec = log_sec
security = log_sec
log_secmsg = log_sec
@ -174,12 +179,12 @@ def log_server(msg, **kwargs):
_log(msg, log.info, prefix="Server", **kwargs)
class GetLogObserver:
"""
Sets up how the system logs are formatted.
"""
component_prefix = ""
event_levels = {
twisted_logger.LogLevel.debug: "??",
@ -207,8 +212,7 @@ class GetLogObserver:
event["log_format"] = str(event.get("log_format", ""))
component_prefix = self.component_prefix or ""
log_msg = twisted_logger.formatEventAsClassicLogText(
event,
formatTime=lambda e: twisted_logger.formatTime(e, _TIME_FORMAT)
event, formatTime=lambda e: twisted_logger.formatTime(e, _TIME_FORMAT)
)
return f"{component_prefix}{log_msg}"
@ -218,14 +222,15 @@ class GetLogObserver:
# Called by server/portal on startup
class GetPortalLogObserver(GetLogObserver):
component_prefix = "|Portal| "
class GetServerLogObserver(GetLogObserver):
component_prefix = ""
# logging overrides
@ -352,6 +357,7 @@ class WeeklyLogFile(logfile.DailyLogFile):
self.lastDate = max(self.lastDate, self.toDate())
self.size += len(data)
# Arbitrary file logger

View file

@ -96,7 +96,6 @@ DEFAULT_SETTING_RESETS = dict(
"evennia.game_template.server.conf.prototypefuncs",
],
BASE_GUEST_TYPECLASS="evennia.accounts.accounts.DefaultGuest",
# a special setting boolean _TEST_ENVIRONMENT is set by the test runner
# while the test suite is running.
)

View file

@ -15,9 +15,7 @@ class TestDbSerialize(TestCase):
"""
def setUp(self):
self.obj = DefaultObject(
db_key="Tester",
)
self.obj = DefaultObject(db_key="Tester")
self.obj.save()
def test_constants(self):
@ -62,10 +60,12 @@ class TestDbSerialize(TestCase):
self.obj.db.test.sort(key=lambda d: str(d))
self.assertEqual(self.obj.db.test, [{0: 1}, {1: 0}])
def test_dict(self):
def test_saverdict(self):
self.obj.db.test = {"a": True}
self.obj.db.test.update({"b": False})
self.assertEqual(self.obj.db.test, {"a": True, "b": False})
self.obj.db.test |= {"c": 5}
self.assertEqual(self.obj.db.test, {"a": True, "b": False, "c": 5})
@parameterized.expand(
[
@ -88,27 +88,88 @@ class TestDbSerialize(TestCase):
self.assertIsInstance(value, base_type)
self.assertNotIsInstance(value, saver_type)
self.assertEqual(value, default_value)
self.obj.db.test = {'a': True}
self.obj.db.test.update({'b': False})
self.assertEqual(self.obj.db.test, {'a': True, 'b': False})
self.obj.db.test = {"a": True}
self.obj.db.test.update({"b": False})
self.assertEqual(self.obj.db.test, {"a": True, "b": False})
def test_defaultdict(self):
from collections import defaultdict
# baseline behavior for a defaultdict
_dd = defaultdict(list)
_dd['a']
self.assertEqual(_dd, {'a': []})
_dd["a"]
self.assertEqual(_dd, {"a": []})
# behavior after defaultdict is set as attribute
dd = defaultdict(list)
self.obj.db.test = dd
self.obj.db.test['a']
self.assertEqual(self.obj.db.test, {'a': []})
self.obj.db.test["a"]
self.assertEqual(self.obj.db.test, {"a": []})
self.obj.db.test['a'].append(1)
self.assertEqual(self.obj.db.test, {'a': [1]})
self.obj.db.test['a'].append(2)
self.assertEqual(self.obj.db.test, {'a': [1, 2]})
self.obj.db.test['a'].append(3)
self.assertEqual(self.obj.db.test, {'a': [1, 2, 3]})
self.obj.db.test["a"].append(1)
self.assertEqual(self.obj.db.test, {"a": [1]})
self.obj.db.test["a"].append(2)
self.assertEqual(self.obj.db.test, {"a": [1, 2]})
self.obj.db.test["a"].append(3)
self.assertEqual(self.obj.db.test, {"a": [1, 2, 3]})
self.obj.db.test |= {"b": [5, 6]}
self.assertEqual(self.obj.db.test, {"a": [1, 2, 3], "b": [5, 6]})
class _InvalidContainer:
"""Container not saveable in Attribute (if obj is dbobj, it 'hides' it)"""
def __init__(self, obj):
self.hidden_obj = obj
class _ValidContainer(_InvalidContainer):
"""Container possible to save in Attribute (handles hidden dbobj explicitly)"""
def __serialize_dbobjs__(self):
self.hidden_obj = dbserialize.dbserialize(self.hidden_obj)
def __deserialize_dbobjs__(self):
self.hidden_obj = dbserialize.dbunserialize(self.hidden_obj)
class DbObjWrappers(TestCase):
"""
Test the `__serialize_dbobjs__` and `__deserialize_dbobjs__` methods.
"""
def setUp(self):
super().setUp()
self.dbobj1 = DefaultObject(db_key="Tester1")
self.dbobj1.save()
self.dbobj2 = DefaultObject(db_key="Tester2")
self.dbobj2.save()
def test_dbobj_hidden_obj__fail(self):
with self.assertRaises(TypeError):
self.dbobj1.db.testarg = _InvalidContainer(self.dbobj1)
def test_consecutive_fetch(self):
con = _ValidContainer(self.dbobj2)
self.dbobj1.db.testarg = con
attrobj = self.dbobj1.attributes.get("testarg", return_obj=True)
self.assertEqual(attrobj.value, con)
self.assertEqual(attrobj.value, con)
self.assertEqual(attrobj.value.hidden_obj, self.dbobj2)
def test_dbobj_hidden_obj__success(self):
con = _ValidContainer(self.dbobj2)
self.dbobj1.db.testarg = con
# accessing the same data twice
res1 = self.dbobj1.db.testarg
res2 = self.dbobj1.db.testarg
self.assertEqual(res1, res2)
self.assertEqual(res1, con)
self.assertEqual(res2, con)
self.assertEqual(res1.hidden_obj, self.dbobj2)
self.assertEqual(res2.hidden_obj, self.dbobj2)

View file

@ -44,6 +44,7 @@ def _double_callable(*args, **kwargs):
def _eval_callable(*args, **kwargs):
if args:
return simple_eval(args[0])
return ""
@ -113,25 +114,25 @@ class TestFuncParser(TestCase):
("$foo() Test noargs5", "_test() Test noargs5"),
("Test args1 $foo(a,b,c)", "Test args1 _test(a, b, c)"),
("Test args2 $bar(foo, bar, too)", "Test args2 _test(foo, bar, too)"),
("Test args3 $bar(foo, bar, ' too')", "Test args3 _test(foo, bar, ' too')"),
("Test args4 $foo('')", "Test args4 _test('')"),
('Test args4 $foo("")', 'Test args4 _test("")'),
(r"Test args3 $bar(foo, bar, ' too')", "Test args3 _test(foo, bar, too)"),
("Test args4 $foo('')", "Test args4 _test()"),
('Test args4 $foo("")', "Test args4 _test()"),
("Test args5 $foo(\(\))", "Test args5 _test(())"),
("Test args6 $foo(\()", "Test args6 _test(()"),
("Test args7 $foo(())", "Test args7 _test(())"),
("Test args8 $foo())", "Test args8 _test())"),
("Test args9 $foo(=)", "Test args9 _test(=)"),
("Test args10 $foo(\,)", "Test args10 _test(,)"),
("Test args10 $foo(',')", "Test args10 _test(',')"),
("Test args10 $foo(',')", "Test args10 _test(,)"),
("Test args11 $foo(()", "Test args11 $foo(()"), # invalid syntax
(
"Test kwarg1 $bar(foo=1, bar='foo', too=ere)",
"Test kwarg1 _test(foo=1, bar='foo', too=ere)",
"Test kwarg1 _test(foo=1, bar=foo, too=ere)",
),
("Test kwarg2 $bar(foo,bar,too=ere)", "Test kwarg2 _test(foo, bar, too=ere)"),
("test kwarg3 $foo(foo = bar, bar = ere )", "test kwarg3 _test(foo=bar, bar=ere)"),
(
"test kwarg4 $foo(foo =' bar ',\" bar \"= ere )",
r"test kwarg4 $foo(foo =\' bar \',\" bar \"= ere )",
"test kwarg4 _test(foo=' bar ', \" bar \"=ere)",
),
(
@ -180,22 +181,29 @@ class TestFuncParser(TestCase):
("Test clr $clr(r, This is a red string!)", "Test clr |rThis is a red string!|n"),
("Test eval1 $eval(21 + 21 - 10)", "Test eval1 32"),
("Test eval2 $eval((21 + 21) / 2)", "Test eval2 21.0"),
("Test eval3 $eval('21' + 'foo' + 'bar')", "Test eval3 21foobar"),
("Test eval4 $eval('21' + '$repl()' + '' + str(10 // 2))", "Test eval4 21rr5"),
("Test eval5 $eval('21' + '\$repl()' + '' + str(10 // 2))", "Test eval5 21$repl()5"),
("Test eval6 $eval('$repl(a)' + '$repl(b)')", "Test eval6 rarrbr"),
("Test eval3 $eval(\"'21' + 'foo' + 'bar'\")", "Test eval3 21foobar"),
(r"Test eval4 $eval(\'21\' + \'$repl()\' + \"''\" + str(10 // 2))", "Test eval4 21rr5"),
(
r"Test eval5 $eval(\'21\' + \'\$repl()\' + \'\' + str(10 // 2))",
"Test eval5 21$repl()5",
),
("Test eval6 $eval(\"'$repl(a)' + '$repl(b)'\")", "Test eval6 rarrbr"),
("Test type1 $typ([1,2,3,4])", "Test type1 <class 'list'>"),
("Test type2 $typ((1,2,3,4))", "Test type2 <class 'tuple'>"),
("Test type3 $typ({1,2,3,4})", "Test type3 <class 'set'>"),
("Test type4 $typ({1:2,3:4})", "Test type4 <class 'dict'>"),
("Test type5 $typ(1), $typ(1.0)", "Test type5 <class 'int'>, <class 'float'>"),
("Test type6 $typ('1'), $typ(\"1.0\")", "Test type6 <class 'str'>, <class 'str'>"),
(
"Test type6 $typ(\"'1'\"), $typ('\"1.0\"')",
"Test type6 <class 'str'>, <class 'str'>",
),
("Test add1 $add(1, 2)", "Test add1 3"),
("Test add2 $add([1,2,3,4], [5,6])", "Test add2 [1, 2, 3, 4, 5, 6]"),
("Test literal1 $sum($lit([1,2,3,4,5,6]))", "Test literal1 21"),
("Test literal2 $typ($lit(1))", "Test literal2 <class 'int'>"),
("Test literal3 $typ($lit(1)aaa)", "Test literal3 <class 'str'>"),
("Test literal4 $typ(aaa$lit(1))", "Test literal4 <class 'str'>"),
("Test spider's thread", "Test spider's thread"),
]
)
def test_parse(self, string, expected):
@ -258,7 +266,11 @@ class TestFuncParser(TestCase):
self.assertEqual([1, 2, 3, 4], ret)
self.assertTrue(isinstance(ret, list))
ret = self.parser.parse_to_any("$lit('')")
ret = self.parser.parse_to_any("$lit(\"''\")")
self.assertEqual("", ret)
self.assertTrue(isinstance(ret, str))
ret = self.parser.parse_to_any(r"$lit(\'\')")
self.assertEqual("", ret)
self.assertTrue(isinstance(ret, str))
@ -390,7 +402,8 @@ class TestDefaultCallables(TestCase):
("Some $rjust(Hello, 30)", "Some Hello"),
("Some $rjust(Hello, width=30)", "Some Hello"),
("Some $cjust(Hello, 30)", "Some Hello "),
("Some $eval('-'*20)Hello", "Some --------------------Hello"),
("Some $eval(\"'-'*20\")Hello", "Some --------------------Hello"),
('$crop("spider\'s silk", 5)', "spide"),
]
)
def test_other_callables(self, string, expected):
@ -455,15 +468,16 @@ class TestDefaultCallables(TestCase):
self.parser.parse(
"this should be $pad('''escaped,''' and '''instead,''' cropped $crop(with a long,5) text., 80)"
),
"this should be '''escaped,''' and '''instead,''' cropped with text. ",
"this should be escaped, and instead, cropped with text. ",
)
def test_escaped2(self):
raw_str = 'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)'
expected = "this should be escaped, and instead, cropped with text. "
result = self.parser.parse(raw_str)
self.assertEqual(
self.parser.parse(
'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)'
),
'this should be """escaped,""" and """instead,""" cropped with text. ',
result,
expected,
)

View file

@ -0,0 +1,50 @@
from evennia.scripts.scripts import DefaultScript
from evennia.utils.test_resources import EvenniaTest
from evennia.utils.search import search_script_attribute, search_script_tag
class TestSearch(EvenniaTest):
def test_search_script_tag(self):
"""Check that a script can be found by its tag."""
script, errors = DefaultScript.create("a-script")
script.tags.add("a-tag")
found = search_script_tag("a-tag")
self.assertEqual(len(found), 1, errors)
self.assertEqual(script.key, found[0].key, errors)
def test_search_script_tag_category(self):
"""Check that a script can be found by its tag and category."""
script, errors = DefaultScript.create("a-script")
script.tags.add("a-tag", category="a-category")
found = search_script_tag("a-tag", category="a-category")
self.assertEqual(len(found), 1, errors)
self.assertEqual(script.key, found[0].key, errors)
def test_search_script_tag_wrong_category(self):
"""Check that a script cannot be found by the wrong category."""
script, errors = DefaultScript.create("a-script")
script.tags.add("a-tag", category="a-category")
found = search_script_tag("a-tag", category="wrong-category")
self.assertEqual(len(found), 0, errors)
def test_search_script_tag_wrong(self):
"""Check that a script cannot be found by the wrong tag."""
script, errors = DefaultScript.create("a-script")
script.tags.add("a-tag", category="a-category")
found = search_script_tag("wrong-tag", category="a-category")
self.assertEqual(len(found), 0, errors)
def test_search_script_attribute(self):
"""Check that a script can be found by its attributes."""
script, errors = DefaultScript.create("a-script")
script.db.an_attribute = "some value"
found = search_script_attribute(key="an_attribute", value="some value")
self.assertEqual(len(found), 1, errors)
self.assertEqual(script.key, found[0].key, errors)
def test_search_script_attribute_wrong(self):
"""Check that a script cannot be found by wrong value of its attributes."""
script, errors = DefaultScript.create("a-script")
script.db.an_attribute = "some value"
found = search_script_attribute(key="an_attribute", value="wrong value")
self.assertEqual(len(found), 0, errors)

View file

@ -7,20 +7,22 @@ import mock
class TestText2Html(TestCase):
def test_re_color(self):
def test_format_styles(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_color("foo"))
self.assertEqual("foo", parser.format_styles("foo"))
self.assertEqual(
'<span class="color-001">red</span>foo',
parser.re_color(ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"),
parser.format_styles(
ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"
),
)
self.assertEqual(
'<span class="bgcolor-001">red</span>foo',
parser.re_color(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"),
parser.format_styles(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"),
)
self.assertEqual(
'<span class="bgcolor-001"><span class="color-002">red</span></span>foo',
parser.re_color(
'<span class="bgcolor-001 color-002">red</span>foo',
parser.format_styles(
ansi.ANSI_BACK_RED
+ ansi.ANSI_UNHILITE
+ ansi.ANSI_GREEN
@ -29,75 +31,25 @@ class TestText2Html(TestCase):
+ "foo"
),
)
@unittest.skip("parser issues")
def test_re_bold(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_bold("foo"))
self.assertEqual(
# "a <strong>red</strong>foo", # TODO: why not?
"a <strong>redfoo</strong>",
parser.re_bold("a " + ansi.ANSI_HILITE + "red" + ansi.ANSI_UNHILITE + "foo"),
'a <span class="underline">red</span>foo',
parser.format_styles("a " + ansi.ANSI_UNDERLINE + "red" + ansi.ANSI_NORMAL + "foo"),
)
self.assertEqual(
'a <span class="blink">red</span>foo',
parser.format_styles("a " + ansi.ANSI_BLINK + "red" + ansi.ANSI_NORMAL + "foo"),
)
self.assertEqual(
'a <span class="bgcolor-007 color-000">red</span>foo',
parser.format_styles("a " + ansi.ANSI_INVERSE + "red" + ansi.ANSI_NORMAL + "foo"),
)
@unittest.skip("parser issues")
def test_re_underline(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_underline("foo"))
self.assertEqual(
'a <span class="underline">red</span>' + ansi.ANSI_NORMAL + "foo",
parser.re_underline(
"a "
+ ansi.ANSI_UNDERLINE
+ "red"
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
+ "foo"
),
)
@unittest.skip("parser issues")
def test_re_blinking(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_blinking("foo"))
self.assertEqual(
'a <span class="blink">red</span>' + ansi.ANSI_NORMAL + "foo",
parser.re_blinking(
"a "
+ ansi.ANSI_BLINK
+ "red"
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
+ "foo"
),
)
@unittest.skip("parser issues")
def test_re_inversing(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.re_inversing("foo"))
self.assertEqual(
'a <span class="inverse">red</span>' + ansi.ANSI_NORMAL + "foo",
parser.re_inversing(
"a "
+ ansi.ANSI_INVERSE
+ "red"
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
+ "foo"
),
)
@unittest.skip("parser issues")
def test_remove_bells(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.remove_bells("foo"))
self.assertEqual(
"a red" + ansi.ANSI_NORMAL + "foo",
parser.remove_bells(
"a "
+ ansi.ANSI_BEEP
+ "red"
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
+ "foo"
),
parser.remove_bells("a " + ansi.ANSI_BEEP + "red" + ansi.ANSI_NORMAL + "foo"),
)
def test_remove_backspaces(self):
@ -110,7 +62,6 @@ class TestText2Html(TestCase):
self.assertEqual("foo", parser.convert_linebreaks("foo"))
self.assertEqual("a<br> redfoo<br>", parser.convert_linebreaks("a\n redfoo\n"))
@unittest.skip("parser issues")
def test_convert_urls(self):
parser = text2html.HTML_PARSER
self.assertEqual("foo", parser.convert_urls("foo"))
@ -118,7 +69,6 @@ class TestText2Html(TestCase):
'a <a href="http://redfoo" target="_blank">http://redfoo</a> runs',
parser.convert_urls("a http://redfoo runs"),
)
# TODO: doesn't URL encode correctly
def test_sub_mxp_links(self):
parser = text2html.HTML_PARSER
@ -186,22 +136,22 @@ class TestText2Html(TestCase):
self.assertEqual("foo", text2html.parse_html("foo"))
self.maxDiff = None
self.assertEqual(
# TODO: note that the blink is currently *not* correctly aborted
# with |n here! This is probably not possible to correctly handle
# with regex - a stateful parser may be needed.
# blink back-cyan normal underline red green yellow blue magenta cyan back-green
text2html.parse_html("|^|[CHello|n|u|rW|go|yr|bl|md|c!|[G!"),
'<span class="blink">'
'<span class="bgcolor-006">Hello</span>' # noqa
'<span class="underline">'
'<span class="color-009">W</span>' # noqa
'<span class="color-010">o</span>'
'<span class="color-011">r</span>'
'<span class="color-012">l</span>'
'<span class="color-013">d</span>'
'<span class="color-014">!'
'<span class="bgcolor-002">!</span>' # noqa
"</span>"
"</span>"
'<span class="blink bgcolor-006">'
"Hello"
'</span><span class="underline color-009">'
"W"
'</span><span class="underline color-010">'
"o"
'</span><span class="underline color-011">'
"r"
'</span><span class="underline color-012">'
"l"
'</span><span class="underline color-013">'
"d"
'</span><span class="underline color-014">'
"!"
'</span><span class="underline bgcolor-002 color-014">'
"!"
"</span>",
)

View file

@ -12,11 +12,10 @@ import re
from html import escape as html_escape
from .ansi import *
# All xterm256 RGB equivalents
XTERM256_FG = "\033[38;5;%sm"
XTERM256_BG = "\033[48;5;%sm"
XTERM256_FG = "\033[38;5;{}m"
XTERM256_BG = "\033[48;5;{}m"
class TextToHTMLparser(object):
@ -25,77 +24,65 @@ class TextToHTMLparser(object):
"""
tabstop = 4
# mapping html color name <-> ansi code.
hilite = ANSI_HILITE
unhilite = ANSI_UNHILITE # this will be stripped - there is no css equivalent.
normal = ANSI_NORMAL # "
underline = ANSI_UNDERLINE
blink = ANSI_BLINK
inverse = ANSI_INVERSE # this will produce an outline; no obvious css equivalent?
colorcodes = [
("color-000", unhilite + ANSI_BLACK), # pure black
("color-001", unhilite + ANSI_RED),
("color-002", unhilite + ANSI_GREEN),
("color-003", unhilite + ANSI_YELLOW),
("color-004", unhilite + ANSI_BLUE),
("color-005", unhilite + ANSI_MAGENTA),
("color-006", unhilite + ANSI_CYAN),
("color-007", unhilite + ANSI_WHITE), # light grey
("color-008", hilite + ANSI_BLACK), # dark grey
("color-009", hilite + ANSI_RED),
("color-010", hilite + ANSI_GREEN),
("color-011", hilite + ANSI_YELLOW),
("color-012", hilite + ANSI_BLUE),
("color-013", hilite + ANSI_MAGENTA),
("color-014", hilite + ANSI_CYAN),
("color-015", hilite + ANSI_WHITE), # pure white
] + [("color-%03i" % (i + 16), XTERM256_FG % ("%i" % (i + 16))) for i in range(240)]
colorback = [
("bgcolor-000", ANSI_BACK_BLACK), # pure black
("bgcolor-001", ANSI_BACK_RED),
("bgcolor-002", ANSI_BACK_GREEN),
("bgcolor-003", ANSI_BACK_YELLOW),
("bgcolor-004", ANSI_BACK_BLUE),
("bgcolor-005", ANSI_BACK_MAGENTA),
("bgcolor-006", ANSI_BACK_CYAN),
("bgcolor-007", ANSI_BACK_WHITE), # light grey
("bgcolor-008", hilite + ANSI_BACK_BLACK), # dark grey
("bgcolor-009", hilite + ANSI_BACK_RED),
("bgcolor-010", hilite + ANSI_BACK_GREEN),
("bgcolor-011", hilite + ANSI_BACK_YELLOW),
("bgcolor-012", hilite + ANSI_BACK_BLUE),
("bgcolor-013", hilite + ANSI_BACK_MAGENTA),
("bgcolor-014", hilite + ANSI_BACK_CYAN),
("bgcolor-015", hilite + ANSI_BACK_WHITE), # pure white
] + [("bgcolor-%03i" % (i + 16), XTERM256_BG % ("%i" % (i + 16))) for i in range(240)]
style_codes = [
# non-color style markers
ANSI_NORMAL,
ANSI_UNDERLINE,
ANSI_HILITE,
ANSI_UNHILITE,
ANSI_INVERSE,
ANSI_BLINK,
ANSI_INV_HILITE,
ANSI_BLINK_HILITE,
ANSI_INV_BLINK,
ANSI_INV_BLINK_HILITE,
]
# make sure to escape [
# colorcodes = [(c, code.replace("[", r"\[")) for c, code in colorcodes]
# colorback = [(c, code.replace("[", r"\[")) for c, code in colorback]
fg_colormap = dict((code, clr) for clr, code in colorcodes)
bg_colormap = dict((code, clr) for clr, code in colorback)
ansi_color_codes = [
# Foreground colors
ANSI_BLACK,
ANSI_RED,
ANSI_GREEN,
ANSI_YELLOW,
ANSI_BLUE,
ANSI_MAGENTA,
ANSI_CYAN,
ANSI_WHITE,
]
# create stop markers
fgstop = "(?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m|\033\[0m|$"
bgstop = "(?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m|\033\[0m|$"
bgfgstop = bgstop[:-2] + fgstop
xterm_fg_codes = [XTERM256_FG.format(i + 16) for i in range(240)]
fgstart = "((?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m)"
bgstart = "((?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m)"
bgfgstart = bgstart + r"((?:\033\[1m|\033\[22m){0,1}\033\[[3-4][0-8].*?m){0,1}"
ansi_bg_codes = [
# Background colors
ANSI_BACK_BLACK,
ANSI_BACK_RED,
ANSI_BACK_GREEN,
ANSI_BACK_YELLOW,
ANSI_BACK_BLUE,
ANSI_BACK_MAGENTA,
ANSI_BACK_CYAN,
ANSI_BACK_WHITE,
]
# extract color markers, tagging the start marker and the text marked
re_fgs = re.compile(fgstart + "(.*?)(?=" + fgstop + ")")
re_bgs = re.compile(bgstart + "(.*?)(?=" + bgstop + ")")
re_bgfg = re.compile(bgfgstart + "(.*?)(?=" + bgfgstop + ")")
xterm_bg_codes = [XTERM256_BG.format(i + 16) for i in range(240)]
re_style = re.compile(
r"({})".format(
"|".join(
style_codes + ansi_color_codes + xterm_fg_codes + ansi_bg_codes + xterm_bg_codes
).replace("[", r"\[")
)
)
colorlist = (
[ANSI_UNHILITE + code for code in ansi_color_codes]
+ [ANSI_HILITE + code for code in ansi_color_codes]
+ xterm_fg_codes
)
bglist = ansi_bg_codes + [ANSI_HILITE + code for code in ansi_bg_codes] + xterm_bg_codes
re_normal = re.compile(normal.replace("[", r"\["))
re_hilite = re.compile("(?:%s)(.*)(?=%s|%s)" % (hilite.replace("[", r"\["), fgstop, bgstop))
re_unhilite = re.compile("(?:%s)(.*)(?=%s|%s)" % (unhilite.replace("[", r"\["), fgstop, bgstop))
re_uline = re.compile("(?:%s)(.*?)(?=%s|%s)" % (underline.replace("[", r"\["), fgstop, bgstop))
re_blink = re.compile("(?:%s)(.*?)(?=%s|%s)" % (blink.replace("[", r"\["), fgstop, bgstop))
re_inverse = re.compile("(?:%s)(.*?)(?=%s|%s)" % (inverse.replace("[", r"\["), fgstop, bgstop))
re_string = re.compile(
r"(?P<htmlchars>[<&>])|(?P<tab>[\t]+)|(?P<lineend>\r\n|\r|\n)",
re.S | re.M | re.I,
@ -106,100 +93,6 @@ class TextToHTMLparser(object):
re_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
re_mxpurl = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)
def _sub_bgfg(self, colormatch):
# print("colormatch.groups()", colormatch.groups())
bgcode, fgcode, text = colormatch.groups()
if not fgcode:
ret = r"""<span class="%s">%s</span>""" % (
self.bg_colormap.get(bgcode, self.fg_colormap.get(bgcode, "err")),
text,
)
else:
ret = r"""<span class="%s"><span class="%s">%s</span></span>""" % (
self.bg_colormap.get(bgcode, self.fg_colormap.get(bgcode, "err")),
self.fg_colormap.get(fgcode, self.bg_colormap.get(fgcode, "err")),
text,
)
return ret
def _sub_fg(self, colormatch):
code, text = colormatch.groups()
return r"""<span class="%s">%s</span>""" % (self.fg_colormap.get(code, "err"), text)
def _sub_bg(self, colormatch):
code, text = colormatch.groups()
return r"""<span class="%s">%s</span>""" % (self.bg_colormap.get(code, "err"), text)
def re_color(self, text):
"""
Replace ansi colors with html color class names. Let the
client choose how it will display colors, if it wishes to.
Args:
text (str): the string with color to replace.
Returns:
text (str): Re-colored text.
"""
text = self.re_bgfg.sub(self._sub_bgfg, text)
text = self.re_fgs.sub(self._sub_fg, text)
text = self.re_bgs.sub(self._sub_bg, text)
text = self.re_normal.sub("", text)
return text
def re_bold(self, text):
"""
Clean out superfluous hilights rather than set <strong>to make
it match the look of telnet.
Args:
text (str): Text to process.
Returns:
text (str): Processed text.
"""
text = self.re_hilite.sub(r"<strong>\1</strong>", text)
return self.re_unhilite.sub(r"\1", text) # strip unhilite - there is no equivalent in css.
def re_underline(self, text):
"""
Replace ansi underline with html underline class name.
Args:
text (str): Text to process.
Returns:
text (str): Processed text.
"""
return self.re_uline.sub(r'<span class="underline">\1</span>', text)
def re_blinking(self, text):
"""
Replace ansi blink with custom blink css class
Args:
text (str): Text to process.
Returns:
text (str): Processed text.
"""
return self.re_blink.sub(r'<span class="blink">\1</span>', text)
def re_inversing(self, text):
"""
Replace ansi inverse with custom inverse css class
Args:
text (str): Text to process.
Returns:
text (str): Processed text.
"""
return self.re_inverse.sub(r'<span class="inverse">\1</span>', text)
def remove_bells(self, text):
"""
Remove ansi specials
@ -211,7 +104,7 @@ class TextToHTMLparser(object):
text (str): Processed text.
"""
return text.replace("\07", "")
return text.replace(ANSI_BEEP, "")
def remove_backspaces(self, text):
"""
@ -315,6 +208,128 @@ class TextToHTMLparser(object):
return text
return None
def format_styles(self, text):
"""
Takes a string with parsed ANSI codes and replaces them with
HTML spans and CSS classes.
Args:
text (str): The string to process.
Returns:
text (str): Processed text.
"""
# split out the ANSI codes and clean out any empty items
str_list = [substr for substr in self.re_style.split(text) if substr]
# initialize all the flags and classes
classes = []
clean = True
inverse = False
# default color is light grey - unhilite + white
hilight = ANSI_UNHILITE
fg = ANSI_WHITE
# default bg is black
bg = ANSI_BACK_BLACK
for i, substr in enumerate(str_list):
# reset all current styling
if substr == ANSI_NORMAL:
# close any existing span if necessary
str_list[i] = "</span>" if not clean else ""
# reset to defaults
classes = []
clean = True
inverse = False
hilight = ANSI_UNHILITE
fg = ANSI_WHITE
bg = ANSI_BACK_BLACK
# change color
elif substr in self.ansi_color_codes + self.xterm_fg_codes:
# erase ANSI code from output
str_list[i] = ""
# set new color
fg = substr
# change bg color
elif substr in self.ansi_bg_codes + self.xterm_bg_codes:
# erase ANSI code from output
str_list[i] = ""
# set new bg
bg = substr
# non-color codes
elif substr in self.style_codes:
# erase ANSI code from output
str_list[i] = ""
# hilight codes
if substr in (ANSI_HILITE, ANSI_UNHILITE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
# set new hilight status
hilight = ANSI_UNHILITE if substr == ANSI_UNHILITE else ANSI_HILITE
# inversion codes
if substr in (ANSI_INVERSE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
inverse = True
# blink codes
if (
substr in (ANSI_BLINK, ANSI_BLINK_HILITE, ANSI_INV_BLINK_HILITE)
and "blink" not in classes
):
classes.append("blink")
# underline
if substr == ANSI_UNDERLINE and "underline" not in classes:
classes.append("underline")
else:
# normal text, add text back to list
if not str_list[i - 1]:
# prior entry was cleared, which means style change
# get indices for the fg and bg codes
bg_index = self.bglist.index(bg)
try:
color_index = self.colorlist.index(hilight + fg)
except ValueError:
# xterm256 colors don't have the hilight codes
color_index = self.colorlist.index(fg)
if inverse:
# inverse means swap fg and bg indices
bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0"))
color_class = "color-{}".format(str(bg_index).rjust(3, "0"))
else:
# use fg and bg indices for classes
bg_class = "bgcolor-{}".format(str(bg_index).rjust(3, "0"))
color_class = "color-{}".format(str(color_index).rjust(3, "0"))
# black bg is the default, don't explicitly style
if bg_class != "bgcolor-000":
classes.append(bg_class)
# light grey text is the default, don't explicitly style
if color_class != "color-007":
classes.append(color_class)
# define the new style span
prefix = '<span class="{}">'.format(" ".join(classes))
# close any prior span
if not clean:
prefix = "</span>" + prefix
# add span to output
str_list[i - 1] = prefix
# clean out color classes to easily update next time
classes = [cls for cls in classes if "color" not in cls]
# flag as currently being styled
clean = False
# close span if necessary
if not clean:
str_list.append("</span>")
# recombine back into string
return "".join(str_list)
def parse(self, text, strip_ansi=False):
"""
Main access function, converts a text containing ANSI codes
@ -328,19 +343,14 @@ class TextToHTMLparser(object):
text (str): Parsed text.
"""
# print(f"incoming text:\n{text}")
# parse everything to ansi first
text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=True)
# convert all ansi to html
result = re.sub(self.re_string, self.sub_text, text)
result = re.sub(self.re_mxplink, self.sub_mxp_links, result)
result = re.sub(self.re_mxpurl, self.sub_mxp_urls, result)
result = self.re_color(result)
result = self.re_bold(result)
result = self.re_underline(result)
result = self.re_blinking(result)
result = self.re_inversing(result)
result = self.remove_bells(result)
result = self.format_styles(result)
result = self.convert_linebreaks(result)
result = self.remove_backspaces(result)
result = self.convert_urls(result)

View file

@ -819,7 +819,7 @@ def latinify(string, default="?", pure_ascii=False):
This is used as a last resort when normal encoding does not work.
Arguments:
string (str): A string to convert to 'safe characters' convertable
string (str): A string to convert to 'safe characters' convertible
to an latin-1 bytestring later.
default (str, optional): Characters resisting mapping will be replaced
with this character or string. The intent is to apply an encode operation
@ -1078,7 +1078,7 @@ def delay(timedelay, callback, *args, **kwargs):
Keep in mind that persistent tasks arguments and callback should not
use memory references.
If persistent is set to True the delay function will return an int
which is the task's id itended for use with TASK_HANDLER's do_task
which is the task's id intended for use with TASK_HANDLER's do_task
and remove methods.
All persistent tasks whose time delays have passed will be called on server startup.
@ -1531,12 +1531,12 @@ def class_from_module(path, defaultpaths=None, fallback=None):
defaultpaths (iterable, optional): If a direct import from `path` fails,
try subsequent imports by prepending those paths to `path`.
fallback (str): If all other attempts fail, use this path as a fallback.
This is intended as a last-resport. In the example of Evennia
This is intended as a last-resort. In the example of Evennia
loading, this would be a path to a default parent class in the
evennia repo itself.
Returns:
class (Class): An uninstatiated class recovered from path.
class (Class): An uninstantiated class recovered from path.
Raises:
ImportError: If all loading failed.
@ -1675,7 +1675,7 @@ def string_partial_matching(alternatives, inp, ret_index=True):
Matching is made from the start of each subword in each
alternative. Case is not important. So e.g. "bi sh sw" or just
"big" or "shiny" or "sw" will match "Big shiny sword". Scoring is
done to allow to separate by most common demoninator. You will get
done to allow to separate by most common denominator. You will get
multiple matches returned if appropriate.
Args:
@ -1749,7 +1749,7 @@ def format_table(table, extra_space=1):
ftable = format_table([[1,2,3], [4,5,6]])
string = ""
for ir, row in enumarate(ftable):
for ir, row in enumerate(ftable):
if ir == 0:
# make first row white
string += "\\n|w" + "".join(row) + "|n"
@ -2695,6 +2695,7 @@ def copy_word_case(base_word, new_word):
+ excess
)
def run_in_main_thread(function_or_method, *args, **kwargs):
"""
Force a callable to execute in the main Evennia thread. This is only relevant when

View file

@ -149,7 +149,7 @@ An "emitter" object must have a function
// kwargs (obj): keyword-args to listener
//
emit: function (cmdname, args, kwargs) {
if (kwargs.cmdid) {
if (kwargs.cmdid && (kwargs.cmdid in cmdmap)) {
cmdmap[kwargs.cmdid].apply(this, [args, kwargs]);
delete cmdmap[kwargs.cmdid];
}