Repository: sunpy/sunpy

Issue:
Map factory does not support `pathlib.Path.glob` 
### Describe the feature

`pathlib.Path.glob` produces a generator for the list of files that match the glob pattern in a particularly directory. However, this not a supported pattern for the map factory. This was surprising to me since we support `pathlib.Path` objects as well as more traditional glob patterns as well as lists.

As an example, I would expect the following to work,

```python
import pathlib
import sunpy.map
sunpy.map.Map(pathlib.Path.home().glob('*.fits'))
```
but instead it throws the following exception
```python-traceback
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In [16], line 1
----> 1 sunpy.map.Map(pathlib.Path.home().glob('*.fits'))

File ~/Documents/codes/sunpy/sunpy/map/map_factory.py:254, in MapFactory.__call__(self, composite, sequence, silence_errors, *args, **kwargs)
    228 def __call__(self, *args, composite=False, sequence=False, silence_errors=False, **kwargs):
    229     """ Method for running the factory. Takes arbitrary arguments and
    230     keyword arguments and passes them to a sequence of pre-registered types
    231     to determine which is the correct Map-type to build.
   (...)
    252     as `memmap` for FITS files.
    253     """
--> 254     data_header_pairs = self._parse_args(*args, silence_errors=silence_errors, **kwargs)
    255     new_maps = list()
    257     # Loop over each registered type and check to see if WidgetType
    258     # matches the arguments.  If it does, use that type.

File ~/Documents/codes/sunpy/sunpy/map/map_factory.py:180, in MapFactory._parse_args(self, silence_errors, *args, **kwargs)
    178 for arg in args:
    179     try:
--> 180         data_header_pairs += self._parse_arg(arg, **kwargs)
    181     except NoMapsInFileError as e:
    182         if not silence_errors:

File ~/Documents/codes/sunpy/sunpy/util/functools.py:18, in seconddispatch.<locals>.wrapper(*args, **kwargs)
     17 def wrapper(*args, **kwargs):
---> 18     return dispatcher.dispatch(args[1].__class__)(*args, **kwargs)

File ~/Documents/codes/sunpy/sunpy/map/map_factory.py:195, in MapFactory._parse_arg(self, arg, **kwargs)
    189 @seconddispatch
    190 def _parse_arg(self, arg, **kwargs):
    191     """
    192     Take a factory input and parse into (data, header) pairs.
    193     Must return a list, even if only one pair is returned.
    194     """
--> 195     raise ValueError(f"Invalid input: {arg}")

ValueError: Invalid input: <generator object Path.glob at 0x104dc71b0>
```

### Proposed solution

Add this as a possible input to the map factory.


Discussion:
Rather than fixing this by adding another class to the factory dispatch, we could alternatively fix this if `sunpy.util.expand_list()` (which is called by the Map factory) automatically converted any generator to a list.  That is, change this line:

https://github.com/sunpy/sunpy/blob/dd2aa897916a7111d18f55bf229ca7abc9c07b1b/sunpy/util/util.py#L100

to be something like:
```python
from collections.abc import Iterable
...
        if not isinstance(item, str) and isinstance(item, Iterable):
```

Resolving patch:
diff --git a/sunpy/timeseries/timeseries_factory.py b/sunpy/timeseries/timeseries_factory.py
--- a/sunpy/timeseries/timeseries_factory.py
+++ b/sunpy/timeseries/timeseries_factory.py
@@ -343,7 +343,7 @@ def _parse_arg(self, arg, **kwargs):
         """
         Parse a single arg and return a list of timeseries.
         """
-        raise NoMatchError("File not found or invalid input")
+        raise NoMatchError(f"File not found or invalid input with {arg} and {kwargs}")
 
     @_parse_arg.register(GenericTimeSeries)
     def _parse_ts(self, ts, **kwargs):
diff --git a/sunpy/util/functools.py b/sunpy/util/functools.py
--- a/sunpy/util/functools.py
+++ b/sunpy/util/functools.py
@@ -11,7 +11,6 @@ def seconddispatch(func):
     """
     A variant of `functools.singledispatch` which dispatches on type of the second argument.
     """
-
     dispatcher = functools.singledispatch(func)
 
     def wrapper(*args, **kwargs):
diff --git a/sunpy/util/util.py b/sunpy/util/util.py
--- a/sunpy/util/util.py
+++ b/sunpy/util/util.py
@@ -10,6 +10,7 @@
 from shutil import get_terminal_size
 from itertools import chain, count
 from collections import UserList
+from collections.abc import Iterator
 
 __all__ = ['unique', 'replacement_filename', 'expand_list',
            'expand_list_generator', 'dict_keys_same', 'hash_file', 'get_width',
@@ -92,12 +93,13 @@ def expand_list(inp):
     ----------
     * https://stackoverflow.com/questions/2185822/expanding-elements-in-a-list/2185971#2185971
     """
-    return [item for item in expand_list_generator(inp)]
+    return list(expand_list_generator(inp))
 
 
 def expand_list_generator(inp):
     for item in inp:
-        if isinstance(item, tuple | list | UserList):
+        # parfive.Results are UserList
+        if isinstance(item, (list | tuple | UserList | Iterator)):
             yield from expand_list_generator(item)
         else:
             yield item


Test patch:
diff --git a/sunpy/map/tests/test_map_factory.py b/sunpy/map/tests/test_map_factory.py
--- a/sunpy/map/tests/test_map_factory.py
+++ b/sunpy/map/tests/test_map_factory.py
@@ -83,15 +83,22 @@ def test_patterns(eit_fits_directory):
     maps = sunpy.map.Map(os.fspath(eit_fits_directory))
     assert isinstance(maps, list)
     assert ([isinstance(amap, sunpy.map.GenericMap) for amap in maps])
+
     # Test that returned maps are sorted
     files_sorted = sorted(list(eit_fits_directory.glob('*')))
     maps_sorted = [sunpy.map.Map(os.fspath(f)) for f in files_sorted]
-    assert all([m.date == m_s.date for m, m_s in zip(maps, maps_sorted)])
+    assert all(m.date == m_s.date for m, m_s in zip(maps, maps_sorted))
 
     # Pathlib
     path = pathlib.Path(AIA_171_IMAGE)
     aiamap = sunpy.map.Map(path)
+
+    # Generator
+    maps = sunpy.map.Map(eit_fits_directory.glob('*.fits'))
+    assert isinstance(maps, list)
     assert isinstance(aiamap, sunpy.map.GenericMap)
+
+    # A directory
     maps = sunpy.map.Map(eit_fits_directory)
     assert isinstance(maps, list)
     assert ([isinstance(amap, sunpy.map.GenericMap) for amap in maps])
@@ -101,16 +108,19 @@ def test_patterns(eit_fits_directory):
     maps = sunpy.map.Map(pattern)
     assert isinstance(maps, list)
     assert ([isinstance(amap, sunpy.map.GenericMap) for amap in maps])
+
     # Test that returned maps are sorted
     files_sorted = sorted(list(pathlib.Path(pattern).parent.glob('*')))
     maps_sorted = [sunpy.map.Map(os.fspath(f)) for f in files_sorted]
-    assert all([m.date == m_s.date for m, m_s in zip(maps, maps_sorted)])
+    assert all(m.date == m_s.date for m, m_s in zip(maps, maps_sorted))
+
     # Single character wildcard (?)
     pattern = os.path.join(eit_fits_directory, "efz20040301.0?0010_s.fits")
     maps = sunpy.map.Map(pattern)
     assert isinstance(maps, list)
     assert len(maps) == 7
     assert ([isinstance(amap, sunpy.map.GenericMap) for amap in maps])
+
     # Character ranges
     pattern = os.path.join(eit_fits_directory, "efz20040301.0[2-6]0010_s.fits")
     maps = sunpy.map.Map(pattern)
diff --git a/sunpy/util/tests/test_util.py b/sunpy/util/tests/test_util.py
--- a/sunpy/util/tests/test_util.py
+++ b/sunpy/util/tests/test_util.py
@@ -1,4 +1,10 @@
+import pytest
+
+from astropy.io import fits
+
+from sunpy.data.test import get_test_filepath
 from sunpy.util import util
+from sunpy.util.parfive_helpers import Results
 
 
 def test_unique():
@@ -49,9 +55,21 @@ def test_expand_list():
     assert util.expand_list(lst) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
 
 
-def test_expand_list_generator():
-    lst = ['a', 'b', [], (['c', 'd']), tuple(), ['e']]
-    assert list(util.expand_list_generator(lst)) == ['a', 'b', 'c', 'd', 'e']
+@pytest.mark.parametrize(('input_data', 'expected_output'), (
+    (['a1234', 'b', [], (['c', 'd']), (), ['e'], b'fghj'], ['a1234', 'b', 'c', 'd', 'e', b'fghj']),
+    (iter(['c', 'd']), ['c', 'd']),
+    (zip(['1', '2'], ['3', '4']), ['1', '3', '2', '4']),
+    (Results(["a", "b", "c"]), ["a", "b", "c"]),
+    ([open(get_test_filepath('aia_171_level1.fits'), 'rb')], open(get_test_filepath('aia_171_level1.fits'), 'rb').readlines())
+))
+def test_expand_list_generator(input_data, expected_output):
+    assert list(util.expand_list_generator(input_data)) == expected_output
+
+
+@pytest.mark.filterwarnings("ignore:Invalid 'BLANK' keyword in header")
+def test_expand_list_generator_map(aia171_test_map):
+    before = after = [aia171_test_map.data, aia171_test_map.wcs, fits.getheader(get_test_filepath('aia_171_level1.fits'))]
+    assert list(util.expand_list_generator(before)) == after
 
 
 def test_partial_key_match():