def describe_path_as_analyzer_cache_hash(
    x: Optional[Union[tf.Operation, tf.Tensor, str]],
    parents: Optional[List[bytes]] = None) -> Optional[bytes]:
  """Constructs a hash to describe a unique TF graph path.

  Note: We do not rely on names for hashing since it can be fragile.

  Args:
    x: The current TF graph node.
    parents: Results of previous calls to this function, where x was an ancestor
      to the current node x.

  Returns:
    A bytes hash of the path from x to its sources. None if x is None.
  """
  # This may happen in cases where tensors are outputs of previous analyzers,
  # we don't need to describe a path for those.
  if x is None:
    assert parents is None
    return None
  parents = parents or []
  if any(p is None for p in parents):
    return None

  if isinstance(x, tf.Operation):
    values = _serialize_op_attr(x.node_def.attr)
  elif isinstance(x, tf.Tensor):
    # No need to add x.op to the hash since that should be included in parents.
    values = [tf.compat.as_str_any(x.value_index)]
  else:
    assert isinstance(x, (str, bytes))
    values = [x]

  h = hashlib.sha1()
  for value in values:
    encoded = tf.compat.as_bytes(value)
    h.update(encoded)

  for p in parents:
    h.update(p)

  return h.digest()
Constructs a hash to describe a unique TF graph path.

  Note: We do not rely on names for hashing since it can be fragile.

  Args:
    x: The current TF graph node.
    parents: Results of previous calls to this function, where x was an ancestor
      to the current node x.

  Returns:
    A bytes hash of the path from x to its sources. None if x is None.
  
tensorflow_transform/graph_tools.py