Repository: terrapower/armi
Pull request #1834 (2024-08-26 20:13:53)

Fixing issue displaying defaultdicts in tabulate
## What is the change?

@albeanth reported an issue in this method, when `armi.utils.tabulate()` takes a `defaultdict` instead of a `dict`:

https://github.com/terrapower/armi/blob/9dc6760de7da3800cbe98e83066cf5f343e81a6a/armi/bookkeeping/report/reportingUtils.py#L438-L444

I have obfuscated his exact data, but reproduced his example generically in a unit test as proof.

## Why is the change being made?

I don't know why this worked in `tabulate`, but we fixed the issue in `armi.utils.tabulate`.

---

## Checklist

- [x] This PR has only [one purpose or idea](https://terrapower.github.io/armi/developer/tooling.html#one-idea-one-pr).
- [x] [Tests](https://terrapower.github.io/armi/developer/tooling.html#test-it) have been added/updated to verify any new/changed code.

<!-- Check the code quality -->

- [x] The code style follows [good practices](https://terrapower.github.io/armi/developer/standards_and_practices.html).
- [x] The commit message(s) follow [good practices](https://terrapower.github.io/armi/developer/tooling.html).

<!-- Check the project-level cruft -->

- [x] The [release notes](https://terrapower.github.io/armi/developer/tooling.html#add-release-notes) have been updated if necessary.
- [x] The [documentation](https://terrapower.github.io/armi/developer/tooling.html#document-it) is still up-to-date in the `doc` folder.
- [x] The dependencies are still up-to-date in `pyproject.toml`.


Patch:
diff --git a/armi/utils/tabulate.py b/armi/utils/tabulate.py
index 9047630f..2f78c3ac 100644
--- a/armi/utils/tabulate.py
+++ b/armi/utils/tabulate.py
@@ -1020,11 +1020,15 @@ def _normalizeTabularData(data, headers, showIndex="default"):
         headers = list(headers)
 
     index = None
-    if type(data) is dict:
+    if hasattr(data, "keys"):
         # dict-like
         keys = data.keys()
-        # columns have to be transposed
-        rows = list(zip_longest(*data.values()))
+
+        # fill out default values, to ensure all data lists are the same length
+        vals = list(data.values())
+        maxLen = max([len(v) for v in vals], default=0)
+        vals = [[v for v in vv] + [None] * (maxLen - len(vv)) for vv in vals]
+        rows = [tuple(v[i] for v in vals) for i in range(maxLen)]
 
         if headers == "keys":
             # headers should be strings


Test patch:
diff --git a/armi/utils/tests/test_tabulate.py b/armi/utils/tests/test_tabulate.py
index 16642cfc..1876cd25 100644
--- a/armi/utils/tests/test_tabulate.py
+++ b/armi/utils/tests/test_tabulate.py
@@ -19,6 +19,7 @@ many arbitrary changes as we need. Thanks to the tabulate team.
 
 https://github.com/astanin/python-tabulate
 """
+from collections import defaultdict
 from collections import namedtuple
 from collections import OrderedDict
 from collections import UserDict
@@ -395,6 +396,38 @@ class TestTabulateInputs(unittest.TestCase):
         result = tabulate(lb, headers=["bytes"])
         self.assertEqual(expected, result)
 
+    def test_tightCouplingExample(self):
+        """Input: Real world-ish example from tight coupling."""
+        # the two examples below should both produce the same output:
+        border = "--  ------------------------------  --------------  ----------------------------"
+        expected = "\n".join(
+            [
+                border,
+                "      criticalCrIteration: keffUnc    dif3d: power    thInterface: THavgCladTemp",
+                border,
+                " 0                     9.01234e-05      0.00876543                    0.00123456",
+                border,
+            ]
+        )
+
+        # the data is a regular dictionry
+        data = {
+            "criticalCrIteration: keffUnc": [9.01234e-05],
+            "dif3d: power": [0.00876543],
+            "thInterface: THavgCladTemp": [0.00123456],
+        }
+        result = tabulate(data, headers="keys", showIndex=True, tableFmt="armi")
+        self.assertEqual(expected, result)
+
+        # the data is a defaultdict
+        dataD = defaultdict(list)
+        for key, vals in data.items():
+            for val in vals:
+                dataD[key].append(val)
+
+        result2 = tabulate(dataD, headers="keys", showIndex=True, tableFmt="armi")
+        self.assertEqual(expected, result2)
+
 
 class TestTabulateInternal(unittest.TestCase):
     def test_alignColumnDecimal(self):