Repository: networkx/networkx

Issue:
Docstring of `johnson` algorithm handling of weight incorrect
<!-- If you have a general question about NetworkX, please use the discussions tab to create a new discussion -->

<!--- Provide a general summary of the issue in the Title above -->

### Current Behavior

The documentation says:
```
If no such edge attribute exists, the weight of the edge is assumed to be one.
```
### Expected Behavior

The behavior to match the docstring or the docstring to match the behavior.

### Steps to Reproduce

```python
In [1]: import networkx as nx

In [2]: G = nx.Graph()

In [3]: G.add_edge(0, 1, weight=2)

In [4]: G.add_edge(0, 2)

In [5]: nx.johnson(G)
---------------------------------------------------------------------------
NetworkXError                             Traceback (most recent call last)
Cell In[5], line 1
----> 1 nx.johnson(G)

File ~/miniconda3/envs/gb8/lib/python3.11/site-packages/networkx/algorithms/shortest_paths/weighted.py:2469, in johnson(G, weight)
   2402 r"""Uses Johnson's Algorithm to compute shortest paths.
   2403
   2404 Johnson's Algorithm finds a shortest path between each pair of
   (...)
   2466
   2467 """
   2468 if not nx.is_weighted(G, weight=weight):
-> 2469     raise nx.NetworkXError("Graph is not weighted.")
   2471 dist = {v: 0 for v in G}
   2472 pred = {v: [] for v in G}

NetworkXError: Graph is not weighted.
```

### Environment

<!--- Please provide details about your local environment -->

Python version: 3.11
NetworkX version: 3.1

### Additional context

It uses `nx.is_weighted`, which requires that _all_ edges have the attribute to be considered weighted.


Discussion:
The parameters part of the docstring may have been copied from the rest of the functions (Dijkstra and Bellman-ford) in the file. These methods assume the weight of the edge to be 1 when not specified.

It makes sense to have similar behavior for the Johnson method as well. We'll only need to remove the `nx.is_weighted` condition and modify/remove the test cases for unweighted and single-node graphs.

No other changes will be required since the Johnson method uses `_bellman_ford` and `_dijkstra` methods that already handle the unweighted edges.

If this approach seems alright, I'll create a PR.
Yes -- that looks like a correct description of what has happened with the documentation of this function. And I agree that it seems straightforward to make it handle edges without an edge attribute.

It'd be nice to include tests of input graphs without edge attributes for some edges and show the function working and maybe even check that results match a case with weight specified as 1... ???  

Resolving patch:
diff --git a/networkx/algorithms/shortest_paths/weighted.py b/networkx/algorithms/shortest_paths/weighted.py
--- a/networkx/algorithms/shortest_paths/weighted.py
+++ b/networkx/algorithms/shortest_paths/weighted.py
@@ -2426,11 +2426,6 @@ def johnson(G, weight="weight"):
     distance : dictionary
         Dictionary, keyed by source and target, of shortest paths.
 
-    Raises
-    ------
-    NetworkXError
-        If given graph is not weighted.
-
     Examples
     --------
     >>> graph = nx.DiGraph()
@@ -2465,9 +2460,6 @@ def johnson(G, weight="weight"):
     all_pairs_bellman_ford_path_length
 
     """
-    if not nx.is_weighted(G, weight=weight):
-        raise nx.NetworkXError("Graph is not weighted.")
-
     dist = {v: 0 for v in G}
     pred = {v: [] for v in G}
     weight = _weight_function(G, weight)


Test patch:
diff --git a/networkx/algorithms/shortest_paths/tests/test_weighted.py b/networkx/algorithms/shortest_paths/tests/test_weighted.py
--- a/networkx/algorithms/shortest_paths/tests/test_weighted.py
+++ b/networkx/algorithms/shortest_paths/tests/test_weighted.py
@@ -865,10 +865,9 @@ def test_zero_cycle_smoke(self):
 
 class TestJohnsonAlgorithm(WeightedTestBase):
     def test_single_node_graph(self):
-        with pytest.raises(nx.NetworkXError):
-            G = nx.DiGraph()
-            G.add_node(0)
-            nx.johnson(G)
+        G = nx.DiGraph()
+        G.add_node(0)
+        assert nx.johnson(G) == {0: {0: [0]}}
 
     def test_negative_cycle(self):
         G = nx.DiGraph()
@@ -915,9 +914,27 @@ def test_negative_weights(self):
         }
 
     def test_unweighted_graph(self):
-        with pytest.raises(nx.NetworkXError):
-            G = nx.path_graph(5)
-            nx.johnson(G)
+        G = nx.Graph()
+        G.add_edges_from([(1, 0), (2, 1)])
+        H = G.copy()
+        nx.set_edge_attributes(H, values=1, name="weight")
+        assert nx.johnson(G) == nx.johnson(H)
+
+    def test_partially_weighted_graph_with_negative_edges(self):
+        G = nx.DiGraph()
+        G.add_edges_from([(0, 1), (1, 2), (2, 0), (1, 0)])
+        G[1][0]["weight"] = -2
+        G[0][1]["weight"] = 3
+        G[1][2]["weight"] = -4
+
+        H = G.copy()
+        H[2][0]["weight"] = 1
+
+        I = G.copy()
+        I[2][0]["weight"] = 8
+
+        assert nx.johnson(G) == nx.johnson(H)
+        assert nx.johnson(G) != nx.johnson(I)
 
     def test_graphs(self):
         validate_path(self.XG, "s", "v", 9, nx.johnson(self.XG)["s"]["v"])