Repository: sunpy/sunpy

Issue:
Creating MapCube.save(prefix_path)
Attempting to build https://github.com/sunpy/sunpy/issues/2210.

Now how do I test this? I tried looking for a test for the save function in map, didn't find any.
I've already found the tests located at `.../map/tests/*`, and once went through the [testing documentation](http://docs.sunpy.org/en/latest/dev_guide/tests.html).

Module level tests are failing in my machine.
```bash
$ py.test sunpy/map/tests/
sunpy/map/tests/test_header.py .
sunpy/map/tests/test_map_factory.py ...........
sunpy/map/tests/test_mapbase.py ............................................................
sunpy/map/tests/test_mapcube.py ....
sunpy/map/tests/test_plotting.py FFFFFFFFFFFFFFFFFFF
```

@wafels @Punyaslok ^_^






Discussion:
Hello @jerinphilip! Thanks for submitting the PR.

 - In the file [`sunpy/map/mapcube.py`](https://github.com/sunpy/sunpy/blob/52415090470231d7c61645357aa5b6c9d2b9a189/sunpy/map/mapcube.py), following are the PEP8 issues :

> [Line 3:1](https://github.com/sunpy/sunpy/blob/52415090470231d7c61645357aa5b6c9d2b9a189/sunpy/map/mapcube.py#L3): [E265](https://duckduckgo.com/?q=pep8%20E265) block comment should start with '# '
> [Line 53:5](https://github.com/sunpy/sunpy/blob/52415090470231d7c61645357aa5b6c9d2b9a189/sunpy/map/mapcube.py#L53): [E265](https://duckduckgo.com/?q=pep8%20E265) block comment should start with '# '
> [Line 95:32](https://github.com/sunpy/sunpy/blob/52415090470231d7c61645357aa5b6c9d2b9a189/sunpy/map/mapcube.py#L95): [E261](https://duckduckgo.com/?q=pep8%20E261) at least two spaces before inline comment
> [Line 174:13](https://github.com/sunpy/sunpy/blob/52415090470231d7c61645357aa5b6c9d2b9a189/sunpy/map/mapcube.py#L174): [E731](https://duckduckgo.com/?q=pep8%20E731) do not assign a lambda expression, use a def
> [Line 347:101](https://github.com/sunpy/sunpy/blob/52415090470231d7c61645357aa5b6c9d2b9a189/sunpy/map/mapcube.py#L347): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (110 > 100 characters)
> [Line 366:5](https://github.com/sunpy/sunpy/blob/52415090470231d7c61645357aa5b6c9d2b9a189/sunpy/map/mapcube.py#L366): [E303](https://duckduckgo.com/?q=pep8%20E303) too many blank lines (2)
> [Line 376:33](https://github.com/sunpy/sunpy/blob/52415090470231d7c61645357aa5b6c9d2b9a189/sunpy/map/mapcube.py#L376): [E228](https://duckduckgo.com/?q=pep8%20E228) missing whitespace around modulo operator
> [Line 380:1](https://github.com/sunpy/sunpy/blob/52415090470231d7c61645357aa5b6c9d2b9a189/sunpy/map/mapcube.py#L380): [W391](https://duckduckgo.com/?q=pep8%20W391) blank line at end of file


Looks pretty straightforward.
@Cadair , @wafels - I think a naming convention would be good here.

@jerinphilip make the pep8 bot happy
Yes, you're right.  There is no map save test.  There should be.  I will create an issue.
@Punyaslok - what do you suggest for a naming convention?
I think the naming convention which @jerinphilip did is fine (I don't use SunPy Maps frequently LOL).
Maybe you guys are more qualified to decide a naming convention. `1.fits` sounds silly really.

Apart from that, suggest other functionality that you might want in this function, like an extra parameter or something. Maybe @jerinphilip can add `filetype='auto'` as an argument and pass it to the `map.save` function, to support more filetypes in the future.
I think the prefix_path will fix this.  You could have /my/path/file.1.fits, where /my/path/file.1. is the prefix.

I think tests for saving the mapcube should be a separate PR as this is a bigger issue than just mapcube.  As you mentioned, there is no test for map.  Once we fix that, we can write the test for the mapcube save.
I think the test_cube_save function should be moved to test_mapcube.py.  This is because the test_cube_save function tests an explicit feature of mapcube.  Also, it is important that the numbering in the file names follows the ordering in the original mapcube.  To test this, I recommend the following

(1) - create a mapcube consisting of 10 maps.  Ten maps will allow testing that the leading zero creation in the file name numbering scheme is acting correctly.

(2) - for each map, change the data value in data[0,0] to explicitly encode where in the order the map is.  For example, `mc[0].data[0,0] = 0`, `mc[1].data[0,0] = 1`, ... `mc[i].data[0,0] = i` for i from 0 to 9.

(3) Save the mapcube using the new mapcube save function.

(4) Read in the mapcube from the saved files.

(5) If the files have been saved correctly, then `assert read_in_mapcube[i].data[0,0] == i`  for i from 0 to 9 should evaluate to true for all i.  Note that some type casting may be required here.

I think that should efficiently test the mapcube saving and reading.  It is important to maintain the order of the files.

This pull request has been automatically marked as stale because it has not had any activity for the past five months. It will be closed if no further activity occurs. If the ideas in this pull request are still worth implementing, please make sure you open an issue to keep track of that idea!

This pull request has been automatically closed since there was no activity for a month since it was marked as stale. If the ideas in this pull request are still worth implementing, please make sure you open an issue to keep track of that idea!


Resolving patch:
diff --git a/sunpy/map/mapsequence.py b/sunpy/map/mapsequence.py
--- a/sunpy/map/mapsequence.py
+++ b/sunpy/map/mapsequence.py
@@ -511,3 +511,37 @@ def all_meta(self):
         Return all the meta objects as a list.
         """
         return [m.meta for m in self.maps]
+
+    def save(self, filepath, filetype='auto', **kwargs):
+        """
+        Saves the sequence, with one file per map.
+
+        Currently SunPy can save files only in the FITS format.
+
+        Parameters
+        ----------
+        filepath : str
+            Location to save the file(s) to.  The string must contain ``"{index}"``,
+            which will be populated with the corresponding index number for each
+            map.  Format specifiers (e.g., ``"{index:03}"``) can be used.
+        filetype : str
+            'auto' or any supported file extension.
+        kwargs :
+            Any additional keyword arguments are passed to
+            `~sunpy.io.write_file`.
+
+        Examples
+        --------
+        >>> from sunpy.map import Map
+        >>> import sunpy.data.sample # doctest: +REMOTE_DATA
+        >>> smap = Map(sunpy.data.sample.HMI_LOS_IMAGE,
+        ...            sunpy.data.sample.AIA_1600_IMAGE,
+        ...            sequence=True)  # doctest: +REMOTE_DATA
+        >>> smap.save('map_{index:03}.fits')  # doctest: +SKIP
+
+        """
+        if filepath.format(index=0) == filepath:
+            raise ValueError("'{index}' must be appear in the string")
+
+        for index, map_seq in enumerate(self.maps):
+            map_seq.save(filepath.format(index=index), filetype, **kwargs)


Test patch:
diff --git a/sunpy/map/tests/conftest.py b/sunpy/map/tests/conftest.py
--- a/sunpy/map/tests/conftest.py
+++ b/sunpy/map/tests/conftest.py
@@ -27,6 +27,7 @@ def hmi_test_map():
     (data, header), = sunpy.io.read_file(os.path.join(testpath, 'resampled_hmi.fits'))
 
     # Get rid of the blank keyword to prevent some astropy fits fixing warnings
+    header.pop('BLANK')
     header.pop('CRDER2')
     header.pop('CRDER1')
     return sunpy.map.Map((data, header))
diff --git a/sunpy/map/tests/test_mapsequence.py b/sunpy/map/tests/test_mapsequence.py
--- a/sunpy/map/tests/test_mapsequence.py
+++ b/sunpy/map/tests/test_mapsequence.py
@@ -7,6 +7,7 @@
 import pytest
 
 import astropy.units as u
+from astropy.tests.helper import assert_quantity_allclose
 
 import sunpy
 import sunpy.data.test
@@ -196,3 +197,32 @@ def test_norm_animator(hmi_test_map):
 def test_map_sequence_plot(aia171_test_map, hmi_test_map):
     seq = sunpy.map.Map([aia171_test_map, hmi_test_map], sequence=True)
     seq.plot()
+
+
+def test_save(aia171_test_map, hmi_test_map, tmp_path):
+    """
+    Tests the MapSequence save function
+    """
+    seq = sunpy.map.Map([aia171_test_map, hmi_test_map], sequence=True)
+
+    with pytest.raises(ValueError, match="'{index}' must be appear in the string"):
+        seq.save("index", filetype='fits', overwrite=True)
+
+    base_str = (tmp_path / "map").as_posix()
+    seq.save(f"{base_str}_{{index:03}}.fits", filetype='auto', overwrite=True)
+
+    test_seq = sunpy.map.Map(f"{base_str}_000.fits",
+                             f"{base_str}_001.fits", sequence=True)
+
+    assert isinstance(test_seq.maps[0], sunpy.map.sources.sdo.AIAMap)
+    assert isinstance(test_seq.maps[1], sunpy.map.sources.sdo.HMIMap)
+
+    assert test_seq.maps[0].meta.keys() == seq.maps[0].meta.keys()
+    for k in seq.maps[0].meta:
+        assert test_seq.maps[0].meta[k] == seq.maps[0].meta[k]
+    assert_quantity_allclose(test_seq.maps[0].data, seq.maps[0].data)
+
+    assert test_seq.maps[1].meta.keys() == seq.maps[1].meta.keys()
+    for k in seq.maps[1].meta:
+        assert test_seq.maps[1].meta[k] == seq.maps[1].meta[k]
+    assert_quantity_allclose(test_seq.maps[1].data, seq.maps[1].data)