code: def _build_comparison_str(
      first: T_AX_BASE_OR_ATTR_DICT,
      second: T_AX_BASE_OR_ATTR_DICT,
      level: int = 0,
      values_in_suffix: str = "",
      skip_db_id_check: bool = False,
  ) -> str:
      """Recursively build a comparison string for classes that extend Ax `Base`
      or two dictionaries (dictionaries are passed in in the recursive case).
      Prints out an 'inequality report' that includes nested fields.
  
      NOTE: Allows recursion only up to level 4, with markers like '1)', then 'a)',
      then 'i)', then '*' for the nested lists.
  
      For example, for two experiments, the 'report' might look like this, if their
      search spaces are unequal because of difference of parameters in parameter
      constraints:
  
      Experiment(test_1) (type `Experiment`) != Experiment(test_2) (type `Experiment`).
  
      Fields with different values:
  
      1) _search_space: ... != ...
  
          Fields with different values in 1):
  
              a) _parameter_constraints: ... != ...
  
                  Fields with different values in a):
  
                      i) _parameter: ... != ...
  
                          Fields with different values in i):
  
                              * db_id: ... != ...
  
      NOTE: If ``skip_db_id_check`` is ``True``, will exclude the ``db_id`` attributes
      from the equality check. Useful for ensuring that all attributes of an object are
      equal except the ids, with which one or both of them are saved to the database
      (e.g. if confirming an object before it was saved, to the version reloaded
      from the DB).
      """
  
      def _unequal_str(
          first: Any, second: Any
      ) -> str:  # pyre-ignore[2]
          return f"{first} (type {type(first)}) != {second} (type {type(second)})."
  
      if first == second:
          return ""
  
      if level > COMPARISON_STR_MAX_LEVEL:
          # Don't go deeper than 4 levels as the inequality report will not be legible.
          return (
              f"\n... also there were unequal fields at levels {level}+; "
              "to see full comparison past this level, adjust `ax.utils.common.testutils."
              "COMPARISON_STR_MAX_LEVEL`"
          )
  
      msg = ""
      indent = " " * level * 4
      (
          unequal_types,
          unequal_val,
      ) = object_attribute_dicts_find_unequal_fields(
          one_dict=first.__dict__ if isinstance(first, Base) else first,
          other_dict=second.__dict__
          if isinstance(second, Base)
          else second,
          fast_return=False,
          skip_db_id_check=skip_db_id_check,
      )
      unequal_types_suffixed = {
          f"{k} (field had values of unequal type)": v
          for k, v in unequal_types.items()
      }
      if level == 0:
          msg += f"{_unequal_str(first=first, second=second)}\n"
  
      msg += (
          f"\n{indent}Fields with different values{values_in_suffix}:\n"
      )
      joint_unequal_field_dict = {
          **unequal_val,
          **unequal_types_suffixed,
      }
      for idx, (field, (first, second)) in enumerate(
          joint_unequal_field_dict.items()
      ):
          # For level 0, use numbers as bullets. For 1, use letters. For 2, use "i".
          # For 3+, use "*".
          bul = "*"
          if level == 0:
              bul = f"{idx + 1})"
          elif level == 1:
              bul = f"{chr(ord('a') + idx)})"
          elif level == 2:
              bul = f"{'i' * (idx + 1)})"
          elif level <= COMPARISON_STR_MAX_LEVEL:
              # Add default for when setting `COMPARISON_STR_MAX_LEVEL` to higher value
              # during debugging.
              bul = "*"
          else:
              raise RuntimeError(
                  "Reached level > `COMPARISON_STR_MAX_LEVEL`, which should've been "
                  "unreachable."
              )
          msg += f"\n{indent}{bul} {field}: {_unequal_str(first=first, second=second)}\n"
          if isinstance(first, (dict, Base)) and isinstance(
              second, (dict, Base)
          ):
              msg += _build_comparison_str(
                  first=first,
                  second=second,
                  level=level + 1,
                  values_in_suffix=f" in {bul}",
                  skip_db_id_check=skip_db_id_check,
              )
          elif isinstance(first, list) and isinstance(second, list):
              # To compare lists recursively via same function, making them into dicts
              # with index keys.
              msg += _build_comparison_str(
                  first=dict(
                      zip([str(x) for x in range(len(first))], first)
                  ),
                  second=dict(
                      zip([str(x) for x in range(len(second))], second)
                  ),
                  level=level + 1,
                  values_in_suffix=f" in {bul}",
                  skip_db_id_check=skip_db_id_check,
              )
      return msg
  
imports: import builtins
  import contextlib
  import io
  import linecache
  import logging
  import signal
  import sys
  import types
  import unittest
  from functools import wraps
  from logging import Logger
  from types import FrameType
  from typing import Any, Callable, ContextManager, Dict, Generator, List, Optional, Tuple, Type, TypeVar, Union
  from unittest.mock import MagicMock
  import yappi
  from ax.utils.common.base import Base
  from ax.utils.common.equality import object_attribute_dicts_find_unequal_fields
  from ax.utils.common.logger import get_logger
  from pyfakefs import fake_filesystem_unittest
label_desc: Use image node_id 3 for calling a global function with example usage: _build_comparison_str(first, second, level, values_in_suffix, skip_db_id_check) and returns: msg, first, str, type, str, second, str, type, str, str, str, level, str