Repository: astropy/astropy

Issue:
`table_to_hdu` warns about reserved keywords 'NAXIS', 'NAXIS1', but does not ignore them
### Description
When converting a table with existing keys with reserved keywords in `table.meta`, `table_to_hdu` duly warns
`Meta-data keyword NAXIS1 will be ignored since it conflicts with a FITS reserved keyword`
but does not in fact ignore or update them to the correct values.
### Expected behavior
E.g. on writing such a table, the header should be written with the correct dimensions, but instead
```
>>> spec = Table([np.arange(5.), np.ones(5)], names=['wave', 'flux'], meta={'NAXIS1': 42, 'EXPTIME': 80.0})
>>> spec.write('table.fits', format='fits', overwrite=True)
WARNING: Meta-data keyword NAXIS1 will be ignored since it conflicts with a FITS reserved keyword [astropy.io.fits.convenience]
>>> fits.open('table.fits')[1].header
XTENSION= 'BINTABLE'           / binary table extension                         
BITPIX  =                    8 / array data type                                
NAXIS   =                    2 / number of array dimensions                     
NAXIS1  =                   42 / length of dimension 1                          
NAXIS2  =                    5 / length of dimension 2                          
PCOUNT  =                    0 / number of group parameters                     
GCOUNT  =                    1 / number of groups                               
TFIELDS =                    2 / number of table fields                         
TTYPE1  = 'wave    '                                                            
TFORM1  = 'D       '                                                            
TTYPE2  = 'flux    '                                                            
TFORM2  = 'D       '                                                            
EXPTIME =                 80.0
```
Similarly, a conflicting `NAXIS = 1` key gives a warning, but in this case fails the final verification before writing the file:
```
astropy.io.fits.verify.VerifyError: 
Verification reported errors:
HDU 1:
    NAXISj keyword out of range ('NAXIS2' when NAXIS == 1)
    'PCOUNT' card at the wrong place (card 5).
    'GCOUNT' card at the wrong place (card 6).
    'NAXIS' card has invalid value '1'.
```
whereas `NAXIS2` is replaced with the correct table dimension; so this seems to be updated at a later point.

It appears that the responsible block in
https://github.com/astropy/astropy/blob/master/astropy/io/fits/convenience.py#L564
```
    for key, value in table.meta.items():
        if is_column_keyword(key.upper()) or key.upper() in REMOVE_KEYWORDS:
            warnings.warn(
                "Meta-data keyword {} will be ignored since it conflicts "
                "with a FITS reserved keyword".format(key), AstropyUserWarning)
```
may just be missing a `continue` after the warning...

### System Details
```
>>> import platform; print(platform.platform())
Darwin-16.7.0-x86_64-i386-64bit
>>> import sys; print("Python", sys.version)
Python 3.7.4 (default, Aug 14 2019, 00:44:22) 
[Clang 9.0.0 (clang-900.0.39.2)]
>>> import numpy; print("Numpy", numpy.__version__)
Numpy 1.17.1
>>> import scipy; print("Scipy", scipy.__version__)
Scipy 1.3.0
>>> import astropy; print("astropy", astropy.__version__)
astropy 3.2.2
```
same for `astropy 4.0.dev26100`

Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 5894060123..4f3c41b319 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -438,6 +438,9 @@ astropy.io.misc
 astropy.io.fits
 ^^^^^^^^^^^^^^^
 
+- Implemented skip (after warning) of header cards with reserved
+  keywords in ``table_to_hdu``. [#9390]
+
 astropy.io.registry
 ^^^^^^^^^^^^^^^^^^^
 
diff --git a/astropy/io/fits/convenience.py b/astropy/io/fits/convenience.py
index bdf365fd51..8cb1dba6a7 100644
--- a/astropy/io/fits/convenience.py
+++ b/astropy/io/fits/convenience.py
@@ -460,8 +460,8 @@ def table_to_hdu(table, character_as_bytes=False):
         unsupported_cols = table.columns.not_isinstance((BaseColumn, Quantity, Time))
         if unsupported_cols:
             unsupported_names = [col.info.name for col in unsupported_cols]
-            raise ValueError('cannot write table with mixin column(s) {}'
-                         .format(unsupported_names))
+            raise ValueError(f'cannot write table with mixin column(s) '
+                             f'{unsupported_names}')
 
         time_cols = table.columns.isinstance(Time)
         if time_cols:
@@ -519,17 +519,17 @@ def table_to_hdu(table, character_as_bytes=False):
             except UnitScaleError:
                 scale = unit.scale
                 raise UnitScaleError(
-                    "The column '{}' could not be stored in FITS format "
-                    "because it has a scale '({})' that "
-                    "is not recognized by the FITS standard. Either scale "
-                    "the data or change the units.".format(col.name, str(scale)))
+                    f"The column '{col.name}' could not be stored in FITS "
+                    f"format because it has a scale '({str(scale)})' that "
+                    f"is not recognized by the FITS standard. Either scale "
+                    f"the data or change the units.")
             except ValueError:
                 # Warn that the unit is lost, but let the details depend on
                 # whether the column was serialized (because it was a
                 # quantity), since then the unit can be recovered by astropy.
                 warning = (
-                    "The unit '{}' could not be saved in native FITS format "
-                    .format(unit.to_string()))
+                    f"The unit '{unit.to_string()}' could not be saved in "
+                    f"native FITS format ")
                 if any('SerializedColumn' in item and 'name: '+col.name in item
                        for item in table.meta.get('comments', [])):
                     warning += (
@@ -564,8 +564,9 @@ def table_to_hdu(table, character_as_bytes=False):
     for key, value in table.meta.items():
         if is_column_keyword(key.upper()) or key.upper() in REMOVE_KEYWORDS:
             warnings.warn(
-                "Meta-data keyword {} will be ignored since it conflicts "
-                "with a FITS reserved keyword".format(key), AstropyUserWarning)
+                f"Meta-data keyword {key} will be ignored since it conflicts "
+                f"with a FITS reserved keyword", AstropyUserWarning)
+            continue
 
         # Convert to FITS format
         if key == 'comments':
@@ -577,17 +578,15 @@ def table_to_hdu(table, character_as_bytes=False):
                     table_hdu.header.append((key, item))
                 except ValueError:
                     warnings.warn(
-                        "Attribute `{}` of type {} cannot be added to "
-                        "FITS Header - skipping".format(key, type(value)),
-                        AstropyUserWarning)
+                        f"Attribute `{key}` of type {type(value)} cannot be "
+                        f"added to FITS Header - skipping", AstropyUserWarning)
         else:
             try:
                 table_hdu.header[key] = value
             except ValueError:
                 warnings.warn(
-                    "Attribute `{}` of type {} cannot be added to FITS "
-                    "Header - skipping".format(key, type(value)),
-                    AstropyUserWarning)
+                    f"Attribute `{key}` of type {type(value)} cannot be "
+                    f"added to FITS Header - skipping", AstropyUserWarning)
     return table_hdu
 
 


Test patch:
diff --git a/astropy/io/fits/tests/test_convenience.py b/astropy/io/fits/tests/test_convenience.py
index 28dd59b87d..44801dd762 100644
--- a/astropy/io/fits/tests/test_convenience.py
+++ b/astropy/io/fits/tests/test_convenience.py
@@ -12,8 +12,10 @@ from astropy import units as u
 from astropy.table import Table
 from astropy.io.fits import printdiff
 from astropy.tests.helper import catch_warnings
+from astropy.utils.exceptions import AstropyUserWarning
 
 from . import FitsTestCase
+from ..connect import REMOVE_KEYWORDS
 
 
 class TestConvenience(FitsTestCase):
@@ -89,6 +91,77 @@ class TestConvenience(FitsTestCase):
         with pytest.raises(ValueError):
             hdu.header.index('comments')
 
+    def test_table_to_hdu_filter_reserved(self):
+        """
+        Regression test for https://github.com/astropy/astropy/issues/9387
+        """
+        diag = 'be ignored since it conflicts with a FITS reserved keyword'
+        ins_cards = {'EXPTIME': 32.1, 'XTENSION': 'NEWTABLE',
+                     'NAXIS': 1, 'NAXIS1': 3, 'NAXIS2': 9,
+                     'PCOUNT': 42, 'OBSERVER': 'Adams'}
+        table = Table([[1, 2, 3], ['a', 'b', 'c'], [2.3, 4.5, 6.7]],
+                      names=['a', 'b', 'c'], dtype=['i4', 'U1', 'f8'])
+        table.meta.update(ins_cards)
+
+        with pytest.warns(AstropyUserWarning,
+                          match=rf'Meta-data keyword \w+ will {diag}') as w:
+            hdu = fits.table_to_hdu(table)
+
+        # This relies on the warnings being raised in the order of the
+        # meta dict (note that the first and last card are legitimate keys)
+        assert len(w) == len(ins_cards) - 2
+        for i, key in enumerate(list(ins_cards)[1:-1]):
+            assert f'Meta-data keyword {key}' in str(w[i].message)
+
+        assert hdu.header.get('XTENSION') == 'BINTABLE'
+        assert hdu.header.get('NAXIS') == 2
+        assert hdu.header.get('NAXIS1') == 13
+        assert hdu.header.get('NAXIS2') == 3
+        assert hdu.header.get('PCOUNT') == 0
+        np.testing.assert_almost_equal(hdu.header.get('EXPTIME'), 3.21e1)
+
+    @pytest.mark.parametrize('card', REMOVE_KEYWORDS)
+    def test_table_to_hdu_warn_reserved(self, card):
+        """
+        Test warning for each keyword in ..connect.REMOVE_KEYWORDS, 1 by 1
+        """
+        diag = 'be ignored since it conflicts with a FITS reserved keyword'
+        res_cards = {'XTENSION': 'BINTABLE', 'BITPIX': 8,
+                     'NAXIS': 2, 'NAXIS1': 12, 'NAXIS2': 3,
+                     'PCOUNT': 0, 'GCOUNT': 1, 'TFIELDS': 2, 'THEAP': None}
+        ins_cards = {'XTENSION': 'TABLE', 'BITPIX': 16,
+                     'NAXIS': 1, 'NAXIS1': 2, 'NAXIS2': 6,
+                     'PCOUNT': 2, 'GCOUNT': 2, 'TFIELDS': 4, 'THEAP': 36}
+
+        table = Table([[1.0, 2.0, 3.0], [2.3, 4.5, 6.7]],
+                      names=['wavelength', 'flux'], dtype=['f8', 'f4'])
+        table.meta['ORIGIN'] = 'Min.Silly Walks'
+        table.meta[card] = ins_cards[card]
+        assert table.meta.get(card) != res_cards[card]
+
+        with pytest.warns(AstropyUserWarning,
+                          match=f'Meta-data keyword {card} will {diag}'):
+            hdu = fits.table_to_hdu(table)
+
+        assert hdu.header.get(card) == res_cards[card]
+        assert hdu.header.get('ORIGIN') == 'Min.Silly Walks'
+
+    def test_table_to_hdu_filter_incompatible(self):
+        """
+        Test removal of unsupported data types from header
+        """
+        table = Table([[1, 2, 3], ['a', 'b', 'c'], [2.3, 4.5, 6.7]],
+                      names=['a', 'b', 'c'], dtype=['i4', 'U1', 'f8'])
+        table.meta.update({'OBSDATE': '2001-05-26', 'RAMP': np.arange(5),
+                           'TARGETS': {'PRIMARY': 1, 'SECONDAR': 3}})
+        with pytest.warns(AstropyUserWarning, match=r'Attribute \S+ of type '
+                          r'.+ cannot be added to FITS Header - skipping'):
+            hdu = fits.table_to_hdu(table)
+
+        assert hdu.header.get('OBSDATE') == '2001-05-26'
+        assert 'RAMP' not in hdu.header
+        assert 'TARGETS' not in hdu.header
+
     def test_table_writeto_header(self):
         """
         Regression test for https://github.com/astropy/astropy/issues/5988