From f12915424d61e0564d649681b0aea900383cc9f8 Mon Sep 17 00:00:00 2001 From: kushasahu7 Date: Sat, 20 Jun 2026 16:58:18 +0530 Subject: [PATCH 1/4] hips2fits: make Pillow an optional dependency with a clear error PIL was imported unconditionally at module level, causing a hard crash on `import astroquery.hips2fits` when Pillow is not installed, even though it is only needed for jpg/png response decoding. Moved the import inside _parse_result behind a try/except ImportError, and added Pillow to the `all` extras in pyproject.toml so it is properly declared as optional. Added unit tests that verify a helpful ImportError is raised for both jpg and png formats when Pillow is absent. Closes #3556 --- astroquery/hips2fits/core.py | 9 +++++-- astroquery/hips2fits/tests/test_hips2fits.py | 25 ++++++++++++++++++++ pyproject.toml | 1 + 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/astroquery/hips2fits/core.py b/astroquery/hips2fits/core.py index c249945430..3bb4e0eb99 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: + 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..a45a4927f0 100644 --- a/astroquery/hips2fits/tests/test_hips2fits.py +++ b/astroquery/hips2fits/tests/test_hips2fits.py @@ -1,4 +1,8 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst +import sys +from unittest.mock import MagicMock, patch + +import pytest from astropy import wcs as astropy_wcs from matplotlib.colors import Colormap from astropy.coordinates import Angle, Longitude, Latitude @@ -7,6 +11,27 @@ from ..core import hips2fits +class TestHips2fits: + + def test_jpg_format_requires_pillow(self): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = b'fake_image_bytes' + + with patch.dict(sys.modules, {'PIL': None, 'PIL.Image': None}): + with pytest.raises(ImportError, match="Pillow is required"): + hips2fits._parse_result(mock_response, verbose=False, format='jpg') + + def test_png_format_requires_pillow(self): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = b'fake_image_bytes' + + with patch.dict(sys.modules, {'PIL': None, 'PIL.Image': None}): + with pytest.raises(ImportError, match="Pillow is required"): + hips2fits._parse_result(mock_response, verbose=False, format='png') + + class TestHips2fitsRemote: # Create a new WCS astropy object 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] From dca0cad4ee20946525f7982f2e80c17dfb3e66ba Mon Sep 17 00:00:00 2001 From: kushasahu7 Date: Sat, 20 Jun 2026 17:16:18 +0530 Subject: [PATCH 2/4] CHANGES: add hips2fits Pillow optional dependency entry for #3619 --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index da84509f75..cce7d581b8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -103,6 +103,13 @@ 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 +^^^^^^^^^ + +- ``Pillow`` is now an optional dependency: importing ``astroquery.hips2fits`` no + longer crashes when Pillow is not installed. 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] From 4303d21fe490188711fb2f134259695ea4f22e25 Mon Sep 17 00:00:00 2001 From: kushasahu7 Date: Mon, 20 Jul 2026 09:37:02 +0530 Subject: [PATCH 3/4] =?UTF-8?q?hips2fits:=20address=20review=20=E2=80=94?= =?UTF-8?q?=20parametrize=20tests,=20use=20skipif=20marker,=20reword=20cha?= =?UTF-8?q?ngelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES.rst | 3 +-- astroquery/hips2fits/tests/test_hips2fits.py | 27 +++++++++----------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index cce7d581b8..28b7986df9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -106,8 +106,7 @@ svo_fps hips2fits ^^^^^^^^^ -- ``Pillow`` is now an optional dependency: importing ``astroquery.hips2fits`` no - longer crashes when Pillow is not installed. A clear ``ImportError`` is raised +- 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 diff --git a/astroquery/hips2fits/tests/test_hips2fits.py b/astroquery/hips2fits/tests/test_hips2fits.py index a45a4927f0..919da7c316 100644 --- a/astroquery/hips2fits/tests/test_hips2fits.py +++ b/astroquery/hips2fits/tests/test_hips2fits.py @@ -1,6 +1,5 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst -import sys -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest from astropy import wcs as astropy_wcs @@ -10,26 +9,24 @@ from ..core import hips2fits +try: + from PIL import Image # noqa: F401 + HAS_PILLOW = True +except ImportError: + HAS_PILLOW = False -class TestHips2fits: - - def test_jpg_format_requires_pillow(self): - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = b'fake_image_bytes' - with patch.dict(sys.modules, {'PIL': None, 'PIL.Image': None}): - with pytest.raises(ImportError, match="Pillow is required"): - hips2fits._parse_result(mock_response, verbose=False, format='jpg') +class TestHips2fits: - def test_png_format_requires_pillow(self): + @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 patch.dict(sys.modules, {'PIL': None, 'PIL.Image': None}): - with pytest.raises(ImportError, match="Pillow is required"): - hips2fits._parse_result(mock_response, verbose=False, format='png') + with pytest.raises(ImportError, match="Pillow is required"): + hips2fits._parse_result(mock_response, verbose=False, format=image_format) class TestHips2fitsRemote: From faed33991aa2f1dc8223f72bde434d38346e10b2 Mon Sep 17 00:00:00 2001 From: kushasahu7 Date: Thu, 23 Jul 2026 18:39:58 +0530 Subject: [PATCH 4/4] hips2fits: test the jpg/png decode path The jpg/png branch of _parse_result had no offline test: the only unit tests covered the missing-Pillow error, which is skipped whenever Pillow is installed. Decode a small generated image to exercise the branch. The except ImportError guard cannot be reached when Pillow is present, so mark it no cover rather than leave it as a permanent coverage gap; it is still exercised by the skipif test in the mandatory-dependencies job. --- astroquery/hips2fits/core.py | 2 +- astroquery/hips2fits/tests/test_hips2fits.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/astroquery/hips2fits/core.py b/astroquery/hips2fits/core.py index 3bb4e0eb99..64ef29460d 100644 --- a/astroquery/hips2fits/core.py +++ b/astroquery/hips2fits/core.py @@ -325,7 +325,7 @@ def _parse_result(self, response, verbose, format): # jpg/png formats try: from PIL import Image - except ImportError: + except ImportError: # pragma: no cover raise ImportError( "Pillow is required to decode jpg/png responses from hips2fits. " "Install it with: pip install Pillow" diff --git a/astroquery/hips2fits/tests/test_hips2fits.py b/astroquery/hips2fits/tests/test_hips2fits.py index 919da7c316..5ac489c4b7 100644 --- a/astroquery/hips2fits/tests/test_hips2fits.py +++ b/astroquery/hips2fits/tests/test_hips2fits.py @@ -1,6 +1,8 @@ # 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 @@ -28,6 +30,22 @@ def test_image_format_requires_pillow(self, image_format): 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: