From 3e10373c89a7746fad667eebba8b66063626f231 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Tue, 5 Dec 2023 15:55:10 -0500 Subject: [PATCH] MNT: Infrastructure and other updates --- .flake8 | 2 + .github/workflows/cron-tests.yml | 59 ++++------ .github/workflows/tox-tests.yml | 88 ++++++-------- .readthedocs.yml => .readthedocs.yaml | 4 +- CHANGES.rst | 31 +++-- MANIFEST.in | 7 +- README.rst | 21 ++-- conftest.py | 24 ++++ docs/conf.py | 37 ++---- pyproject.toml | 104 ++++++++++++++++- setup.cfg | 107 ------------------ setup.py | 66 ----------- specreduce/background.py | 2 +- specreduce/calibration_data.py | 10 +- specreduce/conftest.py | 77 ++++++++++++- specreduce/core.py | 4 +- specreduce/extract.py | 3 +- specreduce/fluxcal.py | 16 ++- specreduce/table_utils.py | 7 +- ...det_image_seq5_MIRIMAGE_P750Lexp1_s2d.fits | Bin 0 -> 72000 bytes specreduce/tests/test_background.py | 29 ++--- specreduce/tests/test_extinction.py | 8 +- specreduce/tests/test_extract.py | 19 +--- .../tests/test_get_reference_file_path.py | 2 +- specreduce/tests/test_image_parsing.py | 51 +++------ specreduce/tests/test_linelists.py | 41 +++---- specreduce/tests/test_specphot_stds.py | 5 +- specreduce/tests/test_synth_data.py | 8 +- specreduce/tests/test_tracing.py | 2 +- .../tests/test_wavelength_calibration.py | 9 +- specreduce/tracing.py | 4 +- specreduce/utils/synth_data.py | 16 +-- specreduce/wavelength_calibration.py | 12 +- tox.ini | 59 ++++------ 34 files changed, 418 insertions(+), 516 deletions(-) create mode 100644 .flake8 rename .readthedocs.yml => .readthedocs.yaml (87%) create mode 100644 conftest.py delete mode 100644 setup.cfg delete mode 100755 setup.py create mode 100644 specreduce/tests/data/transposed_det_image_seq5_MIRIMAGE_P750Lexp1_s2d.fits diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..7da1f960 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 100 diff --git a/.github/workflows/cron-tests.yml b/.github/workflows/cron-tests.yml index 84530154..2bb4da17 100644 --- a/.github/workflows/cron-tests.yml +++ b/.github/workflows/cron-tests.yml @@ -5,48 +5,31 @@ name: Weekly Tests on: + pull_request: + # We also want this workflow triggered if the 'Extra CI' label is added + # or present when PR is updated + types: + - synchronize + - labeled schedule: # run every Monday at 6am UTC - cron: '0 6 * * 1' -env: - TOXARGS: '-v' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - # Set up matrix to run tox tests across lists of os, python version, and tox environment - matrix_tests: - runs-on: ${{ matrix.os }} - strategy: - matrix: - # Github actions supports ubuntu, windows, and macos virtual environments: - # https://help.github.com/en/actions/reference/virtual-environments-for-github-hosted-runners - # - # Only run on ubuntu by default, but can add other os's to the test matrix here. - # For example -- os: [ubuntu-latest, macos-latest, windows-latest] - include: - - os: ubuntu-latest - python: '3.11' - tox_env: 'linkcheck' - - os: ubuntu-latest - python: '3.12' - tox_env: 'py312-test-devdeps' - - os: ubuntu-latest - python: '3.12' - tox_env: 'py312-test-predeps' + tests: + if: (github.repository == 'astropy/specreduce' && (github.event_name == 'schedule' || github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'Extra CI'))) + uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 + with: + submodules: false + coverage: '' + envs: | + - name: Check URLs in docs + linux: linkcheck - steps: - - name: Check out repository - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set up python ${{ matrix.python }} with tox environment ${{ matrix.tox_env }} on ${{ matrix.os }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python }} - - name: Install base dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox - - name: Test with tox - run: | - tox -e ${{ matrix.tox_env }} + - name: Python 3.12 on Linux with pre-releases + linux: py312-test-alldeps-predeps + toxargs: -v diff --git a/.github/workflows/tox-tests.yml b/.github/workflows/tox-tests.yml index 3005622d..75f0dba9 100644 --- a/.github/workflows/tox-tests.yml +++ b/.github/workflows/tox-tests.yml @@ -11,64 +11,44 @@ on: tags: - '*' pull_request: + schedule: + # run every Monday at 6am UTC + - cron: '0 6 * * 1' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -env: - TOXARGS: '-v' - jobs: - # Set up matrix to run tox tests across lists of os, python version, and tox environment - matrix_tests: - runs-on: ${{ matrix.os }} - strategy: - matrix: - # Github actions supports ubuntu, windows, and macos virtual environments: - # https://help.github.com/en/actions/reference/virtual-environments-for-github-hosted-runners - # - # Only run on ubuntu by default, but can add other os's to the test matrix here. - # For example -- os: [ubuntu-latest, macos-latest, windows-latest] - include: - - os: ubuntu-latest - python: '3.10' - tox_env: 'py310-test-cov' - - os: ubuntu-latest - python: '3.11' - tox_env: 'py311-test' - - os: ubuntu-latest - python: '3.12' - tox_env: 'py312-test' - - os: macos-latest - python: '3.12' - tox_env: 'py312-test-devdeps' - - os: ubuntu-latest - python: '3.12' - tox_env: 'codestyle' - - os: ubuntu-latest - python: '3.10' - tox_env: 'py310-test-oldestdeps' + tests: + uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV }} + with: + submodules: false + coverage: '' + envs: | + - name: Codestyle + linux: codestyle + + - name: Python 3.8 on Linux with oldest supported dependencies + linux: py38-test-oldestdeps + toxargs: -v + + - name: Python 3.9 on Windows with minimal dependencies + windows: py39-test + toxargs: -v + + - name: Python 3.10 on OSX with minimal dependencies + macos: py310-test + toxargs: -v + + - name: Python 3.11 on Linux with all dependencies, remote data, and coverage + linux: py311-test-alldeps-cov + coverage: codecov + toxargs: -v + posargs: --remote-data=any - steps: - - name: Check out repository - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set up python ${{ matrix.python }} with tox environment ${{ matrix.tox_env }} on ${{ matrix.os }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python }} - - name: Install base dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox - - name: Test with tox - run: | - tox -e ${{ matrix.tox_env }} - - name: Upload coverage to codecov - if: "contains(matrix.tox_env, '-cov')" - uses: codecov/codecov-action@v3 - with: - file: ./coverage.xml - verbose: true + - name: (Allowed Failure) Python 3.12 on Linux with dev dependencies + linux: py312-test-devdeps + toxargs: -v diff --git a/.readthedocs.yml b/.readthedocs.yaml similarity index 87% rename from .readthedocs.yml rename to .readthedocs.yaml index b9cf0085..16476ed2 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yaml @@ -1,11 +1,11 @@ version: 2 build: - os: ubuntu-20.04 + os: ubuntu-22.04 apt_packages: - graphviz tools: - python: "3.10" + python: "3.11" sphinx: builder: html diff --git a/CHANGES.rst b/CHANGES.rst index 9d005521..4ce17f25 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,30 +5,37 @@ New Features ^^^^^^^^^^^^ - Added 'interpolated_profile' option for HorneExtract. If The ``interpolated_profile`` option -is used, the image will be sampled in various wavelength bins (set by -``n_bins_interpolated_profile``), averaged in those bins, and samples are then -interpolated between (linear by default, interpolation degree can be set with -the ``interp_degree_interpolated_profile`` parameter) to generate a continuously varying -spatial profile that can be evaluated at any wavelength.[ #173] + is used, the image will be sampled in various wavelength bins (set by + ``n_bins_interpolated_profile``), averaged in those bins, and samples are then + interpolated between (linear by default, interpolation degree can be set with + the ``interp_degree_interpolated_profile`` parameter) to generate a continuously varying + spatial profile that can be evaluated at any wavelength. [#173] API Changes ^^^^^^^^^^^ -- Fit residuals exposed for wavelength calibration in WavelengthCalibration1D.fit_residuals. [#446] +- Fit residuals exposed for wavelength calibration in ``WavelengthCalibration1D.fit_residuals``. [#446] Bug Fixes ^^^^^^^^^ - Output 1D spectra from Background no longer include NaNs. Output 1D -spectra from BoxcarExtract no longer include NaNs when none are present -in the extraction window. NaNs in the window will still propagate to -BoxcarExtract's extracted 1D spectrum. [#159] + spectra from BoxcarExtract no longer include NaNs when none are present + in the extraction window. NaNs in the window will still propagate to + BoxcarExtract's extracted 1D spectrum. [#159] -- Backgrounds using median statistic properly ignore zero-weighted pixels -[#159] +- Backgrounds using median statistic properly ignore zero-weighted pixels. + [#159] -- HorneExtract now accepts 'None' as a vaild option for bkgrd_prof [#171] +- HorneExtract now accepts 'None' as a vaild option for ``bkgrd_prof``. [#171] +Other changes +^^^^^^^^^^^^^ + +- The following packages are now optional dependencies because they are not + required for core functionality: ``matplotlib``, ``photutils``, ``synphot``. + To install them anyway, use the ``[all]`` specifier when you install specreduce; e.g.: + ``pip install specreduce[all]`` [#202] 1.3.0 (2022-12-05) ------------------ diff --git a/MANIFEST.in b/MANIFEST.in index 8c13ad01..a43828c6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,16 +1,11 @@ include README.rst include CHANGES.rst -include setup.cfg -include LICENSE.rst include pyproject.toml -recursive-include specreduce *.pyx *.c *.pxd recursive-include docs * recursive-include licenses * -recursive-include scripts * +prune notebook_sandbox prune build prune docs/_build prune docs/api - -global-exclude *.pyc *.o diff --git a/README.rst b/README.rst index f58f086d..b13adbf3 100644 --- a/README.rst +++ b/README.rst @@ -1,22 +1,27 @@ Specreduce ========== -.. image:: https://github.com/astropy/specreduce/workflows/Python%20Tests/badge.svg - :target: https://github.com/astropy/specreduce/actions +.. image:: https://github.com/astropy/specreduce/actions/workflows/tox-tests.yml/badge.svg?branch=main + :target: https://github.com/astropy/specreduce/actions/workflows/tox-tests.yml :alt: CI Status +.. image:: https://codecov.io/gh/astropy/specreduce/graph/badge.svg?token=3fLGjZ2Pe0 + :target: https://codecov.io/gh/astropy/specreduce + :alt: Coverage + .. image:: https://readthedocs.org/projects/specreduce/badge/?version=latest - :target: http://specreduce.readthedocs.io/en/latest/?badge=latest + :target: http://specreduce.readthedocs.io/en/latest/ :alt: Documentation Status -.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.6608788.svg - :target: https://doi.org/10.5281/zenodo.6608788 - :alt: Zenodo DOI 10.5281/zenodo.6608788 +.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.6608787.svg + :target: https://zenodo.org/doi/10.5281/zenodo.6608787 + :alt: Zenodo DOI 10.5281/zenodo.6608787 .. image:: http://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat - :target: http://www.astropy.org/ + :target: http://www.astropy.org/ + :alt: Powered by Astropy -Specreduce is an Astropy affiliated package with the goal of providing a shared +Specreduce is an Astropy coordinated package with the goal of providing a shared set of Python utilities that can be used to reduce and calibrate spectroscopic data. License diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..fc06df18 --- /dev/null +++ b/conftest.py @@ -0,0 +1,24 @@ +"""Need to repeat the astropy header config here for tox.""" + +try: + from pytest_astropy_header.display import PYTEST_HEADER_MODULES, TESTED_VERSIONS # noqa: E501 + ASTROPY_HEADER = True +except ImportError: + ASTROPY_HEADER = False + + +def pytest_configure(config): + + if ASTROPY_HEADER: + + config.option.astropy_header = True + + # Customize the following lines to add/remove entries from the list of + # packages for which version numbers are displayed when running the tests. # noqa: E501 + PYTEST_HEADER_MODULES.pop('Pandas', None) + PYTEST_HEADER_MODULES['scikit-image'] = 'skimage' + PYTEST_HEADER_MODULES['photutils'] = 'photutils' + PYTEST_HEADER_MODULES['synphot'] = 'synphot' + + from specreduce import __version__ + TESTED_VERSIONS["specreduce"] = __version__ diff --git a/docs/conf.py b/docs/conf.py index ea4f7f0b..3cc78f7e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,10 +25,10 @@ # Thus, any C-extensions that are needed to build the documentation will *not* # be accessible, and the documentation will not build correctly. -import os import sys import datetime -from importlib import import_module + +from specreduce import __version__ try: from sphinx_astropy.conf.v1 import * # noqa @@ -36,13 +36,6 @@ print('ERROR: the documentation requires the sphinx-astropy package to be installed') sys.exit(1) -# Get configuration information from setup.cfg -from configparser import ConfigParser -conf = ConfigParser() - -conf.read([os.path.join(os.path.dirname(__file__), '..', 'setup.cfg')]) -setup_cfg = dict(conf.items('metadata')) - # -- General configuration ---------------------------------------------------- # By default, highlight as Python 3. @@ -67,22 +60,19 @@ # -- Project information ------------------------------------------------------ # This does not *have* to match the package name, but typically does -project = setup_cfg['name'] -author = setup_cfg['author'] +project = "specreduce" +author = "Astropy Specreduce contributors" copyright = '{0}, {1}'.format( - datetime.datetime.now().year, setup_cfg['author']) + 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(setup_cfg['name']) -package = sys.modules[setup_cfg['name']] - # The short X.Y version. -version = package.__version__.split('-', 1)[0] +version = __version__.split('-', 1)[0] # The full version, including alpha/beta/rc tags. -release = package.__version__ +release = __version__ # -- Options for HTML output -------------------------------------------------- @@ -154,19 +144,6 @@ # -- Options for the edit_on_github extension --------------------------------- -if setup_cfg.get('edit_on_github').lower() == 'true': - - extensions += ['sphinx_astropy.ext.edit_on_github'] - - edit_on_github_project = setup_cfg['github_project'] - edit_on_github_branch = "main" - - edit_on_github_source_root = "" - edit_on_github_doc_root = "docs" - -# -- Resolving issue number to links in changelog ----------------------------- -github_issues_url = 'https://github.com/{0}/issues/'.format(setup_cfg['github_project']) - # -- Turn on nitpicky mode for sphinx (to warn about references not found) ---- # nitpicky = True diff --git a/pyproject.toml b/pyproject.toml index c83a0e56..f9abbcf2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,109 @@ -[build-system] +[project] +name = "specreduce" +dynamic = ["version"] +authors = [ + { name = "Astropy Specreduce contributors", email = "astropy-dev@googlegroups.com" } +] +license = {file = "licenses/LICENSE.rst"} +description = "Astropy coordinated package for Spectroscopic Reductions" +readme = "README.rst" +requires-python = ">=3.8" +dependencies = [ + "numpy", + "astropy", + "scipy", + "specutils>=1.9.1", +] + +[project.optional-dependencies] +test = [ + "pytest-astropy", + "photutils", +] +docs = [ + "sphinx-astropy", + "matplotlib", + "photutils", +] +all = [ + "matplotlib", + "photutils", + "synphot", +] + +[project.urls] +Homepage = "http://astropy.org/" +Repository = "https://github.com/astropy/specreduce.git" +Documentation = "https://specreduce.readthedocs.io/" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages] +find = {} # Scanning implicit namespaces is active by default +[tool.setuptools.package-data] +"specreduce.tests" = ["data/*.fits"] + +[tool.setuptools_scm] +version_file = "specreduce/version.py" + +[build-system] requires = ["setuptools", "setuptools_scm", ] - build-backend = 'setuptools.build_meta' [tool.pytest.ini_options] +minversion = 7.0 +testpaths = [ + "specreduce", + "docs", +] +astropy_header = true +doctest_plus = "enabled" +text_file_format = "rst" +addopts = [ + "--color=yes", + "--doctest-rst", +] +xfail_strict = true +filterwarnings = [ + "error", + "ignore:numpy\\.ufunc size changed:RuntimeWarning", + "ignore:numpy\\.ndarray size changed:RuntimeWarning", + "ignore:Can\\'t import specreduce_data package", + # Python 3.12 warning from dateutil imported by matplotlib + "ignore:.*utcfromtimestamp:DeprecationWarning", +] + +[tool.coverage] + + [tool.coverage.run] + omit = [ + "specreduce/_astropy_init*", + "specreduce/conftest.py", + "specreduce/tests/*", + "specreduce/version*", + "*/specreduce/_astropy_init*", + "*/specreduce/conftest.py", + "*/specreduce/tests/*", + "*/specreduce/version*", + ] -filterwarnings = ["ignore::DeprecationWarning:datetime",] + [tool.coverage.report] + exclude_lines = [ + # Have to re-enable the standard pragma + "pragma: no cover", + # Don't complain about packages we have installed + "except ImportError", + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + # Don't complain about script hooks + "'def main(.*):'", + # Ignore branches that don't pertain to this version of Python + "pragma: py{ignore_python_version}", + # Don't complain about IPython completion helper + "def _ipython_key_completions_", + ] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3f0fd7d5..00000000 --- a/setup.cfg +++ /dev/null @@ -1,107 +0,0 @@ -[metadata] -name = specreduce -author = Astropy Specreduce contributors -author_email = astropy-dev@googlegroups.com -license = BSD 3-Clause -license_file = licenses/LICENSE.rst -url = http://astropy.org/ -description = Astropy affiliated package for Spectroscopic Reductions -long_description = file: README.rst -long_description_content_type = text/x-rst -edit_on_github = False -github_project = astropy/specreduce - -[options] -zip_safe = False -packages = find: -python_requires = >=3.8 -setup_requires = setuptools_scm -install_requires = - astropy - specutils>=1.9.1 - synphot - matplotlib - photutils - pyparsing - -[options.entry_points] - -[options.extras_require] -test = - pytest-astropy -docs = - sphinx-astropy - -[options.package_data] -specreduce = - datasets/* - datasets/line_lists/* - datasets/line_lists/NIST/* - datasets/line_lists/iraf/* - datasets/line_lists/iraf/air/* - datasets/line_lists/iraf/vacuum/* - datasets/onedstds/* - datasets/onedstds/blackbody/* - datasets/onedstds/bstdscal/* - datasets/onedstds/ctio/* - datasets/onedstds/ctiocal/* - datasets/onedstds/ctionewcal/* - datasets/onedstds/iidscal/* - datasets/onedstds/irscal/* - datasets/onedstds/oke1990/* - datasets/onedstds/redcal/* - datasets/onedstds/spec16cal/* - datasets/onedstds/spec50cal/* - datasets/onedstds/spechayescal/* - datasets/onedstds/snfactory/* - datasets/onedstds/gemini/* - datasets/onedstds/eso/* - datasets/onedstds/eso/Xshooter/* - datasets/onedstds/eso/ctiostan/* - datasets/onedstds/eso/hststan/* - datasets/onedstds/eso/okestan/* - datasets/onedstds/eso/wdstan/* - -[tool:pytest] -testpaths = "specreduce" "docs" -astropy_header = true -doctest_plus = enabled -text_file_format = rst -addopts = --doctest-rst -filterwarnings = - error - ignore:Can\'t import specreduce_data package: - ignore:numpy.ndarray size changed: - -[coverage:run] -omit = - specreduce/_astropy_init* - specreduce/conftest.py - specreduce/*setup_package* - specreduce/tests/* - specreduce/*/tests/* - specreduce/extern/* - specreduce/version* - */specreduce/_astropy_init* - */specreduce/conftest.py - */specreduce/*setup_package* - */specreduce/tests/* - */specreduce/*/tests/* - */specreduce/extern/* - */specreduce/version* - -[coverage:report] -exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - # Don't complain about packages we have installed - except ImportError - # Don't complain if tests don't hit assertions - raise AssertionError - raise NotImplementedError - # Don't complain about script hooks - def main\(.*\): - # Ignore branches that don't pertain to this version of Python - pragma: py{ignore_python_version} - # Don't complain about IPython completion helper - def _ipython_key_completions_ diff --git a/setup.py b/setup.py deleted file mode 100755 index 7d32a44d..00000000 --- a/setup.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python -# Licensed under a 3-clause BSD style license - see LICENSE.rst - -# NOTE: The configuration for the package, including the name, version, and -# other information are set in the setup.cfg file. - -import os -import sys - -from setuptools import setup - - -# First provide helpful messages if contributors try and run legacy commands -# for tests or docs. - -TEST_HELP = """ -Note: running tests is no longer done using 'python setup.py test'. Instead -you will need to run: - - tox -e test - -If you don't already have tox installed, you can install it with: - - pip install tox - -If you only want to run part of the test suite, you can also use pytest -directly with:: - - pip install -e .[test] - pytest - -For more information, see: - - http://docs.astropy.org/en/latest/development/testguide.html#running-tests -""" - -if 'test' in sys.argv: - print(TEST_HELP) - sys.exit(1) - -DOCS_HELP = """ -Note: building the documentation is no longer done using -'python setup.py build_docs'. Instead you will need to run: - - tox -e build_docs - -If you don't already have tox installed, you can install it with: - - pip install tox - -You can also build the documentation with Sphinx directly using:: - - pip install -e .[docs] - cd docs - make html - -For more information, see: - - http://docs.astropy.org/en/latest/install.html#builddocs -""" - -if 'build_docs' in sys.argv or 'build_sphinx' in sys.argv: - print(DOCS_HELP) - sys.exit(1) - -setup(use_scm_version={'write_to': os.path.join('specreduce', 'version.py')}) diff --git a/specreduce/background.py b/specreduce/background.py index 526f46c6..77c6a980 100644 --- a/specreduce/background.py +++ b/specreduce/background.py @@ -4,9 +4,9 @@ from dataclasses import dataclass, field import numpy as np +from astropy import units as u from astropy.nddata import NDData from astropy.utils.decorators import deprecated_attribute -from astropy import units as u from specutils import Spectrum1D from specreduce.core import _ImageParser diff --git a/specreduce/calibration_data.py b/specreduce/calibration_data.py index 339feb89..20052ed4 100644 --- a/specreduce/calibration_data.py +++ b/specreduce/calibration_data.py @@ -6,13 +6,11 @@ import warnings import numpy as np - -import astropy.units as u +from astropy import units as u from astropy.table import Table, vstack, QTable from astropy.utils.data import download_file from astropy.utils.exceptions import AstropyUserWarning -import synphot from specutils import Spectrum1D from specutils.utils.wcs_utils import vac_to_air @@ -111,7 +109,7 @@ def get_reference_file_path( cache : bool (default: False) Set whether file is cached if file is downloaded. - repo_url : str (default: https://raw.githubusercontent.com/astropy/specreduce-data) + repo_url : str Base repository URL for the reference data. repo_branch : str (default: main) @@ -298,6 +296,8 @@ def load_MAST_calspec(filename, cache=True, show_progress=False): If ``remote`` is True, the spectrum will be downloaded from MAST. Set ``remote`` to False to load a local file. + .. note:: This function requires ``synphot`` to be installed separately. + Parameters ---------- filename : str @@ -333,8 +333,10 @@ def load_MAST_calspec(filename, cache=True, show_progress=False): if file_path is None: return None else: + import synphot _, wave, flux = synphot.specio.read_fits_spec(file_path) + # DEV: pllim does not think this is necessary at all but whatever. # the calspec data stores flux in synphot's FLAM units. convert to flux units # supported directly by astropy.units. mJy is chosen since it's the JWST # standard and can easily be converted to/from AB magnitudes. diff --git a/specreduce/conftest.py b/specreduce/conftest.py index 559241af..bcbb55f1 100644 --- a/specreduce/conftest.py +++ b/specreduce/conftest.py @@ -3,10 +3,13 @@ # get picked up when running the tests inside an interpreter using # packagename.test -import astropy.units as u import numpy as np import pytest -from specutils import Spectrum1D +from astropy import units as u +from astropy.io import fits +from astropy.nddata import CCDData, NDData, VarianceUncertainty +from astropy.utils.data import get_pkg_data_filename +from specutils import Spectrum1D, SpectralAxis try: from pytest_astropy_header.display import PYTEST_HEADER_MODULES, TESTED_VERSIONS # noqa: E501 @@ -15,6 +18,72 @@ ASTROPY_HEADER = False +# Test image is comprised of 30 rows with 10 columns each. Row content +# is row index itself. This makes it easy to predict what should be the +# value extracted from a region centered at any arbitrary Y position. +def _mk_test_data(imgtype, nrows=30, ncols=10): + image_ones = np.ones(shape=(nrows, ncols)) + image = image_ones.copy() + for j in range(nrows): + image[j, ::] *= j + if imgtype == "raw": + pass # no extra processing + elif imgtype == "ccddata": + image = CCDData(image, unit=u.Jy) + else: # spectrum + flux = image * u.DN + uncert = VarianceUncertainty(image_ones) + if imgtype == "spec_no_axis": + image = Spectrum1D(flux, uncertainty=uncert) + else: # "spec" + image = Spectrum1D(flux, spectral_axis=np.arange(ncols) * u.um, uncertainty=uncert) + return image + + +@pytest.fixture +def mk_test_img_raw(): + return _mk_test_data("raw") + + +@pytest.fixture +def mk_test_img(): + return _mk_test_data("ccddata") + + +@pytest.fixture +def mk_test_spec_no_spectral_axis(): + return _mk_test_data("spec_no_axis") + + +@pytest.fixture +def mk_test_spec_with_spectral_axis(): + return _mk_test_data("spec") + + +# Test data file already transposed like this: +# fn = download_file('https://stsci.box.com/shared/static/exnkul627fcuhy5akf2gswytud5tazmw.fits', cache=True) # noqa: E501 +# img = fits.getdata(fn).T +@pytest.fixture +def all_images(): + np.random.seed(7) + + filename = get_pkg_data_filename( + "data/transposed_det_image_seq5_MIRIMAGE_P750Lexp1_s2d.fits", package="specreduce.tests") + img = fits.getdata(filename) + flux = img * (u.MJy / u.sr) + sax = SpectralAxis(np.linspace(14.377, 3.677, flux.shape[-1]) * u.um) + unc = VarianceUncertainty(np.random.rand(*flux.shape)) + + all_images = {} + all_images['arr'] = img + all_images['s1d'] = Spectrum1D(flux, spectral_axis=sax, uncertainty=unc) + all_images['s1d_pix'] = Spectrum1D(flux, uncertainty=unc) + all_images['ccd'] = CCDData(img, uncertainty=unc, unit=flux.unit) + all_images['ndd'] = NDData(img, uncertainty=unc, unit=flux.unit) + all_images['qnt'] = img * flux.unit + return all_images + + @pytest.fixture def spec1d(): np.random.seed(7) @@ -56,6 +125,8 @@ def pytest_configure(config): # packages for which version numbers are displayed when running the tests. # noqa: E501 PYTEST_HEADER_MODULES.pop('Pandas', None) PYTEST_HEADER_MODULES['scikit-image'] = 'skimage' + PYTEST_HEADER_MODULES['photutils'] = 'photutils' + PYTEST_HEADER_MODULES['synphot'] = 'synphot' - from . import __version__ + from specreduce import __version__ TESTED_VERSIONS["specreduce"] = __version__ diff --git a/specreduce/core.py b/specreduce/core.py index 69fe01ca..a6ed2a29 100644 --- a/specreduce/core.py +++ b/specreduce/core.py @@ -1,11 +1,11 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst import inspect -import numpy as np +from dataclasses import dataclass +import numpy as np from astropy import units as u from astropy.nddata import VarianceUncertainty -from dataclasses import dataclass from specutils import Spectrum1D __all__ = ['SpecreduceOperation'] diff --git a/specreduce/extract.py b/specreduce/extract.py index 07b74c7e..f3dab4bb 100644 --- a/specreduce/extract.py +++ b/specreduce/extract.py @@ -4,15 +4,14 @@ from dataclasses import dataclass, field import numpy as np - from astropy import units as u from astropy.modeling import Model, models, fitting from astropy.nddata import NDData, VarianceUncertainty from scipy.interpolate import RectBivariateSpline +from specutils import Spectrum1D from specreduce.core import SpecreduceOperation from specreduce.tracing import Trace, FlatTrace -from specutils import Spectrum1D __all__ = ['BoxcarExtract', 'HorneExtract', 'OptimalExtract'] diff --git a/specreduce/fluxcal.py b/specreduce/fluxcal.py index 02d98fd9..aee181fb 100644 --- a/specreduce/fluxcal.py +++ b/specreduce/fluxcal.py @@ -1,16 +1,12 @@ import os import numpy as np - -import matplotlib.pyplot as plt - +from astropy import units as u from astropy.constants import c as cc from astropy.table import Table -import astropy.units as u - from scipy.interpolate import UnivariateSpline - from specutils import Spectrum1D + from specreduce.core import SpecreduceOperation @@ -151,8 +147,8 @@ def onedstd(self, stdstar): subdirectory and file name. For example: - >>> standard_sensfunc(obj_wave, obj_flux, stdstar='spec50cal/bd284211.dat', \ - mode='spline') # doctest: +SKIP + + >>> standard_sensfunc(obj_wave, obj_flux, stdstar='spec50cal/bd284211.dat', mode='spline') # doctest: +SKIP If no std is supplied, or an improper path is given, raises a ValueError. @@ -160,7 +156,7 @@ def onedstd(self, stdstar): ------- standard: astropy.talbe.Table A table with the onedstd data. - """ + """ # noqa: E501 std_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'datasets', 'onedstds') @@ -198,6 +194,7 @@ def standard_sensfunc(self, standard, mode='linear', polydeg=9, (Default is 9.) display : bool, optional If True, plot the sensfunc. (Default is False.) + This requires ``matplotlib`` to be installed. badlines : array-like list A list of values (lines) to mask-out of when generating sensfunc. @@ -260,6 +257,7 @@ def standard_sensfunc(self, standard, mode='linear', polydeg=9, sensfunc_spec = Spectrum1D(spectral_axis=obj_wave, flux=sensfunc_out) if display is True: + import matplotlib.pyplot as plt plt.figure() plt.plot(obj_wave, obj_flux * sensfunc_out, c="C0", label="Observed x sensfunc", alpha=0.5) diff --git a/specreduce/table_utils.py b/specreduce/table_utils.py index 118bc552..76fa5346 100644 --- a/specreduce/table_utils.py +++ b/specreduce/table_utils.py @@ -1,8 +1,9 @@ -"""Utility functions to parse main NIST table. -""" +"""Utility functions to parse main NIST table.""" -from astropy.table import Table, vstack import numpy as np +from astropy.table import Table, vstack + +__all__ = [] def sort_table_by_element(table, elem_list): diff --git a/specreduce/tests/data/transposed_det_image_seq5_MIRIMAGE_P750Lexp1_s2d.fits b/specreduce/tests/data/transposed_det_image_seq5_MIRIMAGE_P750Lexp1_s2d.fits new file mode 100644 index 0000000000000000000000000000000000000000..5a7fb248d0a78c391ea3950cfac0f1b000c8305b GIT binary patch literal 72000 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 J{(rgx{{?@yCEfr4 literal 0 HcmV?d00001 diff --git a/specreduce/tests/test_background.py b/specreduce/tests/test_background.py index 6302facd..11095d59 100644 --- a/specreduce/tests/test_background.py +++ b/specreduce/tests/test_background.py @@ -1,29 +1,16 @@ -import pytest import numpy as np - -import astropy.units as u -from astropy.nddata import VarianceUncertainty +import pytest from specutils import Spectrum1D from specreduce.background import Background from specreduce.tracing import FlatTrace, ArrayTrace -# NOTE: same test image as in test_extract.py -# Test image is comprised of 30 rows with 10 columns each. Row content -# is row index itself. This makes it easy to predict what should be the -# value extracted from a region centered at any arbitrary Y position. -img = np.ones(shape=(30, 10)) -for j in range(img.shape[0]): - img[j, ::] *= j -image = Spectrum1D(img * u.DN, - uncertainty=VarianceUncertainty(np.ones_like(img))) -image_um = Spectrum1D(image.flux, - spectral_axis=np.arange(image.data.shape[1]) * u.um, - uncertainty=VarianceUncertainty(np.ones_like(image.data))) - - -def test_background(): +def test_background(mk_test_img_raw, mk_test_spec_no_spectral_axis, + mk_test_spec_with_spectral_axis): + img = mk_test_img_raw + image = mk_test_spec_no_spectral_axis + image_um = mk_test_spec_with_spectral_axis # # Try combinations of extraction center, and even/odd # extraction aperture sizes. @@ -92,7 +79,9 @@ def test_background(): assert np.isnan(bg.sub_spectrum().flux).sum() == 0 -def test_warnings_errors(): +def test_warnings_errors(mk_test_spec_no_spectral_axis): + image = mk_test_spec_no_spectral_axis + # image.shape (30, 10) with pytest.warns(match="background window extends beyond image boundaries"): Background.two_sided(image, 25, 4, width=3) diff --git a/specreduce/tests/test_extinction.py b/specreduce/tests/test_extinction.py index 000587c4..53b6b1a0 100644 --- a/specreduce/tests/test_extinction.py +++ b/specreduce/tests/test_extinction.py @@ -1,11 +1,9 @@ -import pytest - import numpy as np - -import astropy.units as u +import pytest +from astropy import units as u from astropy.utils.exceptions import AstropyUserWarning -from ..calibration_data import ( +from specreduce.calibration_data import ( AtmosphericExtinction, AtmosphericTransmission, SUPPORTED_EXTINCTION_MODELS diff --git a/specreduce/tests/test_extract.py b/specreduce/tests/test_extract.py index da3d4c88..4465a1bf 100644 --- a/specreduce/tests/test_extract.py +++ b/specreduce/tests/test_extract.py @@ -1,9 +1,8 @@ import numpy as np import pytest - -import astropy.units as u +from astropy import units as u from astropy.modeling import models -from astropy.nddata import CCDData, VarianceUncertainty, UnknownUncertainty +from astropy.nddata import VarianceUncertainty, UnknownUncertainty from astropy.tests.helper import assert_quantity_allclose from specreduce.extract import ( @@ -12,20 +11,6 @@ from specreduce.tracing import FlatTrace, ArrayTrace -# Test image is comprised of 30 rows with 10 columns each. Row content -# is row index itself. This makes it easy to predict what should be the -# value extracted from a region centered at any arbitrary Y position. - - -@pytest.fixture -def mk_test_img(nrows=30, ncols=10): - image = np.ones(shape=(nrows, ncols)) - for j in range(image.shape[0]): - image[j, ::] *= j - image = CCDData(image, unit=u.Jy) - return image - - def add_gaussian_source(image, amps=2, stddevs=2, means=None): """ Modify `image.data` to add a horizontal spectrum across the image. diff --git a/specreduce/tests/test_get_reference_file_path.py b/specreduce/tests/test_get_reference_file_path.py index bea0d823..d619f2c5 100644 --- a/specreduce/tests/test_get_reference_file_path.py +++ b/specreduce/tests/test_get_reference_file_path.py @@ -1,6 +1,6 @@ import pytest -from ..calibration_data import get_reference_file_path, get_pypeit_data_path +from specreduce.calibration_data import get_reference_file_path, get_pypeit_data_path @pytest.mark.remote_data diff --git a/specreduce/tests/test_image_parsing.py b/specreduce/tests/test_image_parsing.py index 8f83210f..39765f37 100644 --- a/specreduce/tests/test_image_parsing.py +++ b/specreduce/tests/test_image_parsing.py @@ -1,42 +1,18 @@ import numpy as np - from astropy import units as u -from astropy.io import fits -from astropy.nddata import CCDData, NDData, VarianceUncertainty -from astropy.utils.data import download_file +from specutils import Spectrum1D from specreduce.extract import HorneExtract from specreduce.tracing import FlatTrace -from specutils import Spectrum1D, SpectralAxis - - -# fetch test image -fn = download_file('https://stsci.box.com/shared/static/exnkul627fcuhy5akf2gswytud5tazmw.fits', - cache=True) - -# duplicate image in all accepted formats -# (one Spectrum1D variant has a physical spectral axis; the other is in pixels) -img = fits.getdata(fn).T -flux = img * u.MJy / u.sr -sax = SpectralAxis(np.linspace(14.377, 3.677, flux.shape[-1]) * u.um) -unc = VarianceUncertainty(np.random.rand(*flux.shape)) - -all_images = {} -all_images['arr'] = img -all_images['s1d'] = Spectrum1D(flux, spectral_axis=sax, uncertainty=unc) -all_images['s1d_pix'] = Spectrum1D(flux, uncertainty=unc) -all_images['ccd'] = CCDData(img, uncertainty=unc, unit=flux.unit) -all_images['ndd'] = NDData(img, uncertainty=unc, unit=flux.unit) -all_images['qnt'] = img * flux.unit - -# save default values used for spectral axis and uncertainty when they are not -# available from the image object or provided by the user -sax_def = np.arange(img.shape[1]) * u.pix -unc_def = np.ones_like(img) # (for use inside tests) -def compare_images(key, collection, compare='s1d'): +def compare_images(all_images, key, collection, compare='s1d'): + # save default values used for spectral axis and uncertainty when they are not + # available from the image object or provided by the user + unc_def = np.ones_like(all_images['arr']) + sax_def = np.arange(unc_def.shape[1]) * u.pix + # was input converted to Spectrum1D? assert isinstance(collection[key], Spectrum1D), (f"image '{key}' not " "of type Spectrum1D") @@ -71,16 +47,19 @@ def compare_images(key, collection, compare='s1d'): # test consistency of general image parser results -def test_parse_general(): +def test_parse_general(all_images): all_images_parsed = {k: FlatTrace._parse_image(object, im) for k, im in all_images.items()} - for key in all_images_parsed.keys(): - compare_images(key, all_images_parsed) + compare_images(all_images, key, all_images_parsed) # use verified general image parser results to check HorneExtract's image parser -def test_parse_horne(): +def test_parse_horne(all_images): + # save default values used for uncertainty when it is + # available from the image object or provided by the user + unc_def = np.ones_like(all_images['arr']) + # HorneExtract's parser is more stringent than the general one, hence the # separate test. Given proper inputs, both should produce the same results. images_collection = {k: {} for k in all_images.keys()} @@ -102,4 +81,4 @@ def test_parse_horne(): col[key] = HorneExtract._parse_image(object, img, **defaults) - compare_images(key, col, compare='general') + compare_images(all_images, key, col, compare='general') diff --git a/specreduce/tests/test_linelists.py b/specreduce/tests/test_linelists.py index 61e1ca45..70b89f66 100644 --- a/specreduce/tests/test_linelists.py +++ b/specreduce/tests/test_linelists.py @@ -1,6 +1,6 @@ import pytest -from ..calibration_data import load_pypeit_calibration_lines +from specreduce.calibration_data import load_pypeit_calibration_lines @pytest.mark.remote_data @@ -10,16 +10,15 @@ def test_pypeit_single(): """ line_tab = load_pypeit_calibration_lines('HeI', cache=True, show_progress=False) assert line_tab is not None - if line_tab is not None: - assert "HeI" in line_tab['ion'] - assert sorted(list(line_tab.columns)) == [ - 'Instr', - 'NIST', - 'Source', - 'amplitude', - 'ion', - 'wave' - ] + assert "HeI" in line_tab['ion'] + assert sorted(list(line_tab.columns)) == [ + 'Instr', + 'NIST', + 'Source', + 'amplitude', + 'ion', + 'wave' + ] @pytest.mark.remote_data @@ -29,9 +28,8 @@ def test_pypeit_list(): """ line_tab = load_pypeit_calibration_lines(['HeI', 'NeI'], cache=True, show_progress=False) assert line_tab is not None - if line_tab is not None: - assert "HeI" in line_tab['ion'] - assert "NeI" in line_tab['ion'] + assert "HeI" in line_tab['ion'] + assert "NeI" in line_tab['ion'] @pytest.mark.remote_data @@ -39,10 +37,9 @@ def test_pypeit_empty(): """ Test to make sure None is returned if an empty list is passed. """ - with pytest.warns() as record: + with pytest.warns(UserWarning, match='No calibration lines'): line_tab = load_pypeit_calibration_lines([], cache=True, show_progress=False) - assert line_tab is None - assert 'No calibration lines' in record[0].message.args[0] + assert line_tab is None @pytest.mark.remote_data @@ -50,10 +47,8 @@ def test_pypeit_input_validation(): """ Check that bad inputs for ``pypeit`` linelists raise the appropriate warnings and exceptions """ - with pytest.raises(ValueError, match=r'.*Invalid calibration lamps specification.*'): - _ = load_pypeit_calibration_lines({}, cache=True, show_progress=False) + with pytest.raises(ValueError, match='.*Invalid calibration lamps specification.*'): + load_pypeit_calibration_lines({}, cache=True, show_progress=False) - with pytest.warns() as record: - _ = load_pypeit_calibration_lines(['HeI', 'ArIII'], cache=True, show_progress=False) - if not record: - pytest.fails("Expected warning about nonexistant linelist for ArIII.") + with pytest.warns(UserWarning, match="ArIII not in the list of supported calibration line lists"): # noqa: E501 + load_pypeit_calibration_lines(['HeI', 'ArIII'], cache=True, show_progress=False) diff --git a/specreduce/tests/test_specphot_stds.py b/specreduce/tests/test_specphot_stds.py index 868315ba..ea153264 100644 --- a/specreduce/tests/test_specphot_stds.py +++ b/specreduce/tests/test_specphot_stds.py @@ -1,9 +1,6 @@ import pytest -from ..calibration_data import ( - load_MAST_calspec, - load_onedstds -) +from specreduce.calibration_data import load_MAST_calspec, load_onedstds @pytest.mark.remote_data diff --git a/specreduce/tests/test_synth_data.py b/specreduce/tests/test_synth_data.py index 276be972..a90efe15 100644 --- a/specreduce/tests/test_synth_data.py +++ b/specreduce/tests/test_synth_data.py @@ -1,10 +1,10 @@ import pytest - -from specreduce.utils.synth_data import make_2d_trace_image, make_2d_arc_image, make_2d_spec_image -from astropy.nddata import CCDData +from astropy import units as u from astropy.modeling import models +from astropy.nddata import CCDData from astropy.wcs import WCS -import astropy.units as u + +from specreduce.utils.synth_data import make_2d_trace_image, make_2d_arc_image, make_2d_spec_image def test_make_2d_trace_image(): diff --git a/specreduce/tests/test_tracing.py b/specreduce/tests/test_tracing.py index a609677b..63bcabfe 100644 --- a/specreduce/tests/test_tracing.py +++ b/specreduce/tests/test_tracing.py @@ -1,7 +1,7 @@ import numpy as np import pytest - from astropy.modeling import models + from specreduce.utils.synth_data import make_2d_trace_image from specreduce.tracing import Trace, FlatTrace, ArrayTrace, FitTrace diff --git a/specreduce/tests/test_wavelength_calibration.py b/specreduce/tests/test_wavelength_calibration.py index 7accf9dd..539e2ea3 100644 --- a/specreduce/tests/test_wavelength_calibration.py +++ b/specreduce/tests/test_wavelength_calibration.py @@ -1,12 +1,11 @@ -from numpy.testing import assert_allclose import numpy as np import pytest - -from astropy.table import QTable -import astropy.units as u -from astropy.modeling.models import Polynomial1D +from astropy import units as u from astropy.modeling.fitting import LinearLSQFitter +from astropy.modeling.models import Polynomial1D +from astropy.table import QTable from astropy.tests.helper import assert_quantity_allclose +from numpy.testing import assert_allclose from specreduce import WavelengthCalibration1D diff --git a/specreduce/tracing.py b/specreduce/tracing.py index eadbbf37..a508dd20 100644 --- a/specreduce/tracing.py +++ b/specreduce/tracing.py @@ -1,14 +1,14 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst +import warnings from copy import deepcopy from dataclasses import dataclass, field -import warnings +import numpy as np from astropy.modeling import Model, fitting, models from astropy.nddata import NDData from astropy.stats import gaussian_sigma_to_fwhm from astropy.utils.decorators import deprecated -import numpy as np from specreduce.core import _ImageParser diff --git a/specreduce/utils/synth_data.py b/specreduce/utils/synth_data.py index cf5b73fd..06dc199e 100644 --- a/specreduce/utils/synth_data.py +++ b/specreduce/utils/synth_data.py @@ -1,14 +1,11 @@ # Licensed under a 3-clause BSD style license - see ../../licenses/LICENSE.rst import numpy as np - -from photutils.datasets import apply_poisson_noise - -import astropy.units as u +from astropy import units as u from astropy.modeling import models from astropy.nddata import CCDData -from astropy.wcs import WCS from astropy.stats import gaussian_fwhm_to_sigma +from astropy.wcs import WCS from specreduce.calibration_data import load_pypeit_calibration_lines @@ -54,7 +51,7 @@ def make_2d_trace_image( Power index of the source's Moffat profile. Use small number here to emulate extended source. add_noise : bool (default=True) - If True, add Poisson noise to the image + If True, add Poisson noise to the image; requires ``photutils`` to be installed. Returns ------- ccd_im : `~astropy.nddata.CCDData` @@ -76,6 +73,7 @@ def make_2d_trace_image( z = background + profile(trace) if add_noise: + from photutils.datasets import apply_poisson_noise trace_image = apply_poisson_noise(z) else: trace_image = z @@ -135,7 +133,7 @@ def make_2d_arc_image( The tilt function to apply along the cross-dispersion axis to simulate tilted or curved emission lines. add_noise : bool (default=True) - If True, add Poisson noise to the image + If True, add Poisson noise to the image; requires ``photutils`` to be installed. Returns ------- @@ -315,6 +313,7 @@ def make_2d_arc_image( z += line_mod(yy) if add_noise: + from photutils.datasets import apply_poisson_noise arc_image = apply_poisson_noise(z) else: arc_image = z @@ -387,7 +386,7 @@ def make_2d_spec_image( Power index of the source's Moffat profile. Use small number here to emulate extended source. add_noise : bool (default=True) - If True, add Poisson noise to the image + If True, add Poisson noise to the image; requires ``photutils`` to be installed. """ arc_image = make_2d_arc_image( nx=nx, @@ -419,6 +418,7 @@ def make_2d_spec_image( spec_image = arc_image.data + trace_image.data + background if add_noise: + from photutils.datasets import apply_poisson_noise spec_image = apply_poisson_noise(spec_image) ccd_im = CCDData(spec_image, unit=u.count, wcs=arc_image.wcs) diff --git a/specreduce/wavelength_calibration.py b/specreduce/wavelength_calibration.py index 43696c92..e52ec206 100644 --- a/specreduce/wavelength_calibration.py +++ b/specreduce/wavelength_calibration.py @@ -1,14 +1,14 @@ -from astropy.modeling.models import Linear1D +from functools import cached_property + +import numpy as np +from astropy import units as u from astropy.modeling.fitting import LMLSQFitter, LinearLSQFitter +from astropy.modeling.models import Linear1D from astropy.table import QTable, hstack -import astropy.units as u -from functools import cached_property -from gwcs import wcs from gwcs import coordinate_frames as cf -import numpy as np +from gwcs import wcs from specutils import Spectrum1D - __all__ = ['WavelengthCalibration1D'] diff --git a/tox.ini b/tox.ini index 29a45ded..6b4f20ef 100644 --- a/tox.ini +++ b/tox.ini @@ -1,21 +1,16 @@ [tox] envlist = - py{310,311,312}-test{,-devdeps,-predeps}{,-cov} - build_docs + py{38,39,310,311,312}-test{,-alldeps}{,-oldestdeps,-devdeps,-predeps}{,-cov} + linkcheck codestyle -requires = - setuptools - pip >= 19.3.1 -isolated_build = true [testenv] # Pass through the following environment variables which may be needed for the CI -passenv = HOME,WINDIR,LC_ALL,LC_CTYPE,CC,CI +passenv = HOME,WINDIR,CI setenv = - devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/astropy/simple https://pypi.anaconda.org/scientific-python-nightly-wheels/simple - py312: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/astropy/simple + devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/astropy/simple https://pypi.anaconda.org/liberfa/simple https://pypi.anaconda.org/scientific-python-nightly-wheels/simple # Run the tests in a temporary directory to make sure that we don't import # this package from the source tree @@ -31,39 +26,43 @@ changedir = .tmp/{envname} # description = run tests - devdeps: with the latest developer version of key dependencies + alldeps: with optional dependencies oldestdeps: with the oldest supported version of key dependencies - cov: enable remote data and measure test coverage + devdeps: with the latest developer version of key dependencies + predeps: with pre-releases of key dependencies + cov: with test coverage # The following provides some specific pinnings for key packages deps = devdeps: numpy>=0.0.dev0 devdeps: scipy>=0.0.dev0 + devdeps: pyerfa>=0.0.dev0 devdeps: astropy>=0.0.dev0 devdeps: git+https://github.com/astropy/specutils.git#egg=specutils devdeps: git+https://github.com/astropy/photutils.git#egg=photutils + devdeps: git+https://github.com/spacetelescope/synphot_refactor.git#egg=synphot - oldestdeps: numpy==1.22.4 - oldestdeps: astropy==5.1 - oldestdeps: scipy==1.8.0 - oldestdeps: matplotlib==3.5 - oldestdeps: photutils==1.0.0 - oldestdeps: specutils==1.9.1 - - # Currently need dev astropy with python 3.12 as well - py312: astropy>=0.0.dev0 + oldestdeps: numpy==1.22.* + oldestdeps: astropy==5.1.* + oldestdeps: scipy==1.8.* + oldestdeps: matplotlib==3.5.* + oldestdeps: photutils==1.0.* + oldestdeps: specutils==1.9.* # The following indicates which extras_require from setup.cfg will be installed extras = - test: test - build_docs: docs + test + alldeps: all + +install_command = + !devdeps: python -I -m pip install + # Force dev dependency with C-extension (synphot) to also build with numpy-dev + devdeps: python -I -m pip install -v --pre commands = # Force numpy-dev after matplotlib downgrades it (https://github.com/matplotlib/matplotlib/issues/26847) devdeps: python -m pip install --pre --upgrade --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy - # Maybe we also have to do this for scipy? - devdeps: python -m pip install --pre --upgrade --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple scipy pip freeze !cov: pytest --pyargs specreduce {toxinidir}/docs {posargs} cov: pytest --pyargs specreduce {toxinidir}/docs --cov specreduce --cov-config={toxinidir}/setup.cfg --remote-data {posargs} @@ -73,14 +72,6 @@ pip_pre = predeps: true !predeps: false -[testenv:build_docs] -changedir = docs -description = invoke sphinx-build to build the HTML docs -extras = docs -commands = - pip freeze - sphinx-build -W -b html . _build/html - [testenv:linkcheck] changedir = docs description = check the links in the HTML docs @@ -92,6 +83,6 @@ commands = [testenv:codestyle] skip_install = true changedir = . -description = check code style, e.g. with flake8 +description = check code style, e.g., with flake8 deps = flake8 -commands = flake8 specreduce --count --max-line-length=100 +commands = flake8 specreduce --count