Repository: networkx/networkx

Issue:
Oddity with networkx.eulerian_path()
This function does not seem to be operating well on a particular example. This function has no restriction on directed or multigraphs, and as far as I know, the graph does not have to be strongly connected because an Eulerian path does not have to have start point and end points equal (as opposed to circuits). Here is an ASCII representation of my network:

```

+-<-+   +-<-+
1   2->-3   4
+->-+   +->-+

```

Inspecting manually, there is an Eulerian path 2->1->2->3->4->3. The output I get from `eulerian_path()` varies oddly depending on the `source` argument.

```
import networkx
print("nx version", networkx.__version__)
# +-<-+   +-<-+
# 1   2->-3   4
# +->-+   +->-+
G = networkx.MultiDiGraph()
G.add_edges_from([
  (1, 2), (2, 1), (2, 3), (3, 4), (4, 3)
])
print("Eulerian path, no source argument", list(networkx.eulerian_path(G)))
print("Eulerian path, source 2", list(networkx.eulerian_path(G, source = 2)))
print("Eulerian path, source 4", list(networkx.eulerian_path(G, source = 4)))
```

Output:

```
nx version 2.4
Eulerian path, no source argument [(2, 1), (1, 2), (2, 3), (3, 4), (4, 3)]
Eulerian path, source 2 [(2, 1), (1, 2)]
Eulerian path, source 4 [(2, 1), (1, 2), (2, 4), (4, 3), (3, 4)]
```

With no source argument, it outputs the expected solution.

With the source argument equal to the source node that was previously found, it does not find a path that covers all arcs.

With a source argument that is an invalid start point for an Eulerian path, it returns a path starting from a different node, contrary to the documentation saying a warning/error will be issued. Edit: moreover, this returned path contains an arc which doesn't exist in the input graph.


Discussion:
I can see several issues in the code and unit tests: 

1) The previous author finds a Euler path/circuit in a reversed directed graph in order to avoid reversing the final Euler path output below:
https://github.com/networkx/networkx/blob/04e1ef2598fa494fc4b0889ef69a71b83ff32493/networkx/algorithms/euler.py#L279-L282 

This works fine if the source is not provided as seen by the first example. In case a source is provided, the source is actually the end of the Euler path in the reversed graph and therefore the code fails in the second example.

2) There are no checks-in "_simplegraph_eulerian_circuit" and "_multigraph_eulerian_circuit" functions whether a Euler path exists at the provided source and therefore the third example fails.

3) Euler path is not properly tested in unit tests :
https://github.com/networkx/networkx/blob/04e1ef2598fa494fc4b0889ef69a71b83ff32493/networkx/algorithms/tests/test_euler.py#L150-L155

@dschult May I take this as a bug fix?
I haven't looked into this much yet, but I can verify that the example shown does produce the stated (incorrect) output.  I've put the label "Defect" on this issue. @manasjoshi14 if you can find a/the bug and can fix it that would be great. Does the "source" keyword even make sense with this algorithm? Maybe that needs to be a separate function?

Resolving patch:
diff --git a/networkx/algorithms/euler.py b/networkx/algorithms/euler.py
--- a/networkx/algorithms/euler.py
+++ b/networkx/algorithms/euler.py
@@ -213,7 +213,7 @@ def eulerian_circuit(G, source=None, keys=False):
         yield from _simplegraph_eulerian_circuit(G, source)
 
 
-def has_eulerian_path(G):
+def has_eulerian_path(G, source=None):
     """Return True iff `G` has an Eulerian path.
 
     An Eulerian path is a path in a graph which uses each edge of a graph
@@ -236,6 +236,9 @@ def has_eulerian_path(G):
     G : NetworkX Graph
         The graph to find an euler path in.
 
+    source : node, optional
+        Starting node for circuit.
+
     Returns
     -------
     Bool : True if G has an eulerian path.
@@ -245,15 +248,21 @@ def has_eulerian_path(G):
     is_eulerian
     eulerian_path
     """
+    if nx.is_eulerian(G):
+        return True
+
     if G.is_directed():
         # Remove isolated nodes (if any) without altering the input graph
         nodes_remove = [v for v in G if G.in_degree[v] == 0 and G.out_degree[v] == 0]
         if nodes_remove:
             G = G.copy()
             G.remove_nodes_from(nodes_remove)
-
         ins = G.in_degree
         outs = G.out_degree
+        # Since we know it is not eulerian, outs - ins must be 1 for source
+        if source is not None and outs[source] - ins[source] != 1:
+            return False
+
         unbalanced_ins = 0
         unbalanced_outs = 0
         for v in G:
@@ -267,9 +276,13 @@ def has_eulerian_path(G):
         return (
             unbalanced_ins <= 1 and unbalanced_outs <= 1 and nx.is_weakly_connected(G)
         )
-
     else:
-        return sum(d % 2 == 1 for v, d in G.degree()) in (0, 2) and nx.is_connected(G)
+        # We know it is not eulerian, so degree of source must be odd.
+        if source is not None and G.degree[source] % 2 != 1:
+            return False
+
+        # Sum is 2 since we know it is not eulerian (which implies sum is 0)
+        return sum(d % 2 == 1 for v, d in G.degree()) == 2 and nx.is_connected(G)
 
 
 def eulerian_path(G, source=None, keys=False):
@@ -293,22 +306,37 @@ def eulerian_path(G, source=None, keys=False):
     Warning: If `source` provided is not the start node of an Euler path
     will raise error even if an Euler Path exists.
     """
-    if not has_eulerian_path(G):
+    if not has_eulerian_path(G, source):
         raise nx.NetworkXError("Graph has no Eulerian paths.")
     if G.is_directed():
         G = G.reverse()
+        if source is None or nx.is_eulerian(G) is False:
+            source = _find_path_start(G)
+        if G.is_multigraph():
+            for u, v, k in _multigraph_eulerian_circuit(G, source):
+                if keys:
+                    yield u, v, k
+                else:
+                    yield u, v
+        else:
+            yield from _simplegraph_eulerian_circuit(G, source)
     else:
         G = G.copy()
-    if source is None:
-        source = _find_path_start(G)
-    if G.is_multigraph():
-        for u, v, k in _multigraph_eulerian_circuit(G, source):
+        if source is None:
+            source = _find_path_start(G)
+        if G.is_multigraph():
             if keys:
-                yield u, v, k
+                yield from reversed(
+                    [(v, u, k) for u, v, k in _multigraph_eulerian_circuit(G, source)]
+                )
             else:
-                yield u, v
-    else:
-        yield from _simplegraph_eulerian_circuit(G, source)
+                yield from reversed(
+                    [(v, u) for u, v, k in _multigraph_eulerian_circuit(G, source)]
+                )
+        else:
+            yield from reversed(
+                [(v, u) for u, v in _simplegraph_eulerian_circuit(G, source)]
+            )
 
 
 @not_implemented_for("directed")


Test patch:
diff --git a/networkx/algorithms/tests/test_euler.py b/networkx/algorithms/tests/test_euler.py
--- a/networkx/algorithms/tests/test_euler.py
+++ b/networkx/algorithms/tests/test_euler.py
@@ -150,6 +150,22 @@ def test_has_eulerian_path_isolated_node(self):
         G.add_node(3)
         assert nx.has_eulerian_path(G)
 
+    def test_has_eulerian_path_not_weakly_connected(self):
+        G = nx.DiGraph()
+        H = nx.Graph()
+        G.add_edges_from([(0, 1), (2, 3), (3, 2)])
+        H.add_edges_from([(0, 1), (2, 3), (3, 2)])
+        assert not nx.has_eulerian_path(G)
+        assert not nx.has_eulerian_path(H)
+
+    def test_has_eulerian_path_unbalancedins_more_than_one(self):
+        G = nx.DiGraph()
+        H = nx.Graph()
+        G.add_edges_from([(0, 1), (2, 3)])
+        H.add_edges_from([(0, 1), (2, 3)])
+        assert not nx.has_eulerian_path(G)
+        assert not nx.has_eulerian_path(H)
+
 
 class TestFindPathStart:
     def testfind_path_start(self):
@@ -171,6 +187,65 @@ def test_eulerian_path(self):
         for e1, e2 in zip(x, nx.eulerian_path(nx.DiGraph(x))):
             assert e1 == e2
 
+    def test_eulerian_path_straight_link(self):
+        G = nx.DiGraph()
+        result = [(1, 2), (2, 3), (3, 4), (4, 5)]
+        G.add_edges_from(result)
+        assert result == list(nx.eulerian_path(G))
+        assert result == list(nx.eulerian_path(G, source=1))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=3))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=4))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=5))
+
+    def test_eulerian_path_multigraph(self):
+        G = nx.MultiDiGraph()
+        result = [(2, 1), (1, 2), (2, 1), (1, 2), (2, 3), (3, 4), (4, 3)]
+        G.add_edges_from(result)
+        assert result == list(nx.eulerian_path(G))
+        assert result == list(nx.eulerian_path(G, source=2))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=3))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=4))
+
+    def test_eulerian_path_eulerian_circuit(self):
+        G = nx.DiGraph()
+        result = [(1, 2), (2, 3), (3, 4), (4, 1)]
+        result2 = [(2, 3), (3, 4), (4, 1), (1, 2)]
+        result3 = [(3, 4), (4, 1), (1, 2), (2, 3)]
+        G.add_edges_from(result)
+        assert result == list(nx.eulerian_path(G))
+        assert result == list(nx.eulerian_path(G, source=1))
+        assert result2 == list(nx.eulerian_path(G, source=2))
+        assert result3 == list(nx.eulerian_path(G, source=3))
+
+    def test_eulerian_path_undirected(self):
+        G = nx.Graph()
+        result = [(1, 2), (2, 3), (3, 4), (4, 5)]
+        result2 = [(5, 4), (4, 3), (3, 2), (2, 1)]
+        G.add_edges_from(result)
+        assert list(nx.eulerian_path(G)) in (result, result2)
+        assert result == list(nx.eulerian_path(G, source=1))
+        assert result2 == list(nx.eulerian_path(G, source=5))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=3))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=2))
+
+    def test_eulerian_path_multigraph_undirected(self):
+        G = nx.MultiGraph()
+        result = [(2, 1), (1, 2), (2, 1), (1, 2), (2, 3), (3, 4)]
+        G.add_edges_from(result)
+        assert result == list(nx.eulerian_path(G))
+        assert result == list(nx.eulerian_path(G, source=2))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=3))
+        with pytest.raises(nx.NetworkXError):
+            list(nx.eulerian_path(G, source=1))
+
 
 class TestEulerize:
     def test_disconnected(self):