Skip to content

Commit d91ef7e

Browse files
authored
Update handling for Pydantic objects in mf_pformat (#1666)
This PR updates the handling for Pydantic objects in `mf_pformat` to check if the `dict()` methods exists instead of the `isinstance(BaseModel)` check. The `BaseModel` is actually from the `dsi_shim` and in some cases with different Pydantic versions, it's possible that the check does not pass. Since it's not foolproof, the formatter falls back to `pprint` if there is an error.
1 parent 05f5566 commit d91ef7e

File tree

1 file changed

+23
-19
lines changed

1 file changed

+23
-19
lines changed

metricflow-semantics/metricflow_semantics/mf_logging/pretty_print.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22

33
import logging
44
import pprint
5-
from collections.abc import Mapping
65
from dataclasses import fields, is_dataclass
76
from enum import Enum
8-
from typing import Any, List, Optional, Sized, Tuple, Union
9-
10-
from dsi_pydantic_shim import BaseModel
7+
from typing import Any, List, Mapping, Optional, Sized, Tuple, Union
118

129
from metricflow_semantics.mf_logging.formatting import indent
1310
from metricflow_semantics.mf_logging.pretty_formattable import MetricFlowPrettyFormattable
@@ -35,11 +32,6 @@ def __init__(
3532
self._include_none_object_fields = include_none_object_fields
3633
self._include_empty_object_fields = include_empty_object_fields
3734

38-
@staticmethod
39-
def _is_pydantic_base_model(obj: Any): # type:ignore
40-
# Checking the attribute as the BaseModel check fails for newer version of Pydantic.
41-
return isinstance(obj, BaseModel) or hasattr(obj, "__config__")
42-
4335
def _handle_sequence_obj(
4436
self, list_like_obj: Union[list, tuple, set, frozenset], remaining_line_width: Optional[int]
4537
) -> str:
@@ -353,16 +345,28 @@ def _handle_any_obj(self, obj: Any, remaining_line_width: Optional[int]) -> str:
353345
remaining_line_width=remaining_line_width,
354346
)
355347

356-
if MetricFlowPrettyFormatter._is_pydantic_base_model(obj):
357-
mapping = {key: getattr(obj, key) for key in obj.dict().keys()}
358-
return self._handle_mapping_like_obj(
359-
mapping,
360-
left_enclose_str=type(obj).__name__ + "(",
361-
key_value_seperator="=",
362-
right_enclose_str=")",
363-
is_dataclass_like_object=True,
364-
remaining_line_width=remaining_line_width,
365-
)
348+
# For Pydantic-like objects with a `dict`-like method that returns field keys / values.
349+
# In Pydantic v1, it's `.dict()`, in v2 it's `.model_dump()`.
350+
# Going with this approach for now to check for a Pydantic model as using `isinstance()` requires more
351+
# consideration when dealing with Pydantic v1 and Pydantic v2 objects.
352+
pydantic_dict_method = getattr(obj, "model_dump", None) or getattr(obj, "dict", None)
353+
if pydantic_dict_method is not None and callable(pydantic_dict_method):
354+
try:
355+
# Calling `dict` on a Pydantic model does not recursively convert nested fields into dictionaries,
356+
# which is what we want. `.model_dump()` does the recursive conversion.
357+
mapping = dict(obj)
358+
return self._handle_mapping_like_obj(
359+
mapping,
360+
left_enclose_str=type(obj).__name__ + "(",
361+
key_value_seperator="=",
362+
right_enclose_str=")",
363+
is_dataclass_like_object=True,
364+
remaining_line_width=remaining_line_width,
365+
)
366+
except Exception:
367+
# Fall back to built-in pretty-print in case the dict method can't be called. e.g. requires arguments.
368+
# Consider logging a warning.
369+
pass
366370

367371
# Any other object that's not handled.
368372
return pprint.pformat(obj, width=self._max_line_width, sort_dicts=False)

0 commit comments

Comments
 (0)