"""Serialise objects to a stable representation."""
from __future__ import annotations
import hashlib
import json
import types
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any
def stable_hash(obj: Any) -> str:
"""Return a stable hash for a Python data structure.
We can't just use the md5 of str(obj) as the order of collections
may be random.
"""
if isinstance(obj, dict):
obj = sorted(map(stable_hash, obj.items()))
if isinstance(obj, (list, tuple, set, frozenset)):
obj = sorted(map(stable_hash, obj))
elif isinstance(obj, (type, types.FunctionType)):
# The default repr() of functions includes the ID, which is not ideal.
# We use the fully qualified name instead.
obj = f'{obj.__module__}.{obj.__qualname__}'
return hashlib.md5(str(obj).encode(), usedforsecurity=False).hexdigest()
def stable_str(obj: Any, *, indent: int | None = None) -> str:
"""Return a stable string representation of a Python data structure.
We can't just use ``str(obj)`` as the order of collections may be random.
"""
return json.dumps(_stable_str_prep(obj), indent=indent)
def _stable_str_prep(obj: Any) -> dict[str, Any] | list[Any] | str:
if isinstance(obj, dict):
# Convert to a sorted dict
lst: list[tuple[Any, Any]] = [
(_stable_str_prep(k), _stable_str_prep(v)) for k, v in obj.items()
]
lst.sort()
return dict(lst)
if isinstance(obj, (list, tuple, set, frozenset)):
# Convert to a sorted list
return sorted(map(_stable_str_prep, obj), key=str)
if isinstance(obj, (type, types.FunctionType)):
# The default repr() of functions includes the ID, which is not ideal.
# We use the fully qualified name instead.
return f'{obj.__module__}.{obj.__qualname__}'
# We can't do any better, just use the string representation
return str(obj)