From f9bd07c3eda29cce11f10a631849a9cc6ecd7e53 Mon Sep 17 00:00:00 2001 From: Aaron McMillin Date: Mon, 16 Sep 2019 17:50:38 -0400 Subject: [PATCH 1/9] [#1928] Helper functions and tests --- evennia/commands/default/building.py | 34 ++++++++++++++++++++++++++ evennia/commands/default/tests.py | 36 ++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 2441a90e5c..531dafe61f 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1603,6 +1603,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,6 +1629,38 @@ 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]", []), + ] + """ + 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, [p.strip('"\'[]') 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. diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 752cae6fc3..3de3c03a8b 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -535,6 +535,42 @@ 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_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]', [])], + # duplicate keys don't cause issues + 'test4[0][0]': [('test4', ['0', '0']), ('test4[0]', ['0']), ('test4[0][0]', [])], + } + + 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([1], 2, not_found) + do_test_single([1], 0, 1) + 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'.") From d83e3d471e6a7678559461dffdb4d652625d0a10 Mon Sep 17 00:00:00 2001 From: Aaron McMillin Date: Wed, 18 Sep 2019 21:36:00 -0400 Subject: [PATCH 2/9] [#1928] Create and access data structures --- evennia/commands/default/building.py | 22 ++++++++++++-- evennia/commands/default/tests.py | 45 +++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 531dafe61f..de974a7f08 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1642,13 +1642,25 @@ class CmdSetAttribute(ObjManipCommand): ("nested['asdf'][0]", []), ] """ + quotes = '"\'' + + def clean_key(val): + val = val.strip('[]') + if val[0] in quotes: + return val.strip(quotes) + + 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, [p.strip('"\'[]') for p in parts[index:]]) + yield (base_attr, [clean_key(p) for p in parts[index:]]) base_attr += part yield (attr, []) @@ -1665,8 +1677,12 @@ class CmdSetAttribute(ObjManipCommand): """ 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)) + for key, nested_keys in self.split_nested_attr(attr): + 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) else: return "\n%s has no attribute '%s'." % (obj.name, attr) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 3de3c03a8b..0f44dcbba8 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -535,6 +535,42 @@ 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): + # 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") + + # removing white space proves real parsing + self.call(building.CmdSetAttribute(), + "Obj/test2={ 'one': 1 }", "Created attribute Obj/test2 = {'one': 1}") + self.call(building.CmdSetAttribute(), "Obj/test2", "Attribute Obj/test2 = {'one': 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(building.CmdSetAttribute(), "Obj/test2[0]", "Obj has no attribute 'test2[0]'.") + + # 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}") + + # 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") + self.call(building.CmdWipe(), "Obj/test4", "Wiped attributes test4 on Obj.") + # Restored access + 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 = { @@ -542,8 +578,13 @@ class TestBuilding(CommandTest): '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 - 'test4[0][0]': [('test4', ['0', '0']), ('test4[0]', ['0']), ('test4[0][0]', [])], + '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(): @@ -562,8 +603,10 @@ class TestBuilding(CommandTest): 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') From 1a1203fcb6bdcf7b5c5cae1cfdf7d9cf725f2d7b Mon Sep 17 00:00:00 2001 From: Aaron McMillin Date: Wed, 18 Sep 2019 21:55:25 -0400 Subject: [PATCH 3/9] [#1928] Delete nested --- evennia/commands/default/building.py | 20 +++++++++++++++----- evennia/commands/default/tests.py | 18 +++++++++++++++--- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index de974a7f08..0437260930 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1688,12 +1688,22 @@ class CmdSetAttribute(ObjManipCommand): 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) + for key, nested_keys in self.split_nested_attr(attr): + if obj.attributes.has(key): + if nested_keys: + del_key = nested_keys[-1] + val = obj.attributes.get(key) + val = self.do_nested_lookup(val, *nested_keys[:-1]) + if val is not self.not_found: + del val[del_key] + obj.attributes.add(key, val) + 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) else: return "\n%s has no attribute '%s'." % (obj.name, attr) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 0f44dcbba8..f54be20150 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -540,13 +540,25 @@ class TestBuilding(CommandTest): 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") + # 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]'.") # removing white space proves real parsing self.call(building.CmdSetAttribute(), - "Obj/test2={ 'one': 1 }", "Created attribute Obj/test2 = {'one': 1}") - self.call(building.CmdSetAttribute(), "Obj/test2", "Attribute Obj/test2 = {'one': 1}") + "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['two']", "Attribute Obj/test2['two'] = 2") + # 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']'.") + self.call(building.CmdSetAttribute(), "Obj/test2", "Attribute Obj/test2 = {'one': 1}") self.call(building.CmdSetAttribute(), "Obj/test2[0]", "Obj has no attribute 'test2[0]'.") # Deaper nesting @@ -565,8 +577,8 @@ class TestBuilding(CommandTest): # 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") - self.call(building.CmdWipe(), "Obj/test4", "Wiped attributes test4 on Obj.") # 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']'.") From 2dfb327f8833fe65d72e6ac78fe24c86d6b25a46 Mon Sep 17 00:00:00 2001 From: Aaron McMillin Date: Wed, 18 Sep 2019 22:16:00 -0400 Subject: [PATCH 4/9] [#1928] fix nested delete bug --- evennia/commands/default/building.py | 6 +++--- evennia/commands/default/tests.py | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 0437260930..1dc61f1bdf 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1695,9 +1695,9 @@ class CmdSetAttribute(ObjManipCommand): if nested_keys: del_key = nested_keys[-1] val = obj.attributes.get(key) - val = self.do_nested_lookup(val, *nested_keys[:-1]) - if val is not self.not_found: - del val[del_key] + deep = self.do_nested_lookup(val, *nested_keys[:-1]) + if deep is not self.not_found: + del deep[del_key] obj.attributes.add(key, val) return "\nDeleted attribute '%s' (= nested) from %s." % (attr, obj.name) else: diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index f54be20150..56e600e2b4 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -566,6 +566,10 @@ class TestBuilding(CommandTest): "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(), From 772cfda6936c5a957e4ee642f49bd35450e859be Mon Sep 17 00:00:00 2001 From: Aaron McMillin Date: Wed, 18 Sep 2019 23:08:10 -0400 Subject: [PATCH 5/9] [#1928] Edit existing nested --- evennia/commands/default/building.py | 15 ++++++++++++++- evennia/commands/default/tests.py | 9 ++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 1dc61f1bdf..8a5a883338 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1708,8 +1708,21 @@ class CmdSetAttribute(ObjManipCommand): return "\n%s has no attribute '%s'." % (obj.name, attr) def set_attr(self, obj, attr, value): + 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: + # TODO - insert/append in lists + deep[acc_key] = value + value = lookup_value + attr = key + 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) return "\n%s attribute %s/%s = %s" % (verb, obj.name, attr, repr(value)) except SyntaxError: diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 56e600e2b4..91d11bae13 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -541,6 +541,8 @@ class TestBuilding(CommandTest): 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.") @@ -553,12 +555,17 @@ class TestBuilding(CommandTest): 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']=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']'.") - self.call(building.CmdSetAttribute(), "Obj/test2", "Attribute Obj/test2 = {'one': 1}") + 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]'.") # Deaper nesting From cb04a71a239f3de770fdb62b75ca8b36b06cf4b6 Mon Sep 17 00:00:00 2001 From: Aaron McMillin Date: Wed, 18 Sep 2019 23:17:19 -0400 Subject: [PATCH 6/9] [#1928] Fix deleting non-existent items --- evennia/commands/default/building.py | 8 +++++--- evennia/commands/default/tests.py | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 8a5a883338..d566f29ca2 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1697,15 +1697,17 @@ class CmdSetAttribute(ObjManipCommand): val = obj.attributes.get(key) deep = self.do_nested_lookup(val, *nested_keys[:-1]) if deep is not self.not_found: - del deep[del_key] + try: + del deep[del_key] + except (IndexError, KeyError, TypeError): + continue obj.attributes.add(key, val) 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) - else: - return "\n%s has no attribute '%s'." % (obj.name, attr) + return "\n%s has no attribute '%s'." % (obj.name, attr) def set_attr(self, obj, attr, value): for key, nested_keys in self.split_nested_attr(attr): diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 91d11bae13..ed88b4e02c 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -548,6 +548,9 @@ class TestBuilding(CommandTest): "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]'.") + # Delete non-existent + self.call(building.CmdSetAttribute(), + "Obj/test1[5] =", "Obj has no attribute 'test1[5]'.") # removing white space proves real parsing self.call(building.CmdSetAttribute(), @@ -567,6 +570,8 @@ class TestBuilding(CommandTest): self.call(building.CmdSetAttribute(), "Obj/test2['two']", "Obj has no attribute 'test2['two']'.") 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]'.") + self.call(building.CmdSetAttribute(), + "Obj/test2['five'] =", "Obj has no attribute 'test2['five']'.") # Deaper nesting self.call(building.CmdSetAttribute(), From ed585022461625df7ff7163ad42cc07301a8226c Mon Sep 17 00:00:00 2001 From: Aaron McMillin Date: Thu, 19 Sep 2019 00:28:13 -0400 Subject: [PATCH 7/9] [#1928] Special handling for lists and tuples --- evennia/commands/default/building.py | 35 ++++++++++++++++++++++++---- evennia/commands/default/tests.py | 26 +++++++++++++++++++-- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index d566f29ca2..2b0b14ffc3 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1701,7 +1701,6 @@ class CmdSetAttribute(ObjManipCommand): del deep[del_key] except (IndexError, KeyError, TypeError): continue - obj.attributes.add(key, val) return "\nDeleted attribute '%s' (= nested) from %s." % (attr, obj.name) else: exists = obj.attributes.has(key) @@ -1710,22 +1709,48 @@ class CmdSetAttribute(ObjManipCommand): return "\n%s has no attribute '%s'." % (obj.name, attr) 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: - # TODO - insert/append in lists - deep[acc_key] = value + # To support appending and inserting to lists + # a key that starts with @ will insert a new item at that + # location, and move the other elements down. + # Just '@' will append to the list + if isinstance(acc_key, str) and acc_key[0] == '@': + try: + if len(acc_key) > 1: + where = int(acc_key[1:]) + deep.insert(where, value) + else: + deep.append(value) + except 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: - - 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 diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index ed88b4e02c..7444ddb086 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -536,7 +536,7 @@ class TestBuilding(CommandTest): self.call(building.CmdWipe(), "Obj2", "Wiped all attributes on Obj2.") def test_nested_attribute_commands(self): - # adding white space proves real parsing + # 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") @@ -551,8 +551,13 @@ class TestBuilding(CommandTest): # Delete non-existent self.call(building.CmdSetAttribute(), "Obj/test1[5] =", "Obj has no attribute 'test1[5]'.") + # 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]") - # removing white space proves real parsing + # 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}") @@ -572,6 +577,23 @@ class TestBuilding(CommandTest): self.call(building.CmdSetAttribute(), "Obj/test2[0]", "Obj has no attribute 'test2[0]'.") self.call(building.CmdSetAttribute(), "Obj/test2['five'] =", "Obj has no attribute 'test2['five']'.") + 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]'.") # Deaper nesting self.call(building.CmdSetAttribute(), From 186bc09b4d93dfef39304beb1c7da6e7688951af Mon Sep 17 00:00:00 2001 From: Aaron McMillin Date: Sat, 21 Sep 2019 16:52:43 -0400 Subject: [PATCH 8/9] [#1928] change to plus for append/insert --- evennia/commands/default/building.py | 7 +++++-- evennia/commands/default/tests.py | 14 +++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 2b0b14ffc3..5ce0f44d12 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -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 @@ -1648,7 +1649,9 @@ class CmdSetAttribute(ObjManipCommand): 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: @@ -1720,7 +1723,7 @@ class CmdSetAttribute(ObjManipCommand): # a key that starts with @ will insert a new item at that # location, and move the other elements down. # Just '@' will append to the list - if isinstance(acc_key, str) and acc_key[0] == '@': + if isinstance(acc_key, str) and acc_key[0] == LIST_APPEND_CHAR: try: if len(acc_key) > 1: where = int(acc_key[1:]) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 7444ddb086..478633d8fe 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -553,9 +553,9 @@ class TestBuilding(CommandTest): "Obj/test1[5] =", "Obj has no attribute 'test1[5]'.") # Append self.call(building.CmdSetAttribute(), - "Obj/test1[@] = 42", "Modified attribute Obj/test1 = [2, 42]") + "Obj/test1[+] = 42", "Modified attribute Obj/test1 = [2, 42]") self.call(building.CmdSetAttribute(), - "Obj/test1[@0] = -1", "Modified attribute Obj/test1 = [-1, 2, 42]") + "Obj/test1[+0] = -1", "Modified attribute Obj/test1 = [-1, 2, 42]") # dict - removing white space proves real parsing self.call(building.CmdSetAttribute(), @@ -578,19 +578,19 @@ class TestBuilding(CommandTest): self.call(building.CmdSetAttribute(), "Obj/test2['five'] =", "Obj has no attribute 'test2['five']'.") self.call(building.CmdSetAttribute(), - "Obj/test2[@]=42", "Modified attribute Obj/test2 = {'one': 99, 'three': 3, '@': 42}") + "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}") + "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)") + "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)") + "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]'.") From ba3db1731d621ecbb2ea1822f40a85ddcfadf4a3 Mon Sep 17 00:00:00 2001 From: Aaron McMillin Date: Sun, 29 Sep 2019 21:45:33 -0400 Subject: [PATCH 9/9] [#1928] PR feedback, documentation, error handling --- evennia/commands/default/building.py | 31 +++++++---- evennia/commands/default/tests.py | 78 ++++++++++++++++++++-------- 2 files changed, 77 insertions(+), 32 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 5ce0f44d12..fee4f75214 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1570,7 +1570,7 @@ class CmdSetAttribute(ObjManipCommand): set / = set / = set / - set */attr = + set */ = Switch: edit: Open the line editor (string values only) @@ -1585,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, @@ -1593,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[]|n in 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. @@ -1680,20 +1682,26 @@ class CmdSetAttribute(ObjManipCommand): """ Look up the value of an attribute and return a string displaying it. """ + 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) - else: - return "\n%s has no attribute '%s'." % (obj.name, attr) + 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, or a nested data structure, and report back. """ + 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] @@ -1709,7 +1717,10 @@ class CmdSetAttribute(ObjManipCommand): exists = obj.attributes.has(key) obj.attributes.remove(attr) return "\nDeleted attribute '%s' (= %s) from %s." % (attr, exists, obj.name) - return "\n%s has no attribute '%s'." % (obj.name, attr) + 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 @@ -1720,9 +1731,9 @@ class CmdSetAttribute(ObjManipCommand): 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 @ will insert a new item at that + # a key that starts with LIST_APPEND_CHAR will insert a new item at that # location, and move the other elements down. - # Just '@' will append to the list + # 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: @@ -1730,7 +1741,7 @@ class CmdSetAttribute(ObjManipCommand): deep.insert(where, value) else: deep.append(value) - except AttributeError: + except (ValueError, AttributeError): pass else: value = lookup_value diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 478633d8fe..b3148dfa3f 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -545,40 +545,65 @@ class TestBuilding(CommandTest): 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.") + "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]'.") + 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]'.") + "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]") + "Obj/test1[+] = 42", + "Modified attribute Obj/test1 = [2, 42]") self.call(building.CmdSetAttribute(), - "Obj/test1[+0] = -1", "Modified attribute Obj/test1 = [-1, 2, 42]") + "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}") + "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}") + "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']=3", "Modified attribute Obj/test2 = {'one': 99, 'two': 2, 'three': 3}") + "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']'.") + "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]'.") self.call(building.CmdSetAttribute(), - "Obj/test2['five'] =", "Obj has no attribute 'test2['five']'.") + "Obj/test2[0]", + "Obj has no attribute 'test2[0]'. (Nested lookups attempted)") self.call(building.CmdSetAttribute(), - "Obj/test2[+]=42", "Modified attribute Obj/test2 = {'one': 99, 'three': 3, '+': 42}") + "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}") @@ -586,31 +611,39 @@ class TestBuilding(CommandTest): # 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)") + "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)") + "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)") + "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]'.") + "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}]") + "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.") + "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'") + "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}]") + "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}") @@ -619,7 +652,8 @@ class TestBuilding(CommandTest): 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']'.") + "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