Merge pull request #1943 from aarcro/1928_nested_attributes

1928 nested attributes
This commit is contained in:
Griatch 2019-09-30 19:00:56 +02:00 committed by GitHub
commit 2cf49882de
2 changed files with 294 additions and 17 deletions

View file

@ -53,6 +53,7 @@ __all__ = (
# used by set
from ast import literal_eval as _LITERAL_EVAL
LIST_APPEND_CHAR = '+'
# used by find
CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
@ -1569,7 +1570,7 @@ class CmdSetAttribute(ObjManipCommand):
set <obj>/<attr> = <value>
set <obj>/<attr> =
set <obj>/<attr>
set *<account>/attr = <value>
set *<account>/<attr> = <value>
Switch:
edit: Open the line editor (string values only)
@ -1584,7 +1585,7 @@ class CmdSetAttribute(ObjManipCommand):
Sets attributes on objects. The second example form above clears a
previously set attribute while the third form inspects the current value of
the attribute (if any). The last one (with the star) is a shortcut for
operatin on a player Account rather than an Object.
operating on a player Account rather than an Object.
The most common data to save with this command are strings and
numbers. You can however also set Python primitives such as lists,
@ -1592,8 +1593,10 @@ class CmdSetAttribute(ObjManipCommand):
the functionality of certain custom objects). This is indicated
by you starting your value with one of |c'|n, |c"|n, |c(|n, |c[|n
or |c{ |n.
Note that you should leave a space after starting a dictionary ('{ ')
so as to not confuse the dictionary start with a colour code like \{g.
Once you have stored a Python primative as noted above, you can include
|c[<key>]|n in <attr> to reference nested values.
Remember that if you use Python primitives like this, you must
write proper Python syntax too - notably you must include quotes
around your strings or you will get an error.
@ -1603,6 +1606,8 @@ class CmdSetAttribute(ObjManipCommand):
key = "set"
locks = "cmd:perm(set) or perm(Builder)"
help_category = "Building"
nested_re = re.compile(r'\[.*?\]')
not_found = object()
def check_obj(self, obj):
"""
@ -1627,30 +1632,139 @@ class CmdSetAttribute(ObjManipCommand):
"""
return attr_name
def split_nested_attr(self, attr):
"""
Yields tuples of (possible attr name, nested keys on that attr).
For performance, this is biased to the deepest match, but allows compatability
with older attrs that might have been named with `[]`'s.
> list(split_nested_attr("nested['asdf'][0]"))
[
('nested', ['asdf', 0]),
("nested['asdf']", [0]),
("nested['asdf'][0]", []),
]
"""
quotes = '"\''
def clean_key(val):
val = val.strip('[]')
if val[0] in quotes:
return val.strip(quotes)
if val[0] == LIST_APPEND_CHAR:
# List insert/append syntax
return val
try:
return int(val)
except ValueError:
return val
parts = self.nested_re.findall(attr)
base_attr = ''
if parts:
base_attr = attr[:attr.find(parts[0])]
for index, part in enumerate(parts):
yield (base_attr, [clean_key(p) for p in parts[index:]])
base_attr += part
yield (attr, [])
def do_nested_lookup(self, value, *keys):
result = value
for key in keys:
try:
result = result.__getitem__(key)
except (IndexError, KeyError, TypeError):
return self.not_found
return result
def view_attr(self, obj, attr):
"""
Look up the value of an attribute and return a string displaying it.
"""
if obj.attributes.has(attr):
return "\nAttribute %s/%s = %s" % (obj.name, attr, obj.attributes.get(attr))
else:
return "\n%s has no attribute '%s'." % (obj.name, attr)
nested = False
for key, nested_keys in self.split_nested_attr(attr):
nested = True
if obj.attributes.has(key):
val = obj.attributes.get(key)
val = self.do_nested_lookup(val, *nested_keys)
if val is not self.not_found:
return "\nAttribute %s/%s = %s" % (obj.name, attr, val)
error = "\n%s has no attribute '%s'." % (obj.name, attr)
if nested:
error += ' (Nested lookups attempted)'
return error
def rm_attr(self, obj, attr):
"""
Remove an attribute from the object, and report back.
Remove an attribute from the object, or a nested data structure, and report back.
"""
if obj.attributes.has(attr):
val = obj.attributes.has(attr)
obj.attributes.remove(attr)
return "\nDeleted attribute '%s' (= %s) from %s." % (attr, val, obj.name)
else:
return "\n%s has no attribute '%s'." % (obj.name, attr)
nested = False
for key, nested_keys in self.split_nested_attr(attr):
nested = True
if obj.attributes.has(key):
if nested_keys:
del_key = nested_keys[-1]
val = obj.attributes.get(key)
deep = self.do_nested_lookup(val, *nested_keys[:-1])
if deep is not self.not_found:
try:
del deep[del_key]
except (IndexError, KeyError, TypeError):
continue
return "\nDeleted attribute '%s' (= nested) from %s." % (attr, obj.name)
else:
exists = obj.attributes.has(key)
obj.attributes.remove(attr)
return "\nDeleted attribute '%s' (= %s) from %s." % (attr, exists, obj.name)
error = "\n%s has no attribute '%s'." % (obj.name, attr)
if nested:
error += ' (Nested lookups attempted)'
return error
def set_attr(self, obj, attr, value):
done = False
for key, nested_keys in self.split_nested_attr(attr):
if obj.attributes.has(key) and nested_keys:
acc_key = nested_keys[-1]
lookup_value = obj.attributes.get(key)
deep = self.do_nested_lookup(lookup_value, *nested_keys[:-1])
if deep is not self.not_found:
# To support appending and inserting to lists
# a key that starts with LIST_APPEND_CHAR will insert a new item at that
# location, and move the other elements down.
# Using LIST_APPEND_CHAR alone will append to the list
if isinstance(acc_key, str) and acc_key[0] == LIST_APPEND_CHAR:
try:
if len(acc_key) > 1:
where = int(acc_key[1:])
deep.insert(where, value)
else:
deep.append(value)
except (ValueError, AttributeError):
pass
else:
value = lookup_value
attr = key
done = True
break
# List magic failed, just use like a key/index
try:
deep[acc_key] = value
except TypeError as err:
# Tuples can't be modified
return "\n%s - %s" % (err, deep)
value = lookup_value
attr = key
done = True
break
verb = "Modified" if obj.attributes.has(attr) else "Created"
try:
verb = "Modified" if obj.attributes.has(attr) else "Created"
obj.attributes.add(attr, value)
if not done:
obj.attributes.add(attr, value)
return "\n%s attribute %s/%s = %s" % (verb, obj.name, attr, repr(value))
except SyntaxError:
# this means literal_eval tried to parse a faulty string

View file

@ -535,6 +535,169 @@ class TestBuilding(CommandTest):
self.call(building.CmdWipe(), "Obj2/test2/test3", "Wiped attributes test2,test3 on Obj2.")
self.call(building.CmdWipe(), "Obj2", "Wiped all attributes on Obj2.")
def test_nested_attribute_commands(self):
# list - adding white space proves real parsing
self.call(building.CmdSetAttribute(), "Obj/test1=[1,2]", "Created attribute Obj/test1 = [1, 2]")
self.call(building.CmdSetAttribute(), "Obj/test1", "Attribute Obj/test1 = [1, 2]")
self.call(building.CmdSetAttribute(), "Obj/test1[0]", "Attribute Obj/test1[0] = 1")
self.call(building.CmdSetAttribute(), "Obj/test1[1]", "Attribute Obj/test1[1] = 2")
self.call(building.CmdSetAttribute(), "Obj/test1[0] = 99", "Modified attribute Obj/test1 = [99, 2]")
self.call(building.CmdSetAttribute(), "Obj/test1[0]", "Attribute Obj/test1[0] = 99")
# list delete
self.call(building.CmdSetAttribute(),
"Obj/test1[0] =",
"Deleted attribute 'test1[0]' (= nested) from Obj.")
self.call(building.CmdSetAttribute(), "Obj/test1[0]", "Attribute Obj/test1[0] = 2")
self.call(building.CmdSetAttribute(),
"Obj/test1[1]",
"Obj has no attribute 'test1[1]'. (Nested lookups attempted)")
# Delete non-existent
self.call(building.CmdSetAttribute(),
"Obj/test1[5] =",
"Obj has no attribute 'test1[5]'. (Nested lookups attempted)")
# Append
self.call(building.CmdSetAttribute(),
"Obj/test1[+] = 42",
"Modified attribute Obj/test1 = [2, 42]")
self.call(building.CmdSetAttribute(),
"Obj/test1[+0] = -1",
"Modified attribute Obj/test1 = [-1, 2, 42]")
# dict - removing white space proves real parsing
self.call(building.CmdSetAttribute(),
"Obj/test2={ 'one': 1, 'two': 2 }",
"Created attribute Obj/test2 = {'one': 1, 'two': 2}")
self.call(building.CmdSetAttribute(), "Obj/test2", "Attribute Obj/test2 = {'one': 1, 'two': 2}")
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(building.CmdSetAttribute(),
"Obj/test2['one']=99",
"Modified attribute Obj/test2 = {'one': 99, 'two': 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(building.CmdSetAttribute(),
"Obj/test2[+'three']",
"Obj has no attribute 'test2[+'three']'. (Nested lookups attempted)")
self.call(building.CmdSetAttribute(),
"Obj/test2[+'three'] = 3",
"Modified attribute Obj/test2 = {'one': 99, 'two': 2, \"+'three'\": 3}")
self.call(building.CmdSetAttribute(),
"Obj/test2[+'three'] =",
"Deleted attribute 'test2[+'three']' (= nested) from Obj.")
self.call(building.CmdSetAttribute(),
"Obj/test2['three']=3",
"Modified attribute Obj/test2 = {'one': 99, 'two': 2, 'three': 3}")
# Dict delete
self.call(building.CmdSetAttribute(),
"Obj/test2['two'] =",
"Deleted attribute 'test2['two']' (= nested) from Obj.")
self.call(building.CmdSetAttribute(),
"Obj/test2['two']",
"Obj has no attribute 'test2['two']'. (Nested lookups attempted)")
self.call(building.CmdSetAttribute(), "Obj/test2", "Attribute Obj/test2 = {'one': 99, 'three': 3}")
self.call(building.CmdSetAttribute(),
"Obj/test2[0]",
"Obj has no attribute 'test2[0]'. (Nested lookups attempted)")
self.call(building.CmdSetAttribute(),
"Obj/test2['five'] =",
"Obj has no attribute 'test2['five']'. (Nested lookups attempted)")
self.call(building.CmdSetAttribute(),
"Obj/test2[+]=42",
"Modified attribute Obj/test2 = {'one': 99, 'three': 3, '+': 42}")
self.call(building.CmdSetAttribute(),
"Obj/test2[+1]=33",
"Modified attribute Obj/test2 = {'one': 99, 'three': 3, '+': 42, '+1': 33}")
# tuple
self.call(building.CmdSetAttribute(), "Obj/tup = (1,2)", "Created attribute Obj/tup = (1, 2)")
self.call(building.CmdSetAttribute(),
"Obj/tup[1] = 99",
"'tuple' object does not support item assignment - (1, 2)")
self.call(building.CmdSetAttribute(),
"Obj/tup[+] = 99",
"'tuple' object does not support item assignment - (1, 2)")
self.call(building.CmdSetAttribute(),
"Obj/tup[+1] = 99",
"'tuple' object does not support item assignment - (1, 2)")
self.call(building.CmdSetAttribute(),
# Special case for tuple, could have a better message
"Obj/tup[1] = ",
"Obj has no attribute 'tup[1]'. (Nested lookups attempted)")
# Deaper nesting
self.call(building.CmdSetAttribute(),
"Obj/test3=[{'one': 1}]",
"Created attribute Obj/test3 = [{'one': 1}]")
self.call(building.CmdSetAttribute(), "Obj/test3[0]['one']", "Attribute Obj/test3[0]['one'] = 1")
self.call(building.CmdSetAttribute(), "Obj/test3[0]", "Attribute Obj/test3[0] = {'one': 1}")
self.call(building.CmdSetAttribute(),
"Obj/test3[0]['one'] =",
"Deleted attribute 'test3[0]['one']' (= nested) from Obj.")
self.call(building.CmdSetAttribute(), "Obj/test3[0]", "Attribute Obj/test3[0] = {}")
self.call(building.CmdSetAttribute(), "Obj/test3", "Attribute Obj/test3 = [{}]")
# Naughty keys
self.call(building.CmdSetAttribute(),
"Obj/test4[0]='foo'",
"Created attribute Obj/test4[0] = 'foo'")
self.call(building.CmdSetAttribute(), "Obj/test4[0]", "Attribute Obj/test4[0] = foo")
self.call(building.CmdSetAttribute(),
"Obj/test4=[{'one': 1}]",
"Created attribute Obj/test4 = [{'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(building.CmdSetAttribute(),
"Obj/test4[0]['one']",
"Obj has no attribute 'test4[0]['one']'.")
def test_split_nested_attr(self):
split_nested_attr = building.CmdSetAttribute().split_nested_attr
test_cases = {
'test1': [('test1', [])],
'test2["dict"]': [('test2', ['dict']), ('test2["dict"]', [])],
# Quotes not actually required
'test3[dict]': [('test3', ['dict']), ('test3[dict]', [])],
'test4["dict]': [('test4', ['dict']), ('test4["dict]', [])],
# duplicate keys don't cause issues
'test5[0][0]': [('test5', [0, 0]), ('test5[0]', [0]), ('test5[0][0]', [])],
# String ints preserved
'test6["0"][0]': [('test6', ['0', 0]), ('test6["0"]', [0]), ('test6["0"][0]', [])],
# Unmatched []
'test7[dict': [('test7[dict', [])],
}
for attr, result in test_cases.items():
self.assertEqual(list(split_nested_attr(attr)), result)
def test_do_nested_lookup(self):
do_nested_lookup = building.CmdSetAttribute().do_nested_lookup
not_found = building.CmdSetAttribute.not_found
def do_test_single(value, key, result):
self.assertEqual(do_nested_lookup(value, key), result)
def do_test_multi(value, keys, result):
self.assertEqual(do_nested_lookup(value, *keys), result)
do_test_single([], 'test1', not_found)
do_test_single([1], 'test2', not_found)
do_test_single([], 0, not_found)
do_test_single([], '0', not_found)
do_test_single([1], 2, not_found)
do_test_single([1], 0, 1)
do_test_single([1], '0', not_found) # str key is str not int
do_test_single({}, 'test3', not_found)
do_test_single({}, 0, not_found)
do_test_single({'foo': 'bar'}, 'foo', 'bar')
do_test_multi({'one': [1, 2, 3]}, ('one', 0), 1)
do_test_multi([{}, {'two': 2}, 3], (1, 'two'), 2)
def test_name(self):
self.call(building.CmdName(), "", "Usage: ")
self.call(building.CmdName(), "Obj2=Obj3", "Object's name changed to 'Obj3'.")