diff --git a/CHANGES.rst b/CHANGES.rst index c6158ea041..38e7a798f8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -104,6 +104,12 @@ svo_fps - Add ``get_filter_metadata`` to allow retrieval of filter metadata. [#3528] - Add ``get_zeropoint`` to allow retrieval of filter zeropoints and allow kwarg passing to ``get_filter_metadata``. [#3545] +hips2fits +^^^^^^^^^ + +- Fix the import error for not available optional dependency Pillow. A clear ``ImportError`` is raised + only when a jpg/png response is actually decoded. [#3619] + heasarc ^^^^^^^ - Add ``query_constraints`` to allow querying of different catalog columns. [#3403] diff --git a/astroquery/hips2fits/core.py b/astroquery/hips2fits/core.py index c249945430..64ef29460d 100644 --- a/astroquery/hips2fits/core.py +++ b/astroquery/hips2fits/core.py @@ -9,8 +9,6 @@ from astropy.io import fits from astropy.coordinates import Angle import astropy.units as u -from PIL import Image - from astropy.utils.exceptions import AstropyUserWarning from ..query import BaseQuery @@ -325,6 +323,13 @@ def _parse_result(self, response, verbose, format): return hdul else: # jpg/png formats + try: + from PIL import Image + except ImportError: # pragma: no cover + raise ImportError( + "Pillow is required to decode jpg/png responses from hips2fits. " + "Install it with: pip install Pillow" + ) bytes = io.BytesIO(bytes_str) im = Image.open(bytes) data = np.asarray(im) diff --git a/astroquery/hips2fits/tests/test_hips2fits.py b/astroquery/hips2fits/tests/test_hips2fits.py index 668f60b9b4..5ac489c4b7 100644 --- a/astroquery/hips2fits/tests/test_hips2fits.py +++ b/astroquery/hips2fits/tests/test_hips2fits.py @@ -1,4 +1,9 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst +import io +from unittest.mock import MagicMock + +import numpy as np +import pytest from astropy import wcs as astropy_wcs from matplotlib.colors import Colormap from astropy.coordinates import Angle, Longitude, Latitude @@ -6,6 +11,41 @@ from ..core import hips2fits +try: + from PIL import Image # noqa: F401 + HAS_PILLOW = True +except ImportError: + HAS_PILLOW = False + + +class TestHips2fits: + + @pytest.mark.skipif(HAS_PILLOW, reason="Pillow is installed; the missing-Pillow path cannot be exercised") + @pytest.mark.parametrize("image_format", ["jpg", "png"]) + def test_image_format_requires_pillow(self, image_format): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = b'fake_image_bytes' + + with pytest.raises(ImportError, match="Pillow is required"): + hips2fits._parse_result(mock_response, verbose=False, format=image_format) + + @pytest.mark.skipif(not HAS_PILLOW, reason="Pillow is required to decode jpg/png responses") + @pytest.mark.parametrize("image_format", ["jpg", "png"]) + def test_image_format_decoded_with_pillow(self, image_format): + buffer = io.BytesIO() + Image.new('RGB', (4, 3), color=(255, 0, 0)).save( + buffer, format='JPEG' if image_format == 'jpg' else 'PNG') + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = buffer.getvalue() + + data = hips2fits._parse_result(mock_response, verbose=False, format=image_format) + + assert isinstance(data, np.ndarray) + assert data.shape == (3, 4, 3) + class TestHips2fitsRemote: diff --git a/pyproject.toml b/pyproject.toml index bbe6c2c6cd..e266b4677e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ all = [ "boto3", "botocore", "regions>=0.5", + "Pillow", ] [build-system]