From b7b29ec7e82e5ebcb644bfc7ea530b1380fe21e3 Mon Sep 17 00:00:00 2001
From: Tom Doherty <tdoherty@hudson-trading.com>
Date: Wed, 7 Aug 2024 15:18:41 +0100
Subject: [PATCH] fix yaml output

In b9be2de, OrderedDict was replaced with HashableOrderedDict.
Add logic to yamldumper to handle the new type.
---
 changelog/66783.fixed.md            |  1 +
 salt/state.py                       |  7 +--
 salt/utils/odict.py                 |  5 ++
 salt/utils/yamldumper.py            |  4 +-
 tests/unit/utils/test_yamldumper.py | 81 +++++++++++++++++++++++++++++
 5 files changed, 91 insertions(+), 7 deletions(-)
 create mode 100644 changelog/66783.fixed.md

diff --git a/changelog/66783.fixed.md b/changelog/66783.fixed.md
new file mode 100644
index 000000000000..2bd08e3411d3
--- /dev/null
+++ b/changelog/66783.fixed.md
@@ -0,0 +1 @@
+fix yaml output
diff --git a/salt/state.py b/salt/state.py
index d60d78c334fe..e515e368b180 100644
--- a/salt/state.py
+++ b/salt/state.py
@@ -51,7 +51,7 @@
 from salt.serializers.msgpack import deserialize as msgpack_deserialize
 from salt.serializers.msgpack import serialize as msgpack_serialize
 from salt.template import compile_template, compile_template_str
-from salt.utils.odict import DefaultOrderedDict, OrderedDict
+from salt.utils.odict import DefaultOrderedDict, HashableOrderedDict
 
 log = logging.getLogger(__name__)
 
@@ -127,11 +127,6 @@
 ).union(STATE_RUNTIME_KEYWORDS)
 
 
-class HashableOrderedDict(OrderedDict):
-    def __hash__(self):
-        return id(self)
-
-
 def split_low_tag(tag):
     """
     Take a low tag and split it back into the low dict that it came from
diff --git a/salt/utils/odict.py b/salt/utils/odict.py
index 2834f1d92469..11a3f3a30977 100644
--- a/salt/utils/odict.py
+++ b/salt/utils/odict.py
@@ -62,3 +62,8 @@ def __repr__(self, _repr_running={}):  # pylint: disable=W0102
         return "DefaultOrderedDict({}, {})".format(
             self.default_factory, super().__repr__()
         )
+
+
+class HashableOrderedDict(OrderedDict):
+    def __hash__(self):
+        return id(self)
diff --git a/salt/utils/yamldumper.py b/salt/utils/yamldumper.py
index 8c6e40394a35..8e694ab4a763 100644
--- a/salt/utils/yamldumper.py
+++ b/salt/utils/yamldumper.py
@@ -13,7 +13,7 @@
 import yaml  # pylint: disable=blacklisted-import
 
 import salt.utils.context
-from salt.utils.odict import OrderedDict
+from salt.utils.odict import HashableOrderedDict, OrderedDict
 
 try:
     from yaml import CDumper as Dumper
@@ -71,7 +71,9 @@ def represent_undefined(dumper, data):
 
 
 OrderedDumper.add_representer(OrderedDict, represent_ordereddict)
+OrderedDumper.add_representer(HashableOrderedDict, represent_ordereddict)
 SafeOrderedDumper.add_representer(OrderedDict, represent_ordereddict)
+SafeOrderedDumper.add_representer(HashableOrderedDict, represent_ordereddict)
 SafeOrderedDumper.add_representer(None, represent_undefined)
 
 OrderedDumper.add_representer(
diff --git a/tests/unit/utils/test_yamldumper.py b/tests/unit/utils/test_yamldumper.py
index 9a1a6ab103ba..65c900ebb25e 100644
--- a/tests/unit/utils/test_yamldumper.py
+++ b/tests/unit/utils/test_yamldumper.py
@@ -2,7 +2,11 @@
     Unit tests for salt.utils.yamldumper
 """
 
+from collections import OrderedDict, defaultdict
+
 import salt.utils.yamldumper
+from salt.utils.context import NamespacedDictWrapper
+from salt.utils.odict import HashableOrderedDict
 from tests.support.unit import TestCase
 
 
@@ -35,3 +39,80 @@ def test_yaml_safe_dump(self):
             salt.utils.yamldumper.safe_dump(data, default_flow_style=False)
             == "foo: bar\n"
         )
+
+    def test_yaml_ordered_dump(self):
+        """
+        Test yaml.dump with OrderedDict
+        """
+        data = OrderedDict([("foo", "bar"), ("baz", "qux")])
+        exp_yaml = "{foo: bar, baz: qux}\n"
+        assert (
+            salt.utils.yamldumper.dump(data, Dumper=salt.utils.yamldumper.OrderedDumper)
+            == exp_yaml
+        )
+
+    def test_yaml_safe_ordered_dump(self):
+        """
+        Test yaml.safe_dump with OrderedDict
+        """
+        data = OrderedDict([("foo", "bar"), ("baz", "qux")])
+        exp_yaml = "{foo: bar, baz: qux}\n"
+        assert salt.utils.yamldumper.safe_dump(data) == exp_yaml
+
+    def test_yaml_indent_safe_ordered_dump(self):
+        """
+        Test yaml.dump with IndentedSafeOrderedDumper
+        """
+        data = OrderedDict([("foo", ["bar", "baz"]), ("qux", "quux")])
+        exp_yaml = "foo:\n- bar\n- baz\nqux: quux\n"
+        assert (
+            salt.utils.yamldumper.dump(
+                data,
+                Dumper=salt.utils.yamldumper.IndentedSafeOrderedDumper,
+                default_flow_style=False,
+            )
+            == exp_yaml
+        )
+
+    def test_yaml_defaultdict_dump(self):
+        """
+        Test yaml.dump with defaultdict
+        """
+        data = defaultdict(list)
+        data["foo"].append("bar")
+        exp_yaml = "foo: [bar]\n"
+        assert salt.utils.yamldumper.safe_dump(data) == exp_yaml
+
+    def test_yaml_namespaced_dict_wrapper_dump(self):
+        """
+        Test yaml.dump with NamespacedDictWrapper
+        """
+        data = NamespacedDictWrapper({"test": {"foo": "bar"}}, "test")
+        exp_yaml = (
+            "!!python/object/new:salt.utils.context.NamespacedDictWrapper\n"
+            "dictitems: {foo: bar}\n"
+            "state:\n"
+            "  _NamespacedDictWrapper__dict:\n"
+            "    test: {foo: bar}\n"
+            "  pre_keys: !!python/tuple [test]\n"
+        )
+        assert salt.utils.yamldumper.dump(data) == exp_yaml
+
+    def test_yaml_undefined_dump(self):
+        """
+        Test yaml.safe_dump with None
+        """
+        data = {"foo": None}
+        exp_yaml = "{foo: null}\n"
+        assert salt.utils.yamldumper.safe_dump(data) == exp_yaml
+
+    def test_yaml_hashable_ordered_dict_dump(self):
+        """
+        Test yaml.dump with HashableOrderedDict
+        """
+        data = HashableOrderedDict([("foo", "bar"), ("baz", "qux")])
+        exp_yaml = "{foo: bar, baz: qux}\n"
+        assert (
+            salt.utils.yamldumper.dump(data, Dumper=salt.utils.yamldumper.OrderedDumper)
+            == exp_yaml
+        )