From d08e7a4e4c31742a5ffc99ef06e24b478b4764cf Mon Sep 17 00:00:00 2001 From: beckermr Date: Thu, 9 Jul 2026 09:57:01 -0500 Subject: [PATCH 01/28] feat: add support for rsfitsio as backend --- .gitignore | 2 + fitsio/__init__.py | 1 + fitsio/fitsio_pywrap.c | 32 ++++- fitsio/tests/test_header.py | 5 + fitsio/tests/test_image.py | 14 +++ fitsio/tests/test_image_compression.py | 119 ++++++++++++++++-- .../tests/test_image_compression_defaults.py | 8 ++ fitsio/tests/test_table.py | 28 +++-- setup.py | 52 +++++++- 9 files changed, 240 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 67cbbd92..697353cd 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,5 @@ prof/ .DS_Store cfitsio-*/ zlib/ + +rsfitsio/ diff --git a/fitsio/__init__.py b/fitsio/__init__.py index 7c37fb3b..cc4004b8 100644 --- a/fitsio/__init__.py +++ b/fitsio/__init__.py @@ -45,6 +45,7 @@ cfitsio_has_bzip2_support, cfitsio_has_curl_support, cfitsio_is_reentrant, + fitsio_backend, ) from .fits_exceptions import FITSFormatError diff --git a/fitsio/fitsio_pywrap.c b/fitsio/fitsio_pywrap.c index 4a713af1..9931e4cf 100644 --- a/fitsio/fitsio_pywrap.c +++ b/fitsio/fitsio_pywrap.c @@ -21,12 +21,27 @@ */ #include "fitsio.h" -#include "fitsio2.h" #include #include // #include "fitsio_pywrap_lists.h" #include +#ifdef FITSIO_BACKEND_RSFITSIO +/* random extras needed by fitsio */ +int CFITS_API fits_unset_compression_request(fitsfile *fptr, int *status); +int ffgbytoff(fitsfile *fptr, long gsize, long ngroups, long offset, + void *buffer, int *status); +int ffseek(FITSfile *fptr, LONGLONG position); +int ffread(FITSfile *fptr, long nbytes, void *buffer, int *status); +#define REPORT_EOF 0 +#define FSTRCMP(a, b) \ + ((a)[0]<(b)[0] ? -1 : (a)[0]>(b)[0] ? 1 : strcmp((a), (b))) +#endif + +#ifdef FITSIO_BACKEND_CFITSIO +#include "fitsio2.h" +#endif + // this is not defined anywhere in cfitsio except in // the fits file structure #define CFITSIO_MAX_ARRAY_DIMS 99 @@ -5821,6 +5836,18 @@ static PyObject *PyFITS_cfitsio_is_reentrant(void) { } } +static PyObject *PyFITS_fitsio_backend(void) { +#ifdef FITSIO_BACKEND_CFITSIO + return PyUnicode_FromString("cfitsio"); +#endif +#ifdef FITSIO_BACKEND_RSFITSIO + return PyUnicode_FromString("rsfitsio"); +#endif + PyErr_SetString(PyExc_ValueError, + "No valid fitsio backend specified in C layer!"); + return NULL; +} + /* 'C', 'L', 'I', 'F' 'X' @@ -6131,6 +6158,9 @@ static PyMethodDef fitstype_methods[] = { METH_NOARGS, "cfitsio_is_reentrant\n\nReturn True if cfitsio was compiled with " "reentrant support."}, + {"fitsio_backend", (PyCFunction)PyFITS_fitsio_backend, METH_NOARGS, + "fitsio_backend\n\nReturn the backend FITS library (e.g., 'cfitsio', " + "'rsfitsio')."}, {"parse_card", (PyCFunction)PyFITS_parse_card, METH_VARARGS, "parse_card\n\nparse the card to get the key name, value (as a string), " "data type and comment."}, diff --git a/fitsio/tests/test_header.py b/fitsio/tests/test_header.py index b19d1694..8bfd66fb 100644 --- a/fitsio/tests/test_header.py +++ b/fitsio/tests/test_header.py @@ -11,6 +11,7 @@ from ..header import FITSHDR from ..hdu.base import INVALID_HDR_CHARS from ..util import cfitsio_version +from .. import fitsio_backend CFITSIO_VERSION = cfitsio_version(asfloat=True) @@ -543,6 +544,10 @@ def test_write_key_dict(): assert h.get_comment('test') == keydict['comment'] +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) @pytest.mark.parametrize("fname", ["test.fits", "mem://"]) def test_header_update_compressed_image_to_table(fname): data = np.arange(10).reshape(5, 2).astype(np.float32) diff --git a/fitsio/tests/test_image.py b/fitsio/tests/test_image.py index 6eed47b9..5930810e 100644 --- a/fitsio/tests/test_image.py +++ b/fitsio/tests/test_image.py @@ -8,6 +8,7 @@ from ..util import cfitsio_version, cfitsio_is_bundled import numpy as np from ..fitslib import FITS +from .. import fitsio_backend CFITSIO_VERSION = cfitsio_version(asfloat=True) DTYPES = ['u1', 'i1', 'u2', 'i2', 'f4', 'f8'] @@ -129,6 +130,9 @@ def test_image_subnormal_float32(with_nan, with_compression): nv = np.array(v, dtype=np.float32) if with_compression: + if fitsio_backend() == "rsfitsio": + pytest.skip(reason="test fails w/ rsfitsio backend") + kwargs = { "compress": "GZIP", "qlevel": 0, @@ -153,6 +157,8 @@ def test_image_subnormal_float64(with_nan, with_compression): nv = np.array(v, dtype=np.float64) if with_compression: + if fitsio_backend() == "rsfitsio": + pytest.skip(reason="test fails w/ rsfitsio backend") kwargs = { "compress": "GZIP", "qlevel": 0, @@ -451,6 +457,10 @@ def test_read_ignore_scaling(with_nan): @pytest.mark.parametrize("sy", [0, 3, 4]) @pytest.mark.parametrize("sz", [0, 2, 5]) def test_image_write_subset_3d(sx, sy, sz, fname, with_nan, compress_kws): + if compress_kws: + if fitsio_backend() == "rsfitsio": + pytest.skip(reason="test fails w/ rsfitsio backend") + rng = np.random.RandomState(seed=10) img = np.arange(300).reshape(6, 5, 10).astype(np.float32) img2 = (rng.normal(size=30).reshape(3, 2, 5) * 1000).astype(np.float32) @@ -527,6 +537,10 @@ def test_image_write_subset_3d(sx, sy, sz, fname, with_nan, compress_kws): def test_image_write_subset_2d( sx, sy, fname, with_nan, compress_kws, with_nan_base_img, xnan, ynan ): + if compress_kws: + if fitsio_backend() == "rsfitsio": + pytest.skip(reason="test fails w/ rsfitsio backend") + rng = np.random.RandomState(seed=10) img = np.arange(100).reshape(10, 10) nse = rng.normal(size=100).reshape(10, 10) diff --git a/fitsio/tests/test_image_compression.py b/fitsio/tests/test_image_compression.py index 5ca191e5..4babd4a6 100644 --- a/fitsio/tests/test_image_compression.py +++ b/fitsio/tests/test_image_compression.py @@ -14,12 +14,10 @@ write, RICE_1, SUBTRACTIVE_DITHER_1, - GZIP_1, - GZIP_2, PLIO_1, - HCOMPRESS_1, ) from ..util import cfitsio_is_bundled, cfitsio_version +from .. import fitsio_backend CFITSIO_VERSION = cfitsio_version(asfloat=True) @@ -44,6 +42,23 @@ def test_compressed_write_read(compress, dtype, with_nan): """ Test writing and reading a rice compressed image """ + + if fitsio_backend() == "rsfitsio": + if compress in [ + "hcompress", + "gzip", + "gzip_2", + "gzip_lossless", + "gzip_2_lossless", + ]: + pytest.skip(reason="test fails w/ rsfitsio backend") + + if compress == "plio" and dtype in ["i1", "i2", "i4", "f4", "f8"]: + pytest.skip(reason="test fails w/ rsfitsio backend") + + if compress == "rice" and dtype in ["f8"]: + pytest.skip(reason="test fails w/ rsfitsio backend") + nrows = 5 ncols = 20 if compress in ['rice', 'hcompress'] or 'gzip' in compress: @@ -126,6 +141,22 @@ def test_compressed_write_read_fitsobj(compress, dtype, with_nan): In this version, keep the fits object open """ + if fitsio_backend() == "rsfitsio": + if compress in [ + "hcompress", + "gzip", + "gzip_2", + "gzip_lossless", + "gzip_2_lossless", + ]: + pytest.skip(reason="test fails w/ rsfitsio backend") + + if compress == "plio" and dtype in ["i1", "i2", "i4", "f4", "f8"]: + pytest.skip(reason="test fails w/ rsfitsio backend") + + if compress == "rice" and dtype in ["f8"]: + pytest.skip(reason="test fails w/ rsfitsio backend") + if ( "gzip" in compress and dtype in ["u2", "i2", "u4", "i4"] @@ -204,6 +235,10 @@ def test_compressed_write_read_fitsobj(compress, dtype, with_nan): assert fits[1].is_compressed(), "is compressed" +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) @pytest.mark.skipif(sys.version_info < (3, 9), reason='importlib bug in 3.8') @pytest.mark.skipif(CFITSIO_VERSION < 3.49, reason='bug in cfitsio < 3.49') def test_gzip_tile_compressed_read_lossless_astropy(): @@ -223,6 +258,10 @@ def test_gzip_tile_compressed_read_lossless_astropy(): compare_array(data, data * 0.0, "astropy lossless compressed image") +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) @pytest.mark.parametrize("with_nan", [False, True]) def test_compress_preserve_zeros(with_nan): """ @@ -293,6 +332,23 @@ def test_compressed_seed( """ Test writing and reading a rice compressed image """ + + if fitsio_backend() == "rsfitsio": + if compress in [ + "hcompress", + "gzip", + "gzip_2", + "gzip_lossless", + "gzip_2_lossless", + ]: + pytest.skip(reason="test fails w/ rsfitsio backend") + + if compress == "plio" and dtype in ["i1", "i2", "i4", "f4", "f8"]: + pytest.skip(reason="test fails w/ rsfitsio backend") + + if compress == "rice" and dtype in ["f8"]: + pytest.skip(reason="test fails w/ rsfitsio backend") + nrows = 5 ncols = 20 @@ -420,6 +476,10 @@ def test_compressed_seed_bad(dither_seed): ) +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) def test_memory_compressed_seed(): import fitsio @@ -462,6 +522,10 @@ def test_memory_compressed_seed(): assert dither1 == dither2 +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) def test_image_compression_inmem_subdither2(): H, W = 100, 100 rng = np.random.RandomState(seed=10) @@ -482,6 +546,10 @@ def test_image_compression_inmem_subdither2(): assert minval == 0 +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) @pytest.mark.parametrize( "kw,val", [ @@ -516,11 +584,11 @@ def test_image_compression_raises_on_python_set(kw, val, set_val_to_none): @pytest.mark.parametrize( "compress", [ - RICE_1, - GZIP_1, - GZIP_2, - PLIO_1, - HCOMPRESS_1, + "rice", + "gzip", + "gzip_2", + "plio", + "hcompress", ], ) @pytest.mark.parametrize( @@ -536,6 +604,17 @@ def test_image_compression_raises_on_python_set(kw, val, set_val_to_none): ) @pytest.mark.parametrize("fname", ["mem://", "test.fits"]) def test_image_compression_inmem_lossess_int(compress, dtype, fname): + if fitsio_backend() == "rsfitsio": + if compress in [ + "hcompress", + "gzip", + "gzip_2", + "gzip_lossless", + "gzip_2_lossless", + "plio", + ]: + pytest.skip(reason="test fails w/ rsfitsio backend") + if not cfitsio_is_bundled(): pytest.xfail( reason=( @@ -580,6 +659,10 @@ def test_image_compression_inmem_lossessgzip_int_zeros(): assert np.array_equal(rimg, img) +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) def test_image_compression_inmem_lossessgzip_float(): rng = np.random.RandomState(seed=10) img = rng.normal(size=(300, 300)) @@ -687,6 +770,10 @@ def test_image_compression_big_gzip(coef): assert np.array_equal(h[-1][name][:], out_list[k]) +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) @pytest.mark.parametrize("nan_value", [np.nan, np.inf, -np.inf]) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) @pytest.mark.parametrize( @@ -738,6 +825,10 @@ def test_image_compression_nulls(fname, dtype, nan_value): ) +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) @pytest.mark.parametrize( "compress,qlevel", [ @@ -823,6 +914,10 @@ def test_image_compression_nulls_patches_with_subnormal( ) +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) def test_image_compression_read_chunks(): data = np.arange(127, dtype='i4') @@ -851,6 +946,10 @@ def test_image_compression_read_chunks(): assert np.all(read_data == data[start:end]) +@pytest.mark.xfail( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) @pytest.mark.xfail( condition=not cfitsio_is_bundled(), reason=( @@ -910,6 +1009,10 @@ def test_image_compression_read_from_osx_arm64(): np.testing.assert_array_equal(data, cdata) +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) def test_image_compression_gzip_subnormal_cast_to_zero(): # test code from astrofrog in https://github.com/esheldon/fitsio/issues/513 data = np.zeros( diff --git a/fitsio/tests/test_image_compression_defaults.py b/fitsio/tests/test_image_compression_defaults.py index c927be3e..108c58c3 100644 --- a/fitsio/tests/test_image_compression_defaults.py +++ b/fitsio/tests/test_image_compression_defaults.py @@ -2,6 +2,14 @@ import tempfile import numpy as np import fitsio +from .. import fitsio_backend + +import pytest + +pytestmark = pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) def test_compression_nocompress(): diff --git a/fitsio/tests/test_table.py b/fitsio/tests/test_table.py index c2ef84f8..f515e43e 100644 --- a/fitsio/tests/test_table.py +++ b/fitsio/tests/test_table.py @@ -15,7 +15,7 @@ from .makedata import make_data from ..fitslib import FITS, write, read from .. import util -from .. import cfitsio_has_bzip2_support +from .. import cfitsio_has_bzip2_support, fitsio_backend CFITSIO_VERSION = util.cfitsio_version(asfloat=True) DTYPES = ['u1', 'i1', 'u2', 'i2', 'f4', 'f8'] @@ -570,6 +570,13 @@ def test_ascii_table_write_read(): Test write and read for an ascii table """ + if fitsio_backend() == "rsfitsio": + tol = 1e-14 + elif fitsio_backend() == "cfitsio": + tol = 2.15e-16 + else: + assert False, "No valid backend specified! got " + fitsio_backend() + adata = make_data() ascii_data = adata['ascii_data'] @@ -590,9 +597,10 @@ def test_ascii_table_write_read(): d = fits[1].read_column(f) if d.dtype == np.float64: # note we should be able to do 1.11e-16 in principle, but - # in practice we get more like 2.15e-16 + # in practice we get more like 2.15e-16 for cfitsio and + # closer to 6e-16 for rsfitsio compare_array_tol( - ascii_data[f], d, 2.15e-16, "table field read '%s'" % f + ascii_data[f], d, tol, "table field read '%s'" % f ) else: compare_array( @@ -606,7 +614,7 @@ def test_ascii_table_write_read(): compare_array_tol( ascii_data[f][rows], d, - 2.15e-16, + tol, "table field read subrows '%s'" % f, ) else: @@ -624,7 +632,7 @@ def test_ascii_table_write_read(): compare_array_tol( ascii_data[f][beg:end], d, - 2.15e-16, + tol, "table field read slice '%s'" % f, ) else: @@ -643,7 +651,7 @@ def test_ascii_table_write_read(): compare_array_tol( ascii_data[f], d, - 2.15e-16, + tol, "table subcol, '%s'" % f, ) else: @@ -658,7 +666,7 @@ def test_ascii_table_write_read(): compare_array_tol( ascii_data[f], d, - 2.15e-16, + tol, "table subcol, '%s'" % f, ) else: @@ -675,7 +683,7 @@ def test_ascii_table_write_read(): compare_array_tol( ascii_data[f][rows], d, - 2.15e-16, + tol, "table subcol, '%s'" % f, ) else: @@ -692,7 +700,7 @@ def test_ascii_table_write_read(): compare_array_tol( ascii_data[f][rows], d, - 2.15e-16, + tol, "table subcol/row, '%s'" % f, ) else: @@ -710,7 +718,7 @@ def test_ascii_table_write_read(): compare_array_tol( ascii_data[f][beg:end], d, - 2.15e-16, + tol, "table subcol/slice, '%s'" % f, ) else: diff --git a/setup.py b/setup.py index 1a937945..7504355d 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,25 @@ else: FITSIO_FAIL_ON_BAD_PATCHES = True +if "--use-rsfitsio" in sys.argv or any( + a.startswith("--use-rsfitsio=") for a in sys.argv +): + if "--use-rsfitsio" in sys.argv: + USE_RSFITSIO = True + ind = sys.argv.index("--use-rsfitsio") + del sys.argv[ind] + else: + for ind in range(len(sys.argv)): + if sys.argv[ind].startswith("--use-rsfitsio="): + break + USE_RSFITSIO = sys.argv[ind].split("=", 1)[1] + del sys.argv[ind] +else: + USE_RSFITSIO = os.environ.get( + "FITSIO_USE_RSFITSIO", + False, + ) + if "--use-system-fitsio" in sys.argv: del sys.argv[sys.argv.index("--use-system-fitsio")] USE_SYSTEM_FITSIO = True @@ -96,6 +115,12 @@ def finalize_options(self): self.include_dirs.insert(0, SYSTEM_FITSIO_INCLUDEDIR) if SYSTEM_FITSIO_LIBDIR is not None: self.library_dirs.insert(0, SYSTEM_FITSIO_LIBDIR) + elif USE_RSFITSIO: + if isinstance(USE_RSFITSIO, str): + self.include_dirs.insert( + 0, os.path.join(USE_RSFITSIO, "..", "..", "c") + ) + self.library_dirs.insert(0, USE_RSFITSIO) else: # We defer configuration of the bundled cfitsio to build_extensions # because we will know the compiler there. @@ -117,7 +142,21 @@ def run(self): build_ext.run(self) def build_extensions(self): - if not USE_SYSTEM_FITSIO: + if not USE_RSFITSIO: + self.compiler.define_macro('FITSIO_BACKEND_CFITSIO') + else: + self.compiler.define_macro('FITSIO_BACKEND_RSFITSIO') + + if USE_RSFITSIO: + if isinstance(USE_RSFITSIO, str): + self.compiler.include_dirs.insert( + 0, os.path.join(USE_RSFITSIO, "c") + ) + self.compiler.library_dirs.insert( + 0, os.path.join(USE_RSFITSIO, "target", "debug") + ) + self.compiler.add_library('rsfitsio') + elif not USE_SYSTEM_FITSIO: # Use the compiler for building python to build cfitsio # for maximized compatibility. @@ -450,7 +489,16 @@ def check_system_cfitsio_objects(self, obj_name): sources = ["fitsio/fitsio_pywrap.c"] -ext = Extension("fitsio._fitsio_wrap", sources, include_dirs=['numpy']) +kwargs = {} +if USE_RSFITSIO and isinstance(USE_RSFITSIO, str): + kwargs["extra_link_args"] = [ + "-Wl,-rpath," + USE_RSFITSIO, + "-L" + USE_RSFITSIO, + ] + +ext = Extension( + "fitsio._fitsio_wrap", sources, include_dirs=['numpy'], **kwargs +) setup( ext_modules=[ext], From c41049268d9c66a667c5845085d3a11364a5ef3e Mon Sep 17 00:00:00 2001 From: beckermr Date: Thu, 9 Jul 2026 10:06:41 -0500 Subject: [PATCH 02/28] fix: wrong test code --- fitsio/tests/test_image_compression.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fitsio/tests/test_image_compression.py b/fitsio/tests/test_image_compression.py index 4babd4a6..5ad9bd0b 100644 --- a/fitsio/tests/test_image_compression.py +++ b/fitsio/tests/test_image_compression.py @@ -14,7 +14,6 @@ write, RICE_1, SUBTRACTIVE_DITHER_1, - PLIO_1, ) from ..util import cfitsio_is_bundled, cfitsio_version from .. import fitsio_backend @@ -623,7 +622,7 @@ def test_image_compression_inmem_lossess_int(compress, dtype, fname): "and https://github.com/HEASARC/cfitsio/pull/99." ), ) - if compress == PLIO_1 and dtype in [np.int16, np.uint32, np.int32]: + if compress == "plio" and dtype in [np.int16, np.uint32, np.int32]: pytest.skip( reason="PLIO lossless compression of int16, uint32, and " "int32 types is not supported by cfitsio", From 0c4691cfef61b8f3323899b70a88782ce00f5e80 Mon Sep 17 00:00:00 2001 From: beckermr Date: Thu, 9 Jul 2026 10:10:01 -0500 Subject: [PATCH 03/28] test: adjust test exclusions more --- .../tests/test_image_compression_defaults.py | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/fitsio/tests/test_image_compression_defaults.py b/fitsio/tests/test_image_compression_defaults.py index 108c58c3..ff4dcba0 100644 --- a/fitsio/tests/test_image_compression_defaults.py +++ b/fitsio/tests/test_image_compression_defaults.py @@ -6,10 +6,10 @@ import pytest -pytestmark = pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) +# pytestmark = pytest.mark.skipif( +# condition=fitsio_backend() == "rsfitsio", +# reason="test fails w/ rsfitsio backend", +# ) def test_compression_nocompress(): @@ -23,6 +23,10 @@ def test_compression_nocompress(): assert len(fits) == 1 +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) def test_compression_diskfile_kwargs(): with tempfile.TemporaryDirectory() as tmpdir: fn = os.path.join(tmpdir, 'test.fits') @@ -68,6 +72,10 @@ def test_compression_efns(): assert hdr[key] == val +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) def test_compression_efns_kwargs(): with tempfile.TemporaryDirectory() as tmpdir: fn = os.path.join(tmpdir, 'test.fits') @@ -88,6 +96,10 @@ def test_compression_efns_kwargs(): assert hdr[key] == val +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) def test_compression_qlevels_none_zero(): default_kws = { "compress": fitsio.GZIP_2, @@ -132,6 +144,10 @@ def test_compression_qlevels_none_zero(): assert rms1 <= rms2 +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) def test_compression_hcomp_args(): with tempfile.TemporaryDirectory() as tmpdir: fn = os.path.join(tmpdir, 'test.fits') @@ -156,6 +172,10 @@ def test_compression_hcomp_args(): assert hdr[key] == val +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) def test_compression_qlevel_default(): # Check that if not specified, qlevel defaults to 4. with tempfile.TemporaryDirectory() as tmpdir: From bd5cea49171ce7681097205eef0a3ff3016aaf27 Mon Sep 17 00:00:00 2001 From: beckermr Date: Thu, 9 Jul 2026 10:15:24 -0500 Subject: [PATCH 04/28] test: add CI for rsfitsio --- .github/workflows/tests-rsfitsio.yml | 99 ++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 .github/workflows/tests-rsfitsio.yml diff --git a/.github/workflows/tests-rsfitsio.yml b/.github/workflows/tests-rsfitsio.yml new file mode 100644 index 00000000..411d61f3 --- /dev/null +++ b/.github/workflows/tests-rsfitsio.yml @@ -0,0 +1,99 @@ +name: tests-rsfitsio + +on: + push: + branches: + - master + pull_request: null + +env: + PY_COLORS: "1" + # These compiler flags force the tests to fail if arrays are + # accessed at the C level from an unaligned location. + TEST_CFLAGS: "-fsanitize=alignment -fno-sanitize-recover=alignment" + +defaults: + run: + shell: bash -leo pipefail {0} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tests-rsfitsio: + name: tests-rsfitsio + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + config: + # - { pyver: "3.10", npver: "1", ft: ""} + # - { pyver: "3.11", npver: "1.26", ft: "python-gil"} + # - { pyver: "3.12", npver: "1.26", ft: "python-gil"} + # - { pyver: "3.11", npver: "2", ft: "python-gil"} + # - { pyver: "3.12", npver: "2", ft: "python-gil"} + # - { pyver: "3.13", npver: "2", ft: "python-gil"} + - { pyver: "3.14", npver: "2", ft: "python-gil"} + - { pyver: "3.14", npver: "2", ft: "python-freethreading"} + + runs-on: ${{ matrix.os }} + env: + PIP_OPTIONS: "--no-cache-dir --no-deps --no-build-isolation -v" + + steps: + - name: free disk space + if: matrix.os == 'ubuntu-latest' + uses: endersonmenezes/free-disk-space@v3 + with: + remove_android: true + remove_dotnet: true + remove_haskell: true + rm_cmd: "rmz" + + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + fetch-tags: true + + - uses: mamba-org/setup-micromamba@d7c9bd84e824b79d2af72a2d4196c7f4300d3476 #v3.0.0 + with: + environment-name: fitsio-dev + environment-file: environment.yml + condarc: | + channels: + - conda-forge + show_channel_urls: true + channel_priority: strict + always_yes: true + create-args: >- + python=${{ matrix.config.pyver }} + numpy=${{ matrix.config.npver }} + ${{ matrix.config.ft }} + rust + + - name: set parallel testing flags + run: | + if [[ "${{ matrix.config.pyver }}" == "3.14" ]]; then + echo "PRP_FLAGS=--parallel-threads 4 --iterations 4" >> ${GITHUB_ENV} + fi + + - name: build rsfitsio + run: | + git clone https://github.com/cruzzil/rsfitsio.git + pushd rsfitsio + RUSTFLAGS="--sysroot=${CONDA_PREFIX}" cargo build --release + popd + + - name: build fitsio + run: | + export CFLAGS="${CFLAGS} ${TEST_CFLAGS}" + export FITSIO_USE_RSFITSIO=`pwd`/rsfitsio/target/release + + pip install ${PIP_OPTIONS} -e . + + pytest -vv --durations=20 ${PRP_FLAGS} fitsio + python -c "import fitsio; assert not fitsio.cfitsio_has_bzip2_support()" + python -c "import fitsio; assert not fitsio.cfitsio_has_curl_support()" + python -c "import fitsio; assert not fitsio.cfitsio_is_reentrant()" + python -c "import fitsio; assert fitsio.fitsio_backend() == 'rsfitsio'" From a1769008f61c90150d4d023e74f45216a8124088 Mon Sep 17 00:00:00 2001 From: beckermr Date: Thu, 9 Jul 2026 10:22:40 -0500 Subject: [PATCH 05/28] fix: update tests --- .github/workflows/tests-rsfitsio.yml | 9 +++++---- fitsio/fitsio_pywrap.c | 6 ++++-- setup.py | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests-rsfitsio.yml b/.github/workflows/tests-rsfitsio.yml index 411d61f3..bb927ebb 100644 --- a/.github/workflows/tests-rsfitsio.yml +++ b/.github/workflows/tests-rsfitsio.yml @@ -84,16 +84,17 @@ jobs: pushd rsfitsio RUSTFLAGS="--sysroot=${CONDA_PREFIX}" cargo build --release popd - + - name: build fitsio run: | export CFLAGS="${CFLAGS} ${TEST_CFLAGS}" export FITSIO_USE_RSFITSIO=`pwd`/rsfitsio/target/release - + pip install ${PIP_OPTIONS} -e . pytest -vv --durations=20 ${PRP_FLAGS} fitsio python -c "import fitsio; assert not fitsio.cfitsio_has_bzip2_support()" - python -c "import fitsio; assert not fitsio.cfitsio_has_curl_support()" - python -c "import fitsio; assert not fitsio.cfitsio_is_reentrant()" + python -c "import fitsio; assert fitsio.cfitsio_has_curl_support()" + python -c "import fitsio; assert not fitsio.cfitsio_is_bundled()" + python -c "import fitsio; assert fitsio.cfitsio_is_reentrant()" python -c "import fitsio; assert fitsio.fitsio_backend() == 'rsfitsio'" diff --git a/fitsio/fitsio_pywrap.c b/fitsio/fitsio_pywrap.c index 9931e4cf..1196dc78 100644 --- a/fitsio/fitsio_pywrap.c +++ b/fitsio/fitsio_pywrap.c @@ -5803,9 +5803,11 @@ static PyObject *PyFITS_cfitsio_version(void) { static PyObject *PyFITS_cfitsio_is_bundled(void) { #ifdef FITSIO_USING_SYSTEM_FITSIO Py_RETURN_FALSE; -#else - Py_RETURN_TRUE; #endif +#ifdef FITSIO_BACKEND_RSFITSIO + Py_RETURN_FALSE; +#endif + Py_RETURN_TRUE; } static PyObject *PyFITS_cfitsio_has_bzip2_support(void) { diff --git a/setup.py b/setup.py index 7504355d..aaa8da24 100644 --- a/setup.py +++ b/setup.py @@ -156,6 +156,7 @@ def build_extensions(self): 0, os.path.join(USE_RSFITSIO, "target", "debug") ) self.compiler.add_library('rsfitsio') + self.compiler.define_macro('FITSIO_HAS_CURL_SUPPORT') elif not USE_SYSTEM_FITSIO: # Use the compiler for building python to build cfitsio # for maximized compatibility. From cf7fdc8fa3b72dc44362c4ed93874d4a2cd91bb8 Mon Sep 17 00:00:00 2001 From: beckermr Date: Thu, 9 Jul 2026 10:34:09 -0500 Subject: [PATCH 06/28] test: skip more tests --- .github/workflows/tests-rsfitsio.yml | 8 ++++---- fitsio/__init__.py | 7 +++++++ fitsio/tests/test_image.py | 8 ++++---- fitsio/tests/test_image_compression.py | 20 +++++++++++++------- fitsio/tests/test_segfault.py | 7 ++++++- 5 files changed, 34 insertions(+), 16 deletions(-) diff --git a/.github/workflows/tests-rsfitsio.yml b/.github/workflows/tests-rsfitsio.yml index bb927ebb..1450ad82 100644 --- a/.github/workflows/tests-rsfitsio.yml +++ b/.github/workflows/tests-rsfitsio.yml @@ -93,8 +93,8 @@ jobs: pip install ${PIP_OPTIONS} -e . pytest -vv --durations=20 ${PRP_FLAGS} fitsio - python -c "import fitsio; assert not fitsio.cfitsio_has_bzip2_support()" - python -c "import fitsio; assert fitsio.cfitsio_has_curl_support()" - python -c "import fitsio; assert not fitsio.cfitsio_is_bundled()" - python -c "import fitsio; assert fitsio.cfitsio_is_reentrant()" + python -c "import fitsio; assert not fitsio.backend_has_bzip2_support()" + python -c "import fitsio; assert fitsio.backend_has_curl_support()" + python -c "import fitsio; assert not fitsio.backend_is_bundled()" + python -c "import fitsio; assert fitsio.backend_is_reentrant()" python -c "import fitsio; assert fitsio.fitsio_backend() == 'rsfitsio'" diff --git a/fitsio/__init__.py b/fitsio/__init__.py index cc4004b8..105339d8 100644 --- a/fitsio/__init__.py +++ b/fitsio/__init__.py @@ -41,6 +41,9 @@ FITSRuntimeWarning, cfitsio_is_bundled, ) + +backend_version = cfitsio_version +backend_is_bundled = cfitsio_is_bundled from ._fitsio_wrap import ( cfitsio_has_bzip2_support, cfitsio_has_curl_support, @@ -48,4 +51,8 @@ fitsio_backend, ) +backend_has_bzip2_support = cfitsio_has_bzip2_support +backend_has_curl_support = cfitsio_has_curl_support +backend_is_reentrant = cfitsio_is_reentrant + from .fits_exceptions import FITSFormatError diff --git a/fitsio/tests/test_image.py b/fitsio/tests/test_image.py index 5930810e..521f26cf 100644 --- a/fitsio/tests/test_image.py +++ b/fitsio/tests/test_image.py @@ -75,9 +75,9 @@ def test_image_write_read_unaligned(dtype, with_nan): by hand to fix a bug. """ - if ( - dtype == ">f4" or ("f" in dtype and with_nan) - ) and not cfitsio_is_bundled(): + if (dtype == ">f4" or ("f" in dtype and with_nan)) and ( + fitsio_backend() == "cfitsio" and not cfitsio_is_bundled() + ): pytest.xfail( reason=( "Non-bundled cfitsio libraries have a bug for " @@ -575,7 +575,7 @@ def test_image_write_subset_2d( with_nan and with_nan_base_img and partial_overlap_str in partial_overlap_str_cases - and not cfitsio_is_bundled() + and (fitsio_backend() == "cfitsio" and not cfitsio_is_bundled()) and compress_kws and compress_kws.get("qlevel", 0) > 0 ): diff --git a/fitsio/tests/test_image_compression.py b/fitsio/tests/test_image_compression.py index 5ad9bd0b..fd24ee6c 100644 --- a/fitsio/tests/test_image_compression.py +++ b/fitsio/tests/test_image_compression.py @@ -159,7 +159,7 @@ def test_compressed_write_read_fitsobj(compress, dtype, with_nan): if ( "gzip" in compress and dtype in ["u2", "i2", "u4", "i4"] - and not cfitsio_is_bundled() + and (fitsio_backend() == "cfitsio" and not cfitsio_is_bundled()) ): pytest.xfail( reason=( @@ -614,7 +614,7 @@ def test_image_compression_inmem_lossess_int(compress, dtype, fname): ]: pytest.skip(reason="test fails w/ rsfitsio backend") - if not cfitsio_is_bundled(): + if fitsio_backend() == "cfitsio" and not cfitsio_is_bundled(): pytest.xfail( reason=( "Non-bundled cfitsio libraries have a bug. " @@ -693,6 +693,10 @@ def test_image_mem_reopen_noop(): assert np.array_equal(rimg, img) +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) @pytest.mark.skipif( condition=struct.calcsize("P") * 8 == 32, reason=( @@ -711,7 +715,8 @@ def test_image_mem_reopen_noop(): 2, marks=[ pytest.mark.xfail( - condition=CFITSIO_VERSION < 4.04, + condition=CFITSIO_VERSION < 4.04 + and fitsio_backend() == "cfitsio", reason=( "Writing compressed binary tables exceeding " "2**32 bytes fails for cfitsio < 4.040!" @@ -731,6 +736,7 @@ def test_image_compression_big_gzip(coef): with tempfile.TemporaryDirectory() as tmpdir: tot = 0 pth = os.path.join(tmpdir, "test.fits.gz") + with FITS(pth, "rw", clobber=True) as out: for i in range(nHDU): out_list = [] @@ -890,7 +896,7 @@ def test_image_compression_nulls_patches_with_subnormal( # however, on read, we send nullcheck=NAN and as a side # effect the subnormal float value in this row gets truncated # to zero - if not cfitsio_is_bundled(): + if fitsio_backend() == "cfitsio" and not cfitsio_is_bundled(): np.testing.assert_array_equal(read_data[1, 1:], 0.0) np.testing.assert_array_equal(read_slice1[1:], 0.0) @@ -950,7 +956,7 @@ def test_image_compression_read_chunks(): reason="test fails w/ rsfitsio backend", ) @pytest.mark.xfail( - condition=not cfitsio_is_bundled(), + condition=(fitsio_backend() == "cfitsio" and not cfitsio_is_bundled()), reason=( "system cfitsio must be compiled without FMA instructions to " "enable reproducible lossy float compression" @@ -983,7 +989,7 @@ def test_image_compression_write_read_comp_to_osx_arm64(): @pytest.mark.xfail( - condition=not cfitsio_is_bundled(), + condition=(fitsio_backend() == "cfitsio" and not cfitsio_is_bundled()), reason=( "system cfitsio must be compiled without FMA instructions to " "enable reproducible lossy float compression" @@ -1027,7 +1033,7 @@ def test_image_compression_gzip_subnormal_cast_to_zero(): ) back = read(fn) - if cfitsio_is_bundled(): + if fitsio_backend() == "cfitsio" and cfitsio_is_bundled(): assert not back.ravel()[0] == 0, back.ravel() else: assert back.ravel()[0] == 0, back.ravel() diff --git a/fitsio/tests/test_segfault.py b/fitsio/tests/test_segfault.py index 21522547..38f4994c 100644 --- a/fitsio/tests/test_segfault.py +++ b/fitsio/tests/test_segfault.py @@ -4,6 +4,7 @@ import numpy as np import fitsio +from .. import fitsio_backend, cfitsio_is_bundled import pytest @@ -51,9 +52,13 @@ def _run_mixed(n): print("completed without abort") +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) @pytest.mark.slow @pytest.mark.skipif( - not fitsio.util.cfitsio_is_bundled(), + (fitsio_backend() == "cfitsio" and not cfitsio_is_bundled()), reason=( "small images cause a memory corruption w/ PLIO " "compression (see https://github.com/heasarc/cfitsio/issues/136)" From 44975cc70fc9b0372d80b4406cd5777aed37db07 Mon Sep 17 00:00:00 2001 From: beckermr Date: Thu, 9 Jul 2026 10:36:51 -0500 Subject: [PATCH 07/28] fix: get linkages right --- setup.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index aaa8da24..880d5805 100644 --- a/setup.py +++ b/setup.py @@ -150,11 +150,9 @@ def build_extensions(self): if USE_RSFITSIO: if isinstance(USE_RSFITSIO, str): self.compiler.include_dirs.insert( - 0, os.path.join(USE_RSFITSIO, "c") - ) - self.compiler.library_dirs.insert( - 0, os.path.join(USE_RSFITSIO, "target", "debug") + 0, os.path.join(USE_RSFITSIO, "..", "..", "c") ) + self.compiler.library_dirs.insert(0, USE_RSFITSIO) self.compiler.add_library('rsfitsio') self.compiler.define_macro('FITSIO_HAS_CURL_SUPPORT') elif not USE_SYSTEM_FITSIO: @@ -271,8 +269,9 @@ def build_extensions(self): self.compiler.add_library('z') - # fitsio requires libm as well. - self.compiler.add_library('m') + if not USE_RSFITSIO: + # fitsio requires libm as well. + self.compiler.add_library('m') # call the original build_extensions build_ext.build_extensions(self) From e3d2571e68d1732eeedd839a64ede380847b6961 Mon Sep 17 00:00:00 2001 From: beckermr Date: Thu, 9 Jul 2026 10:39:21 -0500 Subject: [PATCH 08/28] fix: has bzip2 support too --- .github/workflows/tests-rsfitsio.yml | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests-rsfitsio.yml b/.github/workflows/tests-rsfitsio.yml index 1450ad82..839bcc75 100644 --- a/.github/workflows/tests-rsfitsio.yml +++ b/.github/workflows/tests-rsfitsio.yml @@ -93,7 +93,7 @@ jobs: pip install ${PIP_OPTIONS} -e . pytest -vv --durations=20 ${PRP_FLAGS} fitsio - python -c "import fitsio; assert not fitsio.backend_has_bzip2_support()" + python -c "import fitsio; assert fitsio.backend_has_bzip2_support()" python -c "import fitsio; assert fitsio.backend_has_curl_support()" python -c "import fitsio; assert not fitsio.backend_is_bundled()" python -c "import fitsio; assert fitsio.backend_is_reentrant()" diff --git a/setup.py b/setup.py index 880d5805..b7bb99ee 100644 --- a/setup.py +++ b/setup.py @@ -155,6 +155,7 @@ def build_extensions(self): self.compiler.library_dirs.insert(0, USE_RSFITSIO) self.compiler.add_library('rsfitsio') self.compiler.define_macro('FITSIO_HAS_CURL_SUPPORT') + self.compiler.define_macro('FITSIO_HAS_BZIP2_SUPPORT') elif not USE_SYSTEM_FITSIO: # Use the compiler for building python to build cfitsio # for maximized compatibility. From 41eb68a174532cad759de2446a140f41eb409eb8 Mon Sep 17 00:00:00 2001 From: beckermr Date: Thu, 9 Jul 2026 12:21:37 -0500 Subject: [PATCH 09/28] fix: skip bz2 test as it fails too --- fitsio/tests/test_table.py | 46 +++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/fitsio/tests/test_table.py b/fitsio/tests/test_table.py index f515e43e..41fb8980 100644 --- a/fitsio/tests/test_table.py +++ b/fitsio/tests/test_table.py @@ -1104,6 +1104,10 @@ def test_gz_write_read(): assert stat.st_size != 0, "Making sure the data was flushed to disk" +@pytest.mark.skipif( + condition=fitsio_backend() == "rsfitsio", + reason="test fails w/ rsfitsio backend", +) @pytest.mark.skipif( not cfitsio_has_bzip2_support(), reason='cfitsio was not built with bzip2 support', @@ -1124,29 +1128,29 @@ def test_bz2_read(): bzfname = fname + '.bz2' try: - fits = FITS(fname, 'rw') - fits.write_table(data, header=adata['keys'], extname='mytable') - fits.close() + with FITS(fname, 'rw') as fits: + fits.write_table(data, header=adata['keys'], extname='mytable') os.system('bzip2 %s' % fname) - f2 = FITS(bzfname) - d = f2[1].read() - compare_rec(data, d, "bzip2 read") - - h = f2[1].read_header() - for entry in adata['keys']: - name = entry['name'].upper() - value = entry['value'] - hvalue = h[name] - if isinstance(hvalue, str): - hvalue = hvalue.strip() - - assert value == hvalue, "testing header key '%s'" % name - - if 'comment' in entry: - assert ( - entry['comment'].strip() == h.get_comment(name).strip() - ), "testing comment for header key '%s'" % name + with FITS(bzfname) as f2: + d = f2[1].read() + compare_rec(data, d, "bzip2 read") + + h = f2[1].read_header() + for entry in adata['keys']: + name = entry['name'].upper() + value = entry['value'] + hvalue = h[name] + if isinstance(hvalue, str): + hvalue = hvalue.strip() + + assert value == hvalue, "testing header key '%s'" % name + + if 'comment' in entry: + assert ( + entry['comment'].strip() + == h.get_comment(name).strip() + ), "testing comment for header key '%s'" % name except Exception: import traceback From 0c68c3d12ac3bc94a0554587e1c9ea71c65f63c2 Mon Sep 17 00:00:00 2001 From: "Matthew R. Becker" Date: Thu, 9 Jul 2026 12:23:43 -0500 Subject: [PATCH 10/28] Apply suggestion from @beckermr --- fitsio/tests/test_image_compression.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fitsio/tests/test_image_compression.py b/fitsio/tests/test_image_compression.py index fd24ee6c..0690c6e8 100644 --- a/fitsio/tests/test_image_compression.py +++ b/fitsio/tests/test_image_compression.py @@ -736,7 +736,6 @@ def test_image_compression_big_gzip(coef): with tempfile.TemporaryDirectory() as tmpdir: tot = 0 pth = os.path.join(tmpdir, "test.fits.gz") - with FITS(pth, "rw", clobber=True) as out: for i in range(nHDU): out_list = [] From 200d0b944ec883e89487d541e9e3c61b3c2e62e8 Mon Sep 17 00:00:00 2001 From: beckermr Date: Fri, 10 Jul 2026 04:50:04 -0500 Subject: [PATCH 11/28] test: add simple failing tests --- fitsio/tests/test_image_compression.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/fitsio/tests/test_image_compression.py b/fitsio/tests/test_image_compression.py index 0690c6e8..62b9b47d 100644 --- a/fitsio/tests/test_image_compression.py +++ b/fitsio/tests/test_image_compression.py @@ -1038,6 +1038,25 @@ def test_image_compression_gzip_subnormal_cast_to_zero(): assert back.ravel()[0] == 0, back.ravel() +def test_image_compression_simple(): + data = np.arange(10).reshape(5, 2).astype(np.float32) + + fname = "test.fits" + with tempfile.TemporaryDirectory() as tmpdir: + if "mem://" not in fname: + fpth = os.path.join(tmpdir, fname) + else: + fpth = fname + + with FITS(fpth, "rw") as fits: + fits.write(data, compress="RICE", qlevel=1, dither_seed=10) + + with FITS(fpth, "r") as fits: + rdata = fits[0].read() + + assert not np.array_equal(data, rdata) + + if __name__ == '__main__': test_compressed_seed( compress='rice', From 10c0f6f4e5fe44a375fd6e2a5c792c52d15305ae Mon Sep 17 00:00:00 2001 From: beckermr Date: Sat, 11 Jul 2026 04:30:32 -0500 Subject: [PATCH 12/28] test: remove rsfitsio skips --- fitsio/tests/test_header.py | 5 - fitsio/tests/test_image.py | 13 --- fitsio/tests/test_image_compression.py | 110 ------------------ .../tests/test_image_compression_defaults.py | 28 ----- fitsio/tests/test_segfault.py | 4 - fitsio/tests/test_table.py | 4 - 6 files changed, 164 deletions(-) diff --git a/fitsio/tests/test_header.py b/fitsio/tests/test_header.py index 8bfd66fb..b19d1694 100644 --- a/fitsio/tests/test_header.py +++ b/fitsio/tests/test_header.py @@ -11,7 +11,6 @@ from ..header import FITSHDR from ..hdu.base import INVALID_HDR_CHARS from ..util import cfitsio_version -from .. import fitsio_backend CFITSIO_VERSION = cfitsio_version(asfloat=True) @@ -544,10 +543,6 @@ def test_write_key_dict(): assert h.get_comment('test') == keydict['comment'] -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) @pytest.mark.parametrize("fname", ["test.fits", "mem://"]) def test_header_update_compressed_image_to_table(fname): data = np.arange(10).reshape(5, 2).astype(np.float32) diff --git a/fitsio/tests/test_image.py b/fitsio/tests/test_image.py index 521f26cf..0589fc70 100644 --- a/fitsio/tests/test_image.py +++ b/fitsio/tests/test_image.py @@ -130,9 +130,6 @@ def test_image_subnormal_float32(with_nan, with_compression): nv = np.array(v, dtype=np.float32) if with_compression: - if fitsio_backend() == "rsfitsio": - pytest.skip(reason="test fails w/ rsfitsio backend") - kwargs = { "compress": "GZIP", "qlevel": 0, @@ -157,8 +154,6 @@ def test_image_subnormal_float64(with_nan, with_compression): nv = np.array(v, dtype=np.float64) if with_compression: - if fitsio_backend() == "rsfitsio": - pytest.skip(reason="test fails w/ rsfitsio backend") kwargs = { "compress": "GZIP", "qlevel": 0, @@ -457,10 +452,6 @@ def test_read_ignore_scaling(with_nan): @pytest.mark.parametrize("sy", [0, 3, 4]) @pytest.mark.parametrize("sz", [0, 2, 5]) def test_image_write_subset_3d(sx, sy, sz, fname, with_nan, compress_kws): - if compress_kws: - if fitsio_backend() == "rsfitsio": - pytest.skip(reason="test fails w/ rsfitsio backend") - rng = np.random.RandomState(seed=10) img = np.arange(300).reshape(6, 5, 10).astype(np.float32) img2 = (rng.normal(size=30).reshape(3, 2, 5) * 1000).astype(np.float32) @@ -537,10 +528,6 @@ def test_image_write_subset_3d(sx, sy, sz, fname, with_nan, compress_kws): def test_image_write_subset_2d( sx, sy, fname, with_nan, compress_kws, with_nan_base_img, xnan, ynan ): - if compress_kws: - if fitsio_backend() == "rsfitsio": - pytest.skip(reason="test fails w/ rsfitsio backend") - rng = np.random.RandomState(seed=10) img = np.arange(100).reshape(10, 10) nse = rng.normal(size=100).reshape(10, 10) diff --git a/fitsio/tests/test_image_compression.py b/fitsio/tests/test_image_compression.py index 62b9b47d..421ba3ae 100644 --- a/fitsio/tests/test_image_compression.py +++ b/fitsio/tests/test_image_compression.py @@ -41,23 +41,6 @@ def test_compressed_write_read(compress, dtype, with_nan): """ Test writing and reading a rice compressed image """ - - if fitsio_backend() == "rsfitsio": - if compress in [ - "hcompress", - "gzip", - "gzip_2", - "gzip_lossless", - "gzip_2_lossless", - ]: - pytest.skip(reason="test fails w/ rsfitsio backend") - - if compress == "plio" and dtype in ["i1", "i2", "i4", "f4", "f8"]: - pytest.skip(reason="test fails w/ rsfitsio backend") - - if compress == "rice" and dtype in ["f8"]: - pytest.skip(reason="test fails w/ rsfitsio backend") - nrows = 5 ncols = 20 if compress in ['rice', 'hcompress'] or 'gzip' in compress: @@ -139,23 +122,6 @@ def test_compressed_write_read_fitsobj(compress, dtype, with_nan): In this version, keep the fits object open """ - - if fitsio_backend() == "rsfitsio": - if compress in [ - "hcompress", - "gzip", - "gzip_2", - "gzip_lossless", - "gzip_2_lossless", - ]: - pytest.skip(reason="test fails w/ rsfitsio backend") - - if compress == "plio" and dtype in ["i1", "i2", "i4", "f4", "f8"]: - pytest.skip(reason="test fails w/ rsfitsio backend") - - if compress == "rice" and dtype in ["f8"]: - pytest.skip(reason="test fails w/ rsfitsio backend") - if ( "gzip" in compress and dtype in ["u2", "i2", "u4", "i4"] @@ -234,10 +200,6 @@ def test_compressed_write_read_fitsobj(compress, dtype, with_nan): assert fits[1].is_compressed(), "is compressed" -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) @pytest.mark.skipif(sys.version_info < (3, 9), reason='importlib bug in 3.8') @pytest.mark.skipif(CFITSIO_VERSION < 3.49, reason='bug in cfitsio < 3.49') def test_gzip_tile_compressed_read_lossless_astropy(): @@ -257,10 +219,6 @@ def test_gzip_tile_compressed_read_lossless_astropy(): compare_array(data, data * 0.0, "astropy lossless compressed image") -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) @pytest.mark.parametrize("with_nan", [False, True]) def test_compress_preserve_zeros(with_nan): """ @@ -331,23 +289,6 @@ def test_compressed_seed( """ Test writing and reading a rice compressed image """ - - if fitsio_backend() == "rsfitsio": - if compress in [ - "hcompress", - "gzip", - "gzip_2", - "gzip_lossless", - "gzip_2_lossless", - ]: - pytest.skip(reason="test fails w/ rsfitsio backend") - - if compress == "plio" and dtype in ["i1", "i2", "i4", "f4", "f8"]: - pytest.skip(reason="test fails w/ rsfitsio backend") - - if compress == "rice" and dtype in ["f8"]: - pytest.skip(reason="test fails w/ rsfitsio backend") - nrows = 5 ncols = 20 @@ -475,10 +416,6 @@ def test_compressed_seed_bad(dither_seed): ) -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) def test_memory_compressed_seed(): import fitsio @@ -521,10 +458,6 @@ def test_memory_compressed_seed(): assert dither1 == dither2 -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) def test_image_compression_inmem_subdither2(): H, W = 100, 100 rng = np.random.RandomState(seed=10) @@ -545,10 +478,6 @@ def test_image_compression_inmem_subdither2(): assert minval == 0 -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) @pytest.mark.parametrize( "kw,val", [ @@ -603,17 +532,6 @@ def test_image_compression_raises_on_python_set(kw, val, set_val_to_none): ) @pytest.mark.parametrize("fname", ["mem://", "test.fits"]) def test_image_compression_inmem_lossess_int(compress, dtype, fname): - if fitsio_backend() == "rsfitsio": - if compress in [ - "hcompress", - "gzip", - "gzip_2", - "gzip_lossless", - "gzip_2_lossless", - "plio", - ]: - pytest.skip(reason="test fails w/ rsfitsio backend") - if fitsio_backend() == "cfitsio" and not cfitsio_is_bundled(): pytest.xfail( reason=( @@ -658,10 +576,6 @@ def test_image_compression_inmem_lossessgzip_int_zeros(): assert np.array_equal(rimg, img) -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) def test_image_compression_inmem_lossessgzip_float(): rng = np.random.RandomState(seed=10) img = rng.normal(size=(300, 300)) @@ -693,10 +607,6 @@ def test_image_mem_reopen_noop(): assert np.array_equal(rimg, img) -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) @pytest.mark.skipif( condition=struct.calcsize("P") * 8 == 32, reason=( @@ -774,10 +684,6 @@ def test_image_compression_big_gzip(coef): assert np.array_equal(h[-1][name][:], out_list[k]) -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) @pytest.mark.parametrize("nan_value", [np.nan, np.inf, -np.inf]) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) @pytest.mark.parametrize( @@ -829,10 +735,6 @@ def test_image_compression_nulls(fname, dtype, nan_value): ) -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) @pytest.mark.parametrize( "compress,qlevel", [ @@ -918,10 +820,6 @@ def test_image_compression_nulls_patches_with_subnormal( ) -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) def test_image_compression_read_chunks(): data = np.arange(127, dtype='i4') @@ -950,10 +848,6 @@ def test_image_compression_read_chunks(): assert np.all(read_data == data[start:end]) -@pytest.mark.xfail( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) @pytest.mark.xfail( condition=(fitsio_backend() == "cfitsio" and not cfitsio_is_bundled()), reason=( @@ -1013,10 +907,6 @@ def test_image_compression_read_from_osx_arm64(): np.testing.assert_array_equal(data, cdata) -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) def test_image_compression_gzip_subnormal_cast_to_zero(): # test code from astrofrog in https://github.com/esheldon/fitsio/issues/513 data = np.zeros( diff --git a/fitsio/tests/test_image_compression_defaults.py b/fitsio/tests/test_image_compression_defaults.py index ff4dcba0..c927be3e 100644 --- a/fitsio/tests/test_image_compression_defaults.py +++ b/fitsio/tests/test_image_compression_defaults.py @@ -2,14 +2,6 @@ import tempfile import numpy as np import fitsio -from .. import fitsio_backend - -import pytest - -# pytestmark = pytest.mark.skipif( -# condition=fitsio_backend() == "rsfitsio", -# reason="test fails w/ rsfitsio backend", -# ) def test_compression_nocompress(): @@ -23,10 +15,6 @@ def test_compression_nocompress(): assert len(fits) == 1 -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) def test_compression_diskfile_kwargs(): with tempfile.TemporaryDirectory() as tmpdir: fn = os.path.join(tmpdir, 'test.fits') @@ -72,10 +60,6 @@ def test_compression_efns(): assert hdr[key] == val -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) def test_compression_efns_kwargs(): with tempfile.TemporaryDirectory() as tmpdir: fn = os.path.join(tmpdir, 'test.fits') @@ -96,10 +80,6 @@ def test_compression_efns_kwargs(): assert hdr[key] == val -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) def test_compression_qlevels_none_zero(): default_kws = { "compress": fitsio.GZIP_2, @@ -144,10 +124,6 @@ def test_compression_qlevels_none_zero(): assert rms1 <= rms2 -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) def test_compression_hcomp_args(): with tempfile.TemporaryDirectory() as tmpdir: fn = os.path.join(tmpdir, 'test.fits') @@ -172,10 +148,6 @@ def test_compression_hcomp_args(): assert hdr[key] == val -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) def test_compression_qlevel_default(): # Check that if not specified, qlevel defaults to 4. with tempfile.TemporaryDirectory() as tmpdir: diff --git a/fitsio/tests/test_segfault.py b/fitsio/tests/test_segfault.py index 38f4994c..8ec9cb9a 100644 --- a/fitsio/tests/test_segfault.py +++ b/fitsio/tests/test_segfault.py @@ -52,10 +52,6 @@ def _run_mixed(n): print("completed without abort") -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) @pytest.mark.slow @pytest.mark.skipif( (fitsio_backend() == "cfitsio" and not cfitsio_is_bundled()), diff --git a/fitsio/tests/test_table.py b/fitsio/tests/test_table.py index 41fb8980..f18ed8f1 100644 --- a/fitsio/tests/test_table.py +++ b/fitsio/tests/test_table.py @@ -1104,10 +1104,6 @@ def test_gz_write_read(): assert stat.st_size != 0, "Making sure the data was flushed to disk" -@pytest.mark.skipif( - condition=fitsio_backend() == "rsfitsio", - reason="test fails w/ rsfitsio backend", -) @pytest.mark.skipif( not cfitsio_has_bzip2_support(), reason='cfitsio was not built with bzip2 support', From 5bad27df024df64d807bce9de24a60ca7039c4ff Mon Sep 17 00:00:00 2001 From: beckermr Date: Sat, 11 Jul 2026 04:36:23 -0500 Subject: [PATCH 13/28] fix: set dither seed for lossy compression --- fitsio/tests/test_image_compression_defaults.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fitsio/tests/test_image_compression_defaults.py b/fitsio/tests/test_image_compression_defaults.py index c927be3e..5bdd79f2 100644 --- a/fitsio/tests/test_image_compression_defaults.py +++ b/fitsio/tests/test_image_compression_defaults.py @@ -157,7 +157,7 @@ def test_compression_qlevel_default(): bigimg = np.random.uniform(size=(H, W)) # Default qlevel with fitsio.FITS(fn, 'rw', clobber=True) as fits: - fits.write(bigimg, compress='GZIP') + fits.write(bigimg, compress='GZIP', dither_seed=42) size_def = os.stat(fn).st_size hdr = fitsio.read_header(fn, ext=1) print(hdr) @@ -168,15 +168,15 @@ def test_compression_qlevel_default(): assert hdr[key] == val # qlevel=0 with fitsio.FITS(fn, 'rw', clobber=True) as fits: - fits.write(bigimg, compress='GZIP', qlevel=0) + fits.write(bigimg, compress='GZIP', qlevel=0, dither_seed=42) size_0 = os.stat(fn).st_size # qlevel=4 with fitsio.FITS(fn, 'rw', clobber=True) as fits: - fits.write(bigimg, compress='GZIP', qlevel=4) + fits.write(bigimg, compress='GZIP', qlevel=4, dither_seed=42) size_4 = os.stat(fn).st_size # qlevel=16 with fitsio.FITS(fn, 'rw', clobber=True) as fits: - fits.write(bigimg, compress='GZIP', qlevel=16) + fits.write(bigimg, compress='GZIP', qlevel=16, dither_seed=42) size_16 = os.stat(fn).st_size # zero means NO COMPRESSION assert size_0 > size_4 From bfe6180f4d3556d3df837c2d6777cfd43c1789a9 Mon Sep 17 00:00:00 2001 From: beckermr Date: Sat, 11 Jul 2026 04:40:12 -0500 Subject: [PATCH 14/28] fix: convert to new backend functions --- .github/workflows/tests-external-cfitsio.yml | 16 ++++++++-------- .github/workflows/tests-pypi.yml | 6 +++--- .github/workflows/tests-windows.yaml | 2 +- .github/workflows/tests.yml | 16 ++++++++-------- fitsio/tests/test_table.py | 4 ++-- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/tests-external-cfitsio.yml b/.github/workflows/tests-external-cfitsio.yml index efdecd41..1dcfdf0d 100644 --- a/.github/workflows/tests-external-cfitsio.yml +++ b/.github/workflows/tests-external-cfitsio.yml @@ -149,16 +149,16 @@ jobs: --config-settings="--global-option=--system-fitsio-includedir=$HOME/cfitsio-static-install/include" \ --config-settings="--global-option=--system-fitsio-libdir=$HOME/cfitsio-static-install/lib" - python -c "import fitsio; assert not fitsio.cfitsio_has_bzip2_support()" + python -c "import fitsio; assert not fitsio.backend_has_bzip2_support()" if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then - python -c "import fitsio; assert not fitsio.cfitsio_has_curl_support()" + python -c "import fitsio; assert not fitsio.backend_has_curl_support()" else - python -c "import fitsio; assert fitsio.cfitsio_has_curl_support()" + python -c "import fitsio; assert fitsio.backend_has_curl_support()" fi if [[ "${{ matrix.config.cf}}" != "" ]]; then - python -c "import fitsio; assert fitsio.cfitsio_is_reentrant()" + python -c "import fitsio; assert fitsio.backend_is_reentrant()" else - python -c "import fitsio; assert not fitsio.cfitsio_is_reentrant()" + python -c "import fitsio; assert not fitsio.backend_is_reentrant()" fi pytest -vv --durations=20 ${PRP_FLAGS} fitsio @@ -213,11 +213,11 @@ jobs: export FITSIO_SYSTEM_FITSIO_LIBDIR=$HOME/cfitsio-static-install/lib pip install ${PIP_OPTIONS} -e . - python -c "import fitsio; assert fitsio.cfitsio_has_bzip2_support()" + python -c "import fitsio; assert fitsio.backend_has_bzip2_support()" if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then - python -c "import fitsio; assert not fitsio.cfitsio_has_curl_support()" + python -c "import fitsio; assert not fitsio.backend_has_curl_support()" else - python -c "import fitsio; assert fitsio.cfitsio_has_curl_support()" + python -c "import fitsio; assert fitsio.backend_has_curl_support()" fi pytest -vv --durations=20 ${{ matrix.config.doslow }} ${PRP_FLAGS} fitsio diff --git a/.github/workflows/tests-pypi.yml b/.github/workflows/tests-pypi.yml index 26a97e45..f8a9643e 100644 --- a/.github/workflows/tests-pypi.yml +++ b/.github/workflows/tests-pypi.yml @@ -61,6 +61,6 @@ jobs: - name: test fitsio run: | pytest -vv --durations=20 --slow fitsio - python -c "import fitsio; assert fitsio.cfitsio_has_bzip2_support()" - python -c "import fitsio; assert fitsio.cfitsio_has_curl_support()" - python -c "import fitsio; assert fitsio.cfitsio_is_reentrant()" + python -c "import fitsio; assert fitsio.backend_has_bzip2_support()" + python -c "import fitsio; assert fitsio.backend_has_curl_support()" + python -c "import fitsio; assert fitsio.backend_is_reentrant()" diff --git a/.github/workflows/tests-windows.yaml b/.github/workflows/tests-windows.yaml index d9e59593..5bc1b506 100644 --- a/.github/workflows/tests-windows.yaml +++ b/.github/workflows/tests-windows.yaml @@ -40,5 +40,5 @@ jobs: - name: test run: | - python -c "import fitsio; assert not fitsio.cfitsio_is_reentrant()" + python -c "import fitsio; assert not fitsio.backend_is_reentrant()" pytest -vv --durations=20 --parallel-threads 4 --iterations 4 --ignore-gil-enabled fitsio diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7deae0bc..4ac9fa41 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -88,14 +88,14 @@ jobs: pip install ${PIP_OPTIONS} -e . if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then pytest -vv --durations=20 ${PRP_FLAGS} fitsio - python -c "import fitsio; assert not fitsio.cfitsio_has_bzip2_support()" - python -c "import fitsio; assert not fitsio.cfitsio_has_curl_support()" + python -c "import fitsio; assert not fitsio.backend_has_bzip2_support()" + python -c "import fitsio; assert not fitsio.backend_has_curl_support()" else pytest -vv --durations=20 ${PRP_FLAGS} fitsio - python -c "import fitsio; assert fitsio.cfitsio_has_bzip2_support()" - python -c "import fitsio; assert fitsio.cfitsio_has_curl_support()" + python -c "import fitsio; assert fitsio.backend_has_bzip2_support()" + python -c "import fitsio; assert fitsio.backend_has_curl_support()" fi - python -c "import fitsio; assert fitsio.cfitsio_is_reentrant()" + python -c "import fitsio; assert fitsio.backend_is_reentrant()" - name: install bzip2 and curl on linux if: matrix.os == 'ubuntu-latest' @@ -141,9 +141,9 @@ jobs: pip install ${PIP_OPTIONS} -e . pytest -vv --durations=20 ${PRP_FLAGS} fitsio - python -c "import fitsio; assert fitsio.cfitsio_has_bzip2_support()" - python -c "import fitsio; assert fitsio.cfitsio_has_curl_support()" - python -c "import fitsio; assert fitsio.cfitsio_is_reentrant()" + python -c "import fitsio; assert fitsio.backend_has_bzip2_support()" + python -c "import fitsio; assert fitsio.backend_has_curl_support()" + python -c "import fitsio; assert fitsio.backend_is_reentrant()" popd popd diff --git a/fitsio/tests/test_table.py b/fitsio/tests/test_table.py index f18ed8f1..b07d19b8 100644 --- a/fitsio/tests/test_table.py +++ b/fitsio/tests/test_table.py @@ -15,7 +15,7 @@ from .makedata import make_data from ..fitslib import FITS, write, read from .. import util -from .. import cfitsio_has_bzip2_support, fitsio_backend +from .. import backend_has_bzip2_support, fitsio_backend CFITSIO_VERSION = util.cfitsio_version(asfloat=True) DTYPES = ['u1', 'i1', 'u2', 'i2', 'f4', 'f8'] @@ -1105,7 +1105,7 @@ def test_gz_write_read(): @pytest.mark.skipif( - not cfitsio_has_bzip2_support(), + not backend_has_bzip2_support(), reason='cfitsio was not built with bzip2 support', ) def test_bz2_read(): From fe80eae7c07c22b252dfd10ed8931fcd32a6f811 Mon Sep 17 00:00:00 2001 From: beckermr Date: Sat, 11 Jul 2026 04:46:33 -0500 Subject: [PATCH 15/28] fix: more fixes in tests --- fitsio/tests/test_image.py | 6 +++--- fitsio/tests/test_image_compression.py | 14 +++++++------- fitsio/tests/test_segfault.py | 4 ++-- fitsio/tests/test_util.py | 9 ++++++--- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/fitsio/tests/test_image.py b/fitsio/tests/test_image.py index 0589fc70..e9bfc720 100644 --- a/fitsio/tests/test_image.py +++ b/fitsio/tests/test_image.py @@ -5,7 +5,7 @@ # import warnings from .checks import check_header, compare_array -from ..util import cfitsio_version, cfitsio_is_bundled +from ..util import cfitsio_version, backend_is_bundled import numpy as np from ..fitslib import FITS from .. import fitsio_backend @@ -76,7 +76,7 @@ def test_image_write_read_unaligned(dtype, with_nan): """ if (dtype == ">f4" or ("f" in dtype and with_nan)) and ( - fitsio_backend() == "cfitsio" and not cfitsio_is_bundled() + fitsio_backend() == "cfitsio" and not backend_is_bundled() ): pytest.xfail( reason=( @@ -562,7 +562,7 @@ def test_image_write_subset_2d( with_nan and with_nan_base_img and partial_overlap_str in partial_overlap_str_cases - and (fitsio_backend() == "cfitsio" and not cfitsio_is_bundled()) + and (fitsio_backend() == "cfitsio" and not backend_is_bundled()) and compress_kws and compress_kws.get("qlevel", 0) > 0 ): diff --git a/fitsio/tests/test_image_compression.py b/fitsio/tests/test_image_compression.py index 421ba3ae..b701f1c2 100644 --- a/fitsio/tests/test_image_compression.py +++ b/fitsio/tests/test_image_compression.py @@ -15,7 +15,7 @@ RICE_1, SUBTRACTIVE_DITHER_1, ) -from ..util import cfitsio_is_bundled, cfitsio_version +from ..util import backend_is_bundled, cfitsio_version from .. import fitsio_backend CFITSIO_VERSION = cfitsio_version(asfloat=True) @@ -125,7 +125,7 @@ def test_compressed_write_read_fitsobj(compress, dtype, with_nan): if ( "gzip" in compress and dtype in ["u2", "i2", "u4", "i4"] - and (fitsio_backend() == "cfitsio" and not cfitsio_is_bundled()) + and (fitsio_backend() == "cfitsio" and not backend_is_bundled()) ): pytest.xfail( reason=( @@ -532,7 +532,7 @@ def test_image_compression_raises_on_python_set(kw, val, set_val_to_none): ) @pytest.mark.parametrize("fname", ["mem://", "test.fits"]) def test_image_compression_inmem_lossess_int(compress, dtype, fname): - if fitsio_backend() == "cfitsio" and not cfitsio_is_bundled(): + if fitsio_backend() == "cfitsio" and not backend_is_bundled(): pytest.xfail( reason=( "Non-bundled cfitsio libraries have a bug. " @@ -797,7 +797,7 @@ def test_image_compression_nulls_patches_with_subnormal( # however, on read, we send nullcheck=NAN and as a side # effect the subnormal float value in this row gets truncated # to zero - if fitsio_backend() == "cfitsio" and not cfitsio_is_bundled(): + if fitsio_backend() == "cfitsio" and not backend_is_bundled(): np.testing.assert_array_equal(read_data[1, 1:], 0.0) np.testing.assert_array_equal(read_slice1[1:], 0.0) @@ -849,7 +849,7 @@ def test_image_compression_read_chunks(): @pytest.mark.xfail( - condition=(fitsio_backend() == "cfitsio" and not cfitsio_is_bundled()), + condition=(fitsio_backend() == "cfitsio" and not backend_is_bundled()), reason=( "system cfitsio must be compiled without FMA instructions to " "enable reproducible lossy float compression" @@ -882,7 +882,7 @@ def test_image_compression_write_read_comp_to_osx_arm64(): @pytest.mark.xfail( - condition=(fitsio_backend() == "cfitsio" and not cfitsio_is_bundled()), + condition=(fitsio_backend() == "cfitsio" and not backend_is_bundled()), reason=( "system cfitsio must be compiled without FMA instructions to " "enable reproducible lossy float compression" @@ -922,7 +922,7 @@ def test_image_compression_gzip_subnormal_cast_to_zero(): ) back = read(fn) - if fitsio_backend() == "cfitsio" and cfitsio_is_bundled(): + if fitsio_backend() == "cfitsio" and backend_is_bundled(): assert not back.ravel()[0] == 0, back.ravel() else: assert back.ravel()[0] == 0, back.ravel() diff --git a/fitsio/tests/test_segfault.py b/fitsio/tests/test_segfault.py index 8ec9cb9a..961f20cb 100644 --- a/fitsio/tests/test_segfault.py +++ b/fitsio/tests/test_segfault.py @@ -4,7 +4,7 @@ import numpy as np import fitsio -from .. import fitsio_backend, cfitsio_is_bundled +from .. import fitsio_backend, backend_is_bundled import pytest @@ -54,7 +54,7 @@ def _run_mixed(n): @pytest.mark.slow @pytest.mark.skipif( - (fitsio_backend() == "cfitsio" and not cfitsio_is_bundled()), + (fitsio_backend() == "cfitsio" and not backend_is_bundled()), reason=( "small images cause a memory corruption w/ PLIO " "compression (see https://github.com/heasarc/cfitsio/issues/136)" diff --git a/fitsio/tests/test_util.py b/fitsio/tests/test_util.py index 7fd76b91..aeaefc3c 100644 --- a/fitsio/tests/test_util.py +++ b/fitsio/tests/test_util.py @@ -5,13 +5,16 @@ from ..util import ( _nonfinite_as_cfitsio_floating_null_value, - cfitsio_version, + backend_version, _FLOATING_NULL_VALUE, ) +from .. import fitsio_backend -CFITSIO_VERSION = cfitsio_version(asfloat=True) +BACKEND_VERSION = backend_version(asfloat=True) DTYPES = ['u1', 'i1', 'u2', 'i2', 'f4', 'f8'] -if CFITSIO_VERSION > 3.44: +if ( + fitsio_backend() == "cfitsio" and BACKEND_VERSION > 3.44 +) or fitsio_backend() == "rsfitsio": DTYPES += ["u8"] From 3caac257f6bee49aa18ddba8c066aefb3ebafa98 Mon Sep 17 00:00:00 2001 From: beckermr Date: Sat, 11 Jul 2026 04:50:03 -0500 Subject: [PATCH 16/28] fix: wrong import --- fitsio/tests/test_image.py | 2 +- fitsio/tests/test_image_compression.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fitsio/tests/test_image.py b/fitsio/tests/test_image.py index e9bfc720..a41850ff 100644 --- a/fitsio/tests/test_image.py +++ b/fitsio/tests/test_image.py @@ -5,7 +5,7 @@ # import warnings from .checks import check_header, compare_array -from ..util import cfitsio_version, backend_is_bundled +from .. import cfitsio_version, backend_is_bundled import numpy as np from ..fitslib import FITS from .. import fitsio_backend diff --git a/fitsio/tests/test_image_compression.py b/fitsio/tests/test_image_compression.py index b701f1c2..d4da78bc 100644 --- a/fitsio/tests/test_image_compression.py +++ b/fitsio/tests/test_image_compression.py @@ -15,7 +15,7 @@ RICE_1, SUBTRACTIVE_DITHER_1, ) -from ..util import backend_is_bundled, cfitsio_version +from .. import backend_is_bundled, cfitsio_version from .. import fitsio_backend CFITSIO_VERSION = cfitsio_version(asfloat=True) From cf2958fdaf204ebf39a322a1e2df7556dee8ca3a Mon Sep 17 00:00:00 2001 From: beckermr Date: Sat, 11 Jul 2026 04:52:29 -0500 Subject: [PATCH 17/28] fix: more incorrect imports --- fitsio/tests/test_util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fitsio/tests/test_util.py b/fitsio/tests/test_util.py index aeaefc3c..a4ed7e91 100644 --- a/fitsio/tests/test_util.py +++ b/fitsio/tests/test_util.py @@ -5,10 +5,9 @@ from ..util import ( _nonfinite_as_cfitsio_floating_null_value, - backend_version, _FLOATING_NULL_VALUE, ) -from .. import fitsio_backend +from .. import fitsio_backend, backend_version BACKEND_VERSION = backend_version(asfloat=True) DTYPES = ['u1', 'i1', 'u2', 'i2', 'f4', 'f8'] From 01a708664037b8adb46257706f58e577470a6669 Mon Sep 17 00:00:00 2001 From: "Matthew R. Becker" Date: Sat, 11 Jul 2026 04:57:54 -0500 Subject: [PATCH 18/28] Apply suggestion from @beckermr --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 1ba54aae..73eadcf9 100644 --- a/setup.py +++ b/setup.py @@ -166,7 +166,6 @@ def finalize_options(self): _print_msg(f"Adding include directory '{pth}'") self.include_dirs.insert(0, pth) if SYSTEM_FITSIO_LIBDIR is not None: - self.library_dirs.insert(0, SYSTEM_FITSIO_LIBDIR) for pth in SYSTEM_FITSIO_LIBDIR.split(os.pathsep): _print_msg(f"Adding lib directory '{pth}'") self.library_dirs.insert(0, pth) From d8e70d1b2d2defa24c9f1485bac9acf29a2301bc Mon Sep 17 00:00:00 2001 From: beckermr Date: Sat, 11 Jul 2026 05:05:44 -0500 Subject: [PATCH 19/28] fix: more test fixes --- fitsio/tests/makedata.py | 36 +++++++++++++++++++------- fitsio/tests/test_header.py | 8 +++--- fitsio/tests/test_image.py | 15 ++++++----- fitsio/tests/test_image_compression.py | 14 ++++++---- 4 files changed, 49 insertions(+), 24 deletions(-) diff --git a/fitsio/tests/makedata.py b/fitsio/tests/makedata.py index 83e0394e..2d9c8a11 100644 --- a/fitsio/tests/makedata.py +++ b/fitsio/tests/makedata.py @@ -2,9 +2,9 @@ import numpy as np from functools import lru_cache -from ..util import cfitsio_version +from .. import backend_version, fitsio_backend -CFITSIO_VERSION = cfitsio_version(asfloat=True) +BACKEND_VERSION = backend_version(asfloat=True) lorem_ipsum = ( 'Lorem ipsum dolor sit amet, consectetur adipiscing ' @@ -65,7 +65,9 @@ def make_data(): ('Sarr', Sdtype, ashape), ] - if CFITSIO_VERSION > 4: + if ( + fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 + ) or fitsio_backend() == "rsfitsio": dtype += [ ('u8scalar', 'u8'), ('u8vec', 'u8', nvec), @@ -75,7 +77,9 @@ def make_data(): # cfitsio 3 or earlier does not # handle non-space padded strings # properly - if CFITSIO_VERSION >= 4: + if ( + fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 + ) or fitsio_backend() == "rsfitsio": dtype += [ ('Sscalar_nopad', Sdtype), ('Svec_nopad', Sdtype, nvec), @@ -92,7 +96,9 @@ def make_data(): # cfitsio 3 or earlier does not # handle non-space padded strings # properly - if CFITSIO_VERSION >= 4: + if ( + fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 + ) or fitsio_backend() == "rsfitsio": dtype += [ ('Uscalar_nopad', Udtype), ('Uvec_nopad', Udtype, nvec), @@ -121,7 +127,9 @@ def make_data(): 'c8', 'c16', ] - if CFITSIO_VERSION > 4: + if ( + fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 + ) or fitsio_backend() == "rsfitsio": dtypes += ["u8"] for t in dtypes: @@ -175,7 +183,9 @@ def make_data(): # cfitsio 3 or earlier does not # handle non-space padded strings # properly - if CFITSIO_VERSION >= 4: + if ( + fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 + ) or fitsio_backend() == "rsfitsio": data['Sscalar_nopad'] = ['hello', 'world', 'good', 'bye'] data['Svec_nopad'][:, 0] = 'hello' data['Svec_nopad'][:, 1] = 'world' @@ -198,7 +208,9 @@ def make_data(): # cfitsio 3 or earlier does not # handle non-space padded strings # properly - if CFITSIO_VERSION >= 4: + if ( + fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 + ) or fitsio_backend() == "rsfitsio": data['Uscalar_nopad'] = ['hello', 'world', 'good', 'bye'] data['Uvec_nopad'][:, 0] = 'hello' data['Uvec_nopad'][:, 1] = 'world' @@ -343,7 +355,9 @@ def make_data(): ('Svec', Sdtype, nvec), ('Sarr', Sdtype, ashape), ] - if CFITSIO_VERSION > 4: + if ( + fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 + ) or fitsio_backend() == "rsfitsio": dtype += [ ('u8vec', 'u8', nvec), ('u8arr', 'u8', ashape), @@ -362,7 +376,9 @@ def make_data(): vardata = np.zeros(nrows, dtype=dtype) _dtypes = ['u1', 'i1', 'u2', 'i2', 'u4', 'i4', 'i8', 'f4', 'f8'] - if CFITSIO_VERSION > 4: + if ( + fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 + ) or fitsio_backend() == "rsfitsio": _dtypes += ["u8"] for t in _dtypes: vardata[t + 'scalar'] = 1 + np.arange(nrows, dtype=t) diff --git a/fitsio/tests/test_header.py b/fitsio/tests/test_header.py index b19d1694..02e2b49f 100644 --- a/fitsio/tests/test_header.py +++ b/fitsio/tests/test_header.py @@ -10,9 +10,9 @@ from ..fitslib import FITS, read_header, write from ..header import FITSHDR from ..hdu.base import INVALID_HDR_CHARS -from ..util import cfitsio_version +from .. import backend_version, fitsio_backend -CFITSIO_VERSION = cfitsio_version(asfloat=True) +BACKEND_VERSION = backend_version(asfloat=True) def test_free_form_string(): @@ -106,7 +106,9 @@ def test_header_write_read(): 'unders': '1_000_000', # test string with underscore 'longs': lorem_ipsum, } - if CFITSIO_VERSION > 4.02: + if ( + fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4.02 + ) or fitsio_backend() == "rsfitsio": # force hierarch + continue header["long_keyword_name"] = lorem_ipsum diff --git a/fitsio/tests/test_image.py b/fitsio/tests/test_image.py index a41850ff..b456acea 100644 --- a/fitsio/tests/test_image.py +++ b/fitsio/tests/test_image.py @@ -5,14 +5,15 @@ # import warnings from .checks import check_header, compare_array -from .. import cfitsio_version, backend_is_bundled +from .. import backend_version, backend_is_bundled, fitsio_backend import numpy as np from ..fitslib import FITS -from .. import fitsio_backend -CFITSIO_VERSION = cfitsio_version(asfloat=True) +BACKEND_VERSION = backend_version(asfloat=True) DTYPES = ['u1', 'i1', 'u2', 'i2', 'f4', 'f8'] -if CFITSIO_VERSION > 3.44: +if ( + BACKEND_VERSION > 3.44 and fitsio_backend() == "cfitsio" +) or fitsio_backend() == "rsfitsio": DTYPES += ["u8"] @@ -786,7 +787,7 @@ def test_image_read_write_ulonglong(): with FITS(fname, 'rw') as fits: data = np.arange(5 * 20, dtype='u8').reshape(5, 20) header = {'DTYPE': 'u8', 'NBYTES': data.dtype.itemsize} - if CFITSIO_VERSION < 3.45: + if BACKEND_VERSION < 3.45 and fitsio_backend() == "cfitsio": with pytest.raises(TypeError) as e: fits.write_image(data, header=header) assert ( @@ -802,6 +803,8 @@ def test_image_read_write_ulonglong(): rh = fits[-1].read_header() check_header(header, rh) - if CFITSIO_VERSION >= 3.45: + if ( + BACKEND_VERSION >= 3.45 and fitsio_backend() == "cfitsio" + ) or fitsio_backend() == "rsfitsio": with FITS(fname) as fits: assert not fits[0].is_compressed(), 'not compressed' diff --git a/fitsio/tests/test_image_compression.py b/fitsio/tests/test_image_compression.py index d4da78bc..4240ea84 100644 --- a/fitsio/tests/test_image_compression.py +++ b/fitsio/tests/test_image_compression.py @@ -15,10 +15,10 @@ RICE_1, SUBTRACTIVE_DITHER_1, ) -from .. import backend_is_bundled, cfitsio_version +from .. import backend_is_bundled, backend_version from .. import fitsio_backend -CFITSIO_VERSION = cfitsio_version(asfloat=True) +BACKEND_VERSION = backend_version(asfloat=True) @pytest.mark.parametrize("with_nan", [False, True]) @@ -201,7 +201,10 @@ def test_compressed_write_read_fitsobj(compress, dtype, with_nan): @pytest.mark.skipif(sys.version_info < (3, 9), reason='importlib bug in 3.8') -@pytest.mark.skipif(CFITSIO_VERSION < 3.49, reason='bug in cfitsio < 3.49') +@pytest.mark.skipif( + BACKEND_VERSION < 3.49 and fitsio_backend() == "cfitsio", + reason='bug in cfitsio < 3.49', +) def test_gzip_tile_compressed_read_lossless_astropy(): """ Test reading an image gzip compressed by astropy (fixed by cfitsio 3.49) @@ -625,11 +628,12 @@ def test_image_mem_reopen_noop(): 2, marks=[ pytest.mark.xfail( - condition=CFITSIO_VERSION < 4.04 + condition=BACKEND_VERSION < 4.04 + and fitsio_backend() == "cfitsio" and fitsio_backend() == "cfitsio", reason=( "Writing compressed binary tables exceeding " - "2**32 bytes fails for cfitsio < 4.040!" + "2**32 bytes fails for cfitsio < 4.04!" ), ), pytest.mark.slow, From afe2357261539df6e4b502ab46eb179826656ace Mon Sep 17 00:00:00 2001 From: beckermr Date: Sat, 11 Jul 2026 05:09:58 -0500 Subject: [PATCH 20/28] fix: table test fixes --- fitsio/tests/test_table.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/fitsio/tests/test_table.py b/fitsio/tests/test_table.py index b07d19b8..b75f4b43 100644 --- a/fitsio/tests/test_table.py +++ b/fitsio/tests/test_table.py @@ -15,11 +15,13 @@ from .makedata import make_data from ..fitslib import FITS, write, read from .. import util -from .. import backend_has_bzip2_support, fitsio_backend +from .. import backend_has_bzip2_support, fitsio_backend, backend_version -CFITSIO_VERSION = util.cfitsio_version(asfloat=True) +BACKEND_VERSION = backend_version(asfloat=True) DTYPES = ['u1', 'i1', 'u2', 'i2', 'f4', 'f8'] -if CFITSIO_VERSION > 4: +if ( + BACKEND_VERSION > 4 and fitsio_backend() == "cfitsio" +) or fitsio_backend() == "rsfitsio": DTYPES += ["u8"] @@ -896,7 +898,9 @@ def test_table_resize(): add_data['u4scalar'] = 2**31 add_data['u4vec'] = 2**31 add_data['u4arr'] = 2**31 - if CFITSIO_VERSION > 4: + if ( + BACKEND_VERSION > 4 and fitsio_backend() == "cfitsio" + ) or fitsio_backend() == "rsfitsio": add_data['u8scalar'] = 2**63 add_data['u8vec'] = 2**63 add_data['u8arr'] = 2**63 @@ -1578,7 +1582,9 @@ def test_table_big_col(table_type): pth = os.path.join(tmpdir, "test.fits") # v3 cfitsio that is not bundled fails for big # columns - if table_type == "ascii" or CFITSIO_VERSION < 4: + if table_type == "ascii" or ( + fitsio_backend() == "cfitsio" and BACKEND_VERSION < 4 + ): with pytest.raises(OSError) as e: write(pth, d, table_type=table_type) assert "FITSIO status = 236: column exceeds width of table" in str( @@ -1594,7 +1600,7 @@ def test_table_big_col(table_type): @pytest.mark.xfail( - condition=CFITSIO_VERSION < 4, + condition=fitsio_backend() == "cfitsio" and BACKEND_VERSION < 4, reason=( "cfitsio versions < 4 do not easily support null-terminated strings" ), @@ -1625,7 +1631,7 @@ def test_table_read_write_ulonglong(): fname = os.path.join(tmpdir, 'test.fits') with FITS(fname, 'rw') as fits: - if CFITSIO_VERSION < 3.45: + if fitsio_backend() == "cfitsio" and BACKEND_VERSION < 3.45: with pytest.raises(IOError) as e: fits.write_table( adata, From 67272bc6e72f32ff9b86c0a3b4656646cdd72c96 Mon Sep 17 00:00:00 2001 From: beckermr Date: Tue, 14 Jul 2026 12:27:36 -0500 Subject: [PATCH 21/28] fix: use constants to help avoid bugs --- CHANGES.md | 6 ++++ fitsio/__init__.py | 5 ++++ fitsio/tests/makedata.py | 39 +++++++++++++++----------- fitsio/tests/test_header.py | 11 ++++++-- fitsio/tests/test_image.py | 22 +++++++++------ fitsio/tests/test_image_compression.py | 27 +++++++++++------- fitsio/tests/test_segfault.py | 4 +-- fitsio/tests/test_table.py | 26 ++++++++++------- fitsio/tests/test_util.py | 11 ++++++-- 9 files changed, 98 insertions(+), 53 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7fdaf0f1..09db3300 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,12 @@ Changes +- Added support for `rsfitsio` as a backend in addition to `cfitsio`. +- Added function `fitsio_backend` to return the current backend. +- Renamed `cfitsio_*` functions to `backend_*` functions for clarity. +- Added constant `CFITSIO_BACKEND` and `RSFITSIO_BACKEND` which are + the return values of `fitsio_backend`. + Bug Fixes - Added missing memory allocation checks in C layer. diff --git a/fitsio/__init__.py b/fitsio/__init__.py index 105339d8..a84693f3 100644 --- a/fitsio/__init__.py +++ b/fitsio/__init__.py @@ -55,4 +55,9 @@ backend_has_curl_support = cfitsio_has_curl_support backend_is_reentrant = cfitsio_is_reentrant +# return values of fitsio_backend, here to help make code +# clearer and avoid mispelling errors in testing strings +CFITSIO_BACKEND = "cfitsio" +RSFITSIO_BACKEND = "rsfitsio" + from .fits_exceptions import FITSFormatError diff --git a/fitsio/tests/makedata.py b/fitsio/tests/makedata.py index 2d9c8a11..4c9fa5a4 100644 --- a/fitsio/tests/makedata.py +++ b/fitsio/tests/makedata.py @@ -2,7 +2,12 @@ import numpy as np from functools import lru_cache -from .. import backend_version, fitsio_backend +from .. import ( + backend_version, + fitsio_backend, + CFITSIO_BACKEND, + RSFITSIO_BACKEND, +) BACKEND_VERSION = backend_version(asfloat=True) @@ -66,8 +71,8 @@ def make_data(): ] if ( - fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 - ) or fitsio_backend() == "rsfitsio": + fitsio_backend() == CFITSIO_BACKEND and BACKEND_VERSION > 4 + ) or fitsio_backend() == RSFITSIO_BACKEND: dtype += [ ('u8scalar', 'u8'), ('u8vec', 'u8', nvec), @@ -78,8 +83,8 @@ def make_data(): # handle non-space padded strings # properly if ( - fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 - ) or fitsio_backend() == "rsfitsio": + fitsio_backend() == CFITSIO_BACKEND and BACKEND_VERSION > 4 + ) or fitsio_backend() == RSFITSIO_BACKEND: dtype += [ ('Sscalar_nopad', Sdtype), ('Svec_nopad', Sdtype, nvec), @@ -97,8 +102,8 @@ def make_data(): # handle non-space padded strings # properly if ( - fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 - ) or fitsio_backend() == "rsfitsio": + fitsio_backend() == CFITSIO_BACKEND and BACKEND_VERSION > 4 + ) or fitsio_backend() == RSFITSIO_BACKEND: dtype += [ ('Uscalar_nopad', Udtype), ('Uvec_nopad', Udtype, nvec), @@ -128,8 +133,8 @@ def make_data(): 'c16', ] if ( - fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 - ) or fitsio_backend() == "rsfitsio": + fitsio_backend() == CFITSIO_BACKEND and BACKEND_VERSION > 4 + ) or fitsio_backend() == RSFITSIO_BACKEND: dtypes += ["u8"] for t in dtypes: @@ -184,8 +189,8 @@ def make_data(): # handle non-space padded strings # properly if ( - fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 - ) or fitsio_backend() == "rsfitsio": + fitsio_backend() == CFITSIO_BACKEND and BACKEND_VERSION > 4 + ) or fitsio_backend() == RSFITSIO_BACKEND: data['Sscalar_nopad'] = ['hello', 'world', 'good', 'bye'] data['Svec_nopad'][:, 0] = 'hello' data['Svec_nopad'][:, 1] = 'world' @@ -209,8 +214,8 @@ def make_data(): # handle non-space padded strings # properly if ( - fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 - ) or fitsio_backend() == "rsfitsio": + fitsio_backend() == CFITSIO_BACKEND and BACKEND_VERSION > 4 + ) or fitsio_backend() == RSFITSIO_BACKEND: data['Uscalar_nopad'] = ['hello', 'world', 'good', 'bye'] data['Uvec_nopad'][:, 0] = 'hello' data['Uvec_nopad'][:, 1] = 'world' @@ -356,8 +361,8 @@ def make_data(): ('Sarr', Sdtype, ashape), ] if ( - fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 - ) or fitsio_backend() == "rsfitsio": + fitsio_backend() == CFITSIO_BACKEND and BACKEND_VERSION > 4 + ) or fitsio_backend() == RSFITSIO_BACKEND: dtype += [ ('u8vec', 'u8', nvec), ('u8arr', 'u8', ashape), @@ -377,8 +382,8 @@ def make_data(): _dtypes = ['u1', 'i1', 'u2', 'i2', 'u4', 'i4', 'i8', 'f4', 'f8'] if ( - fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4 - ) or fitsio_backend() == "rsfitsio": + fitsio_backend() == CFITSIO_BACKEND and BACKEND_VERSION > 4 + ) or fitsio_backend() == RSFITSIO_BACKEND: _dtypes += ["u8"] for t in _dtypes: vardata[t + 'scalar'] = 1 + np.arange(nrows, dtype=t) diff --git a/fitsio/tests/test_header.py b/fitsio/tests/test_header.py index 02e2b49f..dd355d56 100644 --- a/fitsio/tests/test_header.py +++ b/fitsio/tests/test_header.py @@ -10,7 +10,12 @@ from ..fitslib import FITS, read_header, write from ..header import FITSHDR from ..hdu.base import INVALID_HDR_CHARS -from .. import backend_version, fitsio_backend +from .. import ( + backend_version, + fitsio_backend, + CFITSIO_BACKEND, + RSFITSIO_BACKEND, +) BACKEND_VERSION = backend_version(asfloat=True) @@ -107,8 +112,8 @@ def test_header_write_read(): 'longs': lorem_ipsum, } if ( - fitsio_backend() == "cfitsio" and BACKEND_VERSION > 4.02 - ) or fitsio_backend() == "rsfitsio": + fitsio_backend() == CFITSIO_BACKEND and BACKEND_VERSION > 4.02 + ) or fitsio_backend() == RSFITSIO_BACKEND: # force hierarch + continue header["long_keyword_name"] = lorem_ipsum diff --git a/fitsio/tests/test_image.py b/fitsio/tests/test_image.py index b456acea..2090d4fa 100644 --- a/fitsio/tests/test_image.py +++ b/fitsio/tests/test_image.py @@ -5,15 +5,21 @@ # import warnings from .checks import check_header, compare_array -from .. import backend_version, backend_is_bundled, fitsio_backend +from .. import ( + backend_version, + backend_is_bundled, + fitsio_backend, + CFITSIO_BACKEND, + RSFITSIO_BACKEND, +) import numpy as np from ..fitslib import FITS BACKEND_VERSION = backend_version(asfloat=True) DTYPES = ['u1', 'i1', 'u2', 'i2', 'f4', 'f8'] if ( - BACKEND_VERSION > 3.44 and fitsio_backend() == "cfitsio" -) or fitsio_backend() == "rsfitsio": + BACKEND_VERSION > 3.44 and fitsio_backend() == CFITSIO_BACKEND +) or fitsio_backend() == RSFITSIO_BACKEND: DTYPES += ["u8"] @@ -77,7 +83,7 @@ def test_image_write_read_unaligned(dtype, with_nan): """ if (dtype == ">f4" or ("f" in dtype and with_nan)) and ( - fitsio_backend() == "cfitsio" and not backend_is_bundled() + fitsio_backend() == CFITSIO_BACKEND and not backend_is_bundled() ): pytest.xfail( reason=( @@ -563,7 +569,7 @@ def test_image_write_subset_2d( with_nan and with_nan_base_img and partial_overlap_str in partial_overlap_str_cases - and (fitsio_backend() == "cfitsio" and not backend_is_bundled()) + and (fitsio_backend() == CFITSIO_BACKEND and not backend_is_bundled()) and compress_kws and compress_kws.get("qlevel", 0) > 0 ): @@ -787,7 +793,7 @@ def test_image_read_write_ulonglong(): with FITS(fname, 'rw') as fits: data = np.arange(5 * 20, dtype='u8').reshape(5, 20) header = {'DTYPE': 'u8', 'NBYTES': data.dtype.itemsize} - if BACKEND_VERSION < 3.45 and fitsio_backend() == "cfitsio": + if BACKEND_VERSION < 3.45 and fitsio_backend() == CFITSIO_BACKEND: with pytest.raises(TypeError) as e: fits.write_image(data, header=header) assert ( @@ -804,7 +810,7 @@ def test_image_read_write_ulonglong(): check_header(header, rh) if ( - BACKEND_VERSION >= 3.45 and fitsio_backend() == "cfitsio" - ) or fitsio_backend() == "rsfitsio": + BACKEND_VERSION >= 3.45 and fitsio_backend() == CFITSIO_BACKEND + ) or fitsio_backend() == RSFITSIO_BACKEND: with FITS(fname) as fits: assert not fits[0].is_compressed(), 'not compressed' diff --git a/fitsio/tests/test_image_compression.py b/fitsio/tests/test_image_compression.py index 4240ea84..9bf21bb7 100644 --- a/fitsio/tests/test_image_compression.py +++ b/fitsio/tests/test_image_compression.py @@ -16,7 +16,7 @@ SUBTRACTIVE_DITHER_1, ) from .. import backend_is_bundled, backend_version -from .. import fitsio_backend +from .. import fitsio_backend, CFITSIO_BACKEND BACKEND_VERSION = backend_version(asfloat=True) @@ -125,7 +125,7 @@ def test_compressed_write_read_fitsobj(compress, dtype, with_nan): if ( "gzip" in compress and dtype in ["u2", "i2", "u4", "i4"] - and (fitsio_backend() == "cfitsio" and not backend_is_bundled()) + and (fitsio_backend() == CFITSIO_BACKEND and not backend_is_bundled()) ): pytest.xfail( reason=( @@ -202,7 +202,7 @@ def test_compressed_write_read_fitsobj(compress, dtype, with_nan): @pytest.mark.skipif(sys.version_info < (3, 9), reason='importlib bug in 3.8') @pytest.mark.skipif( - BACKEND_VERSION < 3.49 and fitsio_backend() == "cfitsio", + BACKEND_VERSION < 3.49 and fitsio_backend() == CFITSIO_BACKEND, reason='bug in cfitsio < 3.49', ) def test_gzip_tile_compressed_read_lossless_astropy(): @@ -535,7 +535,7 @@ def test_image_compression_raises_on_python_set(kw, val, set_val_to_none): ) @pytest.mark.parametrize("fname", ["mem://", "test.fits"]) def test_image_compression_inmem_lossess_int(compress, dtype, fname): - if fitsio_backend() == "cfitsio" and not backend_is_bundled(): + if fitsio_backend() == CFITSIO_BACKEND and not backend_is_bundled(): pytest.xfail( reason=( "Non-bundled cfitsio libraries have a bug. " @@ -629,8 +629,8 @@ def test_image_mem_reopen_noop(): marks=[ pytest.mark.xfail( condition=BACKEND_VERSION < 4.04 - and fitsio_backend() == "cfitsio" - and fitsio_backend() == "cfitsio", + and fitsio_backend() == CFITSIO_BACKEND + and fitsio_backend() == CFITSIO_BACKEND, reason=( "Writing compressed binary tables exceeding " "2**32 bytes fails for cfitsio < 4.04!" @@ -801,7 +801,10 @@ def test_image_compression_nulls_patches_with_subnormal( # however, on read, we send nullcheck=NAN and as a side # effect the subnormal float value in this row gets truncated # to zero - if fitsio_backend() == "cfitsio" and not backend_is_bundled(): + if ( + fitsio_backend() == CFITSIO_BACKEND + and not backend_is_bundled() + ): np.testing.assert_array_equal(read_data[1, 1:], 0.0) np.testing.assert_array_equal(read_slice1[1:], 0.0) @@ -853,7 +856,9 @@ def test_image_compression_read_chunks(): @pytest.mark.xfail( - condition=(fitsio_backend() == "cfitsio" and not backend_is_bundled()), + condition=( + fitsio_backend() == CFITSIO_BACKEND and not backend_is_bundled() + ), reason=( "system cfitsio must be compiled without FMA instructions to " "enable reproducible lossy float compression" @@ -886,7 +891,9 @@ def test_image_compression_write_read_comp_to_osx_arm64(): @pytest.mark.xfail( - condition=(fitsio_backend() == "cfitsio" and not backend_is_bundled()), + condition=( + fitsio_backend() == CFITSIO_BACKEND and not backend_is_bundled() + ), reason=( "system cfitsio must be compiled without FMA instructions to " "enable reproducible lossy float compression" @@ -926,7 +933,7 @@ def test_image_compression_gzip_subnormal_cast_to_zero(): ) back = read(fn) - if fitsio_backend() == "cfitsio" and backend_is_bundled(): + if fitsio_backend() == CFITSIO_BACKEND and backend_is_bundled(): assert not back.ravel()[0] == 0, back.ravel() else: assert back.ravel()[0] == 0, back.ravel() diff --git a/fitsio/tests/test_segfault.py b/fitsio/tests/test_segfault.py index 961f20cb..491e3014 100644 --- a/fitsio/tests/test_segfault.py +++ b/fitsio/tests/test_segfault.py @@ -4,7 +4,7 @@ import numpy as np import fitsio -from .. import fitsio_backend, backend_is_bundled +from .. import fitsio_backend, backend_is_bundled, CFITSIO_BACKEND import pytest @@ -54,7 +54,7 @@ def _run_mixed(n): @pytest.mark.slow @pytest.mark.skipif( - (fitsio_backend() == "cfitsio" and not backend_is_bundled()), + (fitsio_backend() == CFITSIO_BACKEND and not backend_is_bundled()), reason=( "small images cause a memory corruption w/ PLIO " "compression (see https://github.com/heasarc/cfitsio/issues/136)" diff --git a/fitsio/tests/test_table.py b/fitsio/tests/test_table.py index b75f4b43..a94976f7 100644 --- a/fitsio/tests/test_table.py +++ b/fitsio/tests/test_table.py @@ -15,13 +15,19 @@ from .makedata import make_data from ..fitslib import FITS, write, read from .. import util -from .. import backend_has_bzip2_support, fitsio_backend, backend_version +from .. import ( + backend_has_bzip2_support, + fitsio_backend, + backend_version, + CFITSIO_BACKEND, + RSFITSIO_BACKEND, +) BACKEND_VERSION = backend_version(asfloat=True) DTYPES = ['u1', 'i1', 'u2', 'i2', 'f4', 'f8'] if ( - BACKEND_VERSION > 4 and fitsio_backend() == "cfitsio" -) or fitsio_backend() == "rsfitsio": + BACKEND_VERSION > 4 and fitsio_backend() == CFITSIO_BACKEND +) or fitsio_backend() == RSFITSIO_BACKEND: DTYPES += ["u8"] @@ -572,9 +578,9 @@ def test_ascii_table_write_read(): Test write and read for an ascii table """ - if fitsio_backend() == "rsfitsio": + if fitsio_backend() == RSFITSIO_BACKEND: tol = 1e-14 - elif fitsio_backend() == "cfitsio": + elif fitsio_backend() == CFITSIO_BACKEND: tol = 2.15e-16 else: assert False, "No valid backend specified! got " + fitsio_backend() @@ -899,8 +905,8 @@ def test_table_resize(): add_data['u4vec'] = 2**31 add_data['u4arr'] = 2**31 if ( - BACKEND_VERSION > 4 and fitsio_backend() == "cfitsio" - ) or fitsio_backend() == "rsfitsio": + BACKEND_VERSION > 4 and fitsio_backend() == CFITSIO_BACKEND + ) or fitsio_backend() == RSFITSIO_BACKEND: add_data['u8scalar'] = 2**63 add_data['u8vec'] = 2**63 add_data['u8arr'] = 2**63 @@ -1583,7 +1589,7 @@ def test_table_big_col(table_type): # v3 cfitsio that is not bundled fails for big # columns if table_type == "ascii" or ( - fitsio_backend() == "cfitsio" and BACKEND_VERSION < 4 + fitsio_backend() == CFITSIO_BACKEND and BACKEND_VERSION < 4 ): with pytest.raises(OSError) as e: write(pth, d, table_type=table_type) @@ -1600,7 +1606,7 @@ def test_table_big_col(table_type): @pytest.mark.xfail( - condition=fitsio_backend() == "cfitsio" and BACKEND_VERSION < 4, + condition=fitsio_backend() == CFITSIO_BACKEND and BACKEND_VERSION < 4, reason=( "cfitsio versions < 4 do not easily support null-terminated strings" ), @@ -1631,7 +1637,7 @@ def test_table_read_write_ulonglong(): fname = os.path.join(tmpdir, 'test.fits') with FITS(fname, 'rw') as fits: - if fitsio_backend() == "cfitsio" and BACKEND_VERSION < 3.45: + if fitsio_backend() == CFITSIO_BACKEND and BACKEND_VERSION < 3.45: with pytest.raises(IOError) as e: fits.write_table( adata, diff --git a/fitsio/tests/test_util.py b/fitsio/tests/test_util.py index a4ed7e91..3bb651cc 100644 --- a/fitsio/tests/test_util.py +++ b/fitsio/tests/test_util.py @@ -7,13 +7,18 @@ _nonfinite_as_cfitsio_floating_null_value, _FLOATING_NULL_VALUE, ) -from .. import fitsio_backend, backend_version +from .. import ( + fitsio_backend, + backend_version, + CFITSIO_BACKEND, + RSFITSIO_BACKEND, +) BACKEND_VERSION = backend_version(asfloat=True) DTYPES = ['u1', 'i1', 'u2', 'i2', 'f4', 'f8'] if ( - fitsio_backend() == "cfitsio" and BACKEND_VERSION > 3.44 -) or fitsio_backend() == "rsfitsio": + fitsio_backend() == CFITSIO_BACKEND and BACKEND_VERSION > 3.44 +) or fitsio_backend() == RSFITSIO_BACKEND: DTYPES += ["u8"] From 87a68ef326176bc0444b2b20c06992871321708f Mon Sep 17 00:00:00 2001 From: "Matthew R. Becker" Date: Tue, 14 Jul 2026 12:28:42 -0500 Subject: [PATCH 22/28] Apply suggestion from @beckermr --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 73eadcf9..b4d66ff7 100644 --- a/setup.py +++ b/setup.py @@ -404,6 +404,7 @@ def build_cfitsio_unix(self): # link against the .a library in cfitsio; # It should have been a 'static' library of relocatable objects # (-fPIC), since we use the python compiler flags + link_objects = glob.glob(os.path.join(self.cfitsio_build_dir, '*.o')) self.compiler.set_link_objects(link_objects) From f618831ceaa2a112baa2f000bf88ef56a13ba7a3 Mon Sep 17 00:00:00 2001 From: beckermr Date: Tue, 14 Jul 2026 12:33:31 -0500 Subject: [PATCH 23/28] fix: sharpen test for threading --- fitsio/tests/test_threading.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/fitsio/tests/test_threading.py b/fitsio/tests/test_threading.py index a7a7dd97..a241143e 100644 --- a/fitsio/tests/test_threading.py +++ b/fitsio/tests/test_threading.py @@ -61,7 +61,10 @@ def _read_file(i): pass -@pytest.mark.xfail(reason="Threading performance might be flaky!") +@pytest.mark.xfail( + reason="threading performance might be flaky", + condition=not fitsio.backend_is_reentrant(), +) @pytest.mark.parallel_threads_limit(1) @pytest.mark.iterations(1) @pytest.mark.parametrize( @@ -161,7 +164,10 @@ def _remove_files(): ) -@pytest.mark.xfail(reason="Threading performance might be flaky!") +@pytest.mark.xfail( + reason="threading performance might be flaky", + condition=not fitsio.backend_is_reentrant(), +) @pytest.mark.parallel_threads_limit(1) @pytest.mark.iterations(1) def test_threading_read_one_file(): From 27b148063b87fbebc6228bfd7261e5759ea6c968 Mon Sep 17 00:00:00 2001 From: beckermr Date: Tue, 14 Jul 2026 12:40:38 -0500 Subject: [PATCH 24/28] fix: exclude older pythons where GIL is not released --- fitsio/tests/test_threading.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fitsio/tests/test_threading.py b/fitsio/tests/test_threading.py index a241143e..65a5471e 100644 --- a/fitsio/tests/test_threading.py +++ b/fitsio/tests/test_threading.py @@ -1,5 +1,6 @@ from concurrent.futures import ThreadPoolExecutor, as_completed import os +import sys import tempfile import time @@ -63,7 +64,7 @@ def _read_file(i): @pytest.mark.xfail( reason="threading performance might be flaky", - condition=not fitsio.backend_is_reentrant(), + condition=sys.version_info < (3, 13) or not fitsio.backend_is_reentrant(), ) @pytest.mark.parallel_threads_limit(1) @pytest.mark.iterations(1) @@ -166,7 +167,7 @@ def _remove_files(): @pytest.mark.xfail( reason="threading performance might be flaky", - condition=not fitsio.backend_is_reentrant(), + condition=sys.version_info < (3, 13) or not fitsio.backend_is_reentrant(), ) @pytest.mark.parallel_threads_limit(1) @pytest.mark.iterations(1) From da07c59b5859b9cc5b513ab64384d006328fc75d Mon Sep 17 00:00:00 2001 From: beckermr Date: Tue, 14 Jul 2026 12:45:00 -0500 Subject: [PATCH 25/28] test: adjust tolerance to be less strict --- fitsio/tests/test_threading.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fitsio/tests/test_threading.py b/fitsio/tests/test_threading.py index 65a5471e..cd2a8032 100644 --- a/fitsio/tests/test_threading.py +++ b/fitsio/tests/test_threading.py @@ -161,7 +161,7 @@ def _remove_files(): assert t0_threads < t0_serial / 1.5, ( "Threading should be faster than serial! (%f < %f)" - % (t0_threads, t0_serial) + % (t0_threads, t0_serial / 1.5) ) @@ -215,7 +215,7 @@ def _read_file(fname): flush=True, ) - assert t0_threads < t0_serial / 2, ( + assert t0_threads < t0_serial / 1.5, ( "Threading should be faster than serial! (%f < %f)" - % (t0_threads, t0_serial) + % (t0_threads, t0_serial / 1.5) ) From 2b8717133803da2e5a7d3545e77c6ecc6dc91a73 Mon Sep 17 00:00:00 2001 From: beckermr Date: Tue, 14 Jul 2026 12:54:00 -0500 Subject: [PATCH 26/28] test: clean up CI files some more --- .github/workflows/tests-external-cfitsio.yml | 14 ++++++++++---- .github/workflows/tests-pypi.yml | 3 ++- .github/workflows/tests-rsfitsio.yml | 4 +++- .github/workflows/tests-windows.yaml | 7 ++++--- .github/workflows/tests.yml | 18 +++++++++++++----- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/.github/workflows/tests-external-cfitsio.yml b/.github/workflows/tests-external-cfitsio.yml index 1dcfdf0d..bb85cd9d 100644 --- a/.github/workflows/tests-external-cfitsio.yml +++ b/.github/workflows/tests-external-cfitsio.yml @@ -140,7 +140,7 @@ jobs: cd .. cd .. - - name: test non-bundled build + - name: build non-bundled fitsio run: | export CFLAGS="${CFLAGS} ${TEST_CFLAGS}" @@ -149,6 +149,8 @@ jobs: --config-settings="--global-option=--system-fitsio-includedir=$HOME/cfitsio-static-install/include" \ --config-settings="--global-option=--system-fitsio-libdir=$HOME/cfitsio-static-install/lib" + - name: test non-bundled build + run: | python -c "import fitsio; assert not fitsio.backend_has_bzip2_support()" if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then python -c "import fitsio; assert not fitsio.backend_has_curl_support()" @@ -160,8 +162,9 @@ jobs: else python -c "import fitsio; assert not fitsio.backend_is_reentrant()" fi + python -c "import fitsio; assert fitsio.fitsio_backend() == 'cfitsio'" - pytest -vv --durations=20 ${PRP_FLAGS} fitsio + pytest --durations=20 ${PRP_FLAGS} fitsio - name: install bzip2 on linux if: matrix.os == 'ubuntu-latest' @@ -201,7 +204,7 @@ jobs: cd .. cd .. - - name: test non-bundled build w/ env vars w/ bzip2 + - name: build non-bundled fitsio w/ env vars w/ bzip2 run: | export CFLAGS="${CFLAGS} ${TEST_CFLAGS}" @@ -213,11 +216,14 @@ jobs: export FITSIO_SYSTEM_FITSIO_LIBDIR=$HOME/cfitsio-static-install/lib pip install ${PIP_OPTIONS} -e . + - name: test non-bundled build w/ env vars w/ bzip2 + run: | python -c "import fitsio; assert fitsio.backend_has_bzip2_support()" if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then python -c "import fitsio; assert not fitsio.backend_has_curl_support()" else python -c "import fitsio; assert fitsio.backend_has_curl_support()" fi + python -c "import fitsio; assert fitsio.fitsio_backend() == 'cfitsio'" - pytest -vv --durations=20 ${{ matrix.config.doslow }} ${PRP_FLAGS} fitsio + pytest --durations=20 ${{ matrix.config.doslow }} ${PRP_FLAGS} fitsio diff --git a/.github/workflows/tests-pypi.yml b/.github/workflows/tests-pypi.yml index f8a9643e..6c7979e2 100644 --- a/.github/workflows/tests-pypi.yml +++ b/.github/workflows/tests-pypi.yml @@ -60,7 +60,8 @@ jobs: - name: test fitsio run: | - pytest -vv --durations=20 --slow fitsio + pytest --durations=20 --slow fitsio python -c "import fitsio; assert fitsio.backend_has_bzip2_support()" python -c "import fitsio; assert fitsio.backend_has_curl_support()" python -c "import fitsio; assert fitsio.backend_is_reentrant()" + python -c "import fitsio; assert fitsio.fitsio_backend() == 'cfitsio'" diff --git a/.github/workflows/tests-rsfitsio.yml b/.github/workflows/tests-rsfitsio.yml index 839bcc75..979b98b9 100644 --- a/.github/workflows/tests-rsfitsio.yml +++ b/.github/workflows/tests-rsfitsio.yml @@ -92,7 +92,9 @@ jobs: pip install ${PIP_OPTIONS} -e . - pytest -vv --durations=20 ${PRP_FLAGS} fitsio + - name: test fitsio + run: | + pytest --durations=20 ${PRP_FLAGS} fitsio python -c "import fitsio; assert fitsio.backend_has_bzip2_support()" python -c "import fitsio; assert fitsio.backend_has_curl_support()" python -c "import fitsio; assert not fitsio.backend_is_bundled()" diff --git a/.github/workflows/tests-windows.yaml b/.github/workflows/tests-windows.yaml index 5bc1b506..945707be 100644 --- a/.github/workflows/tests-windows.yaml +++ b/.github/workflows/tests-windows.yaml @@ -32,13 +32,14 @@ jobs: - name: make nmake available uses: ilammy/msvc-dev-cmd@v1 - - name: install code + - name: build fitsio run: | python -m pip install --upgrade pip pip install -v -e .[dev] python -c "import fitsio" - - name: test + - name: test fitsio run: | python -c "import fitsio; assert not fitsio.backend_is_reentrant()" - pytest -vv --durations=20 --parallel-threads 4 --iterations 4 --ignore-gil-enabled fitsio + python -c "import fitsio; assert fitsio.fitsio_backend() == 'cfitsio'" + pytest --durations=20 --parallel-threads 4 --iterations 4 --ignore-gil-enabled fitsio diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4ac9fa41..fda7da8a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -77,7 +77,7 @@ jobs: echo "PRP_FLAGS=--parallel-threads 4 --iterations 4" >> ${GITHUB_ENV} fi - - name: test bundled build + - name: build bundled fitsio run: | export CFLAGS="${CFLAGS} ${TEST_CFLAGS}" @@ -86,16 +86,20 @@ jobs: rm -rf $HOME/cfitsio-static-install find . -name "*.so" -type f -delete pip install ${PIP_OPTIONS} -e . + + - name: test bundled build + run: | if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then - pytest -vv --durations=20 ${PRP_FLAGS} fitsio + pytest --durations=20 ${PRP_FLAGS} fitsio python -c "import fitsio; assert not fitsio.backend_has_bzip2_support()" python -c "import fitsio; assert not fitsio.backend_has_curl_support()" else - pytest -vv --durations=20 ${PRP_FLAGS} fitsio + pytest --durations=20 ${PRP_FLAGS} fitsio python -c "import fitsio; assert fitsio.backend_has_bzip2_support()" python -c "import fitsio; assert fitsio.backend_has_curl_support()" fi python -c "import fitsio; assert fitsio.backend_is_reentrant()" + python -c "import fitsio; assert fitsio.fitsio_backend() == 'cfitsio'" - name: install bzip2 and curl on linux if: matrix.os == 'ubuntu-latest' @@ -121,7 +125,7 @@ jobs: cd - pip uninstall fitsio --yes - - name: test sdist + - name: build fitsio from sdist run: | export CFLAGS="${CFLAGS} ${TEST_CFLAGS}" @@ -132,6 +136,9 @@ jobs: rm -rf dist python setup.py sdist + + - name: test fitsio from sdist + run: | pushd dist/ fname=$(ls fitsio*.gz) @@ -140,10 +147,11 @@ jobs: pushd $dname pip install ${PIP_OPTIONS} -e . - pytest -vv --durations=20 ${PRP_FLAGS} fitsio + pytest --durations=20 ${PRP_FLAGS} fitsio python -c "import fitsio; assert fitsio.backend_has_bzip2_support()" python -c "import fitsio; assert fitsio.backend_has_curl_support()" python -c "import fitsio; assert fitsio.backend_is_reentrant()" + python -c "import fitsio; assert fitsio.fitsio_backend() == 'cfitsio'" popd popd From a07525d4d3a3aaa780a08b35ac81dd09edc1e2cf Mon Sep 17 00:00:00 2001 From: beckermr Date: Tue, 14 Jul 2026 13:22:42 -0500 Subject: [PATCH 27/28] test: make more robust by averaging three trials --- fitsio/tests/test_threading.py | 128 +++++++++++++++++++-------------- 1 file changed, 75 insertions(+), 53 deletions(-) diff --git a/fitsio/tests/test_threading.py b/fitsio/tests/test_threading.py index cd2a8032..3059ab1a 100644 --- a/fitsio/tests/test_threading.py +++ b/fitsio/tests/test_threading.py @@ -108,50 +108,61 @@ def _remove_files(): for fname in filenames: create_file(fname) - t0 = time.time() - if not read_only: - create_file(filenames[0]) - if not write_only: - read_file(filenames[0]) - t0_one = time.time() - t0 - print("one file time:", t0_one, flush=True) - if not read_only: - _remove_files() + n_trials = 3 + t0_threads = 0 + t0_one = 0 + t0_serial = 0 + + for _ in range(n_trials): + t0 = time.time() + if not read_only: + create_file(filenames[0]) + if not write_only: + read_file(filenames[0]) + t0_one += time.time() - t0 + if not read_only: + _remove_files() + + t0 = time.time() + with klass(max_workers=nt) as pool: + if not read_only: + futs = [ + pool.submit(create_file, filenames[i]) + for i in range(nt * fac) + ] + for fut in as_completed(futs): + fut.result() + if not write_only: + futs = [ + pool.submit(read_file, filenames[i]) + for i in range(nt * fac) + ] + for fut in as_completed(futs): + fut.result() + t0_threads += time.time() - t0 + if not read_only: + _remove_files() - t0 = time.time() - with klass(max_workers=nt) as pool: + t0 = time.time() if not read_only: - futs = [ - pool.submit(create_file, filenames[i]) - for i in range(nt * fac) - ] - for fut in as_completed(futs): - fut.result() + for fname in filenames: + create_file(fname) if not write_only: - futs = [ - pool.submit(read_file, filenames[i]) - for i in range(nt * fac) - ] - for fut in as_completed(futs): - fut.result() - t0_threads = time.time() - t0 + for fname in filenames: + read_file(fname) + t0_serial += time.time() - t0 + + t0_one /= n_trials + t0_serial /= n_trials + t0_threads /= n_trials + + print("one file time:", t0_one, flush=True) print( "parallel time / one file time", t0_threads / t0_one, "(perfect is %d)" % fac, flush=True, ) - if not read_only: - _remove_files() - - t0 = time.time() - if not read_only: - for fname in filenames: - create_file(fname) - if not write_only: - for fname in filenames: - read_file(fname) - t0_serial = time.time() - t0 print( "serial time / one file time:", t0_serial / t0_one, @@ -159,9 +170,9 @@ def _remove_files(): flush=True, ) - assert t0_threads < t0_serial / 1.5, ( + assert t0_threads < t0_serial, ( "Threading should be faster than serial! (%f < %f)" - % (t0_threads, t0_serial / 1.5) + % (t0_threads, t0_serial) ) @@ -186,28 +197,39 @@ def _read_file(fname): assert (fits[1].read() == -1).all() return True - t0 = time.time() - _read_file(fname) - t0_one = time.time() - t0 - print("\none file time:", t0_one, flush=True) + n_trials = 3 + t0_threads = 0 + t0_one = 0 + t0_serial = 0 - t0 = time.time() - with ThreadPoolExecutor(max_workers=nt) as pool: - futs = [pool.submit(_read_file, fname) for _ in range(nt)] + for _ in range(n_trials): + t0 = time.time() + _read_file(fname) + t0_one += time.time() - t0 + print("\none file time:", t0_one, flush=True) + + t0 = time.time() + with ThreadPoolExecutor(max_workers=nt) as pool: + futs = [pool.submit(_read_file, fname) for _ in range(nt)] + + assert all([fut.result() for fut in as_completed(futs)]) + t0_threads += time.time() - t0 + + t0 = time.time() + for _ in range(nt): + _read_file(fname) + t0_serial += time.time() - t0 + + t0_one /= n_trials + t0_serial /= n_trials + t0_threads /= n_trials - assert all([fut.result() for fut in as_completed(futs)]) - t0_threads = time.time() - t0 print( "parallel time / one file time", t0_threads / t0_one, "(perfect is 1)", flush=True, ) - - t0 = time.time() - for _ in range(nt): - _read_file(fname) - t0_serial = time.time() - t0 print( "serial time / one file time:", t0_serial / t0_one, @@ -215,7 +237,7 @@ def _read_file(fname): flush=True, ) - assert t0_threads < t0_serial / 1.5, ( + assert t0_threads < t0_serial, ( "Threading should be faster than serial! (%f < %f)" - % (t0_threads, t0_serial / 1.5) + % (t0_threads, t0_serial) ) From 4b409e1248c8024b9246bceb76713138fe885a7c Mon Sep 17 00:00:00 2001 From: beckermr Date: Tue, 14 Jul 2026 13:23:37 -0500 Subject: [PATCH 28/28] fix: wrong print out --- fitsio/tests/test_threading.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fitsio/tests/test_threading.py b/fitsio/tests/test_threading.py index 3059ab1a..7c982fa5 100644 --- a/fitsio/tests/test_threading.py +++ b/fitsio/tests/test_threading.py @@ -206,7 +206,6 @@ def _read_file(fname): t0 = time.time() _read_file(fname) t0_one += time.time() - t0 - print("\none file time:", t0_one, flush=True) t0 = time.time() with ThreadPoolExecutor(max_workers=nt) as pool: @@ -224,6 +223,7 @@ def _read_file(fname): t0_serial /= n_trials t0_threads /= n_trials + print("\none file time:", t0_one, flush=True) print( "parallel time / one file time", t0_threads / t0_one,