Skip to content

Commit 5a23e98

Browse files
committed
Address display of whitespace-padded strings in the output of mf query (#1701)
This PR removes the stripping of whitespace around string values in the output of `mf query`.
1 parent 47598f9 commit 5a23e98

File tree

3 files changed

+79
-7
lines changed

3 files changed

+79
-7
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Fixes
2+
body: Address display of whitespace-padded strings in the output of `mf query`
3+
time: 2025-03-28T14:18:17.834268-07:00
4+
custom:
5+
Author: plypaul
6+
Issue: "1701"

metricflow/data_table/mf_table.py

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from __future__ import annotations
22

33
import datetime
4+
import importlib.util
45
import itertools
56
import logging
7+
import threading
8+
import typing
69
from dataclasses import dataclass
710
from decimal import Decimal
811
from typing import Iterable, Iterator, List, Optional, Sequence, Tuple, Type
@@ -15,6 +18,10 @@
1518
from metricflow.data_table.column_types import CellValue, InputCellValue, row_cell_types
1619
from metricflow.data_table.mf_column import ColumnDescription
1720

21+
if typing.TYPE_CHECKING:
22+
from mypy.moduleinspect import ModuleType
23+
24+
1825
logger = logging.getLogger(__name__)
1926

2027

@@ -157,16 +164,12 @@ def text_format(self, float_decimals: Optional[int] = None) -> str:
157164
column_alignment.append("decimal")
158165
else:
159166
column_alignment.append("left")
160-
161167
headers = tuple(column_description.column_name for column_description in self.column_descriptions)
162168

163-
return tabulate.tabulate(
169+
return _IsolatedTabulateRunner.tabulate(
164170
tabular_data=str_rows,
165171
headers=headers,
166-
# `tabulate.tabulate` can detect numeric values and apply special formatting rules. This can
167-
# result in unexpected values when coupled with the `--decimals` option, so disabling this feature.
168-
disable_numparse=True,
169-
colalign=column_alignment,
172+
column_alignment=column_alignment,
170173
)
171174

172175
def with_lower_case_column_names(self) -> MetricFlowDataTable:
@@ -291,3 +294,66 @@ def add_row(self, row: Sequence[InputCellValue], parse_strings: bool = False) ->
291294

292295
def build(self) -> MetricFlowDataTable: # noqa: D102
293296
return self._build_table_from_rows()
297+
298+
299+
class _IsolatedTabulateRunner:
300+
"""Helps run `tabulate` with different module options.
301+
302+
The `tabulate.tabulate` method uses some options defined in the module instead of being provided as arguments to
303+
the function. This runner is used to change those options in isolation by loading a copy of the `tabulate` module.
304+
This helps to ensure that other calls to `tabulate.tabulate` don't see unexpected results.
305+
"""
306+
307+
_TABULATE_MODULE_COPY: Optional[ModuleType] = None
308+
_STATE_LOCK = threading.Lock()
309+
310+
@classmethod
311+
def tabulate(
312+
cls,
313+
tabular_data: Sequence[Sequence[CellValue]],
314+
headers: Sequence[str],
315+
column_alignment: Optional[Sequence[str]],
316+
) -> str:
317+
"""Produce a text table from the given data. Also see class docstring."""
318+
with _IsolatedTabulateRunner._STATE_LOCK:
319+
try:
320+
if _IsolatedTabulateRunner._TABULATE_MODULE_COPY is None:
321+
tabulate_module_spec = importlib.util.find_spec("tabulate")
322+
if tabulate_module_spec is None:
323+
raise RuntimeError("Unable to find spec for `tabulate`.")
324+
tabulate_module_copy = importlib.util.module_from_spec(tabulate_module_spec)
325+
if tabulate_module_copy is None:
326+
raise RuntimeError(f"Unable to load module using {tabulate_module_spec=}")
327+
if tabulate_module_spec.loader is None:
328+
raise RuntimeError(f"Loader missing for {tabulate_module_spec=}")
329+
tabulate_module_spec.loader.exec_module(tabulate_module_copy)
330+
tabulate_module_copy.PRESERVE_WHITESPACE = True # type: ignore[attr-defined]
331+
_IsolatedTabulateRunner._TABULATE_MODULE_COPY = tabulate_module_copy
332+
except Exception:
333+
logger.exception(
334+
"Failed to load a copy of the `tabulate` module. This means that some table-formatting options "
335+
"can't be applied. This is a bug and should be investigated."
336+
)
337+
338+
# `tabulate.tabulate` can detect numeric values and apply special formatting rules. This can
339+
# result in unexpected values when coupled with the `--decimals` option, so disabling that feature.
340+
disable_numparse = True
341+
342+
if _IsolatedTabulateRunner._TABULATE_MODULE_COPY is None:
343+
logger.warning(
344+
"Generating text table without required options set as there was an error loading the "
345+
"`tabulate` module."
346+
)
347+
return tabulate.tabulate(
348+
tabular_data=tabular_data,
349+
headers=headers,
350+
disable_numparse=disable_numparse,
351+
colalign=column_alignment,
352+
)
353+
354+
return _IsolatedTabulateRunner._TABULATE_MODULE_COPY.tabulate(
355+
tabular_data=tabular_data,
356+
headers=headers,
357+
disable_numparse=disable_numparse,
358+
colalign=column_alignment,
359+
)

tests_metricflow/snapshots/test_output_format.py/str/test_print_string__result.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ docstring:
1212
15 null String literal 'null' 1
1313
16 Line 1 String with a newline. 1
1414
Line 2
15-
17 Space Padded String with leading / trailing space (' '). 1
15+
17 Space Padded String with leading / trailing space (' '). 1

0 commit comments

Comments
 (0)