Repository: open-things/loggingex

Issue:
Implement __str__ and __repr__ for ContextChange
Currently `ContextChange` looks ugly in the debugger or when printed.

Implement `__str__` and `__repr__` methods to make it pretty and, most importantly, useful.

Discussion:

Patch:
diff --git a/src/loggingex/context/__init__.py b/src/loggingex/context/__init__.py
index f722f47..23f0337 100644
--- a/src/loggingex/context/__init__.py
+++ b/src/loggingex/context/__init__.py
@@ -128,6 +128,21 @@ class ContextChange:
 
         self.context_restore_token = context_restore_token
 
+    def __str__(self) -> str:
+        parts = [
+            "!" if self.context_restore_token else None,
+            "-*" if self.context_fresh else None,
+        ]
+        parts.extend("-%s" % n for n in sorted(self.context_remove))
+        upd = self.context_update
+        val = self.context_update.__getitem__
+        parts.extend("+%s=%r" % (n, val(n)) for n in sorted(upd.keys()))
+
+        return " ".join(x for x in parts if x)
+
+    def __repr__(self):
+        return "<ContextChange: %s>" % str(self)
+
     @classmethod
     def validate_context_variable_name(
         cls, name: ContextVariableUnvalidatedName


Test patch:
diff --git a/tests/loggingex/context/test_context_change.py b/tests/loggingex/context/test_context_change.py
index 98ab363..91bf5e4 100644
--- a/tests/loggingex/context/test_context_change.py
+++ b/tests/loggingex/context/test_context_change.py
@@ -1,4 +1,6 @@
-from pytest import mark, raises
+from itertools import product, starmap
+
+from pytest import fixture, mark, raises
 
 from loggingex.context import (
     ContextChange,
@@ -249,3 +251,55 @@ class ContextChangeAsDecoratorTests(InitializedContextBase):
                 return fib(n - 2) + fib(n - 1)
 
         assert fib(3) == 3
+
+
+class SerializationTests:
+    VALUE_MAP = {
+        "locked": {False: None, True: "not a None"},
+        "fresh": {False: False, True: True},
+        "remove": {False: set(), True: ("b", "a")},
+        "update": {False: {}, True: {"y": 2.3, "x": 1, "z": "dummy"}},
+    }
+    STR_MAP = {
+        "locked": {False: None, True: "!"},
+        "fresh": {False: None, True: "-*"},
+        "remove": {False: None, True: "-a -b"},
+        "update": {False: None, True: "+x=1 +y=2.3 +z='dummy'"},
+    }
+
+    FIXTURES = product([False, True], repeat=4)
+    FIXTURE_IDS = starmap(
+        "{}__{}__{}__{}".format,
+        product(
+            ("unlocked", "locked"),
+            ("unfresh", "fresh"),
+            ("no_remove", "with_remove"),
+            ("no_update", "with_update"),
+        ),
+    )
+
+    @fixture(params=FIXTURES, ids=FIXTURE_IDS)
+    def context_change_and_expected_str(self, request):
+        locked, fresh, remove, update = request.param
+        change = ContextChange(
+            context_fresh=self.VALUE_MAP["fresh"][fresh],
+            context_remove=self.VALUE_MAP["remove"][remove],
+            context_update=self.VALUE_MAP["update"][update],
+            context_restore_token=self.VALUE_MAP["locked"][locked],
+        )
+        parts = [
+            self.STR_MAP["locked"][locked],
+            self.STR_MAP["fresh"][fresh],
+            self.STR_MAP["remove"][remove],
+            self.STR_MAP["update"][update],
+        ]
+        expected_str = " ".join(x for x in parts if x)
+        return change, expected_str
+
+    def test_str_context_change_test(self, context_change_and_expected_str):
+        change, expected = context_change_and_expected_str
+        assert str(change) == expected
+
+    def test_repr_context_change_test(self, context_change_and_expected_str):
+        change, expected = context_change_and_expected_str
+        assert repr(change) == "<ContextChange: %s>" % expected