diff --git a/CHANGES.rst b/CHANGES.rst index e5bc75291..403304388 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -24,7 +24,7 @@ New Features ^^^^^^^^^^^^ - Added an ASDF extension to provide converters for photutils aperture - and PSF classes. [#2211] + and PSF classes. [#2211, #2268] Bug Fixes ^^^^^^^^^ diff --git a/photutils/converters/__init__.py b/photutils/converters/__init__.py index 3c2099a46..f614d7d8b 100644 --- a/photutils/converters/__init__.py +++ b/photutils/converters/__init__.py @@ -1,20 +1,20 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst """ -ASDF converters. +ASDF converters for photutils objects. + +``CircularApertureConverter`` requires only ``asdf``. ``AiryDiskPSFConverter`` +requires ``asdf-astropy`` for full functionality; it is always registered but +raises a clear ``ImportError`` when ``asdf-astropy`` is not installed. """ -_ASDF_ASTROPY_INSTALLED = True +from .apertures import CircularApertureConverter # noqa: F401 +from .functional_models import AiryDiskPSFConverter # noqa: F401 try: - import asdf_astropy # noqa: F401 -- needed to register the converters + import asdf_astropy # noqa: F401 -- only used to detect availability except ImportError: _ASDF_ASTROPY_INSTALLED = False +else: + _ASDF_ASTROPY_INSTALLED = True -if _ASDF_ASTROPY_INSTALLED: - from .functional_models import AiryDiskPSFConverter - from .apertures import CircularApertureConverter - -__all__ = [ - 'AiryDiskPSFConverter', - 'CircularApertureConverter', -] +__all__ = ['AiryDiskPSFConverter', 'CircularApertureConverter'] diff --git a/photutils/converters/functional_models.py b/photutils/converters/functional_models.py index 41d8466d7..c3ce86246 100644 --- a/photutils/converters/functional_models.py +++ b/photutils/converters/functional_models.py @@ -3,13 +3,15 @@ Converters to and from the ASDF format for photutils.psf.functional_models. """ -from . import _ASDF_ASTROPY_INSTALLED - -if _ASDF_ASTROPY_INSTALLED: +try: from asdf_astropy.converters.transform.core import (TransformConverterBase, parameter_to_value) -else: - TransformConverterBase = object + + _ASDF_ASTROPY_AVAILABLE = True +except ImportError: + from asdf.extension import Converter as TransformConverterBase + + _ASDF_ASTROPY_AVAILABLE = False __all__ = ['AiryDiskPSFConverter'] @@ -22,22 +24,39 @@ class AiryDiskPSFConverter(TransformConverterBase): tags = ('tag:astropy.org:photutils/psf/airy_disk_psf-*',) types = ('photutils.psf.AiryDiskPSF',) - def to_yaml_tree_transform(self, model, tag, ctx): # noqa: ARG002 - return { - 'flux': parameter_to_value(model.flux), - 'x_0': parameter_to_value(model.x_0), - 'y_0': parameter_to_value(model.y_0), - 'radius': parameter_to_value(model.radius), - 'bbox_factor': model.bbox_factor, - } - - def from_yaml_tree_transform(self, node, tag, ctx): # noqa: ARG002 - from photutils.psf import AiryDiskPSF - - return AiryDiskPSF( - flux=node['flux'], - x_0=node['x_0'], - y_0=node['y_0'], - radius=node['radius'], - bbox_factor=node['bbox_factor'], - ) + if _ASDF_ASTROPY_AVAILABLE: + def to_yaml_tree_transform(self, model, tag, ctx): # noqa: ARG002 + return { + 'flux': parameter_to_value(model.flux), + 'x_0': parameter_to_value(model.x_0), + 'y_0': parameter_to_value(model.y_0), + 'radius': parameter_to_value(model.radius), + 'bbox_factor': model.bbox_factor, + } + + def from_yaml_tree_transform(self, node, tag, ctx): # noqa: ARG002 + from photutils.psf import AiryDiskPSF + + return AiryDiskPSF( + flux=node['flux'], + x_0=node['x_0'], + y_0=node['y_0'], + radius=node['radius'], + bbox_factor=node['bbox_factor'], + ) + else: + def to_yaml_tree(self, obj, tag, ctx): # noqa: ARG002 + msg = ( + 'asdf-astropy must be installed to serialize AiryDiskPSF ' + 'to ASDF format. Install it with:\n' + ' pip install asdf-astropy' + ) + raise ImportError(msg) + + def from_yaml_tree(self, node, tag, ctx): # noqa: ARG002 + msg = ( + 'asdf-astropy must be installed to deserialize AiryDiskPSF ' + 'from ASDF format. Install it with:\n' + ' pip install asdf-astropy' + ) + raise ImportError(msg) diff --git a/photutils/converters/tests/test_apertures.py b/photutils/converters/tests/test_apertures.py index 8a5b3932f..41a7f8a61 100644 --- a/photutils/converters/tests/test_apertures.py +++ b/photutils/converters/tests/test_apertures.py @@ -8,7 +8,6 @@ import pytest from photutils.aperture import CircularAperture -from photutils.converters import _ASDF_ASTROPY_INSTALLED apertures = [ CircularAperture(positions=[(1, 2), (3, 4)], r=5), @@ -16,8 +15,6 @@ ] -@pytest.mark.skipif(not _ASDF_ASTROPY_INSTALLED, - reason='asdf-astropy is not installed') @pytest.mark.parametrize('aperture', apertures) def test_aperture_converters(tmp_path, aperture): """ diff --git a/photutils/converters/tests/test_psf.py b/photutils/converters/tests/test_psf.py index 28610c7a4..46015c191 100644 --- a/photutils/converters/tests/test_psf.py +++ b/photutils/converters/tests/test_psf.py @@ -26,3 +26,35 @@ def test_psf_converters(tmp_path, airy_disk_psf): for parameter in pars: assert_array_equal(getattr(psf, parameter), getattr(psf2, parameter)) + + +@pytest.mark.skipif(_ASDF_ASTROPY_INSTALLED, + reason='asdf-astropy is installed; error not expected') +def test_airy_disk_psf_serialize_no_asdf_astropy(tmp_path): + """ + Test that serializing AiryDiskPSF without asdf-astropy raises a + friendly ImportError. + """ + from photutils.psf import AiryDiskPSF + + psf = AiryDiskPSF(flux=1, x_0=0, y_0=0, radius=5) + with asdf.AsdfFile() as af: + af['psf'] = psf + with pytest.raises(ImportError, + match='asdf-astropy must be installed'): + af.write_to(tmp_path / 'psf.asdf') + + +@pytest.mark.skipif(_ASDF_ASTROPY_INSTALLED, + reason='asdf-astropy is installed; error not expected') +def test_airy_disk_psf_deserialize_no_asdf_astropy(): + """ + Test that deserializing AiryDiskPSF without asdf-astropy raises a + friendly ImportError. + """ + from photutils.converters.functional_models import AiryDiskPSFConverter + + converter = AiryDiskPSFConverter() + with pytest.raises(ImportError, + match='asdf-astropy must be installed'): + converter.from_yaml_tree({}, None, None) diff --git a/photutils/extension.py b/photutils/extension.py index dd2394006..798b847eb 100644 --- a/photutils/extension.py +++ b/photutils/extension.py @@ -8,24 +8,23 @@ from asdf.extension import ManifestExtension from asdf.resource import DirectoryResourceMapping -from .converters import apertures # import CircularApertureConverter -from .converters import functional_models # import AiryDiskPSFConverter +from .converters import apertures, functional_models __all__ = [ 'PHOTUTILS_APERTURE_CONVERTERS', + 'PHOTUTILS_CONVERTERS', 'PHOTUTILS_MANIFEST_URIS', 'PHOTUTILS_PSF_CONVERTERS', ] -PHOTUTILS_PSF_CONVERTERS = [ - functional_models.AiryDiskPSFConverter(), -] - PHOTUTILS_APERTURE_CONVERTERS = [ apertures.CircularApertureConverter(), ] +PHOTUTILS_PSF_CONVERTERS = [ + functional_models.AiryDiskPSFConverter(), +] -PHOTUTILS_CONVERTERS = PHOTUTILS_PSF_CONVERTERS + PHOTUTILS_APERTURE_CONVERTERS +PHOTUTILS_CONVERTERS = PHOTUTILS_APERTURE_CONVERTERS + PHOTUTILS_PSF_CONVERTERS # The order here is important; asdf will prefer to use extensions # that occur earlier in the list.