diff --git a/.gitignore b/.gitignore index 7d76d72..4173532 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ coverage.xml .hypothesis/ .pytest_cache/ cover/ +src/astro_image_display_api/version.py # Translations *.mo @@ -74,6 +75,7 @@ instance/ # Sphinx documentation docs/_build/ +docs/api/ # PyBuilder .pybuilder/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f1722a0..30083e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,3 @@ -ci: - autoupdate_commit_msg: 'chore: update pre-commit hooks' - repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 @@ -26,6 +23,7 @@ repos: - id: mixed-line-ending # Makes all line endings unix-style args: [--fix=lf] + exclude: docs/make.bat # Per the ruff documentation, this should be before black @@ -47,3 +45,9 @@ repos: rev: v2.4.1 hooks: - id: codespell + +# Make pre-commit-ci more reasonable +ci: + autofix_prs: false + autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' + autoupdate_schedule: weekly diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..f72c324 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version, and other tools you might need +build: + os: ubuntu-24.04 + tools: + python: "3.13" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally, but recommended, +# declare the Python requirements required to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +# python: +# install: +# - requirements: docs/requirements.txt diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/aida-logo.svg b/docs/_static/aida-logo.svg new file mode 100644 index 0000000..d4c256c --- /dev/null +++ b/docs/_static/aida-logo.svg @@ -0,0 +1,56 @@ + + + + diff --git a/docs/_static/astropy.css b/docs/_static/astropy.css new file mode 100644 index 0000000..ee3a3be --- /dev/null +++ b/docs/_static/astropy.css @@ -0,0 +1,26 @@ +/* Copied from astropy repo */ +/* Main page overview cards */ + +.sd-card .sd-card-img-top { + height: 52px; + width: 52px; + margin-left: auto; + margin-right: auto; + margin-top: 10px; +} + +/* Dark theme tweaking */ +html[data-theme=dark] .sd-card img[src*='.svg'] { + filter: invert(0.82) brightness(0.8) contrast(1.2); +} + +/* Flip the colours on graphviz graphs on dark mode */ +html[data-theme="dark"] div.graphviz > object.inheritance { + filter: invert(0.82) brightness(0.8) contrast(1.2); +} +html[data-theme="dark"] div.graphviz > object.graphviz { + filter: invert(0.82) brightness(0.8) contrast(1.2); +} +html[data-theme="dark"] ul.cooframelegend { + filter: invert(0.82) brightness(0.8) contrast(1.2); +} diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..b791986 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,7 @@ +.. _api_reference: + +AID API Reference +================= + +.. automodapi:: astro_image_display_api.interface_definition + :no-inheritance-diagram: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..2ddd2c1 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,208 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +# +# Astropy documentation build configuration file. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this file. +# +# All configuration values have a default. Some values are defined in +# the global Astropy configuration which is loaded here before anything else. +# See astropy.sphinx.conf for which values are set there. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('..')) +# IMPORTANT: the above commented section was generated by sphinx-quickstart, but +# is *NOT* appropriate for astropy or Astropy affiliated packages. It is left +# commented out with this explanation to make it clear why this should not be +# done. If the sys.path entry above is added, when the astropy.sphinx.conf +# import occurs, it will import the *source* version of astropy instead of the +# version installed (if invoked as "make html" or directly with sphinx), or the +# version in the build directory (if "python setup.py build_sphinx" is used). +# Thus, any C-extensions that are needed to build the documentation will *not* +# be accessible, and the documentation will not build correctly. + +import datetime +import sys +import tomllib +from importlib import import_module +from pathlib import Path + +from sphinx_astropy.conf.v2 import * # noqa: E402, F403 +from sphinx_astropy.conf.v2 import ( # noqa: E402 + exclude_patterns, + html_theme_options, + rst_epilog, +) + +# Grab minversion from pyproject.toml +with (Path(__file__).parents[1] / "pyproject.toml").open("rb") as f: + pyproject = tomllib.load(f) + +__minimum_python_version__ = pyproject["project"]["requires-python"].replace(">=", "") + +# -- General configuration ---------------------------------------------------- + +# By default, highlight as Python 3. +highlight_language = "python3" + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.2' + +# To perform a Sphinx version check that needs to be more specific than +# major.minor, call `check_sphinx_version("x.y.z")` here. +# check_sphinx_version("1.2.1") + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns.append("_templates") + +# This is added to the end of RST files - a good place to put substitutions to +# be used globally. +rst_epilog += """ +""" + +# extensions += ["sphinxcontrib.autodoc_pydantic"] +extensions += [ + "sphinx_design", +] + +# -- Project information ------------------------------------------------------ + +# This does not *have* to match the package name, but typically does +project = pyproject["project"]["name"].replace("-", "_").replace(" ", "_") +author = ", ".join(v["name"] for v in pyproject["project"]["authors"]) +copyright = f"{datetime.datetime.now().year}, {author}" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. + +import_module(project) +package = sys.modules[project] + +# The short X.Y version. +version = package.__version__.split("-", 1)[0] +# The full version, including alpha/beta/rc tags. +release = package.__version__ + +# Only include dev docs in dev version. +dev = "dev" in release +if not dev: + exclude_patterns += ["development/*"] + +# -- Options for HTML output -------------------------------------------------- + +# A NOTE ON HTML THEMES +# The global astropy configuration uses a custom theme, 'bootstrap-astropy', +# which is installed along with astropy. A different theme can be used or +# the options for this theme can be modified by overriding some of the +# variables set in the global configuration. The variables set in the +# global configuration are listed below, commented out. + + +# Add any paths that contain custom themes here, relative to this directory. +# To use a different custom theme, add the directory containing the theme. +# html_theme_path = [] + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. To override the custom theme, set this to the +# name of a builtin theme or the name of a custom theme in html_theme_path. +# html_theme = None + +html_theme_options.update( + { + "github_url": "https://github.com/astropy/astro-image-display-api", + "use_edit_page_button": True, + # https://github.com/pydata/pydata-sphinx-theme/issues/1492 + "navigation_with_keys": False, + } +) + +# A dictionary of values to pass into the template engine's context for all pages. +html_context = { + "default_mode": "dark", + "to_be_indexed": ["stable", "latest"], + "is_development": dev, + "github_user": "astropy", + "github_repo": "astro-image-display-api", + "github_version": "main", + "doc_path": "docs", +} + +html_css_files = ["astropy.css"] + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = '_static/astro-image-display-api-logo.svg' + +html_theme_options.update( + { + "logo": { + "alt_text": "astro-image-display-api - Home", + "text": "AIDA", + "image_light": "_static/aida-logo.svg", + "image_dark": "_static/aida-logo.svg", + } + } +) + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +html_favicon = "_static/favicon.ico" + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '' + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +html_title = f"{project} v{release}" + +# Output file base name for HTML help builder. +htmlhelp_basename = project + "doc" + + +# -- Options for LaTeX output ------------------------------------------------- + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ("index", project + ".tex", project + " Documentation", author, "manual") +] + + +# -- Options for manual page output ------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [("index", project.lower(), project + " Documentation", [author], 1)] + +# -- Turn on nitpicky mode for sphinx (to warn about references not found) ---- +# +# nitpicky = True +# nitpick_ignore = [] +# +# Some warnings are impossible to suppress, and you can list specific references +# that should be ignored in a nitpick-exceptions file which should be inside +# the docs/ directory. The format of the file should be: +# +# +# +# for example: +# +# py:class astropy.io.votable.tree.Element +# py:class astropy.io.votable.tree.SimpleElement +# py:class astropy.io.votable.tree.SimpleElementWithContent +# +# Uncomment the following lines to enable the exceptions: +# +# for line in open('nitpick-exceptions'): +# if line.strip() == "" or line.startswith("#"): +# continue +# dtype, target = line.split(None, 1) +# target = target.strip() +# nitpick_ignore.append((dtype, six.u(target))) diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..c99cba4 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,37 @@ +.. astro-image-display-api documentation master file, created by + sphinx-quickstart on Sun Jun 15 17:17:22 2025. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Astronomical Image Display API +============================== + +Who is this for? +################ + +The purpose of this API is to provide a standard interface for displaying +astronomical images. This package and API is aimed at *developers* who +want to write a package that displays astronomical images and provide a +uniform interface for users to interact with these images programmatically. + +If you are a user looking for a way to display astronomical images, please +see the `aida-backends`_ page, which has a list of packages that implement +this API. + +How to implement this API in your package +######################################### + +1. The API is described in the :ref:`api_reference`. It consists of a set of methods with + type annotations and extensive docstrings that describes the expected behavior. + Note that you do **not** need to subclass the + :py:class:`~astro_image_display_api.interface_definition.ImageViewerInterface`. The next + step explains how to assert that your package implements the API correctly. +2. :ref:`testing_AIDA_implementation` describes how to test your + package against the API. +3. There is a :ref:`reference_implementation` of the API that you can use as a + starting point for your own package. This reference implementation does not + do any image display itself, but provides a set of methods that you can + override to implement your own image display logic on top of the management + of image and catalog labels. You are **not** required to use this reference + implementation; it is just a convenience you can use to get started if you + want to. diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/reference_implementation.rst b/docs/reference_implementation.rst new file mode 100644 index 0000000..37a36c8 --- /dev/null +++ b/docs/reference_implementation.rst @@ -0,0 +1,10 @@ +.. _reference_implementation: + +Reference implementation of AIDA non-display logic +================================================== + +The class `astro_image_display_api.image_viewer_logic.ImageViewerLogic` is provided +as an *example* of how to implement the non-display logic of the Astronomical Image Display API (AIDA). +You do not need to use this class, but it is provided as a convenience to help you +get started with your own implementation. You can feel free to get started by subclassing +it and adding your own display logic on top of it. diff --git a/docs/testing_implementation.rst b/docs/testing_implementation.rst new file mode 100644 index 0000000..9acb8f6 --- /dev/null +++ b/docs/testing_implementation.rst @@ -0,0 +1,31 @@ +.. _testing_AIDA_implementation: + +Testing your implementation of AIDA +=================================== + +Our goal is to make it easy for you to test your implementation of the +Astronomical Image Display API (AIDA). There are two things you need to test: + +1. An instance of your class should pass the test + ``isinstance(your_instance, ImageDisplayInterface)``. + This ensures that your class has all of the attributes and methods in the interface. +2. To test the functionality of your implementation, we provide the class + :py:class:`~astro_image_display_api.api_test.ImageAPITest`. + To use it, create a subclass of :py:class:`~astro_image_display_api.api_test.ImageAPITest` in your test suite, and define + a single class attribute `image_widget_class`. See + :ref:`test_example` for an example of how to do this. + +These tests *do not* test the actual image display functionality of your package, nor +do they test the behaviour of your package via your package's user interface. You should +check that behavior in whatever way is appropriate for your package. + +.. _test_example: + +Example of using the test class +############################### + +.. literalinclude:: ../tests/test_image_viewer_logic.py + :language: python + +.. automodapi:: astro_image_display_api.api_test + :no-inheritance-diagram: diff --git a/pyproject.toml b/pyproject.toml index 4dda4cc..9b946fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ line-length = 88 [tool.ruff] # ruff 0.6.0 started automatically linting notebooks. We are not ready for that yet. -extend-exclude = ["*.ipynb"] +extend-exclude = ["*.ipynb", "docs/make.bat", "docs/conf.py"] [tool.ruff.lint] select = [ diff --git a/src/astro_image_display_api/__init__.py b/src/astro_image_display_api/__init__.py index 2398b83..ae46b16 100644 --- a/src/astro_image_display_api/__init__.py +++ b/src/astro_image_display_api/__init__.py @@ -2,5 +2,10 @@ # # SPDX-License-Identifier: MIT +try: + from .version import version as __version__ +except ImportError: + __version__ = "" + +from .api_test import * # noqa: F403 from .interface_definition import * # noqa: F403 -from .widget_api_test import * # noqa: F403 diff --git a/src/astro_image_display_api/widget_api_test.py b/src/astro_image_display_api/api_test.py similarity index 99% rename from src/astro_image_display_api/widget_api_test.py rename to src/astro_image_display_api/api_test.py index ee08711..af30c05 100644 --- a/src/astro_image_display_api/widget_api_test.py +++ b/src/astro_image_display_api/api_test.py @@ -18,10 +18,10 @@ from .interface_definition import ImageViewerInterface -__all__ = ["ImageWidgetAPITest"] +__all__ = ["ImageAPITest"] -class ImageWidgetAPITest: +class ImageAPITest: cursor_error_classes = ValueError @pytest.fixture diff --git a/src/astro_image_display_api/dummy_viewer.py b/src/astro_image_display_api/image_viewer_logic.py similarity index 99% rename from src/astro_image_display_api/dummy_viewer.py rename to src/astro_image_display_api/image_viewer_logic.py index 998de4b..8b0c028 100644 --- a/src/astro_image_display_api/dummy_viewer.py +++ b/src/astro_image_display_api/image_viewer_logic.py @@ -51,7 +51,7 @@ class ViewportInfo: @dataclass -class ImageViewer: +class ImageViewerLogic: """ This viewer does not do anything except making changes to its internal state to simulate the behavior of a real viewer. diff --git a/src/astro_image_display_api/interface_definition.py b/src/astro_image_display_api/interface_definition.py index 027cfbb..2160453 100644 --- a/src/astro_image_display_api/interface_definition.py +++ b/src/astro_image_display_api/interface_definition.py @@ -40,11 +40,11 @@ def load_image(self, data: Any, image_label: str | None = None) -> None: Parameters ---------- - data : Any + data : The data to load. This can be a FITS file, a 2D array, or an `astropy.nddata.NDData` object. - image_label : str, optional + image_label : optional The label for the image. Notes @@ -65,12 +65,12 @@ def set_cuts( Parameters ---------- - cuts : tuple or any Interval from `astropy.visualization` + cuts: any interval from `astropy.visualization` The cuts to set. If a tuple, it should be of the form ``(min, max)`` and will be interpreted as a `~astropy.visualization.ManualInterval`. - image_label : str, optional + image_label : optional The label of the image to set the cuts for. If not given and there is only one image loaded, the cuts for that image are set. @@ -93,14 +93,14 @@ def get_cuts(self, image_label: str | None = None) -> BaseInterval: Parameters ---------- - image_label : str, optional + image_label : optional The label of the image to get the cuts for. If not given and there is only one image loaded, the cuts for that image are returned. If there are multiple images and no label is provided, an error is raised. Returns ------- - cuts : `~astropy.visualization.BaseInterval` + cuts : `astropy.visualization.BaseInterval` The Astropy interval object representing the current cuts. Raises @@ -163,12 +163,12 @@ def set_colormap(self, map_name: str, image_label: str | None = None) -> None: Parameters ---------- - map_name : str + map_name The name of the colormap to set. This should be a valid colormap name from Matplotlib`_; not all backends will support all colormaps, so the viewer should handle errors gracefully. - image_label : str, optional + image_label : optional The label of the image to set the colormap for. If not given and there is only one image loaded, the colormap for that image is set. If there are multiple images and no label is provided, an error is raised. diff --git a/tests/test_astro_image_display_api.py b/tests/test_astro_image_display_api.py index 89eab94..6f7573e 100644 --- a/tests/test_astro_image_display_api.py +++ b/tests/test_astro_image_display_api.py @@ -12,8 +12,8 @@ def test_api_test_class_completeness(): required_attributes = ImageViewerInterface.__protocol_attrs__ # Get the text of the api tests - widget_api_test_content = get_pkg_data_contents( - "widget_api_test.py", package="astro_image_display_api" + api_test_content = get_pkg_data_contents( + "api_test.py", package="astro_image_display_api" ) # Loop over the attributes and check that the test class has a method # for each one whose name starts with test_ and ends with the attribute @@ -21,7 +21,7 @@ def test_api_test_class_completeness(): attr_present = [] image_viewer_name = "self.image" for attr in required_attributes: - attr_present.append(f"{image_viewer_name}.{attr}" in widget_api_test_content) + attr_present.append(f"{image_viewer_name}.{attr}" in api_test_content) missing_attributes = [ attr diff --git a/tests/test_dummy_viewer.py b/tests/test_dummy_viewer.py deleted file mode 100644 index 204d2e4..0000000 --- a/tests/test_dummy_viewer.py +++ /dev/null @@ -1,15 +0,0 @@ -from astro_image_display_api import ImageViewerInterface, ImageWidgetAPITest -from astro_image_display_api.dummy_viewer import ImageViewer - - -def test_instance(): - # Make sure that the ImageViewer class implements the ImageViewerInterface - image = ImageViewer() - assert isinstance(image, ImageViewerInterface) - - -class TestDummyViewer(ImageWidgetAPITest): - """ - Test functionality of the ImageViewer class.""" - - image_widget_class = ImageViewer diff --git a/tests/test_image_viewer_logic.py b/tests/test_image_viewer_logic.py new file mode 100644 index 0000000..e5b5ec0 --- /dev/null +++ b/tests/test_image_viewer_logic.py @@ -0,0 +1,26 @@ +# All implementations of the ImageViewerInterface will need to import +# these to carry out the tests. +from astro_image_display_api import ImageViewerInterface, ImageAPITest # noqa: I001 + +# This import should be replaced with an import of your specific +# implementation of the ImageViewerInterface. If you keep the +# "as ImageViewer" part, then the test below will work without modification. +from astro_image_display_api.image_viewer_logic import ImageViewerLogic as ImageViewer + +# You should not need to change the test below. + + +def test_instance(): + # Make sure that the ImageViewer has all required methods and attributes + # defined in the ImageViewerInterface. + image = ImageViewer() + assert isinstance(image, ImageViewerInterface) + + +class TestViewer(ImageAPITest): + """ + Test whether the non-display aspects of the ImageViewer + implementation are correct. + """ + + image_widget_class = ImageViewer