# Released under the MIT License. See LICENSE for details. # """Misc utility functionality related to the entity system.""" from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any, Union, Tuple, List from efro.entity._value import CompoundValue from efro.entity._support import BoundCompoundValue def diff_compound_values( obj1: Union[BoundCompoundValue, CompoundValue], obj2: Union[BoundCompoundValue, CompoundValue]) -> str: """Generate a string showing differences between two compound values. Both must be associated with data and have the same set of fields. """ # Ensure fields match and both are attached to data... value1, data1 = get_compound_value_and_data(obj1) if data1 is None: raise ValueError(f'Invalid unbound compound value: {obj1}') value2, data2 = get_compound_value_and_data(obj2) if data2 is None: raise ValueError(f'Invalid unbound compound value: {obj2}') if not have_matching_fields(value1, value2): raise ValueError( f"Can't diff objs with non-matching fields: {value1} and {value2}") # Ok; let 'er rip... diff = _diff(obj1, obj2, 2) return ' ' if diff == '' else diff class CompoundValueDiff: """Wraps diff_compound_values() in an object for efficiency. It is preferable to pass this to logging calls instead of the final diff string since the diff will never be generated if the associated logging level is not being emitted. """ def __init__(self, obj1: Union[BoundCompoundValue, CompoundValue], obj2: Union[BoundCompoundValue, CompoundValue]): self._obj1 = obj1 self._obj2 = obj2 def __repr__(self) -> str: return diff_compound_values(self._obj1, self._obj2) def _diff(obj1: Union[BoundCompoundValue, CompoundValue], obj2: Union[BoundCompoundValue, CompoundValue], indent: int) -> str: from efro.entity._support import BoundCompoundValue bits: List[str] = [] indentstr = ' ' * indent vobj1, _data1 = get_compound_value_and_data(obj1) fields = sorted(vobj1.get_fields().keys()) for field in fields: val1 = getattr(obj1, field) val2 = getattr(obj2, field) # for nested compounds, dive in and do nice piecewise compares if isinstance(val1, BoundCompoundValue): assert isinstance(val2, BoundCompoundValue) diff = _diff(val1, val2, indent + 2) if diff != '': bits.append(f'{indentstr}{field}:') bits.append(diff) # for all else just do a single line # (perhaps we could improve on this for other complex types) else: if val1 != val2: bits.append(f'{indentstr}{field}: {val1} -> {val2}') return '\n'.join(bits) def have_matching_fields(val1: CompoundValue, val2: CompoundValue) -> bool: """Return whether two compound-values have matching sets of fields. Note this just refers to the field configuration; not data. """ # Quick-out: matching types will always have identical fields. if type(val1) is type(val2): return True # Otherwise do a full comparison. return val1.get_fields() == val2.get_fields() def get_compound_value_and_data( obj: Union[BoundCompoundValue, CompoundValue]) -> Tuple[CompoundValue, Any]: """Return value and data for bound or unbound compound values.""" # pylint: disable=cyclic-import from efro.entity._support import BoundCompoundValue from efro.entity._value import CompoundValue if isinstance(obj, BoundCompoundValue): value = obj.d_value data = obj.d_data elif isinstance(obj, CompoundValue): value = obj data = getattr(obj, 'd_data', None) # may not exist else: raise TypeError( f'Expected a BoundCompoundValue or CompoundValue; got {type(obj)}') return value, data def compound_eq(obj1: Union[BoundCompoundValue, CompoundValue], obj2: Union[BoundCompoundValue, CompoundValue]) -> Any: """Compare two compound value/bound-value objects for equality.""" # Criteria for comparison: both need to be a compound value # and both must have data (which implies they are either a entity # or bound to a subfield in an entity). value1, data1 = get_compound_value_and_data(obj1) if data1 is None: return NotImplemented value2, data2 = get_compound_value_and_data(obj2) if data2 is None: return NotImplemented # Ok we can compare them. To consider them equal we look for # matching sets of fields and matching data. Note that there # could be unbound data causing inequality despite their field # values all matching; not sure if that's what we want. return have_matching_fields(value1, value2) and data1 == data2