Repository: ansible/ansible-navigator (language: python)

Issue:
`dataclass` serialization capabilities (`:settings` related)
- Add new dataclass base class for content moving forward
- ContentBase has default dictionary factories which can be overridden per serf/view
- Added pytest-subtests

This is not in use yet, be will be for `:settings`

The last thing to do is to add tests demonstrating full vs. normal views, but I thought I'd put it out for review now.

The one thing I don't like and would appreciate suggestions with is the need for `TYPE_CHECKING`


Discussion / hints:

Fix patch:
diff --git a/src/ansible_navigator/ui_framework/content_defs.py b/src/ansible_navigator/ui_framework/content_defs.py
index a49109549..6077af38c 100644
--- a/src/ansible_navigator/ui_framework/content_defs.py
+++ b/src/ansible_navigator/ui_framework/content_defs.py
@@ -93,3 +93,18 @@ def serialize_yaml_normal(self) -> DictType:
         :returns: A dictionary created from self
         """
         return asdict(self)
+
+    def get(self, attribute: str):
+        """Allow this dataclass to be treated like a dictionary.
+
+        This is a work around until the UI fully supports dataclasses
+        at which time this can be removed.
+
+        Default is intentionally not implemented as a safeguard to enure
+        this is not more work than necessary to remove in the future
+        and will only return attributes in existence.
+
+        :param attribute: The attribute to get
+        :returns: The gotten attribute
+        """
+        return getattr(self, attribute)
diff --git a/src/ansible_navigator/ui_framework/utils.py b/src/ansible_navigator/ui_framework/utils.py
index 7c21f4e0b..d38b9d26b 100644
--- a/src/ansible_navigator/ui_framework/utils.py
+++ b/src/ansible_navigator/ui_framework/utils.py
@@ -3,28 +3,50 @@
 import functools
 import re
 
+from dataclasses import is_dataclass
 from math import floor
+from typing import Any
+from typing import Dict
 from typing import List
+from typing import Union
 
+from .content_defs import ContentBase
 
-def convert_percentage(data_dict: dict, keys: List, progress_bar_width: int) -> None:
-    """convert a string % to a little progress bar
-    not recursive
-    80% = 80%|XXXXXXXX  |
 
-    :param data_dict: The dictionary to update
-    :param keys: The keys to convert in each dictionary
-    :param progress_bar_width: The width of the progress bar
+def convert_percentage(
+    content: Union[Dict[str, Any], ContentBase],
+    columns: List[str],
+    progress_bar_width: int,
+) -> None:
+    """Convert an attribute or value to a progress bar formatted for the TUI in place.
+
+    :param content: The content for which the progress bar will be generated
+    :param columns: The menu columns, only make progress bars for columns
+    :param progress_bar_width: The target width of the progress bar
     """
-    for key in keys:
-        value = data_dict.get(key)
+    for column in columns:
+        value = content.get(column)
         if value and is_percent(str(value)):
-            if value == "100%":
-                data_dict[key] = "COMPLETE".center(progress_bar_width, " ")
-            else:
-                chars_in_progress_bar = floor(progress_bar_width / 100 * int(value[0:-1]))
-                data_dict["_" + key] = value
-                data_dict[key] = ("\u2587" * chars_in_progress_bar).ljust(progress_bar_width)
+            new_value = _string_to_progress(value, progress_bar_width)
+            if isinstance(content, dict):
+                content["_" + column] = value
+                content[column] = new_value
+            elif is_dataclass(content):
+                setattr(content, f"_{column}", value)
+                setattr(content, column, new_value)
+
+
+def _string_to_progress(value: str, progress_bar_width: int) -> str:
+    """Convert a string to a progress bar or text string indicating complete.
+
+    :param value: The percent string
+    :param progress_bar_width: The target width of the progress bar
+    :returns: The resulting progress bar or text string
+    """
+    if value == "100%":
+        return "COMPLETE".center(progress_bar_width, " ")
+    chars_in_progress_bar = floor(progress_bar_width / 100 * int(value[0:-1]))
+    return ("\u2587" * chars_in_progress_bar).ljust(progress_bar_width)
 
 
 @functools.lru_cache(maxsize=None)


Test patch:
diff --git a/tests/unit/ui_framework/test_progress_bar.py b/tests/unit/ui_framework/test_progress_bar.py
new file mode 100644
index 000000000..358d74656
--- /dev/null
+++ b/tests/unit/ui_framework/test_progress_bar.py
@@ -0,0 +1,63 @@
+"""Test for the conversion of percent string to progress bars."""
+from dataclasses import dataclass
+
+from ansible_navigator.ui_framework.content_defs import ContentBase
+from ansible_navigator.ui_framework.utils import convert_percentage
+
+
+def test_dictionary_running() -> None:
+    """Test the conversion of a string within a dictionary to a progress bar when not complete."""
+    test_data = {"progress": "30%", "other": "test value"}
+    convert_percentage(content=test_data, columns=["progress"], progress_bar_width=10)
+    expected = {"progress": "▇▇▇       ", "other": "test value", "_progress": "30%"}
+    assert test_data == expected
+
+
+def test_dictionary_complete() -> None:
+    """Test the conversion of a string within a dictionary to a progress bar when complete."""
+    test_data = {"progress": "100%", "other": "test value"}
+    convert_percentage(content=test_data, columns=["progress"], progress_bar_width=10)
+    expected = {"progress": " COMPLETE ", "other": "test value", "_progress": "100%"}
+    assert test_data == expected
+
+
+@dataclass
+class ContentTest(ContentBase):
+    """Test data for string conversion to a progress bar."""
+
+    progress: str
+    _progress: str = ""
+    other: str = "test_value"
+
+    def get(self, attribute: str):
+        """Allow this dataclass to be treated like a dictionary.
+
+        This is a work around until the UI fully supports dataclasses
+        at which time this can be removed.
+
+        Default is intentionally not implemented as a safeguard to enure
+        this is not more work than necessary to remove in the future
+        and will only return attributes in existence.
+
+        :param attribute: The attribute to get
+        :returns: The gotten attribute
+        """
+        return getattr(self, attribute)
+
+
+def test_dataclass_running() -> None:
+    """Test the conversion of a string within a dataclass to a progress bar when not complete."""
+    test_data = ContentTest(progress="30%")
+    convert_percentage(content=test_data, columns=["progress"], progress_bar_width=10)
+    assert test_data.progress == "▇▇▇       "
+    assert test_data._progress == "30%"  # pylint: disable=protected-access
+    assert test_data.other == "test_value"
+
+
+def test_dataclass_complete() -> None:
+    """Test the conversion of a string within a dataclass to a progress bar when not complete."""
+    test_data = ContentTest(progress="100%")
+    convert_percentage(content=test_data, columns=["progress"], progress_bar_width=10)
+    assert test_data.progress == " COMPLETE "
+    assert test_data._progress == "100%"  # pylint: disable=protected-access
+    assert test_data.other == "test_value"