From d6bd951c377b07dcb1453d9ead760da560a3d9cd Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Tue, 2 Dec 2025 14:31:57 -0500 Subject: [PATCH 1/4] add new so-yaml_test for removefromlist --- salt/manager/tools/sbin/so-yaml_test.py | 122 ++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/salt/manager/tools/sbin/so-yaml_test.py b/salt/manager/tools/sbin/so-yaml_test.py index f33c1300a..9b4055841 100644 --- a/salt/manager/tools/sbin/so-yaml_test.py +++ b/salt/manager/tools/sbin/so-yaml_test.py @@ -457,3 +457,125 @@ class TestRemove(unittest.TestCase): self.assertEqual(result, 1) self.assertIn("Missing filename or key arg", mock_stderr.getvalue()) sysmock.assert_called_once_with(1) + +class TestRemoveFromList(unittest.TestCase): + + def test_removefromlist_missing_arg(self): + with patch('sys.exit', new=MagicMock()) as sysmock: + with patch('sys.stderr', new=StringIO()) as mock_stderr: + sys.argv = ["cmd", "help"] + soyaml.removefromlist(["file", "key"]) + sysmock.assert_called() + self.assertIn("Missing filename, key arg, or list item to remove", mock_stderr.getvalue()) + + def test_removefromlist(self): + filename = "/tmp/so-yaml_test-removefromlist.yaml" + file = open(filename, "w") + file.write("{key1: { child1: 123, child2: abc }, key2: false, key3: [a,b,c]}") + file.close() + + soyaml.removefromlist([filename, "key3", "b"]) + + file = open(filename, "r") + actual = file.read() + file.close() + + expected = "key1:\n child1: 123\n child2: abc\nkey2: false\nkey3:\n- a\n- c\n" + self.assertEqual(actual, expected) + + def test_removefromlist_nested(self): + filename = "/tmp/so-yaml_test-removefromlist.yaml" + file = open(filename, "w") + file.write("{key1: { child1: 123, child2: [a,b,c] }, key2: false, key3: [e,f,g]}") + file.close() + + soyaml.removefromlist([filename, "key1.child2", "b"]) + + file = open(filename, "r") + actual = file.read() + file.close() + + expected = "key1:\n child1: 123\n child2:\n - a\n - c\nkey2: false\nkey3:\n- e\n- f\n- g\n" + self.assertEqual(actual, expected) + + def test_removefromlist_nested_deep(self): + filename = "/tmp/so-yaml_test-removefromlist.yaml" + file = open(filename, "w") + file.write("{key1: { child1: 123, child2: { deep1: 45, deep2: [a,b,c] } }, key2: false, key3: [e,f,g]}") + file.close() + + soyaml.removefromlist([filename, "key1.child2.deep2", "b"]) + + file = open(filename, "r") + actual = file.read() + file.close() + + expected = "key1:\n child1: 123\n child2:\n deep1: 45\n deep2:\n - a\n - c\nkey2: false\nkey3:\n- e\n- f\n- g\n" + self.assertEqual(actual, expected) + + def test_removefromlist_item_not_in_list(self): + filename = "/tmp/so-yaml_test-removefromlist.yaml" + file = open(filename, "w") + file.write("{key1: [a,b,c]}") + file.close() + + soyaml.removefromlist([filename, "key1", "d"]) + + file = open(filename, "r") + actual = file.read() + file.close() + + expected = "key1:\n- a\n- b\n- c\n" + self.assertEqual(actual, expected) + + def test_removefromlist_key_noexist(self): + filename = "/tmp/so-yaml_test-removefromlist.yaml" + file = open(filename, "w") + file.write("{key1: { child1: 123, child2: { deep1: 45, deep2: [a,b,c] } }, key2: false, key3: [e,f,g]}") + file.close() + + with patch('sys.exit', new=MagicMock()) as sysmock: + with patch('sys.stderr', new=StringIO()) as mock_stderr: + sys.argv = ["cmd", "removefromlist", filename, "key4", "h"] + soyaml.main() + sysmock.assert_called() + self.assertEqual("The key provided does not exist. No action was taken on the file.\n", mock_stderr.getvalue()) + + def test_removefromlist_key_noexist_deep(self): + filename = "/tmp/so-yaml_test-removefromlist.yaml" + file = open(filename, "w") + file.write("{key1: { child1: 123, child2: { deep1: 45, deep2: [a,b,c] } }, key2: false, key3: [e,f,g]}") + file.close() + + with patch('sys.exit', new=MagicMock()) as sysmock: + with patch('sys.stderr', new=StringIO()) as mock_stderr: + sys.argv = ["cmd", "removefromlist", filename, "key1.child2.deep3", "h"] + soyaml.main() + sysmock.assert_called() + self.assertEqual("The key provided does not exist. No action was taken on the file.\n", mock_stderr.getvalue()) + + def test_removefromlist_key_nonlist(self): + filename = "/tmp/so-yaml_test-removefromlist.yaml" + file = open(filename, "w") + file.write("{key1: { child1: 123, child2: { deep1: 45, deep2: [a,b,c] } }, key2: false, key3: [e,f,g]}") + file.close() + + with patch('sys.exit', new=MagicMock()) as sysmock: + with patch('sys.stderr', new=StringIO()) as mock_stderr: + sys.argv = ["cmd", "removefromlist", filename, "key1", "h"] + soyaml.main() + sysmock.assert_called() + self.assertEqual("The existing value for the given key is not a list. No action was taken on the file.\n", mock_stderr.getvalue()) + + def test_removefromlist_key_nonlist_deep(self): + filename = "/tmp/so-yaml_test-removefromlist.yaml" + file = open(filename, "w") + file.write("{key1: { child1: 123, child2: { deep1: 45, deep2: [a,b,c] } }, key2: false, key3: [e,f,g]}") + file.close() + + with patch('sys.exit', new=MagicMock()) as sysmock: + with patch('sys.stderr', new=StringIO()) as mock_stderr: + sys.argv = ["cmd", "removefromlist", filename, "key1.child2.deep1", "h"] + soyaml.main() + sysmock.assert_called() + self.assertEqual("The existing value for the given key is not a list. No action was taken on the file.\n", mock_stderr.getvalue()) From e871ec358ed6f0e0f4b881e4acb50eb261d37f02 Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Tue, 2 Dec 2025 14:43:33 -0500 Subject: [PATCH 2/4] need additional line bw class --- salt/manager/tools/sbin/so-yaml_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/salt/manager/tools/sbin/so-yaml_test.py b/salt/manager/tools/sbin/so-yaml_test.py index 9b4055841..3d936e0f5 100644 --- a/salt/manager/tools/sbin/so-yaml_test.py +++ b/salt/manager/tools/sbin/so-yaml_test.py @@ -458,6 +458,7 @@ class TestRemove(unittest.TestCase): self.assertIn("Missing filename or key arg", mock_stderr.getvalue()) sysmock.assert_called_once_with(1) + class TestRemoveFromList(unittest.TestCase): def test_removefromlist_missing_arg(self): From 89eb95c077e0d16f8349bb02c7b8fd06e103755a Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Tue, 2 Dec 2025 14:46:24 -0500 Subject: [PATCH 3/4] add removefromlist --- salt/manager/tools/sbin/so-yaml.py | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/salt/manager/tools/sbin/so-yaml.py b/salt/manager/tools/sbin/so-yaml.py index 4c8544893..c25c71095 100755 --- a/salt/manager/tools/sbin/so-yaml.py +++ b/salt/manager/tools/sbin/so-yaml.py @@ -17,6 +17,7 @@ def showUsage(args): print('Usage: {} [ARGS...]'.format(sys.argv[0]), file=sys.stderr) print(' General commands:', file=sys.stderr) print(' append - Append a list item to a yaml key, if it exists and is a list. Requires KEY and LISTITEM args.', file=sys.stderr) + print(' removefromlist - Remove a list item from a yaml key, if it exists and is a list. Requires KEY and LISTITEM args.', file=sys.stderr) print(' add - Add a new key and set its value. Fails if key already exists. Requires KEY and VALUE args.', file=sys.stderr) print(' get - Displays (to stdout) the value stored in the given key. Requires KEY arg.', file=sys.stderr) print(' remove - Removes a yaml key, if it exists. Requires KEY arg.', file=sys.stderr) @@ -57,6 +58,24 @@ def appendItem(content, key, listItem): return 1 +def removeFromList(content, key, listItem): + pieces = key.split(".", 1) + if len(pieces) > 1: + removeFromList(content[pieces[0]], pieces[1], listItem) + else: + try: + if not isinstance(content[key], list): + raise AttributeError("Value is not a list") + if listItem in content[key]: + content[key].remove(listItem) + except (AttributeError, TypeError): + print("The existing value for the given key is not a list. No action was taken on the file.", file=sys.stderr) + return 1 + except KeyError: + print("The key provided does not exist. No action was taken on the file.", file=sys.stderr) + return 1 + + def convertType(value): if isinstance(value, str) and value.startswith("file:"): path = value[5:] # Remove "file:" prefix @@ -103,6 +122,23 @@ def append(args): return 0 +def removefromlist(args): + if len(args) != 3: + print('Missing filename, key arg, or list item to remove', file=sys.stderr) + showUsage(None) + return 1 + + filename = args[0] + key = args[1] + listItem = args[2] + + content = loadYaml(filename) + removeFromList(content, key, convertType(listItem)) + writeYaml(filename, content) + + return 0 + + def addKey(content, key, value): pieces = key.split(".", 1) if len(pieces) > 1: @@ -211,6 +247,7 @@ def main(): "help": showUsage, "add": add, "append": append, + "removefromlist": removefromlist, "get": get, "remove": remove, "replace": replace, From ef092e28937e27deb6f04ba9c54707c0691bc736 Mon Sep 17 00:00:00 2001 From: Josh Patterson Date: Tue, 2 Dec 2025 15:01:32 -0500 Subject: [PATCH 4/4] rename to removelistitem --- salt/manager/tools/sbin/so-yaml.py | 12 +++--- salt/manager/tools/sbin/so-yaml_test.py | 54 ++++++++++++------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/salt/manager/tools/sbin/so-yaml.py b/salt/manager/tools/sbin/so-yaml.py index c25c71095..00290f18b 100755 --- a/salt/manager/tools/sbin/so-yaml.py +++ b/salt/manager/tools/sbin/so-yaml.py @@ -17,7 +17,7 @@ def showUsage(args): print('Usage: {} [ARGS...]'.format(sys.argv[0]), file=sys.stderr) print(' General commands:', file=sys.stderr) print(' append - Append a list item to a yaml key, if it exists and is a list. Requires KEY and LISTITEM args.', file=sys.stderr) - print(' removefromlist - Remove a list item from a yaml key, if it exists and is a list. Requires KEY and LISTITEM args.', file=sys.stderr) + print(' removelistitem - Remove a list item from a yaml key, if it exists and is a list. Requires KEY and LISTITEM args.', file=sys.stderr) print(' add - Add a new key and set its value. Fails if key already exists. Requires KEY and VALUE args.', file=sys.stderr) print(' get - Displays (to stdout) the value stored in the given key. Requires KEY arg.', file=sys.stderr) print(' remove - Removes a yaml key, if it exists. Requires KEY arg.', file=sys.stderr) @@ -58,10 +58,10 @@ def appendItem(content, key, listItem): return 1 -def removeFromList(content, key, listItem): +def removeListItem(content, key, listItem): pieces = key.split(".", 1) if len(pieces) > 1: - removeFromList(content[pieces[0]], pieces[1], listItem) + removeListItem(content[pieces[0]], pieces[1], listItem) else: try: if not isinstance(content[key], list): @@ -122,7 +122,7 @@ def append(args): return 0 -def removefromlist(args): +def removelistitem(args): if len(args) != 3: print('Missing filename, key arg, or list item to remove', file=sys.stderr) showUsage(None) @@ -133,7 +133,7 @@ def removefromlist(args): listItem = args[2] content = loadYaml(filename) - removeFromList(content, key, convertType(listItem)) + removeListItem(content, key, convertType(listItem)) writeYaml(filename, content) return 0 @@ -247,7 +247,7 @@ def main(): "help": showUsage, "add": add, "append": append, - "removefromlist": removefromlist, + "removelistitem": removelistitem, "get": get, "remove": remove, "replace": replace, diff --git a/salt/manager/tools/sbin/so-yaml_test.py b/salt/manager/tools/sbin/so-yaml_test.py index 3d936e0f5..3b5ec498e 100644 --- a/salt/manager/tools/sbin/so-yaml_test.py +++ b/salt/manager/tools/sbin/so-yaml_test.py @@ -459,23 +459,23 @@ class TestRemove(unittest.TestCase): sysmock.assert_called_once_with(1) -class TestRemoveFromList(unittest.TestCase): +class TestRemoveListItem(unittest.TestCase): - def test_removefromlist_missing_arg(self): + def test_removelistitem_missing_arg(self): with patch('sys.exit', new=MagicMock()) as sysmock: with patch('sys.stderr', new=StringIO()) as mock_stderr: sys.argv = ["cmd", "help"] - soyaml.removefromlist(["file", "key"]) + soyaml.removelistitem(["file", "key"]) sysmock.assert_called() self.assertIn("Missing filename, key arg, or list item to remove", mock_stderr.getvalue()) - def test_removefromlist(self): - filename = "/tmp/so-yaml_test-removefromlist.yaml" + def test_removelistitem(self): + filename = "/tmp/so-yaml_test-removelistitem.yaml" file = open(filename, "w") file.write("{key1: { child1: 123, child2: abc }, key2: false, key3: [a,b,c]}") file.close() - soyaml.removefromlist([filename, "key3", "b"]) + soyaml.removelistitem([filename, "key3", "b"]) file = open(filename, "r") actual = file.read() @@ -484,13 +484,13 @@ class TestRemoveFromList(unittest.TestCase): expected = "key1:\n child1: 123\n child2: abc\nkey2: false\nkey3:\n- a\n- c\n" self.assertEqual(actual, expected) - def test_removefromlist_nested(self): - filename = "/tmp/so-yaml_test-removefromlist.yaml" + def test_removelistitem_nested(self): + filename = "/tmp/so-yaml_test-removelistitem.yaml" file = open(filename, "w") file.write("{key1: { child1: 123, child2: [a,b,c] }, key2: false, key3: [e,f,g]}") file.close() - soyaml.removefromlist([filename, "key1.child2", "b"]) + soyaml.removelistitem([filename, "key1.child2", "b"]) file = open(filename, "r") actual = file.read() @@ -499,13 +499,13 @@ class TestRemoveFromList(unittest.TestCase): expected = "key1:\n child1: 123\n child2:\n - a\n - c\nkey2: false\nkey3:\n- e\n- f\n- g\n" self.assertEqual(actual, expected) - def test_removefromlist_nested_deep(self): - filename = "/tmp/so-yaml_test-removefromlist.yaml" + def test_removelistitem_nested_deep(self): + filename = "/tmp/so-yaml_test-removelistitem.yaml" file = open(filename, "w") file.write("{key1: { child1: 123, child2: { deep1: 45, deep2: [a,b,c] } }, key2: false, key3: [e,f,g]}") file.close() - soyaml.removefromlist([filename, "key1.child2.deep2", "b"]) + soyaml.removelistitem([filename, "key1.child2.deep2", "b"]) file = open(filename, "r") actual = file.read() @@ -514,13 +514,13 @@ class TestRemoveFromList(unittest.TestCase): expected = "key1:\n child1: 123\n child2:\n deep1: 45\n deep2:\n - a\n - c\nkey2: false\nkey3:\n- e\n- f\n- g\n" self.assertEqual(actual, expected) - def test_removefromlist_item_not_in_list(self): - filename = "/tmp/so-yaml_test-removefromlist.yaml" + def test_removelistitem_item_not_in_list(self): + filename = "/tmp/so-yaml_test-removelistitem.yaml" file = open(filename, "w") file.write("{key1: [a,b,c]}") file.close() - soyaml.removefromlist([filename, "key1", "d"]) + soyaml.removelistitem([filename, "key1", "d"]) file = open(filename, "r") actual = file.read() @@ -529,54 +529,54 @@ class TestRemoveFromList(unittest.TestCase): expected = "key1:\n- a\n- b\n- c\n" self.assertEqual(actual, expected) - def test_removefromlist_key_noexist(self): - filename = "/tmp/so-yaml_test-removefromlist.yaml" + def test_removelistitem_key_noexist(self): + filename = "/tmp/so-yaml_test-removelistitem.yaml" file = open(filename, "w") file.write("{key1: { child1: 123, child2: { deep1: 45, deep2: [a,b,c] } }, key2: false, key3: [e,f,g]}") file.close() with patch('sys.exit', new=MagicMock()) as sysmock: with patch('sys.stderr', new=StringIO()) as mock_stderr: - sys.argv = ["cmd", "removefromlist", filename, "key4", "h"] + sys.argv = ["cmd", "removelistitem", filename, "key4", "h"] soyaml.main() sysmock.assert_called() self.assertEqual("The key provided does not exist. No action was taken on the file.\n", mock_stderr.getvalue()) - def test_removefromlist_key_noexist_deep(self): - filename = "/tmp/so-yaml_test-removefromlist.yaml" + def test_removelistitem_key_noexist_deep(self): + filename = "/tmp/so-yaml_test-removelistitem.yaml" file = open(filename, "w") file.write("{key1: { child1: 123, child2: { deep1: 45, deep2: [a,b,c] } }, key2: false, key3: [e,f,g]}") file.close() with patch('sys.exit', new=MagicMock()) as sysmock: with patch('sys.stderr', new=StringIO()) as mock_stderr: - sys.argv = ["cmd", "removefromlist", filename, "key1.child2.deep3", "h"] + sys.argv = ["cmd", "removelistitem", filename, "key1.child2.deep3", "h"] soyaml.main() sysmock.assert_called() self.assertEqual("The key provided does not exist. No action was taken on the file.\n", mock_stderr.getvalue()) - def test_removefromlist_key_nonlist(self): - filename = "/tmp/so-yaml_test-removefromlist.yaml" + def test_removelistitem_key_nonlist(self): + filename = "/tmp/so-yaml_test-removelistitem.yaml" file = open(filename, "w") file.write("{key1: { child1: 123, child2: { deep1: 45, deep2: [a,b,c] } }, key2: false, key3: [e,f,g]}") file.close() with patch('sys.exit', new=MagicMock()) as sysmock: with patch('sys.stderr', new=StringIO()) as mock_stderr: - sys.argv = ["cmd", "removefromlist", filename, "key1", "h"] + sys.argv = ["cmd", "removelistitem", filename, "key1", "h"] soyaml.main() sysmock.assert_called() self.assertEqual("The existing value for the given key is not a list. No action was taken on the file.\n", mock_stderr.getvalue()) - def test_removefromlist_key_nonlist_deep(self): - filename = "/tmp/so-yaml_test-removefromlist.yaml" + def test_removelistitem_key_nonlist_deep(self): + filename = "/tmp/so-yaml_test-removelistitem.yaml" file = open(filename, "w") file.write("{key1: { child1: 123, child2: { deep1: 45, deep2: [a,b,c] } }, key2: false, key3: [e,f,g]}") file.close() with patch('sys.exit', new=MagicMock()) as sysmock: with patch('sys.stderr', new=StringIO()) as mock_stderr: - sys.argv = ["cmd", "removefromlist", filename, "key1.child2.deep1", "h"] + sys.argv = ["cmd", "removelistitem", filename, "key1.child2.deep1", "h"] soyaml.main() sysmock.assert_called() self.assertEqual("The existing value for the given key is not a list. No action was taken on the file.\n", mock_stderr.getvalue())