docs: def add_edge(self, name1, name2, **attr):
          '''
          API: add_edge(self, name1, name2, **attr)
          Description:
          Adds edge to the graph. Sets edge attributes using attr argument.
          Input:
              name1: Name of the source node (if directed).
              name2: Name of the sink node (if directed).
              attr: Edge attributes.
          Pre:
              Graph should not already contain this edge. We do not allow
              multiple edges with same source and sink nodes.
          Post:
              self.edge_attr is updated.
              self.neighbors, self.nodes and self.in_neighbors are updated if
              graph was missing at least one of the nodes.
          '''
          if (name1, name2) in self.edge_attr:
              raise MultipleEdgeException
          if self.graph_type is UNDIRECTED_GRAPH and (name2,name1) in self.edge_attr:
              raise MultipleEdgeException
          self.edge_attr[(name1,name2)] = copy.deepcopy(DEFAULT_EDGE_ATTRIBUTES)
          for a in attr:
              self.edge_attr[(name1,name2)][a] = attr[a]
          if name1 not in self.nodes:
              self.add_node(name1)
          if name2 not in self.nodes:
              self.add_node(name2)
          self.neighbors[name1].append(name2)
          if self.graph_type is UNDIRECTED_GRAPH:
              self.neighbors[name2].append(name1)
          else:
              self.in_neighbors[name2].append(name1)