Repository: networkx/networkx
Pull request: Fix behavior for iterable `sources` argument in `bfs_layers`
This addresses the issue reported in #8009.
It makes sense to me to ensure the function works correctly for generators as well; especially since the fix is as simple as this.
I also thought it was more reasonable to gracefully handle duplicate nodes in the `sources` argument; at every depth other than 0, nodes never appear multiple times (because of the `visited` set), and I see no convincing reason to have the sources be handled any differently.

List of changes:
* Avoid silent failure when `sources` is a generator
* Only handle duplicate nodes once
* Add test cases where `sources` is a generator and list with duplicate elements
* Minor clarifications in docstring

Fixes gh-8009.

Problem statement:
`bfs_layers` revisits source nodes in `sources` if `sources` an generator
<!-- 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

When `sources` is a generator, `nx.bfs_layers` will visit the source node, and the source node ends up in two different layers.

<!--- Tell us what happens instead of the expected behavior -->

### Expected Behavior

Either disallows `sources` being a generator or source nodes do not repeat in two different layers when `sources` is a generator.

<!--- Tell us what should happen -->

### Steps to Reproduce

```
import networkx as nx

G = nx.from_edgelist([(1, 2), (1, 3), (2, 3)])
dict(enumerate(nx.bfs_layers(G, (1 for i in range(1)))))
```

<!--- Provide a minimal example that reproduces the bug -->

### Environment

<!--- Please provide details about your local environment -->

Python version: 3.12.2
NetworkX version: 3.1

### Additional context

<!--- Add any other context about the problem here, screenshots, etc. -->

The documentation for `nx.bfs_layers` does not state that `sources` can be a generator, but silently failing makes it very difficult to debug when this occurs.

Hints from issue discussion:

Commit message: Fix behavior for iterable `sources` argument in `bfs_layers`

* Avoid silent failure when `sources` is a generator
* Only handle duplicate nodes once
* Add test cases where `sources` is a generator and list with duplicate elements
* Minor clarifications in docstring

Fixes gh-8009

Patch to review:
diff --git a/networkx/algorithms/traversal/breadth_first_search.py b/networkx/algorithms/traversal/breadth_first_search.py
index 899dc92b723..d4ea397b506 100644
--- a/networkx/algorithms/traversal/breadth_first_search.py
+++ b/networkx/algorithms/traversal/breadth_first_search.py
@@ -411,13 +411,14 @@ def bfs_layers(G, sources):
     G : NetworkX graph
         A graph over which to find the layers using breadth-first search.
 
-    sources : node in `G` or list of nodes in `G`
-        Specify starting nodes for single source or multiple sources breadth-first search
+    sources : node in `G` or iterable of nodes in `G`
+        Specify starting nodes for single source or multiple sources
+        breadth-first search.
 
     Yields
     ------
-    layer: list of nodes
-        Yields list of nodes at the same distance from sources
+    layers: list of nodes
+        Yields list of nodes at the same distance from `sources`.
 
     Examples
     --------
@@ -434,8 +435,8 @@ def bfs_layers(G, sources):
     if sources in G:
         sources = [sources]
 
-    current_layer = list(sources)
     visited = set(sources)
+    current_layer = list(visited)
 
     for source in current_layer:
         if source not in G:
diff --git a/networkx/algorithms/traversal/tests/test_bfs.py b/networkx/algorithms/traversal/tests/test_bfs.py
index fcfbbc68dc1..7f4f37a31c4 100644
--- a/networkx/algorithms/traversal/tests/test_bfs.py
+++ b/networkx/algorithms/traversal/tests/test_bfs.py
@@ -58,8 +58,8 @@ def test_bfs_layers(self):
             2: [2, 3],
             3: [4],
         }
-        assert dict(enumerate(nx.bfs_layers(self.G, sources=[0]))) == expected
-        assert dict(enumerate(nx.bfs_layers(self.G, sources=0))) == expected
+        for sources in [0, [0], (i for i in [0]), [0, 0]]:
+            assert dict(enumerate(nx.bfs_layers(self.G, sources))) == expected
 
     def test_bfs_layers_missing_source(self):
         with pytest.raises(nx.NetworkXError):


Code review comments:
diff_hunk: @@ -411,13 +411,14 @@ def bfs_layers(G, sources):
     G : NetworkX graph
         A graph over which to find the layers using breadth-first search.
 
-    sources : node in `G` or list of nodes in `G`
-        Specify starting nodes for single source or multiple sources breadth-first search
+    sources : node in `G` or iterable of nodes in `G`
+        Specify starting nodes for single source or multiple sources
+        breadth-first search.
 
     Yields
     ------
-    layer: list of nodes
-        Yields list of nodes at the same distance from sources
+    layers: list of nodes
path: networkx/algorithms/traversal/breadth_first_search.py
text: @user1:
```suggestion
    layer : list of nodes
```
The name and type of values in "Yields" section should refer to a _single_ item that is yielded: https://numpydoc.readthedocs.io/en/latest/format.html#yields

@author:
Makes sense. Thanks!