astropy icon indicating copy to clipboard operation
astropy copied to clipboard

TableHDU.from_columns possible bug

Open SaOgaz opened this issue 8 years ago • 6 comments

Hi,

I think I found a bug in TableHDU.from_columns(). The documentation says that it will take a BinTableHDU as input, but when I try this I get the following error. This is Astropy 3.0 but it was also happening in the later dev version I was running ('3.1.dev21605'). It looks like the columns attribute is getting checked with __getatrr__() and isn't in the KEYWORD_ATTRIBUTES tuple.

<ipython-input-2-12e68d4c959a> in round_trip_ascii(filename, ext)
      3         table = Table.read(filename, hdu=ext)
      4         temp_table = fits.table_to_hdu(table)
----> 5         new_table = fits.TableHDU.from_columns(temp_table)
      6         #hdu = fits.TableHDU.from_columns([col1, col2, col3])
      7         fits_file[ext] = new_table

~/miniconda3/envs/irafdev/lib/python3.5/site-packages/astropy/io/fits/hdu/table.py in from_columns(cls, columns, header, nrows, fill, character_as_bytes, **kwargs)
    126         coldefs = cls._columns_type(columns)
    127         data = FITS_rec.from_columns(coldefs, nrows=nrows, fill=fill,
--> 128                                      character_as_bytes=character_as_bytes)
    129         hdu = cls(data=data, header=header, character_as_bytes=character_as_bytes, **kwargs)
    130         coldefs._add_listener(hdu)

~/miniconda3/envs/irafdev/lib/python3.5/site-packages/astropy/io/fits/fitsrec.py in from_columns(cls, columns, nrows, fill, character_as_bytes)
    305 
    306         # read the delayed data
--> 307         for column in columns:
    308             arr = column.array
    309             if isinstance(arr, Delayed):

~/miniconda3/envs/irafdev/lib/python3.5/site-packages/astropy/io/fits/column.py in __getitem__(self, key)
   1565             key = _get_index(self.names, key)
   1566 
-> 1567         x = self.columns[key]
   1568         if _is_int(key):
   1569             return x

~/miniconda3/envs/irafdev/lib/python3.5/site-packages/astropy/io/fits/column.py in __getattr__(self, name)
   1506                 attr.append(val if val is not None else '')
   1507             return attr
-> 1508         raise AttributeError(name)
   1509 
   1510     @lazyproperty

AttributeError: columns

SaOgaz avatar Mar 27 '18 14:03 SaOgaz

Wait... is this just because it still wants me to pull out the columns from the BinTableHDU myself? If so, the doc strings could use some updating.

SaOgaz avatar Mar 27 '18 14:03 SaOgaz

Just so someone who isn't a maintainer who might be interested to tackle this is able to, please provide a minimal example to reproduce the error, and also a link to the documentation in question. Thank you!

pllim avatar Mar 27 '18 20:03 pllim

It looks like TableHDU.from_columns(hdu.columns) works, and BinTableHDU.from_columns(hdu) as well, so TableHDU.from_columns(hdu) should also be valid I guess. So I think it's a real bug, not a docstring issue.

saimn avatar Mar 27 '18 20:03 saimn

sample code:

temp_table = fits.table_to_hdu(astropy_table)
new_ascii_HDU = fits.TableHDU.from_columns(temp_table)

API doc: http://docs.astropy.org/en/stable/io/fits/api/tables.html#astropy.io.fits.TableHDU.from_columns

SaOgaz avatar Mar 29 '18 16:03 SaOgaz

sad trombone. Tried `TableHDU.from_columns(hdu.columns) as well on my sample file and I get a different error, thought I'd document that here as well.

table = Table.read(filename, hdu=ext)
temp_table = fits.table_to_hdu(table)
new_table = fits.TableHDU.from_columns(temp_table.columns)
<ipython-input-13-e104b5bac7bc> in round_trip_ascii(filename, ext)
      4         temp_table = fits.table_to_hdu(table)
      5         print(temp_table.columns)
----> 6         new_table = fits.TableHDU.from_columns(temp_table.columns)
      7         #hdu = fits.TableHDU.from_columns([col1, col2, col3])
      8         fits_file[ext] = new_table

~/miniconda3/envs/astropydev/lib/python3.6/site-packages/astropy-3.1.dev21606-py3.6-macosx-10.7-x86_64.egg/astropy/io/fits/hdu/table.py in from_columns(cls, columns, header, nrows, fill, character_as_bytes, **kwargs)
    126         coldefs = cls._columns_type(columns)
    127         data = FITS_rec.from_columns(coldefs, nrows=nrows, fill=fill,
--> 128                                      character_as_bytes=character_as_bytes)
    129         hdu = cls(data=data, header=header, character_as_bytes=character_as_bytes, **kwargs)
    130         coldefs._add_listener(hdu)

~/miniconda3/envs/astropydev/lib/python3.6/site-packages/astropy-3.1.dev21606-py3.6-macosx-10.7-x86_64.egg/astropy/io/fits/fitsrec.py in from_columns(cls, columns, nrows, fill, character_as_bytes)
    471         # https://github.com/spacetelescope/PyFITS/issues/99
    472         for idx in range(len(columns)):
--> 473             columns._arrays[idx] = data.field(idx)
    474 
    475         return data

~/miniconda3/envs/astropydev/lib/python3.6/site-packages/astropy-3.1.dev21606-py3.6-macosx-10.7-x86_64.egg/astropy/io/fits/fitsrec.py in field(self, key)
    707                 # Handle all other column data types which are fixed-width
    708                 # fields
--> 709                 converted = self._convert_other(column, field, recformat)
    710 
    711             # Note: Never assign values directly into the self._converted dict;

~/miniconda3/envs/astropydev/lib/python3.6/site-packages/astropy-3.1.dev21606-py3.6-macosx-10.7-x86_64.egg/astropy/io/fits/fitsrec.py in _convert_other(self, column, field, recformat)
    925         # TODO: This also needs to be fixed in the effort to make Columns
    926         # responsible for scaling their arrays to/from FITS native values
--> 927         if not column.ascii and column.format.p_format:
    928             format_code = column.format.p_format
    929         else:

AttributeError: '_AsciiColumnFormat' object has no attribute 'p_format'

SaOgaz avatar Mar 29 '18 16:03 SaOgaz

I've been digging into this but I'm currently stuck. Let me try and summarise my findings so far. First, I've rewritten the MRE as 6 tests, 4 of which already pass on main, and the two failing ones show the bug from different perspective. Namely, test_from_hdu[TableHDU] exercises public API only, and test_core_issue[TableHDU] reproduces the exception by doing less, but using private API.

import pytest
from astropy.io import fits

@pytest.mark.parametrize("cls", [fits.BinTableHDU, fits.TableHDU])
class Test7330:
    # see https://github.com/astropy/astropy/issues/7330
    def test_from_columns(self, cls):
        hdu = fits.BinTableHDU()
        cls.from_columns(hdu.columns)
    
    def test_from_hdu(self, cls):
        hdu = fits.BinTableHDU()
        cls.from_columns(hdu)

    def test_core_issue(self, cls):
        hdu = fits.BinTableHDU()
        coldefs = cls._columns_type(hdu)
        assert isinstance(coldefs, fits.column.ColDefs)
        assert hasattr(coldefs, "columns")

Let's now focus on test_core_issue[TableHDU]. I note that TableHDU._columns_type is astropy.io.fits.column._AsciiColDefs. This class inherits from astropy.io.fits.column.ColDefs, which has an interesting __new__ method. In effect, this means that in some cases, one might call _AsciiColDefs(hdu) and get a ColDefs instance in return (not an instance of the child class _AsciiColDefs). Now what I do not understand at the moment is that, in the cases I'm studying neither _AsciiColDefs.__init__ nor ColDefs.__init__ appear to be called following ColDefs.__new__, so we seemingly get an uninitialised (and broken) ColDefs instance that, in particular, doesn't have columns attribute. At the time of writing I have no clue why this happens, but I'm confident that this is key to resolving this bug.

It looks like TableHDU.from_columns(hdu.columns) works, and BinTableHDU.from_columns(hdu) as well, so TableHDU.from_columns(hdu) should also be valid I guess. So I think it's a real bug, not a docstring issue.

I also want to warmly thank @saimn for pointing this out, this was extremely helpful from an outsider's perspective !

neutrinoceros avatar Feb 12 '24 12:02 neutrinoceros