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,355 +2431,383 @@ 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.
Args: Attributes:
crop (bool): If output should be cropped if too long. text (str): The text to send.
attr (str): Attribute key.
category (str): Attribute category.
value (any): Attribute value.
Returns:
"""
global _FUNCPARSER
if not _FUNCPARSER:
_FUNCPARSER = funcparser.FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES)
if attr is None:
return "No such attribute was found."
value = utils.to_str(value)
if crop:
value = utils.crop(value)
value = _FUNCPARSER.parse(ansi_raw(value), escape=True)
if category:
return f"{attr}[{category}] = {value}"
else:
return f"{attr} = {value}"
def format_attributes(self, obj, attrname=None, crop=True):
"""
Helper function that returns info about attributes and/or
non-persistent data stored on object
""" """
if attrname: self.caller.msg(text=(text, {"type": "examine"}))
if obj.attributes.has(attrname):
db_attr = [(attrname, obj.attributes.get(attrname), None)]
else:
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 = {} def format_key(self, obj):
if db_attr and db_attr[0]: return f"{obj.name} ({obj.dbref})"
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]:
output["Non-Persistent attribute(s)"] = " \n" + " \n".join(
sorted(self.list_attribute(crop, attr, None, value) for attr, value in ndb_attr)
)
return output
def format_output(self, obj, current_cmdset): def format_aliases(self, obj):
"""
Helper function that creates a nice report about an object.
Args:
obj (any): Object to analyze.
current_cmdset (CmdSet): Current cmdset for object.
Returns:
str: The formatted string.
"""
hclr = self.header_color
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(): if hasattr(obj, "aliases") and obj.aliases.all():
output["Aliases"] = ", ".join(utils.make_iter(str(obj.aliases))) return ", ".join(utils.make_iter(str(obj.aliases)))
# typeclass
output["Typeclass"] = f"{obj.typename} ({obj.typeclass_path})" def format_typeclass(self, obj):
# sessions if hasattr(obj, "typeclass"):
if hasattr(obj, "sessions") and obj.sessions.all(): return f"{obj.typename} ({obj.typeclass_path})"
output["Session id(s)"] = ", ".join(f"#{sess.sessid}" for sess in obj.sessions.all())
# email, if any 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: if hasattr(obj, "email") and obj.email:
output["Email"] = f"{dclr}{obj.email}|n" return f"{self.detail_color}{obj.email}|n"
# account, for puppeted objects
if hasattr(obj, "has_account") and obj.has_account: def format_account_key(self, account):
output["Account"] = f"{dclr}{obj.account.name}|n ({obj.account.dbref})" return f"{self.detail_color}{account.name}|n ({account.dbref})"
# account typeclass
output[" Account Typeclass"] = f"{obj.account.typename} ({obj.account.typeclass_path})" def format_account_typeclass(self, account):
# account permissions return f"{account.typename} ({account.typeclass_path})"
perms = obj.account.permissions.all()
if obj.account.is_superuser: def format_account_permissions(self, account):
perms = account.permissions.all()
if account.is_superuser:
perms = ["<Superuser>"] perms = ["<Superuser>"]
elif not perms: elif not perms:
perms = ["<None>"] perms = ["<None>"]
perms = ", ".join(perms) perms = ", ".join(perms)
if obj.account.attributes.has("_quell"): if account.attributes.has("_quell"):
perms += f" {qclr}(quelled)|n" perms += f" {self.quell_color}(quelled)|n"
output[" Account Permissions"] = perms return perms
# location
if hasattr(obj, "location"): def format_location(self, obj):
loc = str(obj.location) if hasattr(obj, "location") and obj.location:
if obj.location: return f"{obj.location.key} (#{obj.location.id})"
loc += f" (#{obj.location.id})"
output["Location"] = loc def format_home(self, obj):
# home if hasattr(obj, "home") and obj.home:
if hasattr(obj, "home"): return f"{obj.home.key} (#{obj.home.id})"
home = str(obj.home)
if obj.home: def format_destination(self, obj):
home += f" (#{obj.home.id})"
output["Home"] = home
# destination, for exits
if hasattr(obj, "destination") and obj.destination: if hasattr(obj, "destination") and obj.destination:
dest = str(obj.destination) return f"{obj.destination.key} (#{obj.destination.id})"
if obj.destination:
dest += f" (#{obj.destination.id})" def format_permissions(self, obj):
output["Destination"] = dest
# main permissions
perms = obj.permissions.all() perms = obj.permissions.all()
perms_string = ""
if perms: if perms:
perms_string = ", ".join(perms) perms_string = ", ".join(perms)
if obj.is_superuser: if obj.is_superuser:
perms_string += " <Superuser>" perms_string += " <Superuser>"
if perms_string: return perms_string
output["Permissions"] = perms_string
# locks def format_locks(self, obj):
locks = str(obj.locks) locks = str(obj.locks)
if locks: if locks:
locks_string = "\n" + utils.fill( return utils.fill(
"; ".join([lock for lock in locks.split(";")]), indent=2 "; ".join([lock for lock in locks.split(";")]), indent=2
) )
else: return "Default"
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): def format_scripts(self, obj):
"""helper for cmdset-option display""" 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): def _truefalse(string, value):
if value is None: if value is None:
return "" return ""
if value: if value:
return f"{string}: T" return f"{string}: T"
return f"{string}: F" return f"{string}: F"
options = ", ".join( return ", ".join(
_truefalse(opt, getattr(cmdset, opt)) _truefalse(opt, getattr(cmdset, opt))
for opt in ("no_exits", "no_objs", "no_channels", "duplicates") for opt in ("no_exits", "no_objs", "no_channels", "duplicates")
if getattr(cmdset, opt) is not None if getattr(cmdset, opt) is not None
) )
options = ", " + options if options else ""
return options
# cmdset stored on us 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) stored_cmdsets = sorted(obj.cmdset.all(), key=lambda x: x.priority, reverse=True)
stored = []
for cmdset in stored_cmdsets: for cmdset in stored_cmdsets:
if cmdset.key == "_EMPTY_CMDSET": if cmdset.key != "_EMPTY_CMDSET":
continue stored_cmdset_strings.append(self.format_single_cmdset(cmdset))
options = _format_options(cmdset) return "\n ".join(stored_cmdset_strings)
stored.append(
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype}, prio {cmdset.priority}{options})") def format_merged_cmdsets(self, obj, current_cmdset):
output["Stored Cmdset(s)"] = "\n " + "\n ".join(stored) if not hasattr(obj, "cmdset"):
return None
# this gets all components of the currently merged set
all_cmdsets = [(cmdset.key, cmdset) for cmdset in current_cmdset.merged_from] 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 # we always at least try to add account- and session sets since these are ignored
# if we merge on the object level. # if we merge on the object level.
if hasattr(obj, "account") and obj.account: 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()]) all_cmdsets.extend([(cmdset.key, cmdset) for cmdset in obj.account.cmdset.all()])
if obj.sessions.count(): if obj.sessions.count():
# if there are more sessions than one on objects it's because of multisession mode 3. # 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 # we only show the first session's cmdset here (it is -in principle- possible
# different sessions have different cmdsets but for admins who want such madness # that different sessions have different cmdsets but for admins who want such
# it is better that they overload with their own CmdExamine to handle it). # madness it is better that they overload with their own CmdExamine to handle it).
all_cmdsets.extend( all_cmdsets.extend([(cmdset.key, cmdset)
[ for cmdset in obj.account.sessions.all()[0].cmdset.all()])
(cmdset.key, cmdset)
for cmdset in obj.account.sessions.all()[0].cmdset.all()
]
)
else: else:
try: try:
# we have to protect this since many objects don't have sessions. # we have to protect this since many objects don't have sessions.
all_cmdsets.extend( all_cmdsets.extend([(cmdset.key, cmdset)
[ for cmdset in obj.get_session(obj.sessions.get()).cmdset.all()])
(cmdset.key, cmdset)
for cmdset in obj.get_session(obj.sessions.get()).cmdset.all()
]
)
except (TypeError, AttributeError): except (TypeError, AttributeError):
# an error means we are merging an object without a session # an error means we are merging an object without a session
pass pass
all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()] all_cmdsets = [cmdset for cmdset in dict(all_cmdsets).values()]
all_cmdsets.sort(key=lambda x: x.priority, reverse=True) all_cmdsets.sort(key=lambda x: x.priority, reverse=True)
# the resulting merged cmdset merged_cmdset_strings = []
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: for cmdset in all_cmdsets:
options = _format_options(cmdset) if cmdset.key != "_EMPTY_CMDSET":
merged.append( merged_cmdset_strings.append(self.format_single_cmdset(cmdset))
f"{cmdset.path} [{cmdset.key}] ({cmdset.mergetype} prio {cmdset.priority}{options})") return "\n ".join(merged_cmdset_strings)
output["Merged Cmdset(s)"] = "\n " + "\n ".join(merged)
# list the commands available to this object def format_current_cmds(self, obj, current_cmdset):
current_commands = sorted([cmd.key for cmd in current_cmdset if cmd.access(obj, "cmd")]) current_commands = sorted([cmd.key for cmd in current_cmdset if cmd.access(obj, "cmd")])
cmdsetstr = "\n" + utils.fill(", ".join(current_commands), indent=2) return "\n" + utils.fill(", ".join(current_commands), indent=2)
output[f"Commands available to {obj.key} (result of Merged CmdSets)"] = str(cmdsetstr)
# scripts def _get_attribute_value_type(self, attrvalue):
if hasattr(obj, "scripts") and hasattr(obj.scripts, "all") and obj.scripts.all(): typ = ""
output["Scripts"] = "\n " + f"{obj.scripts}" if not isinstance(attrvalue, str):
# add the attributes try:
output.update(self.format_attributes(obj)) name = attrvalue.__class__.__name__
# Tags except AttributeError:
tags = obj.tags.all(return_key_and_category=True) try:
tags_string = "\n" + utils.fill( name = attrvalue.__name__
", ".join(sorted(f"{tag}[{category}]" for tag, category in tags)), indent=2, except AttributeError:
) name = attrvalue
if tags: if str(name).startswith("_Saver"):
output["Tags[category]"] = tags_string try:
# Contents of object typ = str(type(deserialize(attrvalue)))
exits = [] except Exception:
pobjs = [] typ = str(type(deserialize(attrvalue)))
things = []
if hasattr(obj, "contents"):
for content in obj.contents:
if content.destination:
exits.append(content)
elif content.account:
pobjs.append(content)
else: else:
things.append(content) typ = str(type(attrvalue))
if exits: return typ
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
max_width = -1
for block in output.values():
max_width = max(max_width, max(display_len(line) for line in block.split("\n")))
max_width = max(0, min(self.client_width(), max_width))
def format_single_attribute_detail(self, obj, 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)
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:
return f"{self.header_color}{key}|n[{category}]={value}{typ}"
else:
return f"{self.header_color}{key}|n={value}{typ}"
def format_attributes(self, obj):
return "\n " + "\n ".join(
sorted(self.format_single_attribute(attr)
for attr in obj.db_attributes.all())
)
def format_nattributes(self, obj):
try:
ndb_attr = obj.nattributes.all(return_tuples=True)
except Exception:
return
if ndb_attr and ndb_attr[0]:
return "\n " + " \n".join(
sorted(self.format_single_attribute(attr)
for attr, value in ndb_attr)
)
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):
"""
Formats the full examine page return.
"""
objdata = self.get_formatted_obj_data(obj, current_cmdset)
# format output
main_str = []
max_width = -1
for header, block in objdata.items():
if block is not None:
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
ret = "\n".join( attrs = [attr for attr in obj.db_attributes.all() if attr.db_key in obj_attrs]
f"{self.header_color}{header}|n:{value}" if not attrs:
for header, value in self.format_attributes( self.msg("No attributes found on {obj.name}.")
obj, attrname, crop=False
).items()
)
self.caller.msg(ret)
else: else:
out_strings = []
for attr in attrs:
out_strings.append(self.format_single_attribute_detail(obj, attr))
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 session = None
if obj.sessions.count(): if obj.sessions.count():
mergemode = "session" mergemode = "session"
@ -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(