Make numbered_names use get_display_name; make dbref display separate method

This commit is contained in:
Griatch 2024-03-09 20:02:18 +01:00
parent d893cfd46e
commit cbe3d4c738
14 changed files with 242 additions and 122 deletions

View file

@ -2,16 +2,30 @@
## Evennia Main branch
- Feature: *Backwards incompatible*: `DefaultObject.get_numbered_name` now gets object's
name via `.get_display_name` for better compatibility with recog systems.
- Feature: *Backwards incompatible*: Removed the (#dbref) display from
`DefaultObject.get_display_name`, instead using new `.get_extra_display_name_info`
method for getting this info. The Object's display template was extended for
optionally adding this information. This makes showing extra object info to
admins an explicit action and opens up `get_display_name` for general use.
- Feature: Add `ON_DEMAND_HANDLER.set_dt(key, category, dt)` and
`.set_stage(key, category, stage)` to allow manual tweaking of task timings,
for example for a spell speeding a plant's growth (Griatch)
- Feature: Add `use_assertequal` kwarg to the `EvenniaCommandTestMixin` testing
class; this uses django's `assertEqual` over the default more lenient checker,
which can be useful for testing table whitespace (Griatch)
- Feature: New `utils.group_objects_by_key_and_desc` for grouping a list of
objects based on the visible key and desc. Useful for inventory listings (Griatch)
- Feature: Add `DefaultObject.get_numbered_name` `return_string` bool kwarg, for only
returning singular/plural based on count instead of a tuple with both (Griatch)
- Fix: `DefaultObject.get_numbered_name` used `.name` instead of
`.get_display_name` which broke recog systems. May lead to object's #dbref
will show for admins in some more places (Griatch)
- [Fix][pull3420]: Refactor Clothing contrib's inventory command align with
Evennia core's version (michaelfaith84, Griatch)
- Fix: Resolve a bug when loading on-demand-handler data from database (Griatch)
- Doc fixes (iLPdev, Griatch)
- Doc fixes (iLPdev, Griatch, CloudKeeper)
[pull3420]: https://github.com/evennia/evennia/pull/3420

View file

@ -3,9 +3,8 @@ General Character commands usually available to all characters
"""
import re
from django.conf import settings
import evennia
from django.conf import settings
from evennia.typeclasses.attributes import NickTemplateInvalid
from evennia.utils import utils
@ -370,11 +369,10 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
from evennia.utils.ansi import raw as raw_ansi
table = self.styled_table(border="header")
for item in items:
singular, _ = item.get_numbered_name(1, self.caller)
for key, desc, _ in utils.group_objects_by_key_and_desc(items, caller=self.caller):
table.add_row(
f"|C{singular}|n",
"{}|n".format(utils.crop(raw_ansi(item.db.desc or ""), width=50) or ""),
f"|C{key}|n",
"{}|n".format(utils.crop(raw_ansi(desc or ""), width=50) or ""),
)
string = f"|wYou are carrying:\n{table}"
self.msg(text=(string, {"type": "inventory"}))

View file

@ -14,13 +14,10 @@ main test suite started with
import datetime
from unittest.mock import MagicMock, Mock, patch
import evennia
from anything import Anything
from django.conf import settings
from django.test import override_settings
from parameterized import parameterized
from twisted.internet import task
import evennia
from evennia import (
DefaultCharacter,
DefaultExit,
@ -32,14 +29,7 @@ from evennia import (
from evennia.commands import cmdparser
from evennia.commands.cmdset import CmdSet
from evennia.commands.command import Command, InterruptCommand
from evennia.commands.default import (
account,
admin,
batchprocess,
building,
comms,
general,
)
from evennia.commands.default import account, admin, batchprocess, building, comms, general
from evennia.commands.default import help as help_module
from evennia.commands.default import syscommands, system, unloggedin
from evennia.commands.default.cmdset_character import CharacterCmdSet
@ -48,6 +38,8 @@ from evennia.prototypes import prototypes as protlib
from evennia.utils import create, gametime, utils
from evennia.utils.test_resources import BaseEvenniaCommandTest # noqa
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaCommandTest
from parameterized import parameterized
from twisted.internet import task
# ------------------------------------------------------------
# Command testing
@ -116,13 +108,13 @@ class TestGeneral(BaseEvenniaCommandTest):
self.call(general.CmdNick(), "/list", "Defined Nicks:")
def test_get_and_drop(self):
self.call(general.CmdGet(), "Obj", "You pick up an Obj.")
self.call(general.CmdDrop(), "Obj", "You drop an Obj.")
self.call(general.CmdGet(), "Obj", "You pick up an Obj")
self.call(general.CmdDrop(), "Obj", "You drop an Obj")
def test_give(self):
self.call(general.CmdGive(), "Obj to Char2", "You aren't carrying Obj.")
self.call(general.CmdGive(), "Obj = Char2", "You aren't carrying Obj.")
self.call(general.CmdGet(), "Obj", "You pick up an Obj.")
self.call(general.CmdGet(), "Obj", "You pick up an Obj")
self.call(general.CmdGive(), "Obj to Char2", "You give")
self.call(general.CmdGive(), "Obj = Char", "You give", caller=self.char2)
@ -569,7 +561,7 @@ class TestAdmin(BaseEvenniaCommandTest):
self.call(
admin.CmdForce(),
"Char2=say test",
'Char2(#{}) says, "test"|You have forced Char2 to: say test'.format(cid),
'Char2 says, "test"|You have forced Char2 to: say test',
)
@ -781,17 +773,14 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call(building.CmdExamine(), "*TestAccount")
def test_set_obj_alias(self):
oid = self.obj1.id
self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj")
self.call(
building.CmdSetObjAlias(),
"Obj = TestObj1b",
"Alias(es) for 'Obj(#{})' set to 'testobj1b'.".format(oid),
building.CmdSetObjAlias(), "Obj = TestObj1b", "Alias(es) for 'Obj' set to 'testobj1b'."
)
self.call(building.CmdSetObjAlias(), "", "Usage: ")
self.call(building.CmdSetObjAlias(), "NotFound =", "Could not find 'NotFound'.")
self.call(building.CmdSetObjAlias(), "Obj", "Aliases for Obj(#{}): 'testobj1b'".format(oid))
self.call(building.CmdSetObjAlias(), "Obj", "Aliases for Obj: 'testobj1b'")
self.call(building.CmdSetObjAlias(), "Obj2 =", "Cleared aliases from Obj2")
self.call(building.CmdSetObjAlias(), "Obj2 =", "No aliases to clear.")
@ -1228,9 +1217,7 @@ class TestBuilding(BaseEvenniaCommandTest):
def test_desc(self):
oid = self.obj2.id
self.call(
building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2(#{}).".format(oid)
)
self.call(building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2.")
self.call(building.CmdDesc(), "", "Usage: ")
with patch("evennia.commands.default.building.EvEditor") as mock_ed:
@ -1251,7 +1238,7 @@ class TestBuilding(BaseEvenniaCommandTest):
oid = self.obj2.id
o2d = self.obj2.db.desc
r1d = self.room1.db.desc
self.call(building.CmdDesc(), "Obj2=", "The description was set on Obj2(#{}).".format(oid))
self.call(building.CmdDesc(), "Obj2=", "The description was set on Obj2.")
assert self.obj2.db.desc == "" and self.obj2.db.desc != o2d
assert self.room1.db.desc == r1d
@ -1260,7 +1247,7 @@ class TestBuilding(BaseEvenniaCommandTest):
rid = self.room1.id
o2d = self.obj2.db.desc
r1d = self.room1.db.desc
self.call(building.CmdDesc(), "Obj2", "The description was set on Room(#{}).".format(rid))
self.call(building.CmdDesc(), "Obj2", "The description was set on Room.")
assert self.obj2.db.desc == o2d
assert self.room1.db.desc == "Obj2" and self.room1.db.desc != r1d
@ -1283,16 +1270,11 @@ class TestBuilding(BaseEvenniaCommandTest):
building.CmdDestroy(), settings.DEFAULT_HOME, "You are trying to delete"
) # DEFAULT_HOME should not be deleted
self.char2.location = self.room2
charid = self.char2.id
room1id = self.room1.id
room2id = self.room2.id
self.call(
building.CmdDestroy(),
self.room2.dbref,
"Char2(#{}) arrives to Room(#{}) from Room2(#{}).|Room2 was destroyed.".format(
charid, room1id, room2id
),
)
"Char2 arrives to Room from Room2.|Room2 was destroyed.",
),
building.CmdDestroy.confirm = confirm
def test_destroy_sequence(self):
@ -1640,9 +1622,6 @@ class TestBuilding(BaseEvenniaCommandTest):
self.assertFalse(script3.pk)
def test_teleport(self):
oid = self.obj1.id
rid = self.room1.id
rid2 = self.room2.id
self.call(building.CmdTeleport(), "", "Usage: ")
self.call(building.CmdTeleport(), "Obj = Room", "Obj is already at Room.")
self.call(
@ -1653,9 +1632,7 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call(
building.CmdTeleport(),
"Obj = Room2",
"Obj(#{}) is leaving Room(#{}), heading for Room2(#{}).|Teleported Obj -> Room2.".format(
oid, rid, rid2
),
"Obj is leaving Room, heading for Room2.|Teleported Obj -> Room2.",
)
self.call(building.CmdTeleport(), "NotFound = Room", "Could not find 'NotFound'.")
self.call(
@ -1663,7 +1640,7 @@ class TestBuilding(BaseEvenniaCommandTest):
)
self.call(building.CmdTeleport(), "/tonone Obj2", "Teleported Obj2 -> None-location.")
self.call(building.CmdTeleport(), "/quiet Room2", "Room2(#{})".format(rid2))
self.call(building.CmdTeleport(), "/quiet Room2", "Room2")
self.call(
building.CmdTeleport(),
"/t", # /t switch is abbreviated form of /tonone
@ -1777,7 +1754,8 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call(
building.CmdSpawn(),
"{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', "
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref,
"'key':'goblin', 'location':'%s'}"
% spawnLoc.dbref,
"Spawned goblin",
)
goblin = get_object(self, "goblin")
@ -1825,7 +1803,8 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call(
building.CmdSpawn(),
"/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo',"
" 'location':'%s'}" % spawnLoc.dbref,
" 'location':'%s'}"
% spawnLoc.dbref,
"Spawned Ball",
)
ball = get_object(self, "Ball")

View file

@ -183,7 +183,7 @@ class TestEvscaperoomCommands(BaseEvenniaCommandTest):
self.call(
commands.CmdEmote(),
"/me smiles to /obj",
f"Char(#{self.char1.id}) smiles to Obj(#{self.obj1.id})",
f"Char smiles to Obj.",
)
def test_focus_interaction(self):

View file

@ -77,7 +77,15 @@ from collections import defaultdict
from django.conf import settings
from evennia import DefaultCharacter, DefaultObject, default_cmds
from evennia.commands.default.muxcommand import MuxCommand
from evennia.utils import at_search_result, crop, evtable, inherits_from, int2str, iter_to_str
from evennia.utils import (
at_search_result,
crop,
evtable,
group_objects_by_key_and_desc,
inherits_from,
int2str,
iter_to_str,
)
from evennia.utils.ansi import raw as raw_ansi
# Options start here.
@ -660,11 +668,10 @@ class CmdInventory(MuxCommand):
carried = [obj for obj in items if not obj.db.worn]
carry_table = self.styled_table(border="header")
for item in carried:
singular, _ = item.get_numbered_name(1, self.caller)
for key, desc, _ in group_objects_by_key_and_desc(carried, caller=self.caller):
carry_table.add_row(
f"{singular}|n",
"{}|n".format(crop(raw_ansi(item.db.desc or ""), width=50) or ""),
f"{key}|n",
"{}|n".format(crop(raw_ansi(desc or ""), width=50) or ""),
)
message_list.extend(
["|wYou are carrying:|n", str(carry_table) if carry_table.nrows > 0 else " Nothing."]
@ -674,18 +681,17 @@ class CmdInventory(MuxCommand):
worn = [obj for obj in items if obj.db.worn]
wear_table = self.styled_table(border="header")
for item in worn:
singular, _ = item.get_numbered_name(1, self.caller)
for key, desc, _ in group_objects_by_key_and_desc(worn, caller=self.caller):
wear_table.add_row(
f"{singular}|n",
"{}|n".format(crop(raw_ansi(item.db.desc or ""), width=50) or ""),
f"{key}|n",
"{}|n".format(crop(raw_ansi(desc or ""), width=50) or ""),
)
message_list.extend(
["You are wearing:|n", str(wear_table) if wear_table.nrows > 0 else " Nothing."]
)
# return the composite message
self.caller.msg("\n".join(message_list))
self.caller.msg(text=("\n".join(message_list), {"type": "inventory"}))
class ClothedCharacterCmdSet(default_cmds.CharacterCmdSet):

View file

@ -19,9 +19,11 @@ class TestClothingCmd(BaseEvenniaCommandTest):
self.wearer.location = self.room
# Make a test hat
self.test_hat = create_object(clothing.ContribClothing, key="test hat")
self.test_hat.db.desc = "A test hat."
self.test_hat.db.clothing_type = "hat"
# Make a test scarf
self.test_scarf = create_object(clothing.ContribClothing, key="test scarf")
self.test_scarf.db.desc = "A test scarf."
self.test_scarf.db.clothing_type = "accessory"
def test_clothingcommands(self):
@ -40,7 +42,10 @@ class TestClothingCmd(BaseEvenniaCommandTest):
self.call(
clothing.CmdInventory(),
"",
"You are carrying:\n a test scarf \n a test hat \nYou are wearing:\n Nothing.",
(
"You are carrying:\n a test hat A test hat. \n a test scarf A test"
" scarf. \nYou are wearing:\n Nothing."
),
caller=self.wearer,
use_assertequal=True,
)
@ -71,7 +76,10 @@ class TestClothingCmd(BaseEvenniaCommandTest):
self.call(
clothing.CmdInventory(),
"",
"You are carrying:\n Nothing.\nYou are wearing:\n a test scarf \n a test hat ",
(
"You are carrying:\n Nothing.\nYou are wearing:\n a test hat A test hat. \n"
" a test scarf A test scarf. "
),
caller=self.wearer,
use_assertequal=True,
)

View file

@ -1,5 +1,6 @@
from evennia import create_object
from evennia.utils.test_resources import BaseEvenniaCommandTest, BaseEvenniaTest # noqa
from evennia.utils.test_resources import BaseEvenniaCommandTest # noqa
from evennia.utils.test_resources import BaseEvenniaTest
from .containers import CmdContainerGet, CmdContainerLook, CmdPut, ContribContainer
@ -40,9 +41,17 @@ class TestContainerCmds(BaseEvenniaCommandTest):
# get normally
self.call(CmdContainerGet(), "Obj", "You pick up an Obj.")
# put in the container
self.call(CmdPut(), "obj in box", "You put an Obj in a Box.")
self.call(
CmdPut(),
"obj in box",
"You put an Obj in a Box.",
)
# get from the container
self.call(CmdContainerGet(), "obj from box", "You get an Obj from a Box.")
self.call(
CmdContainerGet(),
"obj from box",
"You get an Obj from a Box.",
)
def test_locked_get_put(self):
# lock container

View file

@ -6,11 +6,10 @@ Testing of ExtendedRoom contrib
import datetime
from django.conf import settings
from mock import Mock, patch
from parameterized import parameterized
from evennia import create_object
from evennia.utils.test_resources import BaseEvenniaCommandTest, EvenniaTestCase
from mock import Mock, patch
from parameterized import parameterized
from . import extended_room
@ -195,7 +194,7 @@ class TestExtendedRoomCommands(BaseEvenniaCommandTest):
extended_room.CmdExtendedRoomDesc(),
"",
f"""
Room Room(#{self.room1.id}) Season: autumn. Time: afternoon. States: None
Room Room Season: autumn. Time: afternoon. States: None
Room state (default) (active):
Base room description.
@ -218,7 +217,7 @@ Base room description.
extended_room.CmdExtendedRoomDesc(),
"",
f"""
Room Room(#{self.room1.id}) Season: autumn. Time: afternoon. States: None
Room Room Season: autumn. Time: afternoon. States: None
Room state burning:
Burning description.
@ -235,8 +234,10 @@ Base room description.
self.call(
extended_room.CmdExtendedRoomDesc(),
"/del/burning/spring",
"The burning-description was deleted, if it existed.|The spring-description was"
" deleted, if it existed",
(
"The burning-description was deleted, if it existed.|The spring-description was"
" deleted, if it existed"
),
)
# add autumn, which should be active
self.call(
@ -248,7 +249,7 @@ Base room description.
extended_room.CmdExtendedRoomDesc(),
"",
f"""
Room Room(#{self.room1.id}) Season: autumn. Time: afternoon. States: None
Room Room Season: autumn. Time: afternoon. States: None
Room state autumn (active):
Autumn description.
@ -285,8 +286,8 @@ test: Test detail.
self.call(
extended_room.CmdExtendedRoomDetail(),
"",
f"""
The room Room(#{self.room1.id}) doesn't have any details.
"""
The room Room doesn't have any details.
""".strip(),
)
@ -306,7 +307,7 @@ The room Room(#{self.room1.id}) doesn't have any details.
self.call(
extended_room.CmdExtendedRoomState(),
"",
f"Room states (not counting automatic time/season) on Room(#{self.room1.id}):\n None",
"Room states (not counting automatic time/season) on Room:\n None",
)
# add room states
@ -323,8 +324,7 @@ The room Room(#{self.room1.id}) doesn't have any details.
self.call(
extended_room.CmdExtendedRoomState(),
"",
f"Room states (not counting automatic time/season) on Room(#{self.room1.id}):\n "
"'burning' and 'windy'",
f"Room states (not counting automatic time/season) on Room:\n 'burning' and 'windy'",
)
# toggle windy
self.call(
@ -335,8 +335,7 @@ The room Room(#{self.room1.id}) doesn't have any details.
self.call(
extended_room.CmdExtendedRoomState(),
"",
f"Room states (not counting automatic time/season) on Room(#{self.room1.id}):\n "
"'burning'",
f"Room states (not counting automatic time/season) on Room:\n 'burning'",
)
# add a autumn state and make sure we override it
self.room1.add_desc("Autumn description.", room_state="autumn")
@ -387,13 +386,17 @@ The room Room(#{self.room1.id}) doesn't have any details.
self.call(
extended_room.CmdExtendedRoomLook(),
"",
f"Room(#{self.room1.id})\nThis is a nice autumnal forest. The afternoon sun is"
" shining through the trees.",
(
f"Room(#{self.room1.id})\nThis is a nice autumnal forest. The afternoon sun is"
" shining through the trees."
),
)
self.room1.add_room_state("burning")
self.call(
extended_room.CmdExtendedRoomLook(),
"",
f"Room(#{self.room1.id})\nThis is a nice autumnal forest. The afternoon sun is"
" shining through the trees and this place is on fire!",
(
f"Room(#{self.room1.id})\nThis is a nice autumnal forest. The afternoon sun is"
" shining through the trees and this place is on fire!"
),
)

View file

@ -154,18 +154,12 @@ from string import punctuation
import inflect
from django.conf import settings
from evennia.commands.cmdset import CmdSet
from evennia.commands.command import Command
from evennia.objects.models import ObjectDB
from evennia.objects.objects import DefaultCharacter, DefaultObject
from evennia.utils import ansi, logger
from evennia.utils.utils import (
iter_to_str,
lazy_property,
make_iter,
variable_from_module,
)
from evennia.utils.utils import iter_to_str, lazy_property, make_iter, variable_from_module
_INFLECT = inflect.engine()
@ -1343,13 +1337,15 @@ class ContribRPObject(DefaultObject):
# in eventual error reporting later (not their keys). Doing
# it like this e.g. allows for use of the typeclass kwarg
# limiter.
results.extend([obj for obj in search_obj(candidate.key) if obj not in results])
results.extend(
[obj for obj in search_obj(candidate.key, **kwargs) if obj not in results]
)
if not results and is_builder:
# builders get a chance to search only by key+alias
results = search_obj(searchdata, candidates=candidates, **kwargs)
# builders get to do a global search by key+alias
results = search_obj(searchdata, **kwargs)
else:
# global searches / #drefs end up here. Global searches are
# global searches with #drefs end up here. Global searches are
# only done in code, so is controlled, #dbrefs are turned off
# for non-Builders.
results = search_obj(searchdata, **kwargs)
@ -1409,10 +1405,6 @@ class ContribRPObject(DefaultObject):
# use own sdesc as a fallback
sdesc = self.sdesc.get()
# add dbref is looker has control access and `noid` is not set
if self.access(looker, access_type="control") and not kwargs.get("noid", False):
sdesc = f"{sdesc}(#{self.id})"
return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc
def get_display_characters(self, looker, pose=True, **kwargs):
@ -1545,10 +1537,6 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
# use own sdesc as a fallback
sdesc = self.sdesc.get()
# add dbref is looker has control access and `noid` is not set
if self.access(looker, access_type="control") and not kwargs.get("noid", False):
sdesc = f"{sdesc}(#{self.id})"
return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc
def at_object_creation(self):

View file

@ -5,7 +5,6 @@ Tests for RP system
import time
from anything import Anything
from evennia import DefaultObject, create_object, default_cmds
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.test_resources import BaseEvenniaTest
@ -414,14 +413,14 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
expected_first_call = [
"More than one match for 'Mushroom' (please narrow target):",
f" Mushroom({mushroom1.dbref})-1 []",
f" Mushroom({mushroom2.dbref})-2 []",
f" Mushroom-1 []",
f" Mushroom-2 []",
]
self.call(default_cmds.CmdLook(), "Mushroom", "\n".join(expected_first_call)) # PASSES
expected_second_call = f"Mushroom({mushroom1.dbref})\nThe first mushroom is brown."
expected_second_call = f"Mushroom(#{mushroom1.id})\nThe first mushroom is brown."
self.call(default_cmds.CmdLook(), "Mushroom-1", expected_second_call) # FAILS
expected_third_call = f"Mushroom({mushroom2.dbref})\nThe second mushroom is red."
expected_third_call = f"Mushroom(#{mushroom2.id})\nThe second mushroom is red."
self.call(default_cmds.CmdLook(), "Mushroom-2", expected_third_call) # FAILS

View file

@ -19,7 +19,6 @@ from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import validate_comma_separated_integer_list
from django.db import models
from evennia.objects.manager import ObjectDBManager
from evennia.typeclasses.models import TypedObject
from evennia.utils import logger

View file

@ -219,7 +219,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# populated by `return_appearance`
appearance_template = """
{header}
|c{name}|n
|c{name}{extra_name_info}|n
{desc}
{exits}{characters}{things}
{footer}
@ -316,7 +316,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
"obj.location to move an object here.".format(self.__class__)
)
contents = property(contents_get, contents_set, contents_set)
contents = property(contents_get, contents_set, contents_set, contents_set)
@property
def exits(self):
@ -827,6 +827,16 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
for session in sessions:
session.data_out(**kwargs)
def get_contents_unique(self, caller=None):
"""
Get a mapping of contents that are visually unique to the caller, along with
how many of each there are.
Args:
caller (Object, optional): The object to check visibility from. If not given,
the current object will be used.
"""
def for_contents(self, func, exclude=None, **kwargs):
"""
Runs a function on every object contained within this one.
@ -1436,10 +1446,28 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
and is expected to produce something useful for builders.
"""
if looker and self.locks.check_lockstring(looker, "perm(Builder)"):
return "{}(#{})".format(self.name, self.id)
return self.name
def get_extra_display_name_info(self, looker=None, **kwargs):
"""
Adds any extra display information to the object's name. By default this is is the
object's dbref in parentheses, if the looker has permission to see it.
Args:
looker (Object): The object looking at this object.
Returns:
str: The dbref of this object, if the looker has permission to see it. Otherwise, an
empty string is returned.
Notes:
By default, this becomes a string (#dbref) attached to the object's name.
"""
if looker and self.locks.check_lockstring(looker, "perm(Builder)"):
return f"(#{self.id})"
return ""
def get_numbered_name(self, count, looker, **kwargs):
"""
Return the numbered (singular, plural) forms of this object's key. This is by default called
@ -1453,8 +1481,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
looker (Object): Onlooker. Not used by default.
Keyword Args:
key (str): Optional key to pluralize. If not given, the object's `.name` property is
used.
key (str): Optional key to pluralize. If not given, the object's `.get_display_name()`
method is used.
return_string (bool): If `True`, return only the singular form if count is 0,1 or
the plural form otherwise. If `False` (default), return both forms as a tuple.
Returns:
tuple: This is a tuple `(str, str)` with the singular and plural forms of the key
@ -1466,7 +1496,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
"""
plural_category = "plural_key"
key = kwargs.get("key", self.name)
key = kwargs.get("key", self.get_display_name(looker))
raw_key = self.name
key = ansi.ANSIString(key) # this is needed to allow inflection of colored names
try:
plural = _INFLECT.plural(key, count)
@ -1482,6 +1513,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# save the singular form as an alias here too so we can display "an egg" and also
# look at 'an egg'.
self.aliases.add(singular, category=plural_category)
if kwargs.get("return_string"):
return singular if count in (0, 1) else plural
return singular, plural
def get_display_header(self, looker, **kwargs):
@ -1645,6 +1680,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
return self.format_appearance(
self.appearance_template.format(
name=self.get_display_name(looker, **kwargs),
extra_name_info=self.get_extra_display_name_info(looker, **kwargs),
desc=self.get_display_desc(looker, **kwargs),
header=self.get_display_header(looker, **kwargs),
footer=self.get_display_footer(looker, **kwargs),

View file

@ -11,12 +11,11 @@ from datetime import datetime, timedelta
import mock
from django.test import TestCase
from parameterized import parameterized
from twisted.internet import task
from evennia.utils import utils
from evennia.utils.ansi import ANSIString
from evennia.utils.test_resources import BaseEvenniaTest
from parameterized import parameterized
from twisted.internet import task
class TestIsIter(TestCase):
@ -775,6 +774,54 @@ class TestJustify(TestCase):
self.assertIn(ANSI_RED, str(result))
class TestGroupObjectsByKeyAndDesc(TestCase):
"""
Test the utils.group_objects_by_key_and_desc function.
"""
class MockObject:
def __init__(self, key, desc):
self.key = key
self.desc = desc
def get_display_name(self, looker, **kwargs):
return self.key + f" (looker: {looker.key})"
def get_display_desc(self, looker, **kwargs):
return self.desc + f" (looker: {looker.key})"
def get_numbered_name(self, count, looker, **kwargs):
return f"{count} {self.key} (looker: {looker.key})"
def __repr__(self):
return f"MockObject({self.key}, {self.desc})"
def test_group_by_key_and_desc(self):
ma1 = self.MockObject("itemA", "descA")
ma2 = self.MockObject("itemA", "descA")
ma3 = self.MockObject("itemA", "descA")
ma4 = self.MockObject("itemA", "descA")
mb1 = self.MockObject("itemB", "descB")
mb2 = self.MockObject("itemB", "descB")
mb3 = self.MockObject("itemB", "descB")
me = self.MockObject("Looker", "DescLooker")
result = utils.group_objects_by_key_and_desc([ma1, ma2, ma3, ma4, mb1, mb2, mb3], caller=me)
self.assertEqual(
list(result),
[
("4 itemA (looker: Looker)", "descA (looker: Looker)", [ma1, ma2, ma3, ma4]),
("3 itemB (looker: Looker)", "descB (looker: Looker)", [mb1, mb2, mb3]),
],
)
# Create a list of objects
class TestMatchIP(TestCase):
"""
test utils.match_ip

View file

@ -28,6 +28,7 @@ from os.path import join as osjoin
from string import punctuation
from unicodedata import east_asian_width
import evennia
from django.apps import apps
from django.conf import settings
from django.core.exceptions import ValidationError as DjangoValidationError
@ -35,14 +36,12 @@ from django.core.validators import validate_email as django_validate_email
from django.utils import timezone
from django.utils.html import strip_tags
from django.utils.translation import gettext as _
from evennia.utils import logger
from simpleeval import simple_eval
from twisted.internet import reactor, threads
from twisted.internet.defer import returnValue # noqa - used as import target
from twisted.internet.task import deferLater
import evennia
from evennia.utils import logger
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
_EVENNIA_DIR = settings.EVENNIA_DIR
_GAME_DIR = settings.GAME_DIR
@ -1767,6 +1766,41 @@ def string_partial_matching(alternatives, inp, ret_index=True):
return []
def group_objects_by_key_and_desc(objects, caller=None, **kwargs):
"""
Groups a list of objects by their key and description. This is used to group
visibly identical objects together, for example for inventory listings.
Args:
objects (list): A list of objects to group. These must be DefaultObject.
caller (Object, optional): The object looking at the objects, used to get the
description and key of each object.
**kwargs: Passed into each object's `get_display_name/desc` methods.
Returns:
iterable: An iterable of tuples, where each tuple is on the form
`(numbered_name, description, [objects])`.
"""
key_descs = defaultdict(list)
return_string = kwargs.pop("return_string", True)
for obj in objects:
key_descs[
(obj.get_display_name(caller, **kwargs), obj.get_display_desc(caller, **kwargs))
].append(obj)
return (
(
objs[0].get_numbered_name(len(objs), caller, return_string=return_string, **kwargs),
desc,
objs,
)
for (key, desc), objs in sorted(key_descs.items(), key=lambda tup: tup[0][0])
)
def format_table(table, extra_space=1):
"""
Format a 2D array of strings into a multi-column table.