Repository: matplotlib/matplotlib

Issue:
[Bug]: legend no longer works with itertools.chain
### Bug summary

Calling `plt.legend(...)` with arguments of type `<class 'itertools.chain'>` no longer works, and raises `TypeError: object of type 'itertools.chain' has no len()`. This used to work - some update to the source code seems to have caused this bug.

### Code for reproduction

```Python
import matplotlib.pyplot as plt
import numpy as np
import itertools

def flip(items, ncol):
    return itertools.chain(*[items[i::ncol] for i in range(ncol)])

x = np.arange(-2*np.pi, 2*np.pi, 0.1)
ax = plt.subplot(111)
ax.plot(x, np.sin(x), label='Sine')
ax.plot(x, np.cos(x), label='Cosine')
ax.plot(x, np.arctan(x), label='Inverse tan')

handles, labels = ax.get_legend_handles_labels()
plt.legend(flip(handles, 2), flip(labels, 2), loc=9, ncol=2)

plt.grid('on')
plt.show()
```

### Actual outcome

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\jonny\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\matplotlib\pyplot.py", line 3628, in legend
    return gca().legend(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\jonny\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\matplotlib\axes\_axes.py", line 337, in legend
    self.legend_ = mlegend.Legend(self, handles, labels, **kwargs)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\jonny\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\matplotlib\legend.py", line 462, in __init__
    if len(handles) < 2:
       ^^^^^^^^^^^^
TypeError: object of type 'itertools.chain' has no len()

### Expected outcome

For the legend to be correctly applied, as previously.

### Additional information

_No response_

### Operating system

_No response_

### Matplotlib Version

3.10.3

### Matplotlib Backend

tkagg

### Python version

_No response_

### Jupyter version

_No response_

### Installation

pip


Discussion:
We used to explicitly cast to list right before the length check, but that was removed in #24811 on the grounds it was then redundant.  However we have since removed the loop that created the list in #28874.

The original decision to cast to list goes [way back](https://github.com/matplotlib/matplotlib/commit/feaccf15973dad0f9ecb28d489fe02214eaca567#diff-e7a00bb4a353fa623c1d0c1de5c6ca602b7ab38e8906d4842640983133b9196a).
:+1: we should put the cast to list back in.



Patch:
diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py
index d01a8dca0847..2fb14e52c58c 100644
--- a/lib/matplotlib/legend.py
+++ b/lib/matplotlib/legend.py
@@ -459,6 +459,7 @@ def __init__(
             labels = [*reversed(labels)]
             handles = [*reversed(handles)]
 
+        handles = list(handles)
         if len(handles) < 2:
             ncols = 1
         self._ncols = ncols if ncols != 1 else ncol


Test patch:
diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py
index 9c708598e27c..d80c6b2ed92a 100644
--- a/lib/matplotlib/tests/test_legend.py
+++ b/lib/matplotlib/tests/test_legend.py
@@ -42,6 +42,18 @@ def test_legend_ordereddict():
               loc='center left', bbox_to_anchor=(1, .5))
 
 
+def test_legend_generator():
+    # smoketest that generator inputs work
+    fig, ax = plt.subplots()
+    ax.plot([0, 1])
+    ax.plot([0, 2])
+
+    handles = (line for line in ax.get_lines())
+    labels = (label for label in ['spam', 'eggs'])
+
+    ax.legend(handles, labels, loc='upper left')
+
+
 @image_comparison(['legend_auto1.png'], remove_text=True)
 def test_legend_auto1():
     """Test automatic legend placement"""