Make set aware of Attribute categories

This commit is contained in:
Griatch 2021-12-16 00:22:24 +01:00
parent 5b85ec128e
commit 72ad633071
5 changed files with 182 additions and 97 deletions

View file

@ -126,6 +126,8 @@ Up requirements to Django 3.2+, Twisted 21+
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 - Expand `examine` command's code to much more extensible and modular. Show
attribute categories and value types (when not strings). attribute categories and value types (when not strings).
- `AttributeHandler.remove(key, return_exception=False, category=None, ...)` changed
to `.remove(key, category=None, return_exception=False, ...)` for consistency.
### Evennia 0.9.5 (2019-2020) ### Evennia 0.9.5 (2019-2020)
@ -216,6 +218,7 @@ without arguments starts a full interactive Python console.
- Add `PermissionHandler.check` method for straight string perm-checks without needing lockstrings. - Add `PermissionHandler.check` method for straight string perm-checks without needing lockstrings.
- Add `evennia.utils.utils.strip_unsafe_input` for removing html/newlines/tags from user input. The - Add `evennia.utils.utils.strip_unsafe_input` for removing html/newlines/tags from user input. The
`INPUT_CLEANUP_BYPASS_PERMISSIONS` is a list of perms that bypass this safety stripping. `INPUT_CLEANUP_BYPASS_PERMISSIONS` is a list of perms that bypass this safety stripping.
- Make default `set` and `examine` commands aware of Attribute categories.
## Evennia 0.9 (2018-2019) ## Evennia 0.9 (2018-2019)

View file

@ -1577,10 +1577,10 @@ class CmdSetAttribute(ObjManipCommand):
set attribute on an object or account set attribute on an object or account
Usage: Usage:
set <obj>/<attr> = <value> set[/switch] <obj>/<attr>[:category] = <value>
set <obj>/<attr> = set[/switch] <obj>/<attr>[:category] = # delete attribute
set <obj>/<attr> set[/switch] <obj>/<attr>[:category] # view attribute
set *<account>/<attr> = <value> set[/switch] *<account>/<attr>[:category] = <value>
Switch: Switch:
edit: Open the line editor (string values only) edit: Open the line editor (string values only)
@ -1631,7 +1631,7 @@ class CmdSetAttribute(ObjManipCommand):
""" """
return True return True
def check_attr(self, obj, attr_name): def check_attr(self, obj, attr_name, category):
""" """
This may be overridden by subclasses in case restrictions need to be This may be overridden by subclasses in case restrictions need to be
placed on what attributes can be set by who beyond the normal lock. placed on what attributes can be set by who beyond the normal lock.
@ -1688,7 +1688,7 @@ class CmdSetAttribute(ObjManipCommand):
return self.not_found return self.not_found
return result return result
def view_attr(self, obj, attr): def view_attr(self, obj, attr, category):
""" """
Look up the value of an attribute and return a string displaying it. Look up the value of an attribute and return a string displaying it.
""" """
@ -1699,45 +1699,49 @@ class CmdSetAttribute(ObjManipCommand):
val = obj.attributes.get(key) val = obj.attributes.get(key)
val = self.do_nested_lookup(val, *nested_keys) val = self.do_nested_lookup(val, *nested_keys)
if val is not self.not_found: if val is not self.not_found:
return "\nAttribute %s/%s = %s" % (obj.name, attr, val) return f"\nAttribute {obj.name}/|w{attr}|n [category:{category}] = {val}"
error = "\n%s has no attribute '%s'." % (obj.name, attr) error = f"\nAttribute {obj.name}/|w{attr} [category:{category}] does not exist."
if nested: if nested:
error += " (Nested lookups attempted)" error += " (Nested lookups attempted)"
return error return error
def rm_attr(self, obj, attr): def rm_attr(self, obj, attr, category):
""" """
Remove an attribute from the object, or a nested data structure, and report back. Remove an attribute from the object, or a nested data structure, and report back.
""" """
nested = False nested = False
for key, nested_keys in self.split_nested_attr(attr): for key, nested_keys in self.split_nested_attr(attr):
nested = True nested = True
if obj.attributes.has(key): if obj.attributes.has(key, category):
if nested_keys: if nested_keys:
del_key = nested_keys[-1] del_key = nested_keys[-1]
val = obj.attributes.get(key) val = obj.attributes.get(key, category=category)
deep = self.do_nested_lookup(val, *nested_keys[:-1]) deep = self.do_nested_lookup(val, *nested_keys[:-1])
if deep is not self.not_found: if deep is not self.not_found:
try: try:
del deep[del_key] del deep[del_key]
except (IndexError, KeyError, TypeError): except (IndexError, KeyError, TypeError):
continue continue
return "\nDeleted attribute '%s' (= nested) from %s." % (attr, obj.name) return f"\nDeleted attribute {obj.name}/|w{attr}|n [category:{category}]."
else: else:
exists = obj.attributes.has(key) exists = obj.attributes.has(key, category)
obj.attributes.remove(attr) if exists:
return "\nDeleted attribute '%s' (= %s) from %s." % (attr, exists, obj.name) obj.attributes.remove(attr, category=category)
error = "\n%s has no attribute '%s'." % (obj.name, attr) return f"\nDeleted attribute {obj.name}/|w{attr}|n [category:{category}]."
else:
return (f"\nNo attribute {obj.name}/|w{attr}|n [category: {category}] "
"was found to delete.")
error = f"\nNo attribute {obj.name}/|w{attr}|n [category: {category}] was found to delete."
if nested: if nested:
error += " (Nested lookups attempted)" error += " (Nested lookups attempted)"
return error return error
def set_attr(self, obj, attr, value): def set_attr(self, obj, attr, value, category):
done = False done = False
for key, nested_keys in self.split_nested_attr(attr): for key, nested_keys in self.split_nested_attr(attr):
if obj.attributes.has(key) and nested_keys: if obj.attributes.has(key, category) and nested_keys:
acc_key = nested_keys[-1] acc_key = nested_keys[-1]
lookup_value = obj.attributes.get(key) lookup_value = obj.attributes.get(key, category)
deep = self.do_nested_lookup(lookup_value, *nested_keys[:-1]) deep = self.do_nested_lookup(lookup_value, *nested_keys[:-1])
if deep is not self.not_found: if deep is not self.not_found:
# To support appending and inserting to lists # To support appending and inserting to lists
@ -1764,7 +1768,7 @@ class CmdSetAttribute(ObjManipCommand):
deep[acc_key] = value deep[acc_key] = value
except TypeError as err: except TypeError as err:
# Tuples can't be modified # Tuples can't be modified
return "\n%s - %s" % (err, deep) return f"\n{err} - {deep}"
value = lookup_value value = lookup_value
attr = key attr = key
@ -1774,8 +1778,8 @@ class CmdSetAttribute(ObjManipCommand):
verb = "Modified" if obj.attributes.has(attr) else "Created" verb = "Modified" if obj.attributes.has(attr) else "Created"
try: try:
if not done: if not done:
obj.attributes.add(attr, value) obj.attributes.add(attr, value, category)
return "\n%s attribute %s/%s = %s" % (verb, obj.name, attr, repr(value)) return f"\n{verb} attribute {obj.name}/|w{attr}|n [category:{category}] = {value}"
except SyntaxError: except SyntaxError:
# this means literal_eval tried to parse a faulty string # this means literal_eval tried to parse a faulty string
return ( return (
@ -1861,13 +1865,14 @@ class CmdSetAttribute(ObjManipCommand):
caller = self.caller caller = self.caller
if not self.args: if not self.args:
caller.msg("Usage: set obj/attr = value. Use empty value to clear.") caller.msg("Usage: set obj/attr[:category] = value. Use empty value to clear.")
return return
# get values prepared by the parser # get values prepared by the parser
value = self.rhs value = self.rhs
objname = self.lhs_objattr[0]["name"] objname = self.lhs_objattr[0]["name"]
attrs = self.lhs_objattr[0]["attrs"] attrs = self.lhs_objattr[0]["attrs"]
category = self.lhs_objs[0].get("option") # None if unset
obj = self.search_for_obj(objname) obj = self.search_for_obj(objname)
if not obj: if not obj:
@ -1897,11 +1902,11 @@ class CmdSetAttribute(ObjManipCommand):
if self.rhs is None: if self.rhs is None:
# no = means we inspect the attribute(s) # no = means we inspect the attribute(s)
if not attrs: if not attrs:
attrs = [attr.key for attr in obj.attributes.all()] attrs = [attr.key for attr in obj.attributes.get(category=None)]
for attr in attrs: for attr in attrs:
if not self.check_attr(obj, attr): if not self.check_attr(obj, attr, category):
continue continue
result.append(self.view_attr(obj, attr)) result.append(self.view_attr(obj, attr, category))
# we view it without parsing markup. # we view it without parsing markup.
self.caller.msg("".join(result).strip(), options={"raw": True}) self.caller.msg("".join(result).strip(), options={"raw": True})
return return
@ -1911,19 +1916,19 @@ class CmdSetAttribute(ObjManipCommand):
caller.msg("You don't have permission to edit %s." % obj.key) caller.msg("You don't have permission to edit %s." % obj.key)
return return
for attr in attrs: for attr in attrs:
if not self.check_attr(obj, attr): if not self.check_attr(obj, attr, category):
continue continue
result.append(self.rm_attr(obj, attr)) result.append(self.rm_attr(obj, attr, category))
else: else:
# setting attribute(s). Make sure to convert to real Python type before saving. # setting attribute(s). Make sure to convert to real Python type before saving.
if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")): if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")):
caller.msg("You don't have permission to edit %s." % obj.key) caller.msg("You don't have permission to edit %s." % obj.key)
return return
for attr in attrs: for attr in attrs:
if not self.check_attr(obj, attr): if not self.check_attr(obj, attr, category):
continue continue
value = _convert_from_string(self, value) value = _convert_from_string(self, value)
result.append(self.set_attr(obj, attr, value)) result.append(self.set_attr(obj, attr, value, category))
# send feedback # send feedback
caller.msg("".join(result).strip("\n")) caller.msg("".join(result).strip("\n"))

View file

@ -965,15 +965,17 @@ class TestBuilding(CommandTest):
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
'Obj/test1="value1"', 'Obj/test1="value1"',
"Created attribute Obj/test1 = 'value1'", "Created attribute Obj/test1 [category:None] = value1",
) )
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
'Obj2/test2="value2"', 'Obj2/test2="value2"',
"Created attribute Obj2/test2 = 'value2'", "Created attribute Obj2/test2 [category:None] = value2",
) )
self.call(building.CmdSetAttribute(), "Obj2/test2", "Attribute Obj2/test2 = value2") self.call(building.CmdSetAttribute(),
self.call(building.CmdSetAttribute(), "Obj2/NotFound", "Obj2 has no attribute 'notfound'.") "Obj2/test2", "Attribute Obj2/test2 [category:None] = value2")
self.call(building.CmdSetAttribute(),
"Obj2/NotFound", "Attribute Obj2/notfound [category:None] does not exist.")
with patch("evennia.commands.default.building.EvEditor") as mock_ed: with patch("evennia.commands.default.building.EvEditor") as mock_ed:
self.call(building.CmdSetAttribute(), "/edit Obj2/test3") self.call(building.CmdSetAttribute(), "/edit Obj2/test3")
@ -982,14 +984,18 @@ class TestBuilding(CommandTest):
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
'Obj2/test3="value3"', 'Obj2/test3="value3"',
"Created attribute Obj2/test3 = 'value3'", "Created attribute Obj2/test3 [category:None] = value3",
) )
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj2/test3 = ", "Obj2/test3 = ",
"Deleted attribute 'test3' (= True) from Obj2.", "Deleted attribute Obj2/test3 [category:None].",
)
self.call(
building.CmdSetAttribute(),
"Obj2/test4:Foo = 'Bar'",
"Created attribute Obj2/test4 [category:Foo] = Bar",
) )
self.call( self.call(
building.CmdCpAttr(), building.CmdCpAttr(),
"/copy Obj2/test2 = Obj2/test3", "/copy Obj2/test2 = Obj2/test3",
@ -1008,123 +1014,162 @@ class TestBuilding(CommandTest):
def test_nested_attribute_commands(self): def test_nested_attribute_commands(self):
# list - adding white space proves real parsing # list - adding white space proves real parsing
self.call( self.call(
building.CmdSetAttribute(), "Obj/test1=[1,2]", "Created attribute Obj/test1 = [1, 2]" building.CmdSetAttribute(),
"Obj/test1=[1,2]", "Created attribute Obj/test1 [category:None] = [1, 2]"
) )
self.call(building.CmdSetAttribute(), "Obj/test1", "Attribute Obj/test1 = [1, 2]") self.call(building.CmdSetAttribute(),
self.call(building.CmdSetAttribute(), "Obj/test1[0]", "Attribute Obj/test1[0] = 1") "Obj/test1",
self.call(building.CmdSetAttribute(), "Obj/test1[1]", "Attribute Obj/test1[1] = 2") "Attribute Obj/test1 [category:None] = [1, 2]")
self.call(building.CmdSetAttribute(),
"Obj/test1[0]",
"Attribute Obj/test1[0] [category:None] = 1")
self.call(building.CmdSetAttribute(),
"Obj/test1[1]",
"Attribute Obj/test1[1] [category:None] = 2")
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test1[0] = 99", "Obj/test1[0] = 99",
"Modified attribute Obj/test1 = [99, 2]", "Modified attribute Obj/test1 [category:None] = [99, 2]",
)
self.call(
building.CmdSetAttribute(),
"Obj/test1[0]",
"Attribute Obj/test1[0] [category:None] = 99"
) )
self.call(building.CmdSetAttribute(), "Obj/test1[0]", "Attribute Obj/test1[0] = 99")
# list delete # list delete
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test1[0] =", "Obj/test1[0] =",
"Deleted attribute 'test1[0]' (= nested) from Obj.", "Deleted attribute Obj/test1[0] [category:None].",
)
self.call(
building.CmdSetAttribute(),
"Obj/test1[0]",
"Attribute Obj/test1[0] [category:None] = 2"
) )
self.call(building.CmdSetAttribute(), "Obj/test1[0]", "Attribute Obj/test1[0] = 2")
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test1[1]", "Obj/test1[1]",
"Obj has no attribute 'test1[1]'. (Nested lookups attempted)", "Attribute Obj/test1[1] [category:None] does not exist. (Nested lookups attempted)",
) )
# Delete non-existent # Delete non-existent
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test1[5] =", "Obj/test1[5] =",
"Obj has no attribute 'test1[5]'. (Nested lookups attempted)", "No attribute Obj/test1[5] [category: None] was found to "
"delete. (Nested lookups attempted)"
) )
# Append # Append
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test1[+] = 42", "Obj/test1[+] = 42",
"Modified attribute Obj/test1 = [2, 42]", "Modified attribute Obj/test1 [category:None] = [2, 42]",
) )
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test1[+0] = -1", "Obj/test1[+0] = -1",
"Modified attribute Obj/test1 = [-1, 2, 42]", "Modified attribute Obj/test1 [category:None] = [-1, 2, 42]",
) )
# dict - removing white space proves real parsing # dict - removing white space proves real parsing
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test2={ 'one': 1, 'two': 2 }", "Obj/test2={ 'one': 1, 'two': 2 }",
"Created attribute Obj/test2 = {'one': 1, 'two': 2}", "Created attribute Obj/test2 [category:None] = {'one': 1, 'two': 2}",
) )
self.call( self.call(
building.CmdSetAttribute(), "Obj/test2", "Attribute Obj/test2 = {'one': 1, 'two': 2}" building.CmdSetAttribute(),
"Obj/test2", "Attribute Obj/test2 [category:None] = {'one': 1, 'two': 2}"
)
self.call(
building.CmdSetAttribute(),
"Obj/test2['one']",
"Attribute Obj/test2['one'] [category:None] = 1"
)
self.call(
building.CmdSetAttribute(),
"Obj/test2['one]",
"Attribute Obj/test2['one] [category:None] = 1"
) )
self.call(building.CmdSetAttribute(), "Obj/test2['one']", "Attribute Obj/test2['one'] = 1")
self.call(building.CmdSetAttribute(), "Obj/test2['one]", "Attribute Obj/test2['one] = 1")
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test2['one']=99", "Obj/test2['one']=99",
"Modified attribute Obj/test2 = {'one': 99, 'two': 2}", "Modified attribute Obj/test2 [category:None] = {'one': 99, 'two': 2}",
)
self.call(
building.CmdSetAttribute(),
"Obj/test2['one']",
"Attribute Obj/test2['one'] [category:None] = 99"
)
self.call(
building.CmdSetAttribute(),
"Obj/test2['two']",
"Attribute Obj/test2['two'] [category:None] = 2"
) )
self.call(building.CmdSetAttribute(), "Obj/test2['one']", "Attribute Obj/test2['one'] = 99")
self.call(building.CmdSetAttribute(), "Obj/test2['two']", "Attribute Obj/test2['two'] = 2")
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test2[+'three']", "Obj/test2[+'three']",
"Obj has no attribute 'test2[+'three']'. (Nested lookups attempted)", "Attribute Obj/test2[+'three'] [category:None] does not exist. (Nested lookups attempted)"
) )
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test2[+'three'] = 3", "Obj/test2[+'three'] = 3",
"Modified attribute Obj/test2 = {'one': 99, 'two': 2, \"+'three'\": 3}", "Modified attribute Obj/test2 [category:None] = {'one': 99, 'two': 2, \"+'three'\": 3}",
) )
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test2[+'three'] =", "Obj/test2[+'three'] =",
"Deleted attribute 'test2[+'three']' (= nested) from Obj.", "Deleted attribute Obj/test2[+'three'] [category:None]."
) )
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test2['three']=3", "Obj/test2['three']=3",
"Modified attribute Obj/test2 = {'one': 99, 'two': 2, 'three': 3}", "Modified attribute Obj/test2 [category:None] = {'one': 99, 'two': 2, 'three': 3}",
) )
# Dict delete # Dict delete
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test2['two'] =", "Obj/test2['two'] =",
"Deleted attribute 'test2['two']' (= nested) from Obj.", "Deleted attribute Obj/test2['two'] [category:None].",
) )
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test2['two']", "Obj/test2['two']",
"Obj has no attribute 'test2['two']'. (Nested lookups attempted)", "Attribute Obj/test2['two'] [category:None] does not exist. (Nested lookups attempted)"
) )
self.call( self.call(
building.CmdSetAttribute(), "Obj/test2", "Attribute Obj/test2 = {'one': 99, 'three': 3}" building.CmdSetAttribute(),
"Obj/test2",
"Attribute Obj/test2 [category:None] = {'one': 99, 'three': 3}"
) )
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test2[0]", "Obj/test2[0]",
"Obj has no attribute 'test2[0]'. (Nested lookups attempted)", "Attribute Obj/test2[0] [category:None] does not exist. (Nested lookups attempted)"
) )
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test2['five'] =", "Obj/test2['five'] =",
"Obj has no attribute 'test2['five']'. (Nested lookups attempted)", "No attribute Obj/test2['five'] [category: None] "
"was found to delete. (Nested lookups attempted)"
) )
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test2[+]=42", "Obj/test2[+]=42",
"Modified attribute Obj/test2 = {'one': 99, 'three': 3, '+': 42}", "Modified attribute Obj/test2 [category:None] = {'one': 99, 'three': 3, '+': 42}",
) )
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test2[+1]=33", "Obj/test2[+1]=33",
"Modified attribute Obj/test2 = {'one': 99, 'three': 3, '+': 42, '+1': 33}", "Modified attribute Obj/test2 [category:None] = "
"{'one': 99, 'three': 3, '+': 42, '+1': 33}",
) )
# tuple # tuple
self.call( self.call(
building.CmdSetAttribute(), "Obj/tup = (1,2)", "Created attribute Obj/tup = (1, 2)" building.CmdSetAttribute(),
"Obj/tup = (1,2)",
"Created attribute Obj/tup [category:None] = (1, 2)"
) )
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
@ -1145,54 +1190,85 @@ class TestBuilding(CommandTest):
building.CmdSetAttribute(), building.CmdSetAttribute(),
# Special case for tuple, could have a better message # Special case for tuple, could have a better message
"Obj/tup[1] = ", "Obj/tup[1] = ",
"Obj has no attribute 'tup[1]'. (Nested lookups attempted)", "No attribute Obj/tup[1] [category: None] "
"was found to delete. (Nested lookups attempted)"
) )
# Deaper nesting # Deaper nesting
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test3=[{'one': 1}]", "Obj/test3=[{'one': 1}]",
"Created attribute Obj/test3 = [{'one': 1}]", "Created attribute Obj/test3 [category:None] = [{'one': 1}]",
) )
self.call( self.call(
building.CmdSetAttribute(), "Obj/test3[0]['one']", "Attribute Obj/test3[0]['one'] = 1" building.CmdSetAttribute(),
"Obj/test3[0]['one']",
"Attribute Obj/test3[0]['one'] [category:None] = 1"
)
self.call(
building.CmdSetAttribute(),
"Obj/test3[0]",
"Attribute Obj/test3[0] [category:None] = {'one': 1}"
) )
self.call(building.CmdSetAttribute(), "Obj/test3[0]", "Attribute Obj/test3[0] = {'one': 1}")
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test3[0]['one'] =", "Obj/test3[0]['one'] =",
"Deleted attribute 'test3[0]['one']' (= nested) from Obj.", "Deleted attribute Obj/test3[0]['one'] [category:None]."
)
self.call(
building.CmdSetAttribute(),
"Obj/test3[0]",
"Attribute Obj/test3[0] [category:None] = {}")
self.call(
building.CmdSetAttribute(),
"Obj/test3",
"Attribute Obj/test3 [category:None] = [{}]"
) )
self.call(building.CmdSetAttribute(), "Obj/test3[0]", "Attribute Obj/test3[0] = {}")
self.call(building.CmdSetAttribute(), "Obj/test3", "Attribute Obj/test3 = [{}]")
# Naughty keys # Naughty keys
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test4[0]='foo'", "Obj/test4[0]='foo'",
"Created attribute Obj/test4[0] = 'foo'", "Created attribute Obj/test4[0] [category:None] = foo",
) )
self.call(building.CmdSetAttribute(), "Obj/test4[0]", "Attribute Obj/test4[0] = foo") self.call(
building.CmdSetAttribute(),
"Obj/test4[0]",
"Attribute Obj/test4[0] [category:None] = foo")
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test4=[{'one': 1}]", "Obj/test4=[{'one': 1}]",
"Created attribute Obj/test4 = [{'one': 1}]", "Created attribute Obj/test4 [category:None] = [{'one': 1}]",
) )
self.call(
building.CmdSetAttribute(), "Obj/test4[0]['one']", "Attribute Obj/test4[0]['one'] = 1"
)
# Prefer nested items
self.call(building.CmdSetAttribute(), "Obj/test4[0]", "Attribute Obj/test4[0] = {'one': 1}")
self.call(
building.CmdSetAttribute(), "Obj/test4[0]['one']", "Attribute Obj/test4[0]['one'] = 1"
)
# Restored access
self.call(building.CmdWipe(), "Obj/test4", "Wiped attributes test4 on Obj.")
self.call(building.CmdSetAttribute(), "Obj/test4[0]", "Attribute Obj/test4[0] = foo")
self.call( self.call(
building.CmdSetAttribute(), building.CmdSetAttribute(),
"Obj/test4[0]['one']", "Obj/test4[0]['one']",
"Obj has no attribute 'test4[0]['one']'.", "Attribute Obj/test4[0]['one'] [category:None] = 1"
)
# Prefer nested items
self.call(
building.CmdSetAttribute(),
"Obj/test4[0]",
"Attribute Obj/test4[0] [category:None] = {'one': 1}"
)
self.call(
building.CmdSetAttribute(),
"Obj/test4[0]['one']",
"Attribute Obj/test4[0]['one'] [category:None] = 1"
)
# Restored access
self.call(
building.CmdWipe(),
"Obj/test4",
"Wiped attributes test4 on Obj.")
self.call(
building.CmdSetAttribute(),
"Obj/test4[0]",
"Attribute Obj/test4[0] [category:None] = foo")
self.call(
building.CmdSetAttribute(),
"Obj/test4[0]['one']",
"Attribute Obj/test4[0]['one'] [category:None] does not exist. (Nested lookups attempted)"
) )
def test_split_nested_attr(self): def test_split_nested_attr(self):
@ -1864,7 +1940,7 @@ class TestBuilding(CommandTest):
) )
import evennia.commands.default.comms as cmd_comms # noqa
from evennia.utils.create import create_channel # noqa from evennia.utils.create import create_channel # noqa
class TestCommsChannel(CommandTest): class TestCommsChannel(CommandTest):
@ -1878,7 +1954,7 @@ class TestCommsChannel(CommandTest):
key="testchannel", key="testchannel",
desc="A test channel") desc="A test channel")
self.channel.connect(self.char1) self.channel.connect(self.char1)
self.cmdchannel = comms.CmdChannel self.cmdchannel = cmd_comms.CmdChannel
self.cmdchannel.account_caller = False self.cmdchannel.account_caller = False
def tearDown(self): def tearDown(self):

View file

@ -1248,8 +1248,8 @@ class AttributeHandler:
def remove( def remove(
self, self,
key=None, key=None,
raise_exception=False,
category=None, category=None,
raise_exception=False,
accessing_obj=None, accessing_obj=None,
default_access=True, default_access=True,
): ):
@ -1260,11 +1260,11 @@ class AttributeHandler:
key (str or list, optional): An Attribute key to remove or a list of keys. If key (str or list, optional): An Attribute key to remove or a list of keys. If
multiple keys, they must all be of the same `category`. If None and multiple keys, they must all be of the same `category`. If None and
category is not given, remove all Attributes. category is not given, remove all Attributes.
category (str, optional): The category within which to
remove the Attribute.
raise_exception (bool, optional): If set, not finding the raise_exception (bool, optional): If set, not finding the
Attribute to delete will raise an exception instead of Attribute to delete will raise an exception instead of
just quietly failing. just quietly failing.
category (str, optional): The category within which to
remove the Attribute.
accessing_obj (object, optional): An object to check accessing_obj (object, optional): An object to check
against the `attredit` lock. If not given, the check will against the `attredit` lock. If not given, the check will
be skipped. be skipped.

View file

@ -395,12 +395,13 @@ def iter_to_str(iterable, endsep=", and", addquote=False):
""" """
if not iterable: if not iterable:
return "" return ""
iterable = list(make_iter(iterable))
len_iter = len(iterable) len_iter = len(iterable)
if addquote: if addquote:
iterable = tuple(f'"{val}"' for val in make_iter(iterable)) iterable = tuple(f'"{val}"' for val in iterable)
else: else:
iterable = tuple(str(val) for val in make_iter(iterable)) iterable = tuple(str(val) for val in iterable)
if endsep.startswith(","): if endsep.startswith(","):
# oxford comma alternative # oxford comma alternative