Make examine command more modular; show attr-categories and value types.

See #1805.
This commit is contained in:
Griatch 2021-11-21 03:37:32 +01:00
parent fa3c2aacb7
commit 09f51a644a
4 changed files with 377 additions and 339 deletions

View file

@ -124,6 +124,8 @@ Up requirements to Django 3.2+, Twisted 21+
by another command (like `open` and `@open`. If no duplicate, @ is optional. by another command (like `open` and `@open`. If no duplicate, @ is optional.
- Move legacy channel-management commands (`ccreate`, `addcom` etc) to a contrib - Move legacy channel-management commands (`ccreate`, `addcom` etc) to a contrib
since their work is now fully handled by the single `channel` command. since their work is now fully handled by the single `channel` command.
- Expand `examine` command's code to much more extensible and modular. Show
attribute categories and value types (when not strings).
### Evennia 0.9.5 (2019-2020) ### Evennia 0.9.5 (2019-2020)

View file

@ -11,6 +11,7 @@ from evennia.objects.models import ObjectDB
from evennia.locks.lockhandler import LockException from evennia.locks.lockhandler import LockException
from evennia.commands.cmdhandler import get_and_merge_cmdsets from evennia.commands.cmdhandler import get_and_merge_cmdsets
from evennia.utils import create, utils, search, logger, funcparser from evennia.utils import create, utils, search, logger, funcparser
from evennia.utils.dbserialize import deserialize
from evennia.utils.utils import ( from evennia.utils.utils import (
inherits_from, inherits_from,
class_from_module, class_from_module,
@ -2430,379 +2431,407 @@ class CmdExamine(ObjManipCommand):
quell_color = "|r" quell_color = "|r"
separator = "-" separator = "-"
def list_attribute(self, crop, attr, category, value): def msg(self, text):
""" """
Formats a single attribute line. Central point for sending messages to the caller. This tags
the message as 'examine' for eventual custom markup in the client.
Attributes:
text (str): The text to send.
Args:
crop (bool): If output should be cropped if too long.
attr (str): Attribute key.
category (str): Attribute category.
value (any): Attribute value.
Returns:
""" """
self.caller.msg(text=(text, {"type": "examine"}))
def format_key(self, obj):
return f"{obj.name} ({obj.dbref})"
def format_aliases(self, obj):
if hasattr(obj, "aliases") and obj.aliases.all():
return ", ".join(utils.make_iter(str(obj.aliases)))
def format_typeclass(self, obj):
if hasattr(obj, "typeclass"):
return f"{obj.typename} ({obj.typeclass_path})"
def format_sessions(self, obj):
if hasattr(obj, "sessions"):
sessions = obj.sessions.all()
if sessions:
return ", ".join(f"#{sess.sessid}" for sess in obj.sessions.all())
def format_email(self, obj):
if hasattr(obj, "email") and obj.email:
return f"{self.detail_color}{obj.email}|n"
def format_account_key(self, account):
return f"{self.detail_color}{account.name}|n ({account.dbref})"
def format_account_typeclass(self, account):
return f"{account.typename} ({account.typeclass_path})"
def format_account_permissions(self, account):
perms = account.permissions.all()
if account.is_superuser:
perms = ["<Superuser>"]
elif not perms:
perms = ["<None>"]
perms = ", ".join(perms)
if account.attributes.has("_quell"):
perms += f" {self.quell_color}(quelled)|n"
return perms
def format_location(self, obj):
if hasattr(obj, "location") and obj.location:
return f"{obj.location.key} (#{obj.location.id})"
def format_home(self, obj):
if hasattr(obj, "home") and obj.home:
return f"{obj.home.key} (#{obj.home.id})"
def format_destination(self, obj):
if hasattr(obj, "destination") and obj.destination:
return f"{obj.destination.key} (#{obj.destination.id})"
def format_permissions(self, obj):
perms = obj.permissions.all()
if perms:
perms_string = ", ".join(perms)
if obj.is_superuser:
perms_string += " <Superuser>"
return perms_string
def format_locks(self, obj):
locks = str(obj.locks)
if locks:
return utils.fill(
"; ".join([lock for lock in locks.split(";")]), indent=2
)
return "Default"
def format_scripts(self, obj):
if hasattr(obj, "scripts") and hasattr(obj.scripts, "all") and obj.scripts.all():
return f"{obj.scripts}"
def format_single_tag(self, tag):
if tag.db_category:
return f"{tag.db_key}[{tag.db_category}]"
else:
return f"{tag.db_key}"
def format_tags(self, obj):
if hasattr(obj, "tags"):
tags = sorted(obj.tags.all(return_objs=True))
if tags:
formatted_tags = [self.format_single_tag(tag) for tag in tags]
return utils.fill(", ".join(formatted_tags), indent=2)
def format_single_cmdset_options(self, cmdset):
def _truefalse(string, value):
if value is None:
return ""
if value:
return f"{string}: T"
return f"{string}: F"
return ", ".join(
_truefalse(opt, getattr(cmdset, opt))
for opt in ("no_exits", "no_objs", "no_channels", "duplicates")
if getattr(cmdset, opt) is not None
)
def format_single_cmdset(self, cmdset):
options = self.format_single_cmdset_options(cmdset)
return f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority}{options}"
def format_stored_cmdsets(self, obj):
if hasattr(obj, "cmdset"):
stored_cmdset_strings = []
stored_cmdsets = sorted(obj.cmdset.all(), key=lambda x: x.priority, reverse=True)
for cmdset in stored_cmdsets:
if cmdset.key != "_EMPTY_CMDSET":
stored_cmdset_strings.append(self.format_single_cmdset(cmdset))
return "\n ".join(stored_cmdset_strings)
def format_merged_cmdsets(self, obj, current_cmdset):
if not hasattr(obj, "cmdset"):
return None
all_cmdsets = [(cmdset.key, cmdset) for cmdset in current_cmdset.merged_from]
# we always at least try to add account- and session sets since these are ignored
# if we merge on the object level.
if hasattr(obj, "account") and obj.account:
# get Attribute-cmdsets if they exist
all_cmdsets.extend([(cmdset.key, cmdset) for cmdset in obj.account.cmdset.all()])
if obj.sessions.count():
# if there are more sessions than one on objects it's because of multisession mode
# we only show the first session's cmdset here (it is -in principle- possible
# that different sessions have different cmdsets but for admins who want such
# madness it is better that they overload with their own CmdExamine to handle it).
all_cmdsets.extend([(cmdset.key, cmdset)
for cmdset in obj.account.sessions.all()[0].cmdset.all()])
else:
try:
# we have to protect this since many objects don't have sessions.
all_cmdsets.extend([(cmdset.key, cmdset)
for cmdset in obj.get_session(obj.sessions.get()).cmdset.all()])
except (TypeError, AttributeError):
# an error means we are merging an object without a session
pass
all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()]
all_cmdsets.sort(key=lambda x: x.priority, reverse=True)
merged_cmdset_strings = []
for cmdset in all_cmdsets:
if cmdset.key != "_EMPTY_CMDSET":
merged_cmdset_strings.append(self.format_single_cmdset(cmdset))
return "\n ".join(merged_cmdset_strings)
def format_current_cmds(self, obj, current_cmdset):
current_commands = sorted([cmd.key for cmd in current_cmdset if cmd.access(obj, "cmd")])
return "\n" + utils.fill(", ".join(current_commands), indent=2)
def _get_attribute_value_type(self, attrvalue):
typ = ""
if not isinstance(attrvalue, str):
try:
name = attrvalue.__class__.__name__
except AttributeError:
try:
name = attrvalue.__name__
except AttributeError:
name = attrvalue
if str(name).startswith("_Saver"):
try:
typ = str(type(deserialize(attrvalue)))
except Exception:
typ = str(type(deserialize(attrvalue)))
else:
typ = str(type(attrvalue))
return typ
def format_single_attribute_detail(self, obj, attr):
global _FUNCPARSER global _FUNCPARSER
if not _FUNCPARSER: if not _FUNCPARSER:
_FUNCPARSER = funcparser.FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES) _FUNCPARSER = funcparser.FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES)
if attr is None: key, category, value = attr.db_key, attr.db_category, attr.value
return "No such attribute was found." typ = self._get_attribute_value_type(value)
typ = f" |B[type: {typ}]|n" if typ else ""
value = utils.to_str(value) value = utils.to_str(value)
if crop:
value = utils.crop(value)
value = _FUNCPARSER.parse(ansi_raw(value), escape=True) value = _FUNCPARSER.parse(ansi_raw(value), escape=True)
return (f"Attribute {obj.name}/{self.header_color}{key}|n "
f"[category={category}]{typ}:\n\n{value}")
def format_single_attribute(self, attr):
global _FUNCPARSER
if not _FUNCPARSER:
_FUNCPARSER = funcparser.FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES)
key, category, value = attr.db_key, attr.db_category, attr.value
typ = self._get_attribute_value_type(value)
typ = f" |B[type: {typ}]|n" if typ else ""
value = utils.to_str(value)
value = _FUNCPARSER.parse(ansi_raw(value), escape=True)
value = utils.crop(value)
if category: if category:
return f"{attr}[{category}] = {value}" return f"{self.header_color}{key}|n[{category}]={value}{typ}"
else: else:
return f"{attr} = {value}" return f"{self.header_color}{key}|n={value}{typ}"
def format_attributes(self, obj, attrname=None, crop=True): def format_attributes(self, obj):
""" return "\n " + "\n ".join(
Helper function that returns info about attributes and/or sorted(self.format_single_attribute(attr)
non-persistent data stored on object for attr in obj.db_attributes.all())
)
""" def format_nattributes(self, obj):
if attrname: try:
if obj.attributes.has(attrname): ndb_attr = obj.nattributes.all(return_tuples=True)
db_attr = [(attrname, obj.attributes.get(attrname), None)] except Exception:
else: return
db_attr = None
try:
ndb_attr = [(attrname, object.__getattribute__(obj.ndb, attrname))]
except Exception:
ndb_attr = None
if not (db_attr or ndb_attr):
return {"Attribue(s)": f"\n No Attribute '{attrname}' found on {obj.name}"}
else:
db_attr = [(attr.key, attr.value, attr.category) for attr in obj.db_attributes.all()]
try:
ndb_attr = obj.nattributes.all(return_tuples=True)
except Exception:
ndb_attr = (None, None, None)
output = {}
if db_attr and db_attr[0]:
output["Persistent attribute(s)"] = "\n " + "\n ".join(
sorted(
self.list_attribute(crop, attr, category, value)
for attr, value, category in db_attr
)
)
if ndb_attr and ndb_attr[0]: if ndb_attr and ndb_attr[0]:
output["Non-Persistent attribute(s)"] = " \n" + " \n".join( return "\n " + " \n".join(
sorted(self.list_attribute(crop, attr, None, value) for attr, value in ndb_attr) sorted(self.format_single_attribute(attr)
for attr, value in ndb_attr)
) )
return output
def format_exits(self, obj):
if hasattr(obj, "exits"):
exits = ", ".join(f"{exit.name}({exit.dbref})" for exit in obj.exits)
return exits if exits else None
def format_chars(self, obj):
if hasattr(obj, "contents"):
chars = ", ".join(f"{obj.name}({obj.dbref})" for obj in obj.contents
if obj.account)
return chars if chars else None
def format_things(self, obj):
if hasattr(obj, "contents"):
things = ", ".join(f"{obj.name}({obj.dbref})" for obj in obj.contents
if not obj.account and not obj.destination)
return things if things else None
def get_formatted_obj_data(self, obj, current_cmdset):
"""
Calls all other `format_*` methods.
"""
objdata = {}
objdata["Name/key"] = self.format_key(obj)
objdata["Aliases"] = self.format_aliases(obj)
objdata["Typeclass"] = self.format_typeclass(obj)
objdata["Sessions"] = self.format_sessions(obj)
objdata["Email"] = self.format_email(obj)
if hasattr(obj, "has_account") and obj.has_account:
objdata["Account"] = self.format_account_key(obj.account)
objdata[" Account Typeclass"] = self.format_account_typeclass(obj.account)
objdata[" Account Permissions"] = self.format_account_permissions(obj.account)
objdata["Location"] = self.format_location(obj)
objdata["Home"] = self.format_home(obj)
objdata["Destination"] = self.format_destination(obj)
objdata["Permissions"] = self.format_permissions(obj)
objdata["Locks"] = self.format_locks(obj)
if (current_cmdset
and not (len(obj.cmdset.all()) == 1
and obj.cmdset.current.key == "_EMPTY_CMDSET")):
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))"] = (
self.format_current_cmds(obj, current_cmdset))
objdata["Scripts"] = self.format_scripts(obj)
objdata["Tags"] = self.format_tags(obj)
objdata["Persistent Attributes"] = self.format_attributes(obj)
objdata["Non-Persistent Attributes"] = self.format_nattributes(obj)
objdata["Exits"] = self.format_exits(obj)
objdata["Characters"] = self.format_chars(obj)
objdata["Content"] = self.format_things(obj)
return objdata
def format_output(self, obj, current_cmdset): def format_output(self, obj, current_cmdset):
""" """
Helper function that creates a nice report about an object. Formats the full examine page return.
Args:
obj (any): Object to analyze.
current_cmdset (CmdSet): Current cmdset for object.
Returns:
str: The formatted string.
""" """
hclr = self.header_color objdata = self.get_formatted_obj_data(obj, current_cmdset)
dclr = self.detail_color
qclr = self.quell_color
output = {}
# main key
output["Name/key"] = f"{dclr}{obj.name}|n ({obj.dbref})"
# aliases
if hasattr(obj, "aliases") and obj.aliases.all():
output["Aliases"] = ", ".join(utils.make_iter(str(obj.aliases)))
# typeclass
output["Typeclass"] = f"{obj.typename} ({obj.typeclass_path})"
# sessions
if hasattr(obj, "sessions") and obj.sessions.all():
output["Session id(s)"] = ", ".join(f"#{sess.sessid}" for sess in obj.sessions.all())
# email, if any
if hasattr(obj, "email") and obj.email:
output["Email"] = f"{dclr}{obj.email}|n"
# account, for puppeted objects
if hasattr(obj, "has_account") and obj.has_account:
output["Account"] = f"{dclr}{obj.account.name}|n ({obj.account.dbref})"
# account typeclass
output[" Account Typeclass"] = f"{obj.account.typename} ({obj.account.typeclass_path})"
# account permissions
perms = obj.account.permissions.all()
if obj.account.is_superuser:
perms = ["<Superuser>"]
elif not perms:
perms = ["<None>"]
perms = ", ".join(perms)
if obj.account.attributes.has("_quell"):
perms += f" {qclr}(quelled)|n"
output[" Account Permissions"] = perms
# location
if hasattr(obj, "location"):
loc = str(obj.location)
if obj.location:
loc += f" (#{obj.location.id})"
output["Location"] = loc
# home
if hasattr(obj, "home"):
home = str(obj.home)
if obj.home:
home += f" (#{obj.home.id})"
output["Home"] = home
# destination, for exits
if hasattr(obj, "destination") and obj.destination:
dest = str(obj.destination)
if obj.destination:
dest += f" (#{obj.destination.id})"
output["Destination"] = dest
# main permissions
perms = obj.permissions.all()
perms_string = ""
if perms:
perms_string = ", ".join(perms)
if obj.is_superuser:
perms_string += " <Superuser>"
if perms_string:
output["Permissions"] = perms_string
# locks
locks = str(obj.locks)
if locks:
locks_string = "\n" + utils.fill(
"; ".join([lock for lock in locks.split(";")]), indent=2
)
else:
locks_string = " Default"
output["Locks"] = locks_string
# cmdsets
if current_cmdset and not (
len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "_EMPTY_CMDSET"):
# all() returns a 'stack', so make a copy to sort.
def _format_options(cmdset):
"""helper for cmdset-option display"""
def _truefalse(string, value):
if value is None:
return ""
if value:
return f"{string}: T"
return f"{string}: F"
options = ", ".join(
_truefalse(opt, getattr(cmdset, opt))
for opt in ("no_exits", "no_objs", "no_channels", "duplicates")
if getattr(cmdset, opt) is not None
)
options = ", " + options if options else ""
return options
# cmdset stored on us
stored_cmdsets = sorted(obj.cmdset.all(), key=lambda x: x.priority, reverse=True)
stored = []
for cmdset in stored_cmdsets:
if cmdset.key == "_EMPTY_CMDSET":
continue
options = _format_options(cmdset)
stored.append(
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority}{options})")
output["Stored Cmdset(s)"] = "\n " + "\n ".join(stored)
# this gets all components of the currently merged set
all_cmdsets = [(cmdset.key, cmdset) for cmdset in current_cmdset.merged_from]
# we always at least try to add account- and session sets since these are ignored
# if we merge on the object level.
if hasattr(obj, "account") and obj.account:
all_cmdsets.extend([(cmdset.key, cmdset) for cmdset in obj.account.cmdset.all()])
if obj.sessions.count():
# if there are more sessions than one on objects it's because of multisession mode 3.
# we only show the first session's cmdset here (it is -in principle- possible that
# different sessions have different cmdsets but for admins who want such madness
# it is better that they overload with their own CmdExamine to handle it).
all_cmdsets.extend(
[
(cmdset.key, cmdset)
for cmdset in obj.account.sessions.all()[0].cmdset.all()
]
)
else:
try:
# we have to protect this since many objects don't have sessions.
all_cmdsets.extend(
[
(cmdset.key, cmdset)
for cmdset in obj.get_session(obj.sessions.get()).cmdset.all()
]
)
except (TypeError, AttributeError):
# an error means we are merging an object without a session
pass
all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()]
all_cmdsets.sort(key=lambda x: x.priority, reverse=True)
# the resulting merged cmdset
options = _format_options(current_cmdset)
merged = [
f"<Current merged cmdset> ({current_cmdset.mergetype} prio {current_cmdset.priority}{options})"]
# the merge stack
for cmdset in all_cmdsets:
options = _format_options(cmdset)
merged.append(
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype} prio {cmdset.priority}{options})")
output["Merged Cmdset(s)"] = "\n " + "\n ".join(merged)
# list the commands available to this object
current_commands = sorted([cmd.key for cmd in current_cmdset if cmd.access(obj, "cmd")])
cmdsetstr = "\n" + utils.fill(", ".join(current_commands), indent=2)
output[f"Commands available to {obj.key} (result of Merged CmdSets)"] = str(cmdsetstr)
# scripts
if hasattr(obj, "scripts") and hasattr(obj.scripts, "all") and obj.scripts.all():
output["Scripts"] = "\n " + f"{obj.scripts}"
# add the attributes
output.update(self.format_attributes(obj))
# Tags
tags = obj.tags.all(return_key_and_category=True)
tags_string = "\n" + utils.fill(
", ".join(sorted(f"{tag}[{category}]" for tag, category in tags)), indent=2,
)
if tags:
output["Tags[category]"] = tags_string
# Contents of object
exits = []
pobjs = []
things = []
if hasattr(obj, "contents"):
for content in obj.contents:
if content.destination:
exits.append(content)
elif content.account:
pobjs.append(content)
else:
things.append(content)
if exits:
output["Exits (has .destination)"] = ", ".join(
f"{exit.name}({exit.dbref})" for exit in exits
)
if pobjs:
output["Characters"] = ", ".join(
f"{dclr}{pobj.name}|n({pobj.dbref})" for pobj in pobjs
)
if things:
output["Contents"] = ", ".join(
f"{cont.name}({cont.dbref})"
for cont in obj.contents
if cont not in exits and cont not in pobjs
)
# format output # format output
main_str = []
max_width = -1 max_width = -1
for block in output.values(): for header, block in objdata.items():
max_width = max(max_width, max(display_len(line) for line in block.split("\n"))) if block is not None:
max_width = max(0, min(self.client_width(), max_width)) blockstr = f"{self.header_color}{header}|n: {block}"
max_width = max(max_width, max(display_len(line) for line in blockstr.split("\n")))
main_str.append(blockstr)
main_str = "\n".join(main_str)
max_width = max(0, min(self.client_width(), max_width))
sep = self.separator * max_width sep = self.separator * max_width
mainstr = "\n".join(f"{hclr}{header}|n: {block}" for (header, block) in output.items())
return f"{sep}\n{mainstr}\n{sep}" return f"{sep}\n{main_str}\n{sep}"
def parse(self):
super().parse()
self.examine_objs = []
if not self.args:
# If no arguments are provided, examine the invoker's location.
if hasattr(self.caller, "location"):
self.examine_objs.append((self.caller.location, None))
else:
self.msg("You need to supply a target to examine.")
raise InterruptCommand
else:
for objdef in self.lhs_objattr:
obj = None
obj_name = objdef["name"] # name
obj_attrs = objdef["attrs"] # /attrs
self.account_mode = (
utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount")
or "account" in self.switches
or obj_name.startswith("*")
)
if self.account_mode:
try:
obj = self.caller.search_account(obj_name.lstrip("*"))
except AttributeError:
# this means we are calling examine from an account object
obj = self.caller.search(
obj_name.lstrip("*"), search_object="object" in self.switches
)
else:
obj = self.caller.search(obj_name)
if obj:
self.examine_objs.append((obj, obj_attrs))
def func(self): def func(self):
"""Process command""" """Process command"""
caller = self.caller def get_cmdset_callback(current_cmdset):
def get_cmdset_callback(cmdset):
""" """
We make use of the cmdhandeler.get_and_merge_cmdsets below. This We make use of the cmdhandler.get_and_merge_cmdsets below. This
is an asynchronous function, returning a Twisted deferred. is an asynchronous function, returning a Twisted deferred.
So in order to properly use this we need use this callback; So in order to properly use this we need use this callback;
it is called with the result of get_and_merge_cmdsets, whenever it is called with the result of get_and_merge_cmdsets, whenever
that function finishes. Taking the resulting cmdset, we continue that function finishes. Taking the resulting cmdset, we continue
to format and output the result. to format and output the result.
""" """
self.msg(self.format_output(obj, cmdset).strip()) self.msg(self.format_output(obj, current_cmdset).strip())
if not self.args: for obj, obj_attrs in self.examine_objs:
# If no arguments are provided, examine the invoker's location. # these are parsed out in .parse already
if hasattr(caller, "location"):
obj = caller.location
if not obj.access(caller, "examine"):
# If we don't have special info access, just look at the object instead.
self.msg(caller.at_look(obj))
return
obj_session = obj.sessions.get()[0] if obj.sessions.count() else None
# using callback for printing result whenever function returns. if not obj.access(self.caller, "examine"):
get_and_merge_cmdsets(
obj, obj_session, self.account, obj, "object", self.raw_string
).addCallback(get_cmdset_callback)
else:
self.msg("You need to supply a target to examine.")
return
# we have given a specific target object
for objdef in self.lhs_objattr:
obj = None
obj_name = objdef["name"]
obj_attrs = objdef["attrs"]
self.account_mode = (
utils.inherits_from(caller, "evennia.accounts.accounts.DefaultAccount")
or "account" in self.switches
or obj_name.startswith("*")
)
if self.account_mode:
try:
obj = caller.search_account(obj_name.lstrip("*"))
except AttributeError:
# this means we are calling examine from an account object
obj = caller.search(
obj_name.lstrip("*"), search_object="object" in self.switches
)
else:
obj = caller.search(obj_name)
if not obj:
continue
if not obj.access(caller, "examine"):
# If we don't have special info access, just look # If we don't have special info access, just look
# at the object instead. # at the object instead.
self.msg(caller.at_look(obj)) self.msg(self.caller.at_look(obj))
continue continue
if obj_attrs: if obj_attrs:
for attrname in obj_attrs: # we are only interested in specific attributes
# we are only interested in specific attributes attrs = [attr for attr in obj.db_attributes.all() if attr.db_key in obj_attrs]
ret = "\n".join( if not attrs:
f"{self.header_color}{header}|n:{value}" self.msg("No attributes found on {obj.name}.")
for header, value in self.format_attributes( else:
obj, attrname, crop=False out_strings = []
).items() for attr in attrs:
) out_strings.append(self.format_single_attribute_detail(obj, attr))
self.caller.msg(ret) out_str = "\n".join(out_strings)
max_width = max(display_len(line) for line in out_strings)
max_width = max(0, min(max_width, self.client_width()))
sep = self.separator * max_width
self.msg(f"{sep}\n{out_str}")
return
# examine the obj itself
# get the cmdset status
session = None
if obj.sessions.count():
mergemode = "session"
session = obj.sessions.get()[0]
elif self.account_mode:
mergemode = "account"
else: else:
session = None mergemode = "object"
if obj.sessions.count():
mergemode = "session"
session = obj.sessions.get()[0]
elif self.account_mode:
mergemode = "account"
else:
mergemode = "object"
account = None account = None
objct = None objct = None
if self.account_mode: if self.account_mode:
account = obj account = obj
else: else:
account = obj.account account = obj.account
objct = obj objct = obj
# this is usually handled when a command runs, but when we examine # this is usually handled when a command runs, but when we examine
# we may have leftover inherited cmdsets directly after a move etc. # we may have leftover inherited cmdsets directly after a move etc.
obj.cmdset.update() obj.cmdset.update()
# using callback to print results whenever function returns. # using callback to print results whenever function returns.
get_and_merge_cmdsets( get_and_merge_cmdsets(
obj, session, account, objct, mergemode, self.raw_string obj, session, account, objct, mergemode, self.raw_string
).addCallback(get_cmdset_callback) ).addCallback(get_cmdset_callback)
class CmdFind(COMMAND_DEFAULT_CLASS): class CmdFind(COMMAND_DEFAULT_CLASS):
@ -3312,7 +3341,7 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
) )
# last N table # last N table
objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim) :] objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim): ]
latesttable = self.styled_table( latesttable = self.styled_table(
"|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", align="l", border="table" "|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", align="l", border="table"
) )

View file

@ -918,7 +918,7 @@ class TestBuilding(CommandTest):
self.call(building.CmdExamine(), "*TestAccount", "Name/key: TestAccount") self.call(building.CmdExamine(), "*TestAccount", "Name/key: TestAccount")
self.char1.db.test = "testval" self.char1.db.test = "testval"
self.call(building.CmdExamine(), "self/test", "Persistent attribute(s):\n test = testval") self.call(building.CmdExamine(), "self/test", "Attribute Char/test [category=None]:\n\ntestval")
self.call(building.CmdExamine(), "NotFound", "Could not find 'NotFound'.") self.call(building.CmdExamine(), "NotFound", "Could not find 'NotFound'.")
self.call(building.CmdExamine(), "out", "Name/key: out") self.call(building.CmdExamine(), "out", "Name/key: out")
@ -927,7 +927,7 @@ class TestBuilding(CommandTest):
self.call( self.call(
building.CmdExamine(), building.CmdExamine(),
"self/test2", "self/test2",
"Persistent attribute(s):\n test2 = this is a \$random() value.", "Attribute Char/test2 [category=None]:\n\nthis is a \$random() value."
) )
self.room1.scripts.add(self.script.__class__) self.room1.scripts.add(self.script.__class__)
@ -1864,6 +1864,7 @@ class TestBuilding(CommandTest):
) )
from evennia.utils.create import create_channel # noqa from evennia.utils.create import create_channel # noqa
class TestCommsChannel(CommandTest): class TestCommsChannel(CommandTest):
@ -2085,6 +2086,21 @@ class TestCommsChannel(CommandTest):
) )
from evennia.comms import comms # noqa
class TestComms(CommandTest):
def test_page(self):
self.call(
comms.CmdPage(),
"TestAccount2 = Test",
"TestAccount2 is offline. They will see your message if they list their pages later."
"|You paged TestAccount2 with: 'Test'.",
receiver=self.account,
)
class TestBatchProcess(CommandTest): class TestBatchProcess(CommandTest):
@patch("evennia.contrib.tutorial_examples.red_button.repeat") @patch("evennia.contrib.tutorial_examples.red_button.repeat")

View file

@ -3418,15 +3418,6 @@ class TestLegacyMuxComms(CommandTest):
receiver=self.account, receiver=self.account,
) )
def test_page(self):
self.call(
comms.CmdPage(),
"TestAccount2 = Test",
"TestAccount2 is offline. They will see your message if they list their pages later."
"|You paged TestAccount2 with: 'Test'.",
receiver=self.account,
)
def test_cboot(self): def test_cboot(self):
# No one else connected to boot # No one else connected to boot
self.call( self.call(