docs: def add_hyperedge(self, nodes, attr_dict=None, **attr):
          """Adds a hyperedge to the hypergraph, along with any related
              attributes of the hyperedge.
              This method will automatically add any node from the node set
              that was not in the hypergraph.
              A hyperedge without a "weight" attribute specified will be
              assigned the default value of 1.
  
          :param nodes: iterable container of references to nodes in the
                      hyperedge to be added.
          :param attr_dict: dictionary of attributes of the hyperedge being
                          added.
          :param attr: keyword arguments of attributes of the hyperedge;
                      attr's values will override attr_dict's values
                      if both are provided.
          :returns: str -- the ID of the hyperedge that was added.
          :raises: ValueError -- nodes arguments cannot be empty.
  
          Examples:
          ::
  
              >>> H = UndirectedHypergraph()
              >>> x = H.add_hyperedge(["A", "B", "C"])
              >>> y = H.add_hyperedge(("A", "D"), weight=2)
              >>> z = H.add_hyperedge(set(["B", "D"]), {color: "red"})
  
          """
          attr_dict = self._combine_attribute_arguments(attr_dict, attr)
  
          # Don't allow empty node set (invalid hyperedge)
          if not nodes:
              raise ValueError("nodes argument cannot be empty.")
  
          # Use frozensets for node sets to allow for hashable keys
          frozen_nodes = frozenset(nodes)
  
          is_new_hyperedge = not self.has_hyperedge(frozen_nodes)
          if is_new_hyperedge:
              # Add nodes to graph (if not already present)
              self.add_nodes(frozen_nodes)
  
              # Create new hyperedge name to use as reference for that hyperedge
              hyperedge_id = self._assign_next_hyperedge_id()
  
              # For each node in the node set, add hyperedge to the node's star
              for node in frozen_nodes:
                  self._star[node].add(hyperedge_id)
  
              # Add the hyperedge ID as the hyperedge that the node set composes
              self._node_set_to_hyperedge[frozen_nodes] = hyperedge_id
  
              # Assign some special attributes to this hyperedge. We assign
              # a default weight of 1 to the hyperedge. We also store the
              # original node set in order to return them exactly as the
              # user passed them into add_hyperedge.
              self._hyperedge_attributes[hyperedge_id] = \
                  {"nodes": nodes, "__frozen_nodes": frozen_nodes, "weight": 1}
          else:
              # If its not a new hyperedge, just get its ID to update attributes
              hyperedge_id = self._node_set_to_hyperedge[frozen_nodes]
  
          # Set attributes and return hyperedge ID
          self._hyperedge_attributes[hyperedge_id].update(attr_dict)
          return hyperedge_id