From be19611d8f3c11793a3e2239546d9f4043b4133d Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Wed, 26 Jul 2023 14:47:42 -0400 Subject: [PATCH 01/21] New requires function decorator. --- docs/development/index.rst | 4 ++- sbpy/utils/__init__.py | 4 +-- sbpy/utils/decorators.py | 60 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 sbpy/utils/decorators.py diff --git a/docs/development/index.rst b/docs/development/index.rst index 951b8d22b..815eafb9b 100644 --- a/docs/development/index.rst +++ b/docs/development/index.rst @@ -121,7 +121,9 @@ Technical requirements * customized exceptions and warnings are encouraged, and should ultimately be derived from the base classes in `sbpy.exceptions` * if you use `~sbpy.data.DataClass` objects, extend the :ref:`field - name list` list where it makes sense + name list` list where it makes sense +* consider using the `sbpy.utils.decorators.requires` decorator to test for the + presence of optional dependencies. * a CHANGELOG entry is required as of v0.2; update the :doc:`/status` where applicable diff --git a/sbpy/utils/__init__.py b/sbpy/utils/__init__.py index d132be49d..c894a77b8 100644 --- a/sbpy/utils/__init__.py +++ b/sbpy/utils/__init__.py @@ -1,6 +1,6 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst -# This sub-module is destined for common non-package specific utility -# functions. +"""For common non-sub-module specific utility functions.""" +from . import decorators from .core import * diff --git a/sbpy/utils/decorators.py b/sbpy/utils/decorators.py new file mode 100644 index 000000000..09cdada3e --- /dev/null +++ b/sbpy/utils/decorators.py @@ -0,0 +1,60 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst + +"""Common sbpy method/function decorators.""" + +from functools import wraps +from ..exceptions import RequiredPackageUnavailable + +def requires(*packages): + """Decorator that verifies the arguments are valid packages. + + Parameters + ---------- + *modules : str + The names of packages to test. + + + Raises + ------ + + `~sbpy.exceptions.RequiredPackageUnavailable` + + + Examples + -------- + + >>> try: + ... import unavailable_package + ... except ImportError: + ... unavailable_package = None + >>> + >>> from sbpy.utils.decorators import requires + >>> + >>> @requires(unavailable_package=unavailable_package) + ... def f(): + ... pass + >>> + >>> f() + Traceback (most recent call last): + ... + sbpy.exceptions.RequiredPackageUnavailable: unavailable_package is required for f. + + """ + + def decorator(wrapped_function): + @wraps(wrapped_function) + def wrapper(*func_args, **func_kwargs): + for package in packages: + try: + __import__(package) + except ImportError: + function_name = '.'.join((wrapped_function.__module__, + wrapped_function.__qualname__)) + raise RequiredPackageUnavailable( + f"{package} is required for {function_name}." + ) + return wrapped_function(*func_args, **func_kwargs) + + return wrapper + + return decorator From b18807ae7e6330a219f7ac4412551eb2a23792f7 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Wed, 26 Jul 2023 15:59:17 -0400 Subject: [PATCH 02/21] New `optional` decorator for optional imports. --- docs/development/index.rst | 7 ++- sbpy/utils/decorators.py | 70 ++++++++++++++++++++++++----- sbpy/utils/tests/__init__.py | 0 sbpy/utils/tests/test_decorators.py | 20 +++++++++ 4 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 sbpy/utils/tests/__init__.py create mode 100644 sbpy/utils/tests/test_decorators.py diff --git a/docs/development/index.rst b/docs/development/index.rst index 815eafb9b..d64f79b54 100644 --- a/docs/development/index.rst +++ b/docs/development/index.rst @@ -122,8 +122,11 @@ Technical requirements ultimately be derived from the base classes in `sbpy.exceptions` * if you use `~sbpy.data.DataClass` objects, extend the :ref:`field name list` list where it makes sense -* consider using the `sbpy.utils.decorators.requires` decorator to test for the - presence of optional dependencies. +* consider using the `sbpy.utils.decorators.requires` or + `sbpy.utils.decorators.optional` decorators to test for the presence of + optional dependencies. + * `~sbpy.utils.decorators.requires` raises an exception if a package cannot be imported. + * `~sbpy.utils.decorators.optional` warns the user if a package cannot be imported. * a CHANGELOG entry is required as of v0.2; update the :doc:`/status` where applicable diff --git a/sbpy/utils/decorators.py b/sbpy/utils/decorators.py index 09cdada3e..399bfa38d 100644 --- a/sbpy/utils/decorators.py +++ b/sbpy/utils/decorators.py @@ -2,8 +2,11 @@ """Common sbpy method/function decorators.""" +__all__ = ["requires", "optional"] + +from warnings import warn from functools import wraps -from ..exceptions import RequiredPackageUnavailable +from ..exceptions import RequiredPackageUnavailable, OptionalPackageUnavailable def requires(*packages): """Decorator that verifies the arguments are valid packages. @@ -23,21 +26,14 @@ def requires(*packages): Examples -------- - >>> try: - ... import unavailable_package - ... except ImportError: - ... unavailable_package = None - >>> >>> from sbpy.utils.decorators import requires - >>> - >>> @requires(unavailable_package=unavailable_package) + >>> @requires("unavailable_package") ... def f(): ... pass - >>> >>> f() Traceback (most recent call last): ... - sbpy.exceptions.RequiredPackageUnavailable: unavailable_package is required for f. + sbpy.exceptions.RequiredPackageUnavailable: unavailable_package is required for sbpy.utils.decorators.f. """ @@ -58,3 +54,57 @@ def wrapper(*func_args, **func_kwargs): return wrapper return decorator + + +def optional(*packages, note=None): + """Decorator that warns if the arguments are not valid packages. + + Parameters + ---------- + *modules : str + The names of packages to test. + + note : str + An additional note to show the user, e.g., a description of the fallback + behavior. + + + Warnings + -------- + + `~sbpy.exceptions.OptionalPackageUnavailable` + + + Examples + -------- + + >>> from sbpy.utils.decorators import requires + >>> @optional("unavailable_package") + ... def f(): + ... pass + >>> f() # doctest: +IGNORE_OUTPUT + sbpy/utils/decorators.py::sbpy.utils.decorators.optional + ... + OptionalPackageUnavailable: Optional package unavailable_package is not available for sbpy.utils.decorators.f. + + """ + + def decorator(wrapped_function): + @wraps(wrapped_function) + def wrapper(*func_args, **func_kwargs): + for package in packages: + try: + __import__(package) + except ImportError: + function_name = '.'.join((wrapped_function.__module__, + wrapped_function.__qualname__)) + warn(OptionalPackageUnavailable( + f"Optional package {package} is not available " + f"for {function_name}." + + ("" if note is None else note) + )) + return wrapped_function(*func_args, **func_kwargs) + + return wrapper + + return decorator diff --git a/sbpy/utils/tests/__init__.py b/sbpy/utils/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sbpy/utils/tests/test_decorators.py b/sbpy/utils/tests/test_decorators.py new file mode 100644 index 000000000..b64113550 --- /dev/null +++ b/sbpy/utils/tests/test_decorators.py @@ -0,0 +1,20 @@ +from warnings import catch_warnings +import pytest +from ..decorators import requires, optional +from ...exceptions import RequiredPackageUnavailable, OptionalPackageUnavailable + +def test_requires(): + @requires("unavailable_package") + def f(): + pass + + with pytest.raises(RequiredPackageUnavailable): + f() + +def test_optional(): + @optional("unavailable_package") + def f(): + pass + + with pytest.warns(OptionalPackageUnavailable): + f() From 9b8161686dfd63df2f438bcb99af6039d41a25e7 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Thu, 27 Jul 2023 13:16:10 -0400 Subject: [PATCH 03/21] New `optional` and `required` functions. Edited the related decorators to use them. --- docs/development/index.rst | 14 +++--- sbpy/utils/__init__.py | 2 +- sbpy/utils/core.py | 91 ++++++++++++++++++++++++++++++++++- sbpy/utils/decorators.py | 62 ++++++++++++------------ sbpy/utils/tests/test_core.py | 29 +++++++++++ 5 files changed, 157 insertions(+), 41 deletions(-) create mode 100644 sbpy/utils/tests/test_core.py diff --git a/docs/development/index.rst b/docs/development/index.rst index d64f79b54..a2bc5435e 100644 --- a/docs/development/index.rst +++ b/docs/development/index.rst @@ -122,13 +122,13 @@ Technical requirements ultimately be derived from the base classes in `sbpy.exceptions` * if you use `~sbpy.data.DataClass` objects, extend the :ref:`field name list` list where it makes sense -* consider using the `sbpy.utils.decorators.requires` or - `sbpy.utils.decorators.optional` decorators to test for the presence of - optional dependencies. - * `~sbpy.utils.decorators.requires` raises an exception if a package cannot be imported. - * `~sbpy.utils.decorators.optional` warns the user if a package cannot be imported. -* a CHANGELOG entry is required as of v0.2; update the :doc:`/status` - where applicable +* consider using the sbpy function and decorator helpers to test for the + presence of optional dependencies: + * `~sbpy.utils.requires` and `~sbpy.utils.decorators.requires` raise an + exception if a package cannot be imported. + * `~sbpy.utils.optional` and `~sbpy.utils.decorators.optional` warn the user + if a package cannot be imported. +* a CHANGELOG entry is required; also update the :doc:`/status` where applicable diff --git a/sbpy/utils/__init__.py b/sbpy/utils/__init__.py index c894a77b8..e92a75e4a 100644 --- a/sbpy/utils/__init__.py +++ b/sbpy/utils/__init__.py @@ -2,5 +2,5 @@ """For common non-sub-module specific utility functions.""" -from . import decorators from .core import * +from . import decorators diff --git a/sbpy/utils/core.py b/sbpy/utils/core.py index 4ed73a3a3..39de5972a 100644 --- a/sbpy/utils/core.py +++ b/sbpy/utils/core.py @@ -6,4 +6,93 @@ """ -__all__ = [] +__all__ = ["requires", "optional"] + +from importlib import import_module +from warnings import warn +from ..exceptions import RequiredPackageUnavailable, OptionalPackageUnavailable + + +def requires(*packages, message=None): + """Verifies the arguments are valid packages. + + Parameters + ---------- + *modules : str + The names of packages to test. + + message : str + An additional message to show the user, e.g., the reason for the + requirement. + + Raises + ------ + + `~sbpy.exceptions.RequiredPackageUnavailable` + + + Examples + -------- + + >>> from sbpy.utils import requires + >>> requires("unavailable_package") + Traceback (most recent call last): + ... + sbpy.exceptions.RequiredPackageUnavailable: `unavailable_package` is required. + + """ + + for package in packages: + try: + import_module(package) + except ModuleNotFoundError as exc: + _message = "" if message is None else " " + message + raise RequiredPackageUnavailable(f"`{package}` is required.{_message}") from None + + +def optional(*packages, message=None): + """Decorator that warns if the arguments are not valid packages. + + Parameters + ---------- + *modules : str + The names of packages to test. + + message : str + An additional note to show the user, e.g., a description of the fallback + behavior. + + + Returns + ------- + success : bool + ``True`` if all optional packages are available. + + + Warnings + -------- + + `~sbpy.exceptions.OptionalPackageUnavailable` + + + Examples + -------- + + >>> from sbpy.utils import optional + >>> optional("unavailable_package") # doctest: +SKIP + OptionalPackageUnavailable: Optional package `unavailable_package` is unavailable. + + """ + # the doctest line is skipped to avoid polluting the testing suite with a warning + + for package in packages: + try: + import_module(package) + except ModuleNotFoundError: + _message = "" if message is None else " " + message + warn( + f"`{package}`.{_message}", + OptionalPackageUnavailable, + ) + return False + return True diff --git a/sbpy/utils/decorators.py b/sbpy/utils/decorators.py index 399bfa38d..15f10ca59 100644 --- a/sbpy/utils/decorators.py +++ b/sbpy/utils/decorators.py @@ -4,11 +4,12 @@ __all__ = ["requires", "optional"] -from warnings import warn from functools import wraps -from ..exceptions import RequiredPackageUnavailable, OptionalPackageUnavailable +from . import core +from ..exceptions import RequiredPackageUnavailable -def requires(*packages): + +def requires(*packages, message=None): """Decorator that verifies the arguments are valid packages. Parameters @@ -33,30 +34,31 @@ def requires(*packages): >>> f() Traceback (most recent call last): ... - sbpy.exceptions.RequiredPackageUnavailable: unavailable_package is required for sbpy.utils.decorators.f. + sbpy.exceptions.RequiredPackageUnavailable: `unavailable_package` is required. (sbpy.utils.decorators.f) """ def decorator(wrapped_function): + function_name = ".".join( + (wrapped_function.__module__, wrapped_function.__qualname__) + ) + _message = ("" if message is None else f"{message} ") + f"({function_name})" + @wraps(wrapped_function) def wrapper(*func_args, **func_kwargs): - for package in packages: - try: - __import__(package) - except ImportError: - function_name = '.'.join((wrapped_function.__module__, - wrapped_function.__qualname__)) - raise RequiredPackageUnavailable( - f"{package} is required for {function_name}." - ) + try: + core.requires(*packages, message=_message) + except RequiredPackageUnavailable as exc: + # trim a couple levels of the traceback to clean up error messages + raise exc.with_traceback(exc.__traceback__.tb_next.tb_next) return wrapped_function(*func_args, **func_kwargs) - + return wrapper return decorator -def optional(*packages, note=None): +def optional(*packages, message=None): """Decorator that warns if the arguments are not valid packages. Parameters @@ -64,9 +66,9 @@ def optional(*packages, note=None): *modules : str The names of packages to test. - note : str - An additional note to show the user, e.g., a description of the fallback - behavior. + message : str + An additional message to show the user, e.g., a description of the + fallback behavior. Warnings @@ -82,29 +84,25 @@ def optional(*packages, note=None): >>> @optional("unavailable_package") ... def f(): ... pass - >>> f() # doctest: +IGNORE_OUTPUT + >>> f() # doctest: +SKIP sbpy/utils/decorators.py::sbpy.utils.decorators.optional ... - OptionalPackageUnavailable: Optional package unavailable_package is not available for sbpy.utils.decorators.f. + OptionalPackageUnavailable: Optional package `unavailable_package` is unavailable. (sbpy.utils.decorators.f) """ + # the doctest line is skipped to avoid polluting the testing suite with a warning def decorator(wrapped_function): + function_name = ".".join( + (wrapped_function.__module__, wrapped_function.__qualname__) + ) + _message = ("" if message is None else f"{message} ") + f"({function_name})" + @wraps(wrapped_function) def wrapper(*func_args, **func_kwargs): - for package in packages: - try: - __import__(package) - except ImportError: - function_name = '.'.join((wrapped_function.__module__, - wrapped_function.__qualname__)) - warn(OptionalPackageUnavailable( - f"Optional package {package} is not available " - f"for {function_name}." - + ("" if note is None else note) - )) + core.optional(*packages, message=_message) return wrapped_function(*func_args, **func_kwargs) - + return wrapper return decorator diff --git a/sbpy/utils/tests/test_core.py b/sbpy/utils/tests/test_core.py new file mode 100644 index 000000000..7383a0b79 --- /dev/null +++ b/sbpy/utils/tests/test_core.py @@ -0,0 +1,29 @@ +import pytest +from ..core import requires, optional +from ...exceptions import RequiredPackageUnavailable, OptionalPackageUnavailable + + +def test_requires(): + with pytest.raises(RequiredPackageUnavailable): + requires("unavailable_package") + + +def test_requires_message(): + try: + message = "Because these integrations are tricky." + requires("unavailable_package", message=message) + except RequiredPackageUnavailable as exc: + assert message in str(exc) + + +def test_optional(): + with pytest.warns(OptionalPackageUnavailable): + assert not optional("unavailable_package") + + assert optional("astropy") + + +def test_optional_message(): + message = "Using linear interpolation." + with pytest.warns(OptionalPackageUnavailable, match=message) as record: + optional("unavailable_package", message=message) From f0bd851206c37c1fdfe2773b32f7eb395219cb10 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Fri, 28 Jul 2023 11:57:44 -0400 Subject: [PATCH 04/21] Make scipy, ads, astroquery, and synphot optional. --- CHANGES.rst | 36 +++++++++ docs/install.rst | 14 +++- docs/sbpy/photometry.rst | 2 + docs/sbpy/spectroscopy/index.rst | 2 +- docs/sbpy/units.rst | 12 ++- sbpy/activity/dust.py | 24 +++--- sbpy/activity/gas/core.py | 42 +++------- sbpy/activity/gas/data/__init__.py | 23 ++---- sbpy/activity/gas/productionrate.py | 35 ++++++--- sbpy/activity/gas/tests/test_core.py | 77 ++++++++----------- sbpy/activity/gas/tests/test_data.py | 7 +- sbpy/activity/gas/tests/test_prodrate.py | 3 + .../gas/tests/test_prodrate_remote.py | 15 +--- sbpy/activity/tests/test_dust.py | 15 +++- sbpy/bib/core.py | 16 +++- sbpy/calib/core.py | 25 +++--- sbpy/calib/tests/test_core.py | 76 ++++++++++-------- sbpy/calib/tests/test_sun.py | 25 +++--- sbpy/calib/tests/test_vega.py | 15 ++-- sbpy/data/ephem.py | 35 +++++---- sbpy/data/obs.py | 15 +++- sbpy/data/orbit.py | 31 ++++---- sbpy/data/phys.py | 12 ++- sbpy/data/tests/test_ephem.py | 20 ++--- sbpy/data/tests/test_ephem_remote.py | 8 +- sbpy/data/tests/test_orbit.py | 24 ++---- sbpy/data/tests/test_orbit_remote.py | 15 ++-- sbpy/data/tests/test_phys_remote.py | 1 + sbpy/photometry/bandpass.py | 12 ++- sbpy/photometry/core.py | 18 ++++- sbpy/photometry/iau.py | 11 ++- sbpy/photometry/tests/test_bandpass.py | 8 +- sbpy/photometry/tests/test_core.py | 2 + sbpy/photometry/tests/test_iau.py | 6 ++ sbpy/spectroscopy/core.py | 20 +---- sbpy/spectroscopy/sources.py | 28 ++++--- sbpy/spectroscopy/tests/test_sources.py | 13 +--- sbpy/spectroscopy/tests/test_specgrad.py | 11 ++- sbpy/units/core.py | 11 ++- sbpy/units/tests/test_core.py | 38 +++++---- setup.cfg | 14 +++- 41 files changed, 456 insertions(+), 361 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 38358e53c..b19ad279a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,39 @@ +0.4.1 (unreleased) +================== + +- Revised required and optional packages: + - Only numpy and astropy are required; synphot, ads, and astroquery are now optional dependences. + - Created an option to install a recommended list of packages, e.g., ``pip install sbpy[recommended]``. + + +New Features +------------ + +sbpy.utils.decorators +^^^^^^^^^^^^^^^^^^^^^ + +- Added the `requires` function decorator to test for the presence of optional + packages. + +- Added the `optional` function decorator to test for the presence of optional + packages. + + +API Changes +----------- + +sbpy.sources +^^^^^^^^^^^^ +* Deprecated ``SynphotRequired``. Use ``sbpy.execptions.RequiredPackageUnavailable``. + + +Bug Fixes +--------- +* ``sbpy.sources.SpectralSource`` now correctly raises + ``RequiredPackageUnavailable`` when ``synphot`` is not available, replacing a + locally defined ``SynphotRequired`` or the generic ``ImportError``. + + 0.4.0 (2023-06-30) ================== diff --git a/docs/install.rst b/docs/install.rst index aaebd1cb4..37f9070b9 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -9,22 +9,22 @@ Requirements care of with installation using pip: * Python 3.8 or later -* `ads `__ 0.12 or later, to fetch citation details for bibliography tracking. * `astropy `__ 4.3 or later. -* `astroquery `__ 0.4.5 or later, for retrieval of online data, e.g., ephemerides and orbits. * `numpy `__ 1.18 or later. -* `scipy `__: 1.3 or later, for numerical integrations in `sbpy.activity.gas` and `sbpy.photometry`, among others. -* `synphot `__ 1.1.1 or later, for calibration with respect to the Sun and Vega, filtering spectra through bandpasses. Optional dependencies ^^^^^^^^^^^^^^^^^^^^^ +* `ads `__ 0.12 or later, to fetch citation details for bibliography tracking. **Recommended** +* `astroquery `__ 0.4.5 or later, for retrieval of online data, e.g., ephemerides and orbits. **Recommended** * Python extensions for `oorb `__: For orbit transformations (`~sbpy.data.Orbit.oo_transform`) and propagations (`~sbpy.data.Orbit.oo_propagate`), as well as ephemerides calculations (`~sbpy.data.Ephem.from_oo`). * `pyradex `__: For non-LTE production rate calculations related to cometary activity (`~sbpy.activity.gas.NonLTE`). +* `scipy `__: 1.3 or later, for numerical integrations in `sbpy.activity.gas` and `sbpy.photometry`, among others. **Recommended** +* `synphot `__ 1.1.1 or later, for calibration with respect to the Sun and Vega, filtering spectra through bandpasses. **Recommended** * `ginga `__ and `photutils `__: To interactively enhance images of comets with the `~sbpy.imageanalysis.CometaryEnhancement` Ginga @@ -40,6 +40,12 @@ The latest stable version of `sbpy` can be installed with: $ pip install sbpy +Recommended dependencies may be installed via: + +.. code-block:: bash + + $ pip install sbpy[recommended] + Most optional dependencies may be installed via: .. code-block:: bash diff --git a/docs/sbpy/photometry.rst b/docs/sbpy/photometry.rst index 43914868f..2e2a7f5cd 100644 --- a/docs/sbpy/photometry.rst +++ b/docs/sbpy/photometry.rst @@ -161,6 +161,8 @@ A few filter bandpasses are included with `sbpy` for internal tests and your con For other bandpasses, obtain the photon-counting relative spectral response curves as a two-column file. If the first column is wavelength in Angstroms, and the second is the response, then read the file with: +.. doctest-requires:: synphot + >>> from synphot import SpectralElement # doctest: +SKIP >>> bp = SpectralElement.from_file('filename.txt') # doctest: +SKIP diff --git a/docs/sbpy/spectroscopy/index.rst b/docs/sbpy/spectroscopy/index.rst index d42a01f46..7c78f957f 100644 --- a/docs/sbpy/spectroscopy/index.rst +++ b/docs/sbpy/spectroscopy/index.rst @@ -74,7 +74,7 @@ Renormalize to 1.0 μm: Spectral Reddening ------------------ -Linear spectral reddening is enabled by class `~sbpy.spectroscopy.Reddening`, +Linear spectral reddening is enabled by the class `~sbpy.spectroscopy.Reddening`, which is based on `~synphot.spectrum.BaseUnitlessSpectrum`. Initialize a `~sbpy.spectroscopy.Reddening` class from a spectral gradient: diff --git a/docs/sbpy/units.rst b/docs/sbpy/units.rst index 7bf520351..1d676bdd6 100644 --- a/docs/sbpy/units.rst +++ b/docs/sbpy/units.rst @@ -9,8 +9,10 @@ Introduction >>> import sbpy.units >>> sbpy.units.enable() # doctest: +IGNORE_OUTPUT -This will make them searchable via strings, such as -``u.Unit('mag(VEGA)')``. +This will make them searchable via strings, such as: + + >> print(u.Unit("mag(VEGA)")) + VEGAmag Spectral Gradient Units @@ -43,6 +45,8 @@ sbpy defines two new spectral flux density units: ``VEGA`` and ``JM``. ``VEGA`` Unit conversions between flux density and Vega-based magnitudes use the `astropy.units equivalency system `_. sbpy's :func:`~sbpy.units.spectral_density_vega` provides the equivalencies, which astropy would use to convert the units. The function requires a wavelength, frequency, or bandpass: +.. doctest-requires:: synphot + >>> import astropy.units as u >>> from sbpy.units import VEGAmag, spectral_density_vega >>> wave = 5500 * u.AA @@ -111,6 +115,8 @@ phase angle can be calculated: >>> cross_sec = np.pi * (radius)**2 >>> ref = mag.to('1/sr', reflectance('V', cross_section=cross_sec)) +.. doctest-requires:: synphot + >>> from sbpy.photometry import bandpass >>> V = bandpass('Johnson V') >>> ref = 0.0287 / u.sr @@ -121,6 +127,8 @@ phase angle can be calculated: `~sbpy.units.reflectance` also supports conversion between a flux spectrum and a reflectance spectrum: +.. doctest-requires:: synphot + >>> wave = [1.046, 1.179, 1.384, 1.739, 2.416] * u.um >>> flux = [1.636e-18, 1.157e-18, 8.523e-19, 5.262e-19, 1.9645e-19] \ ... * u.Unit('W/(m2 um)') diff --git a/sbpy/activity/dust.py b/sbpy/activity/dust.py index 2f9438efc..fa19283a5 100644 --- a/sbpy/activity/dust.py +++ b/sbpy/activity/dust.py @@ -10,7 +10,10 @@ __all__ = ["phase_HalleyMarcus", "Afrho", "Efrho"] -__doctest_requires__ = {"Afrho.to_fluxd": ["astropy>=5.3"]} +__doctest_requires__ = { + "Afrho.to_fluxd": ["astropy>=5.3", "synphot"], + "Efrho.to_fluxd": ["synphot"], +} from warnings import warn import abc @@ -19,15 +22,16 @@ import astropy.units as u try: - from astropy.utils.misc import InheritDocstrings + import scipy + from scipy.interpolate import splrep, splev except ImportError: - InheritDocstrings = None + scipy = None from .. import bib from ..calib import Sun from ..spectroscopy import BlackbodySource +from ..utils import optional from .. import data as sbd -from .. import exceptions as sbe from .. import units as sbu from ..spectroscopy.sources import SinglePointSpectrumError from .core import Aperture @@ -95,11 +99,6 @@ def phase_HalleyMarcus(phase): 5.8720e-01 """ - try: - import scipy - from scipy.interpolate import splrep, splev - except ImportError: - scipy = None th = np.arange(181) ph = np.array( @@ -290,14 +289,9 @@ def phase_HalleyMarcus(phase): _phase = np.abs(u.Quantity(phase, "deg").value) - if scipy: + if optional("scipy"): Phi = splev(_phase, splrep(th, ph)) else: - warn( - sbe.OptionalPackageUnavailable( - "scipy is not present, using linear interpolation." - ) - ) Phi = np.interp(_phase, th, ph) if np.iterable(_phase): diff --git a/sbpy/activity/gas/core.py b/sbpy/activity/gas/core.py index 68ef75445..716a625ab 100644 --- a/sbpy/activity/gas/core.py +++ b/sbpy/activity/gas/core.py @@ -15,8 +15,6 @@ # distutils is deprecated in python 3.10 and will be removed in 3.12 (PEP 632). # Migration from distutils.log -> logging -from distutils.log import warn -import warnings from dataclasses import dataclass from typing import Callable, Tuple @@ -27,14 +25,15 @@ import scipy from scipy import special from scipy.integrate import quad, dblquad, romberg - from scipy.interpolate import CubicSpline + from scipy.interpolate import CubicSpline, PPoly except ImportError: scipy = None + PPoly = None from ... import bib from ... import data as sbd from ... import units as sbu -from ...exceptions import RequiredPackageUnavailable, TestingNeeded +from ...utils.decorators import requires from ..core import ( Aperture, RectangularAperture, @@ -415,6 +414,7 @@ def _column_density(self, rho): """ + @requires("scipy") def _integrate_volume_density(self, rho, epsabs=1.49e-8): """Integrate volume density along the line of sight. @@ -441,9 +441,6 @@ def _integrate_volume_density(self, rho, epsabs=1.49e-8): """ - if not scipy: - raise RequiredPackageUnavailable("scipy") - def f(s, rho2): r = np.sqrt(rho2 + s**2) return self._volume_density(r) @@ -462,6 +459,7 @@ def f(s, rho2): return sigma, err + @requires("scipy") def _integrate_column_density(self, aper, epsabs=1.49e-8): """Integrate column density over an aperture. @@ -487,9 +485,6 @@ def _integrate_column_density(self, aper, epsabs=1.49e-8): """ - if not scipy: - raise RequiredPackageUnavailable("scipy") - if isinstance(aper, (CircularAperture, AnnularAperture)): if isinstance(aper, CircularAperture): limits = (0, aper.radius.to_value("m")) @@ -622,16 +617,13 @@ def _volume_density(self, r): def _iK0(self, x): """Integral of the modified Bessel function of 2nd kind, 0th order.""" - if not scipy: - raise RequiredPackageUnavailable("scipy") return special.iti0k0(x)[1] def _K1(self, x): """Modified Bessel function of 2nd kind, 1st order.""" - if not scipy: - raise RequiredPackageUnavailable("scipy") return special.k1(x) + @requires("scipy") @bib.cite({"model": "1978Icar...35..360N"}) def _column_density(self, rho): sigma = (self.Q / self.v).to_value("1/m") / rho / 2 / np.pi @@ -647,6 +639,7 @@ def _column_density(self, rho): ) return sigma + @requires("scipy") def _total_number(self, aper): # Inspect aper and handle as appropriate if isinstance(aper, (RectangularAperture, GaussianAperture)): @@ -876,8 +869,8 @@ class VMResult: fragment_sputter: VMFragmentSputterPolar = None solid_angle_sputter: VMFragmentSputterPolar = None - volume_density_interpolation: scipy.interpolate.PPoly = None - column_density_interpolation: scipy.interpolate.PPoly = None + volume_density_interpolation: PPoly = None + column_density_interpolation: PPoly = None collision_sphere_radius: u.Quantity = None max_grid_radius: u.Quantity = None @@ -971,15 +964,6 @@ def __init__( max_fragment_lifetimes=8.0, print_progress=False, ): - # warnings.warn( - # "Literature tests with the Vectorial model are generally" - # " in agreement at the 20% level or better. The cause" - # " for the differences with the Festou FORTRAN code are" - # " not yet precisely known. Help testing this feature is" - # " appreciated.", - # TestingNeeded, - # ) - super().__init__(base_q, parent["v_outflow"][0]) # Calculations are done internally in meters and seconds to match the @@ -1585,6 +1569,7 @@ def _compute_fragment_density(self) -> None: # Tag with proper units self.vmr.volume_density = self.fast_voldens / (u.m**3) + @requires("scipy") def _interpolate_volume_density(self) -> None: """Interpolate the volume density as a function of radial distance from the nucleus. @@ -1594,9 +1579,6 @@ def _interpolate_volume_density(self) -> None: """ - if not scipy: - raise RequiredPackageUnavailable("scipy") - if self.print_progress: print("Interpolating radial fragment density...") @@ -1652,6 +1634,7 @@ def column_density_integrand(z): # result is in 1/m^2 return c_dens + @requires("scipy") def _compute_column_density(self) -> None: """Compute the column density on the grid and interpolate the results. @@ -1662,9 +1645,6 @@ def _compute_column_density(self) -> None: """ - if not scipy: - raise RequiredPackageUnavailable("scipy") - if self.print_progress: print("Computing column densities...") diff --git a/sbpy/activity/gas/data/__init__.py b/sbpy/activity/gas/data/__init__.py index 6cf8bb03c..a9654ca66 100644 --- a/sbpy/activity/gas/data/__init__.py +++ b/sbpy/activity/gas/data/__init__.py @@ -8,22 +8,15 @@ 'OHFluorescenceSA88' ] -from warnings import warn - import numpy as np -try: - import scipy - from scipy import interpolate -except ImportError: - scipy = None import astropy.units as u from astropy.io import ascii -from astropy.utils.data import get_pkg_data_filename +from astropy.utils.data import get_pkg_data_path from .... import data as sbd -from .... import exceptions as sbe from .... import bib +from ....utils import optional photo_lengthscale = { # (value, {key feature: ADS bibcode}) 'H2O': { @@ -113,7 +106,7 @@ class OHFluorescenceSA88: '0-1', '0-2', '1-2', '2-0', '2-1'] def __init__(self, band): - fn = get_pkg_data_filename('schleicher88.txt') + fn = get_pkg_data_path('schleicher88.txt') self.table5 = ascii.read(fn) self._rdot = self.table5['rdot'].data * u.km / u.s self._inversion = self.table5['I'] @@ -129,16 +122,16 @@ def __init__(self, band): self._LN = u.Quantity(self.table5[k].data * self.scales[i], 'erg / s') - if scipy: - self._tck = interpolate.splrep(self.rdot.value, self.LN.value) + if optional("scipy"): + from scipy.interpolate import splrep + self._tck = splrep(self.rdot.value, self.LN.value) self._interp = self._spline else: - warn(sbe.OptionalPackageUnavailable( - 'scipy unavailable, using linear interpolation.')) self._interp = self._linear def _spline(self, rdot, rh): - return interpolate.splev(rdot, self._tck, ext=2) / rh**2 + from scipy.interpolate import splev + return splev(rdot, self._tck, ext=2) / rh**2 def _linear(self, rdot, rh): return np.interp(rdot, self.rdot.value, self.LN.value) / rh**2 diff --git a/sbpy/activity/gas/productionrate.py b/sbpy/activity/gas/productionrate.py index cc8e12068..9efa46f03 100644 --- a/sbpy/activity/gas/productionrate.py +++ b/sbpy/activity/gas/productionrate.py @@ -13,10 +13,23 @@ import numpy as np import astropy.constants as con import astropy.units as u -from astroquery.jplspec import JPLSpec -from astroquery.lamda import Lamda + +try: + import astroquery + from astroquery.jplspec import JPLSpec + from astroquery.lamda import Lamda +except ImportError: + astroquery = None + +try: + import pyradex +except ImportError: + pyradex = None + from ...bib import register from ...data import Phys +from ...exceptions import RequiredPackageUnavailable +from ...utils.decorators import requires __all__ = ['LTE', 'NonLTE', 'einstein_coeff', 'intensity_conversion', 'beta_factor', 'total_number', @@ -24,7 +37,7 @@ __doctest_requires__ = { "LTE.from_Drahus": ["astropy>=5.0", "astroquery>=0.4.7"], - "NonLTE.from_pyradex": ["astroquery>=0.4.7"], + "NonLTE.from_pyradex": ["astroquery>=0.4.7", "pyradex"], "from_Haser": ["astroquery>=0.4.7"], } @@ -244,6 +257,9 @@ def beta_factor(mol_data, ephemobj): r = (orb['r']) if not isinstance(mol_data['mol_tag'][0], str): + if astroquery is None: + raise RequiredPackageUnavailable(f"mol_tag = {mol_data['mol_tag'][0]} requires astroquery") + cat = JPLSpec.get_species_table() mol = cat[cat['TAG'] == mol_data['mol_tag'][0]] name = mol['NAME'].data[0] @@ -643,12 +659,13 @@ def from_Drahus(self, integrated_flux, mol_data, ephemobj, vgas=1 * u.km/u.s, class NonLTE(): """ - Class method for non LTE production rate models - Not Yet implemented + Class for non LTE production rate models. """ - def from_pyradex(self, integrated_flux, mol_data, line_width=1.0 * u.km / u.s, + @staticmethod + @requires(pyradex, astroquery) + def from_pyradex(integrated_flux, mol_data, line_width=1.0 * u.km / u.s, escapeProbGeom='lvg', iter=100, collider_density={'H2': 900*2.2}): """ @@ -770,12 +787,6 @@ def from_pyradex(self, integrated_flux, mol_data, line_width=1.0 * u.km / u.s, """ - try: - import pyradex - except ImportError: - raise ImportError('Pyradex not installed. Please see \ - https://github.com/keflavich/pyradex/blob/master/INSTALL.rst') - if not isinstance(mol_data, Phys): raise ValueError('mol_data must be a `sbpy.data.phys` instance.') diff --git a/sbpy/activity/gas/tests/test_core.py b/sbpy/activity/gas/tests/test_core.py index 4caffa750..d6599d335 100644 --- a/sbpy/activity/gas/tests/test_core.py +++ b/sbpy/activity/gas/tests/test_core.py @@ -2,6 +2,10 @@ import pytest import numpy as np +try: + import scipy +except ImportError: + scipy = None import astropy.units as u import astropy.constants as const from .. import core @@ -12,10 +16,8 @@ VectorialModel, Haser, ) -from .... import exceptions as sbe from ....data import Phys - def test_photo_lengthscale(): gamma = photo_lengthscale("OH", "CS93") assert gamma == 1.6e5 * u.km @@ -71,49 +73,16 @@ def test_fluorescence_band_strength_error(): fluorescence_band_strength("OH 0-0", source="asdf") -def test_gascoma_scipy_error(monkeypatch): - monkeypatch.setattr(core, "scipy", None) - test = Haser(1 / u.s, 1 * u.km / u.s, 1e6 * u.km) - with pytest.raises(sbe.RequiredPackageUnavailable): - test._integrate_volume_density(1e5) - - with pytest.raises(sbe.RequiredPackageUnavailable): - aper = core.CircularAperture(1000 * u.km) - test._integrate_column_density(aper) - - class TestHaser: - def test_volume_density(self): - """Test a set of dummy values.""" - Q = 1e28 / u.s - v = 1 * u.km / u.s - parent = 1e4 * u.km - daughter = 1e5 * u.km - r = np.logspace(1, 7) * u.km - n = Haser(Q, v, parent, daughter).volume_density(r) - rel = ( - daughter - / (parent - daughter) - * (np.exp(-r / parent) - np.exp(-r / daughter)) - ) - # test radial profile - assert np.allclose((n / n[0]).value, (rel / rel[0] * (r[0] / r) ** 2).value) - - # test parent-only coma near nucleus against that expected for - # a long-lived species; will be close, but not exact - n = Haser(Q, v, parent).volume_density(10 * u.km) - assert np.isclose( - n.decompose().value, - (Q / v / 4 / np.pi / (10 * u.km) ** 2).decompose().value, - rtol=0.001, - ) - def test_column_density_small_aperture(self): """Test column density for aperture << lengthscale. Should be within 1% of ideal value. """ + + pytest.importorskip("scipy") + Q = 1e28 / u.s v = 1 * u.km / u.s rho = 1 * u.km @@ -130,6 +99,9 @@ def test_column_density_small_angular_aperture(self): Should be within 1% of ideal value. """ + + pytest.importorskip("scipy") + Q = 1e28 / u.s v = 1 * u.km / u.s rho = 0.001 * u.arcsec @@ -145,6 +117,9 @@ def test_column_density(self): Test column density for aperture = lengthscale. """ + + pytest.importorskip("scipy") + Q = 1e28 / u.s v = 1 * u.km / u.s rho = 1000 * u.km @@ -156,6 +131,9 @@ def test_column_density(self): def test_total_number_large_aperture(self): """Test column density for aperture >> lengthscale.""" + + pytest.importorskip("scipy") + Q = 1 / u.s v = 1 * u.km / u.s rho = 1000 * u.km @@ -172,6 +150,8 @@ def test_total_number_circular_aperture_angular(self): Code initially from Quanzhi Ye. """ + + pytest.importorskip("scipy") Q = 1e25 / u.s v = 1 * u.km / u.s @@ -228,6 +208,8 @@ def test_total_number_rho_AC75(self): # [0.893, 39084, 3.147e31, 1.129e31, 2.393e31, 27.12, 26.54, 27.0] # ] + pytest.importorskip("scipy") + # Computed by sbpy. 0.893 C2 and C3 matches are not great, # the rest are OK: tab = [ @@ -267,6 +249,8 @@ def test_circular_integration_0step(self): parent only """ + + pytest.importorskip("scipy") # Nobs = 2.314348613550494e+27 parent = 1.4e4 * u.km @@ -286,6 +270,8 @@ def test_circular_integration_1step(self): """ + pytest.importorskip("scipy") + # Nobs = 6.41756750e26 parent = 1.4e4 * u.km daughter = 1.7e5 * u.km @@ -300,6 +286,8 @@ def test_circular_integration_1step(self): def test_total_number_annulus(self): """Test column density for annular aperture.""" + + pytest.importorskip("scipy") Q = 1 / u.s v = 1 * u.km / u.s @@ -333,6 +321,8 @@ def test_total_number_rectangular_ap(self): """ + pytest.importorskip("scipy") + parent = 1.4e4 * u.km daughter = 1.7e5 * u.km Q = 5.8e23 / u.s @@ -365,6 +355,8 @@ def test_total_number_gaussian_ap(self): which is within 0.5% of the test value below """ + + pytest.importorskip("scipy") parent = 1.4e4 * u.km Q = 5.8e23 / u.s @@ -375,15 +367,8 @@ def test_total_number_gaussian_ap(self): assert np.isclose(N, 5.146824269306973e27, rtol=0.005) - def test_missing_scipy(self, monkeypatch): - monkeypatch.setattr(core, "scipy", None) - test = Haser(1 / u.s, 1 * u.km / u.s, 1e6 * u.km) - with pytest.raises(sbe.RequiredPackageUnavailable): - test._iK0(1) - with pytest.raises(sbe.RequiredPackageUnavailable): - test._K1(1) - +@pytest.mark.skipif("scipy is None") class TestVectorialModel: def test_small_vphoto(self): """ diff --git a/sbpy/activity/gas/tests/test_data.py b/sbpy/activity/gas/tests/test_data.py index 631fef436..53411ae3c 100644 --- a/sbpy/activity/gas/tests/test_data.py +++ b/sbpy/activity/gas/tests/test_data.py @@ -1,14 +1,19 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst +import importlib import pytest import numpy as np import astropy.units as u from .. import data +def patched_import_module(name): + if name == "scipy": + raise ModuleNotFoundError + __import__(name) class TestOHFluorescenceSA88: def test_linear_interpolation(self, monkeypatch): - monkeypatch.setattr(data, 'scipy', None) + monkeypatch.setattr(importlib, "import_module", patched_import_module) model = data.OHFluorescenceSA88('0-0') LN = model(-1 * u.km / u.s) assert np.isclose(LN.value, 1.54e-15) diff --git a/sbpy/activity/gas/tests/test_prodrate.py b/sbpy/activity/gas/tests/test_prodrate.py index d210454bb..30397ac9a 100644 --- a/sbpy/activity/gas/tests/test_prodrate.py +++ b/sbpy/activity/gas/tests/test_prodrate.py @@ -1,5 +1,6 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst +import pytest import numpy as np import astropy.units as u from .. import Haser, photo_timescale @@ -86,6 +87,8 @@ def test_simple_prodrate(self): def test_Haser_prodrate(self): """Test a set of dummy values.""" + pytest.importorskip("scipy") + temp_estimate = 47. * u.K vgas = 0.8 * u.km / u.s aper = 30 * u.m diff --git a/sbpy/activity/gas/tests/test_prodrate_remote.py b/sbpy/activity/gas/tests/test_prodrate_remote.py index 7099aadfc..a23dcdd8b 100644 --- a/sbpy/activity/gas/tests/test_prodrate_remote.py +++ b/sbpy/activity/gas/tests/test_prodrate_remote.py @@ -6,20 +6,13 @@ import astropy.units as u from astropy.time import Time from astropy.table import Table -from astroquery.lamda import Lamda -from astroquery.jplspec import JPLSpec -import pytest -try: - import pyradex -except ImportError: - pyradex = None +import pytest from .. import (Haser, photo_timescale, LTE, NonLTE, einstein_coeff, intensity_conversion, beta_factor, total_number, from_Haser) from ....data import Ephem, Phys - class MockPyradex: """ Class to be the mock return value of NonLTE.from_pyradex @@ -268,9 +261,9 @@ def test_Haser_prodrate(): ''' -@pytest.mark.skipif('pyradex is None') @pytest.mark.remote_data def test_Haser_pyradex(): + pytest.importorskip("pyradex") pytest.importorskip("astroquery", minversion="0.4.7") co = Table.read(data_path('CO.csv'), format="ascii.csv") @@ -387,9 +380,9 @@ def test_betafactor_case(): ''' -@pytest.mark.skipif('pyradex is None') @pytest.mark.remote_data def test_pyradex_case(): + pytest.importorskip("pyradex") pytest.importorskip("astroquery", minversion="0.4.7") transition_freq = (177.196 * u.GHz).to(u.MHz) @@ -410,9 +403,9 @@ def test_pyradex_case(): assert np.isclose(cdensity.value[0], 1.134e14) -@pytest.mark.skipif('pyradex is None') @pytest.mark.remote_data def test_Haser_prodrate_pyradex(mock_nonlte): + pytest.importorskip("pyradex") pytest.importorskip("astroquery", minversion="0.4.7") co = Table.read(data_path('CO.csv'), format="ascii.csv") diff --git a/sbpy/activity/tests/test_dust.py b/sbpy/activity/tests/test_dust.py index 4f05e676b..a9627aa3f 100644 --- a/sbpy/activity/tests/test_dust.py +++ b/sbpy/activity/tests/test_dust.py @@ -1,11 +1,11 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst import sys -import mock +from unittest import mock import pytest import numpy as np import astropy.units as u -import synphot + from ..dust import * from ..core import CircularAperture from ...calib import solar_fluxd @@ -18,6 +18,7 @@ def box(center, width): + synphot = pytest.importorskip("synphot") return synphot.SpectralElement( synphot.Box1D, x_0=center * u.um, width=width * u.um) @@ -36,9 +37,9 @@ def test_phase_HalleyMarcus(phase, value): (15, 5.8720e-01), (14.5, (6.0490e-01 + 5.8720e-01) / 2) )) +@mock.patch.dict(sys.modules, {'scipy': None}) def test_phase_HalleyMarcus_linear_interp(phase, value): - with mock.patch.dict(sys.modules, {'scipy': None}): - assert np.isclose(phase_HalleyMarcus(phase * u.deg), value) + assert np.isclose(phase_HalleyMarcus(phase * u.deg), value) class TestAfrho: @@ -102,6 +103,9 @@ def test_from_fluxd(self, wfb, fluxd0, rho, rh, delta, S, afrho0, tol): synphot.units.convert_flux(6182, 11.97 * u.ABmag, u.STmag) """ + + pytest.importorskip("synphot") + eph = Ephem.from_dict(dict(rh=rh * u.au, delta=delta * u.au)) cal = {} @@ -249,6 +253,9 @@ def test_fluxd(self, efrho0, wfb, fluxd0, rho, rh, delta, unit, B, T, tol): * Added a significant figure to Encke εfρ: 31 → 31.1. """ + + pytest.importorskip("synphot") + eph = dict(rh=rh * u.au, delta=delta * u.au) efrho = (Efrho.from_fluxd(wfb, fluxd0, rho, eph, Tscale=1.1, B=B, T=T) .to('cm')) diff --git a/sbpy/bib/core.py b/sbpy/bib/core.py index 627a4f507..4eb8dc8b4 100644 --- a/sbpy/bib/core.py +++ b/sbpy/bib/core.py @@ -26,8 +26,16 @@ from functools import wraps from collections import OrderedDict, defaultdict import atexit + +try: + import ads +except ImportError: + ads = None + from astropy import log +from ..exceptions import RequiredPackageUnavailable + def register(task, citations): """Register a citation with the `sbpy` bibliography tracker. @@ -238,7 +246,9 @@ def to_text(filter=None): the bibcodes. """ - import ads + + if ads is None: + raise RequiredPackageUnavailable("requires ads") output = '' for task, ref in _filter(filter).items(): @@ -328,7 +338,9 @@ def _to_format(format, filter=None): for each reference. """ - import ads + + if ads is None: + raise RequiredPackageUnavailable("requires ads") output = '' for task, ref in _filter(filter).items(): diff --git a/sbpy/calib/core.py b/sbpy/calib/core.py index 55778ac9c..65fededdc 100644 --- a/sbpy/calib/core.py +++ b/sbpy/calib/core.py @@ -22,28 +22,27 @@ import os from abc import ABC -from warnings import warn from functools import wraps import inspect import json import numpy as np from astropy.utils.state import ScienceState -from astropy.utils.data import get_pkg_data_filename +from astropy.utils.data import get_pkg_data_path from astropy.table import Table import astropy.units as u -from ..spectroscopy.sources import SpectralSource, SynphotRequired -from ..exceptions import SbpyException, OptionalPackageUnavailable +from ..spectroscopy.sources import SpectralSource +from ..exceptions import SbpyException from .. import bib from . import solar_sources, vega_sources +from ..utils.decorators import optional, requires try: import synphot from synphot import SpectralElement except ImportError: synphot = None - class SpectralElement: pass @@ -123,12 +122,13 @@ def from_builtin(cls, name): if not _is_url(parameters['filename']): # find in the module's location - parameters['filename'] = get_pkg_data_filename( + parameters['filename'] = get_pkg_data_path( os.path.join('data', parameters['filename'])) return cls.from_file(**parameters) @classmethod + @optional("synphot") def from_default(cls): """Initialize new spectral standard from current default. @@ -136,9 +136,6 @@ def from_default(cls): """ if synphot is None: - warn(OptionalPackageUnavailable( - 'synphot is not installed, returning an empty spectral' - ' standard.')) standard = cls(None) else: standard = cls._spectrum_state.get() @@ -235,17 +232,13 @@ def observe(self, wfb, unit=None, interpolate=False, **kwargs): return fluxd @wraps(SpectralSource.observe_bandpass) + @requires("synphot") def observe_bandpass(self, *args, **kwargs): - if synphot is None: - raise SynphotRequired( - 'synphot is required for observations through bandpass') return super().observe_bandpass(*args, **kwargs) @wraps(SpectralSource.observe_spectrum) + @requires("synphot") def observe_spectrum(self, *args, **kwargs): - if synphot is None: - raise SynphotRequired( - 'synphot is required for observations through bandpass') return super().observe_spectrum(*args, **kwargs) def observe_filter_name(self, filt, unit=None): @@ -449,7 +442,7 @@ def validate(cls, value): # only load the data once, save to hidden attribute ``_data`` data = source.get('data', None) if data is None: - fn = get_pkg_data_filename( + fn = get_pkg_data_path( os.path.join('data', source['filename'])) with open(fn, 'r') as inf: phot = json.load(inf) diff --git a/sbpy/calib/tests/test_core.py b/sbpy/calib/tests/test_core.py index 1855051f0..dbd925881 100644 --- a/sbpy/calib/tests/test_core.py +++ b/sbpy/calib/tests/test_core.py @@ -3,44 +3,34 @@ import pytest import numpy as np import astropy.units as u -import synphot + from ... import units as sbu from ... import bib from ...photometry import bandpass -from ...spectroscopy import sources from ..core import SpectralStandard -from .. import core as calib_core from .. import * class Star(SpectralStandard): pass - class TestSpectralStandard: def test_from_array(self): + pytest.importorskip("synphot") w = u.Quantity(np.linspace(0.3, 1.0), 'um') f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') s = Star.from_array(w, f) assert np.allclose(f.value, s(w, f.unit).value) - def test_from_array_importerror(self, monkeypatch): - monkeypatch.setattr(sources, 'synphot', None) - with pytest.raises(ImportError): - s = Star.from_array(None, None) - - # from_file tested by Sun and Vega - - def test_from_file_importerror(self, monkeypatch): - monkeypatch.setattr(sources, 'synphot', None) - with pytest.raises(ImportError): - s = Star.from_file(None) + # from_file() is tested by Sun and Vega def test_description(self): + pytest.importorskip("synphot") s = Star(None, description='asdf') assert s.description == 'asdf' def test_wave(self): + synphot = pytest.importorskip("synphot") w = u.Quantity(np.linspace(0.3, 1.0), 'um') f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') source = synphot.SourceSpectrum(synphot.Empirical1D, points=w, @@ -49,6 +39,7 @@ def test_wave(self): assert np.allclose(s.wave.to(w.unit).value, w.value) def test_fluxd(self): + synphot = pytest.importorskip("synphot") w = u.Quantity(np.linspace(0.3, 1.0), 'um') f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') source = synphot.SourceSpectrum(synphot.Empirical1D, points=w, @@ -57,6 +48,7 @@ def test_fluxd(self): assert np.allclose(s.fluxd.to(f.unit).value, f.value) def test_source(self): + synphot = pytest.importorskip("synphot") w = u.Quantity(np.linspace(0.3, 1.0), 'um') f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') source = synphot.SourceSpectrum(synphot.Empirical1D, points=w, @@ -68,6 +60,7 @@ def test_source(self): # meta tested by Sun def test_call_frequency(self): + pytest.importorskip("synphot") nu = u.Quantity(np.linspace(300, 1000), 'THz') f = u.Quantity(0.5 * nu.value + 0.1, 'Jy') s = Star.from_array(nu, f) @@ -75,6 +68,7 @@ def test_call_frequency(self): assert np.allclose(s(nu).value, 0.5 * nu.value + 0.1, rtol=0.002) def test_observe_units(self): + pytest.importorskip("synphot") w = u.Quantity(np.linspace(0.3, 1.0), 'um') f = u.Quantity(np.ones(len(w)) * 0.35 / w.value, 'W/(m2 um)') s = Star.from_array(w, f) @@ -92,6 +86,7 @@ def test_observe_units(self): a.value, d.to(a.unit, u.spectral_density(w)).value) def test_observe_wavelength(self): + pytest.importorskip("synphot") w = u.Quantity(np.linspace(0.3, 1.0), 'um') f = u.Quantity(np.ones(len(w)) * 0.35 / w.value, 'W/(m2 um)') s = Star.from_array(w, f) @@ -100,6 +95,7 @@ def test_observe_wavelength(self): assert np.isclose(s.observe(w, interpolate=True).value[1], 1.0) def test_observe_frequency(self): + pytest.importorskip("synphot") nu = u.Quantity(np.linspace(300, 1000), 'THz') f = u.Quantity(np.ones(len(nu)) * nu.value / 350, 'Jy') s = Star.from_array(nu, f) @@ -107,6 +103,8 @@ def test_observe_frequency(self): assert np.isclose(s.observe(nu).value[1], 1.0, rtol=0.004) def test_observe_bandpass(self): + synphot = pytest.importorskip("synphot") + w = u.Quantity(np.linspace(0.3, 1.0), 'um') f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') s = Star.from_array(w, f) @@ -127,6 +125,7 @@ def test_observe_bandpass(self): assert np.allclose(fluxd.value, [1, 1]) def test_observe_singlepointspectrumerror(self): + pytest.importorskip("synphot") w = u.Quantity(np.linspace(0.3, 1.0), 'um') f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') s = Star.from_array(w, f) @@ -136,6 +135,7 @@ def test_observe_singlepointspectrumerror(self): s.observe([1] * u.um) def test_observe_filter_name(self): + pytest.importorskip("synphot") s = Star(None) s._fluxd_state = solar_fluxd with solar_fluxd.set({'B': 0.327 * u.ABmag}): @@ -145,18 +145,9 @@ def test_observe_filter_name(self): with pytest.raises(FilterLookupError): fluxd = s.observe_filter_name('B', unit=u.ABmag) - def test_observe_synphotrequired(self, monkeypatch): - s = Star(None) - - monkeypatch.setattr(calib_core, 'synphot', None) - - with pytest.raises(sources.SynphotRequired): - s.observe_bandpass([None]) - - with pytest.raises(sources.SynphotRequired): - s.observe_spectrum([1, 2, 3] * u.um) - def test_observe_bad_wfb(self): + synphot = pytest.importorskip("synphot") + w = u.Quantity(np.linspace(0.3, 1.0), 'um') f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') source = synphot.SourceSpectrum(synphot.Empirical1D, points=w, @@ -166,6 +157,8 @@ def test_observe_bad_wfb(self): s.observe(np.arange(5)) def test_bibcode(self): + pytest.importorskip("synphot") + w = u.Quantity(np.linspace(0.3, 1.0), 'um') f = u.Quantity(np.ones(len(w)) * 0.35 / w.value, 'W/(m2 um)') s = Star.from_array(w, f, bibcode='asdf', description='fdsa') @@ -177,6 +170,8 @@ def test_bibcode(self): bib.reset() def test_observe_vegamag(self): + pytest.importorskip("synphot") + w = u.Quantity(np.linspace(0.3, 1.0), 'um') f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') s = Star.from_array(w, f) @@ -185,11 +180,11 @@ def test_observe_vegamag(self): # -18.60 is -2.5 * log10(3636e-11) assert np.isclose(mag.value, -18.60, atol=0.02) - @pytest.mark.parametrize('wfb, test, atol', ( - ((bandpass('johnson v'), bandpass('cousins i')), - 0.0140 * sbu.VEGAmag, 0.004), - ((600 * u.nm, 750 * u.nm), -0.2422 * u.ABmag, 0.001) - )) + @pytest.mark.parametrize( + 'wfb, test, atol', + ((('johnson v', 'cousins i'), 0.0140 * sbu.VEGAmag, 0.004), + ((600 * u.nm, 750 * u.nm), -0.2422 * u.ABmag, 0.001)) + ) def test_color_index(self, wfb, test, atol): """Test color index. @@ -205,6 +200,11 @@ def test_color_index(self, wfb, test, atol): -0.7268 - -0.4846 = -0.2422 """ + + pytest.importorskip("synphot") + + if type(wfb) is str: + wfb = bandpass(wfb) w = u.Quantity(np.linspace(0.3, 1.0), 'um') f = u.Quantity(np.ones(len(w)) * w.value**-3, 'W/(m2 um)') s = Star.from_array(w, f) @@ -212,6 +212,8 @@ def test_color_index(self, wfb, test, atol): assert np.isclose(ci.value, test.value, atol=atol) def test_color_index_typeerror(self): + pytest.importorskip("synphot") + w = u.Quantity(np.linspace(0.3, 1.0), 'um') f = u.Quantity(np.ones(len(w)) * w.value**-3, 'W/(m2 um)') s = Star.from_array(w, f) @@ -221,9 +223,11 @@ def test_color_index_typeerror(self): class Test_solar_spectrum: def test_validate_str(self): + pytest.importorskip("synphot") assert isinstance(solar_spectrum.validate('E490_2014'), Sun) def test_validate_Sun(self): + pytest.importorskip("synphot") wave = [1, 2] * u.um fluxd = [1, 2] * u.Jy sun = Sun.from_array(wave, fluxd, description='dummy source') @@ -238,32 +242,36 @@ def test_validate_error(self): ('E490_2014LR', solar_sources.SolarSpectra.E490_2014LR)) ) def test_set_string(self, name, source): + pytest.importorskip("synphot") with solar_spectrum.set(name): assert solar_spectrum.get().description == source['description'] - @pytest.mark.skipif('True') @pytest.mark.remote_data @pytest.mark.parametrize('name,source', ( ('Kurucz1993', solar_sources.SolarSpectra.Kurucz1993), ('Castelli1996', solar_sources.SolarSpectra.Castelli1996)) ) def test_set_string_remote(self, name, source): + pytest.importorskip("synphot") with solar_spectrum.set(name): assert solar_spectrum.get().description == source['description'] def test_set_source(self): + pytest.importorskip("synphot") wave = [1, 2] * u.um fluxd = [1, 2] * u.Jy source = Sun.from_array(wave, fluxd, description='dummy source') with solar_spectrum.set(source): assert solar_spectrum.get().description == 'dummy source' - class Test_vega_spectrum: def test_validate_str(self): + pytest.importorskip("synphot") + # pytest.importorskip("synphot") assert isinstance(vega_spectrum.validate('Bohlin2014'), Vega) def test_validate_Vega(self): + pytest.importorskip("synphot") wave = [1, 2] * u.um fluxd = [1, 2] * u.Jy vega = Vega.from_array(wave, fluxd, description='dummy source') @@ -274,11 +282,13 @@ def test_validate_error(self): vega_spectrum.validate(1) def test_set_string(self): + pytest.importorskip("synphot") with vega_spectrum.set('Bohlin2014'): assert vega_spectrum.get( ).description == vega_sources.VegaSpectra.Bohlin2014['description'] def test_set_source(self): + pytest.importorskip("synphot") wave = [1, 2] * u.um fluxd = [1, 2] * u.Jy source = Vega.from_array(wave, fluxd, description='dummy source') diff --git a/sbpy/calib/tests/test_sun.py b/sbpy/calib/tests/test_sun.py index 3395404d3..a3f9e8d16 100644 --- a/sbpy/calib/tests/test_sun.py +++ b/sbpy/calib/tests/test_sun.py @@ -8,16 +8,10 @@ from ...photometry import bandpass from .. import * -try: - import scipy - - HAS_SCIPY = True -except ImportError: - HAS_SCIPY = False - class TestSun: def test___repr__(self): + pytest.importorskip("synphot") with solar_spectrum.set("E490_2014LR"): assert repr(Sun.from_default()) == ( "" def test_from_builtin(self): + pytest.importorskip("synphot") sun = Sun.from_builtin("E490_2014LR") assert ( sun.description @@ -39,6 +34,7 @@ def test_from_builtin_unknown(self): Sun.from_builtin("not a solar spectrum") def test_from_default(self): + pytest.importorskip("synphot") with solar_spectrum.set("E490_2014LR"): sun = Sun.from_default() assert ( @@ -47,19 +43,22 @@ def test_from_default(self): ) def test_call_single_wavelength(self): + pytest.importorskip("synphot") with solar_spectrum.set("E490_2014"): sun = solar_spectrum.get() f = sun(0.5555 * u.um) assert np.isclose(f.value, 1897) def test_call_single_frequency(self): + pytest.importorskip("synphot") with solar_spectrum.set("E490_2014"): sun = solar_spectrum.get() f = sun(3e14 * u.Hz) assert np.isclose(f.value, 2.49484251e14) - @pytest.mark.skipif("not HAS_SCIPY") def test_sun_observe_wavelength_array(self): + pytest.importorskip("scipy") + pytest.importorskip("synphot") from scipy.integrate import trapz unit = "W/(m2 um)" @@ -91,6 +90,7 @@ def test_filt_units(self): """Colina et al. V=-26.75 mag, for zero-point flux density 36.7e-10 ergs/s/cm2/Å. """ + pytest.importorskip("synphot") sun = Sun.from_builtin("E490_2014") V = bandpass("johnson v") weff, fluxd = sun.observe_bandpass(V, unit="erg/(s cm2 AA)") @@ -104,6 +104,7 @@ def test_filt_vegamag(self): agreement is good. """ + pytest.importorskip("synphot") sun = Sun.from_builtin("E490_2014") V = bandpass("johnson v") fluxd = sun.observe(V, unit=JMmag) @@ -116,6 +117,7 @@ def test_filt_abmag(self): optical. """ + pytest.importorskip("synphot") sun = Sun.from_builtin("E490_2014") V = bandpass("johnson v") fluxd = sun.observe(V, unit=u.ABmag) @@ -128,6 +130,7 @@ def test_filt_stmag(self): optical. """ + pytest.importorskip("synphot") sun = Sun.from_builtin("E490_2014") V = bandpass("johnson v") fluxd = sun.observe(V, unit=u.STmag) @@ -140,6 +143,7 @@ def test_filt_solar_fluxd(self): assert np.isclose(fluxd.value, -26.76) def test_meta(self): + pytest.importorskip("synphot") sun = Sun.from_builtin("E490_2014") assert sun.meta is None @@ -152,6 +156,7 @@ def test_kurucz_nan_error(self): NaNs in Kurucz file should not affect this calculation. """ + pytest.importorskip("synphot") sun = Sun.from_builtin("Kurucz1993") V = bandpass("johnson v") fluxd = sun.observe(V, unit=u.ABmag) @@ -171,7 +176,7 @@ def test_castelli96(self): 2022-06-05: sbpy calculates 184.5 ergs/s/cm^2/A; agreement within 0.2% """ - + pytest.importorskip("synphot") sun = Sun.from_builtin("Castelli1996") V = bandpass("johnson v") fluxd = sun.observe(V, unit="erg/(s cm2 AA)") @@ -180,7 +185,7 @@ def test_castelli96(self): @pytest.mark.remote_data def test_calspec(self): """Verify CALSPEC solar model calibration.""" - + pytest.importorskip("synphot") sun = Sun.from_builtin("calspec") V = bandpass("johnson v") fluxd = sun.observe(V, unit=VEGAmag) diff --git a/sbpy/calib/tests/test_vega.py b/sbpy/calib/tests/test_vega.py index 7ba5928d3..97caacd1a 100644 --- a/sbpy/calib/tests/test_vega.py +++ b/sbpy/calib/tests/test_vega.py @@ -1,5 +1,5 @@ -import sys import inspect +import importlib import pytest import numpy as np import astropy.units as u @@ -8,17 +8,16 @@ from .. import core from .. import * -try: - import scipy - HAS_SCIPY = True -except ImportError: - HAS_SCIPY = False +pytest.importorskip("synphot") +def patched_import_module(name): + if name == "synphot": + raise ModuleNotFoundError + __import__(name) class Star(core.SpectralStandard): pass - class TestVega: def test___repr__(self): with vega_spectrum.set('Bohlin2014'): @@ -37,7 +36,7 @@ def test_call_wavelength(self, fluxd0): assert np.isclose(fluxd.value, fluxd0.value) def test_source_error(self, monkeypatch): - monkeypatch.setattr(core, 'synphot', None) + monkeypatch.setattr(importlib, "import_module", patched_import_module) vega = Vega.from_default() with pytest.raises(UndefinedSourceError): vega.source diff --git a/sbpy/data/ephem.py b/sbpy/data/ephem.py index d8068752e..40a5699db 100644 --- a/sbpy/data/ephem.py +++ b/sbpy/data/ephem.py @@ -15,23 +15,28 @@ from numpy import ndarray, hstack, iterable from astropy.time import Time from astropy.table import vstack, Column, QTable -from astropy.coordinates import Angle +from astropy.coordinates import Angle, EarthLocation import astropy.units as u -from astroquery.jplhorizons import Horizons -from astroquery.mpc import MPC -from astroquery.imcce import Miriade -from astroquery.exceptions import InvalidQueryError -from astropy.coordinates import EarthLocation + +# optional imports +try: + from astroquery.jplhorizons import Horizons + from astroquery.mpc import MPC + from astroquery.imcce import Miriade + from astroquery.exceptions import InvalidQueryError +except ImportError: + pass try: import pyoorb except ImportError: - pyoorb = None + pass from ..bib import cite from .core import DataClass, Conf, QueryError, TimeScaleWarning from ..exceptions import RequiredPackageUnavailable from .orbit import Orbit, OpenOrbError +from ..utils.decorators import requires __all__ = ['Ephem'] @@ -45,8 +50,9 @@ class Ephem(DataClass): """Class for querying, manipulating, and calculating ephemerides""" @classmethod - @cite({'data source': '1996DPS....28.2504G'}) - @cite({'software: astroquery': '2019AJ....157...98G'}) + @requires("astroquery") + @cite({'data source': '1996DPS....28.2504G', + 'software: astroquery': '2019AJ....157...98G'}) def from_horizons(cls, targetids, id_type='smallbody', epochs=None, location='500', **kwargs): """Load target ephemerides from @@ -246,9 +252,10 @@ def from_horizons(cls, targetids, id_type='smallbody', return cls.from_table(all_eph) @classmethod + @requires("astroquery") @cite({'data source': - 'https://minorplanetcenter.net/iau/MPEph/MPEph.html'}) - @cite({'software: astroquery': '2019AJ....157...98G'}) + 'https://minorplanetcenter.net/iau/MPEph/MPEph.html', + 'software: astroquery': '2019AJ....157...98G'}) def from_mpc(cls, targetids, epochs=None, location='500', ra_format=None, dec_format=None, **kwargs): """Load ephemerides from the @@ -464,8 +471,9 @@ def from_mpc(cls, targetids, epochs=None, location='500', ra_format=None, return cls.from_table(all_eph) @classmethod - @cite({'data source': 'http://vo.imcce.fr/webservices/miriade/'}) - @cite({'software: astroquery': '2019AJ....157...98G'}) + @requires("astroquery") + @cite({'data source': 'http://vo.imcce.fr/webservices/miriade/', + 'software: astroquery': '2019AJ....157...98G'}) def from_miriade(cls, targetids, objtype='asteroid', epochs=None, location='500', **kwargs): """Load target ephemerides from @@ -643,6 +651,7 @@ def from_miriade(cls, targetids, objtype='asteroid', return cls.from_table(all_eph) @classmethod + @requires("oorb") @cite({'method': '2009M&PS...44.1853G', 'software': 'https://github.com/oorb/oorb'}) def from_oo(cls, orbit, epochs=None, location='500', scope='full', diff --git a/sbpy/data/obs.py b/sbpy/data/obs.py index 23ad875c1..c400711c0 100644 --- a/sbpy/data/obs.py +++ b/sbpy/data/obs.py @@ -10,12 +10,18 @@ """ from astropy.time import Time -from astroquery.mpc import MPC + +try: + from astroquery.mpc import MPC +except ImportError: + pass + from astropy.table import vstack, hstack from .ephem import Ephem from .core import QueryError from ..bib import cite +from ..utils.decorators import requires __all__ = ['Obs'] @@ -24,9 +30,9 @@ class Obs(Ephem): """Class for querying, storing, and manipulating observations """ @classmethod - @cite({'data source': - 'https://minorplanetcenter.net/db_search'}) - @cite({'software: astroquery': '2019AJ....157...98G'}) + @requires("astroquery") + @cite({'data source': 'https://minorplanetcenter.net/db_search', + 'software: astroquery': '2019AJ....157...98G'}) def from_mpc(cls, targetid, id_type=None, **kwargs): """Load available observations for a target from the `Minor Planet Center `_ using @@ -95,6 +101,7 @@ def from_mpc(cls, targetid, id_type=None, **kwargs): return cls.from_table(results) + @requires("astroquery") @cite({'software: astroquery': '2019AJ....157...98G'}) def supplement(self, service='jplhorizons', id_field='targetname', epoch_field='epoch', location='500', diff --git a/sbpy/data/orbit.py b/sbpy/data/orbit.py index a06a768b0..376a2a1cc 100644 --- a/sbpy/data/orbit.py +++ b/sbpy/data/orbit.py @@ -10,28 +10,30 @@ """ import os import itertools +from warnings import warn import numpy as np from numpy import array, ndarray, double, arange from astropy.time import Time from astropy.table import vstack, QTable -from astroquery.jplhorizons import Horizons -from astroquery.mpc import MPC import astropy.units as u -from warnings import warn + +# optional imports +try: + from astroquery.jplhorizons import Horizons + from astroquery.mpc import MPC +except ImportError: + pass try: import pyoorb except ImportError: - pyoorb = None + pass from ..bib import cite, register from ..exceptions import RequiredPackageUnavailable, SbpyException from . import Conf, DataClass, QueryError, TimeScaleWarning +from ..utils.decorators import requires -try: - import pyoorb -except ImportError: - pyoorb = None __all__ = ['Orbit', 'OrbitError', 'OpenOrbError'] @@ -51,8 +53,9 @@ class Orbit(DataClass): elements""" @classmethod - @cite({'data source': '1996DPS....28.2504G'}) - @cite({'software: astroquery': '2019AJ....157...98G'}) + @requires("astroquery") + @cite({'data source': '1996DPS....28.2504G', + 'software: astroquery': '2019AJ....157...98G'}) def from_horizons(cls, targetids, id_type='smallbody', epochs=None, center='500@10', **kwargs): @@ -193,9 +196,9 @@ def from_horizons(cls, targetids, id_type='smallbody', return cls.from_table(all_elem) @classmethod - @cite({'data source': - 'https://minorplanetcenter.net/iau/MPEph/MPEph.html'}) - @cite({'software: astroquery': '2019AJ....157...98G'}) + @requires("astroquery") + @cite({'data source': 'https://minorplanetcenter.net/iau/MPEph/MPEph.html', + 'software: astroquery': '2019AJ....157...98G'}) def from_mpc(cls, targetids, id_type=None, target_type=None, **kwargs): """Load latest orbital elements from the `Minor Planet Center `_. @@ -471,6 +474,7 @@ def _from_oo_propagatation(oo_orbits, orbittype, timescale): return Orbit._from_oo(oo_orbits, orbittype, timescale) + @requires("oorb") @cite({'method': '2009M&PS...44.1853G', 'software': 'https://github.com/oorb/oorb'}) def oo_transform(self, orbittype, ephfile='de430'): @@ -585,6 +589,7 @@ def oo_transform(self, orbittype, ephfile='de430'): return orbits + @requires("oorb") @cite({'method': '2009M&PS...44.1853G', 'software': 'https://github.com/oorb/oorb'}) def oo_propagate(self, epochs, dynmodel='N', ephfile='de430'): diff --git a/sbpy/data/phys.py b/sbpy/data/phys.py index 61c11f256..6063d6875 100644 --- a/sbpy/data/phys.py +++ b/sbpy/data/phys.py @@ -13,12 +13,18 @@ import numpy as np import astropy.units as u -from astroquery.jplsbdb import SBDB -from astroquery.jplspec import JPLSpec + +# optional package +try: + from astroquery.jplsbdb import SBDB + from astroquery.jplspec import JPLSpec +except ImportError: + pass from .core import DataClass from ..bib import cite from ..exceptions import SbpyException +from ..utils.decorators import requires __all__ = ['Phys'] @@ -33,6 +39,7 @@ class Phys(DataClass): """Class for storing and querying physical properties""" @classmethod + @requires("astroquery") @cite({'software: astroquery': '2019AJ....157...98G'}) def from_sbdb(cls, targetids, references=False, notes=False): """Load physical properties from `JPL Small-Body Database (SBDB) @@ -181,6 +188,7 @@ def from_sbdb(cls, targetids, references=False, notes=False): return cls.from_columns(coldata, names=columnnames) @classmethod + @requires("astroquery") @cite({'software: astroquery': '2019AJ....157...98G'}) def from_jplspec(cls, temp_estimate, transition_freq, mol_tag): """Constants from JPLSpec catalog and energy calculations diff --git a/sbpy/data/tests/test_ephem.py b/sbpy/data/tests/test_ephem.py index 84a3ebcea..9c33dd72c 100644 --- a/sbpy/data/tests/test_ephem.py +++ b/sbpy/data/tests/test_ephem.py @@ -7,14 +7,8 @@ from ... import exceptions as sbe from ... import bib -from .. import ephem from .. import Ephem, Orbit -try: - import pyoorb -except ImportError: - pyoorb = None - # retreived from Horizons on 23 Apr 2020 CERES = { 'targetname': '1 Ceres', @@ -36,14 +30,10 @@ } -@pytest.mark.skipif('pyoorb is None') class TestEphemFromOorb: - def test_missing_pyoorb(self, monkeypatch): - monkeypatch.setattr(ephem, 'pyoorb', None) - with pytest.raises(sbe.RequiredPackageUnavailable): - Ephem.from_oo(CERES) - def test_units(self): + pytest.importorskip("pyoorb") + orbit1 = Orbit.from_dict(CERES) eph1 = Ephem.from_oo(orbit1) @@ -66,17 +56,23 @@ def test_units(self): assert u.isclose(eph1[k], eph2[k]) def test_basic(self): + pytest.importorskip("pyoorb") + orbit = Orbit.from_dict(CERES) oo_ephem = Ephem.from_oo(orbit, scope='basic') assert 'dec_rate' not in oo_ephem.field_names def test_timescale(self): + pytest.importorskip("pyoorb") + orbit = Orbit.from_dict(CERES) epoch = Time.now() oo_ephem = Ephem.from_oo(orbit, epochs=epoch, scope='basic') assert oo_ephem['epoch'].scale == epoch.scale def test_bib(self): + pytest.importorskip("pyoorb") + with bib.Tracking(): orbit = Orbit.from_dict(CERES) oo_ephem = Ephem.from_oo(orbit, scope='basic') diff --git a/sbpy/data/tests/test_ephem_remote.py b/sbpy/data/tests/test_ephem_remote.py index 6f42c607d..71a06766c 100644 --- a/sbpy/data/tests/test_ephem_remote.py +++ b/sbpy/data/tests/test_ephem_remote.py @@ -14,10 +14,7 @@ from ... import bib from .. import Ephem, Orbit, QueryError -try: - import pyoorb -except ImportError: - pyoorb = None +pytest.importorskip("astroquery") # retreived from Horizons on 23 Apr 2020 CERES = { @@ -407,11 +404,12 @@ def test_bib(self): @pytest.mark.remote_data -@pytest.mark.skipif('pyoorb is None') class TestEphemFromOorb: def test_by_comparison(self): """test from_oo method""" + pytest.importorskip("pyoorb") + orbit = Orbit.from_horizons('Ceres') horizons_ephem = Ephem.from_horizons('Ceres', location='500') oo_ephem = Ephem.from_oo(orbit) diff --git a/sbpy/data/tests/test_orbit.py b/sbpy/data/tests/test_orbit.py index 164323321..a4260b241 100644 --- a/sbpy/data/tests/test_orbit.py +++ b/sbpy/data/tests/test_orbit.py @@ -10,11 +10,6 @@ from .. import orbit as sbo from ..orbit import Orbit -try: - import pyoorb # noqa -except ImportError: - pyoorb = None - # CERES and CERES2 retrieved from Horizons on 24 Sep 2021 CERES = { 'targetname': '1 Ceres (A801 AA)', @@ -56,16 +51,12 @@ } -@pytest.mark.skipif('pyoorb is None') class TestOOTransform: - def test_missing_pyoorb(self, monkeypatch): - monkeypatch.setattr(sbo, 'pyoorb', None) - with pytest.raises(sbe.RequiredPackageUnavailable): - Orbit.from_dict(CERES).oo_transform('CART') - def test_oo_transform_kep(self): """ test oo_transform method""" + pytest.importorskip("pyoorb") + orbit = Orbit.from_dict(CERES) # transform to cart and back @@ -91,6 +82,8 @@ def test_oo_transform_kep(self): def test_oo_transform_com(self): """ test oo_transform method""" + pytest.importorskip("pyoorb") + orbit = Orbit.from_dict(CERES) # transform to cart and back @@ -121,6 +114,8 @@ def test_oo_transform_com(self): def test_timescales(self): """ test with input in UTC scale """ + pytest.importorskip("pyoorb") + orbit = Orbit.from_dict(CERES) orbit['epoch'] = orbit['epoch'].utc @@ -141,15 +136,12 @@ def test_timescales(self): assert kep_orbit['epoch'].scale == 'utc' -@pytest.mark.skipif('pyoorb is None') class TestOOPropagate: - def test_missing_pyoorb(self, monkeypatch): - monkeypatch.setattr(sbo, 'pyoorb', None) - with pytest.raises(sbe.RequiredPackageUnavailable): - Orbit.from_dict(CERES).oo_transform('CART') def test_oo_propagate(self): """ test oo_propagate method""" + + pytest.importorskip("pyoorb") orbit = Orbit.from_dict(CERES) future_orbit = Orbit.from_dict(CERES2) diff --git a/sbpy/data/tests/test_orbit_remote.py b/sbpy/data/tests/test_orbit_remote.py index b569cf60b..dc79b511d 100644 --- a/sbpy/data/tests/test_orbit_remote.py +++ b/sbpy/data/tests/test_orbit_remote.py @@ -11,15 +11,11 @@ from ..names import TargetNameParseError from ... import bib -try: - import pyoorb -except ImportError: - pyoorb = None +pytest.importorskip("astroquery") @pytest.mark.remote_data class TestOrbitFromHorizons: - def test_now(self): # current epoch now = Time.now() @@ -112,12 +108,13 @@ def test_break(self): Orbit.from_mpc('does not exist') -@pytest.mark.skipif('pyoorb is None') @pytest.mark.remote_data class TestOOTransform: def test_oo_transform(self): """ test oo_transform method""" + pytest.importorskip("pyoorb") + orbit = Orbit.from_horizons('Ceres') cart_orbit = orbit.oo_transform('CART') @@ -142,7 +139,8 @@ def test_oo_transform(self): u.isclose(orbit['epoch'][0].utc.jd, kep_orbit['epoch'][0].utc.jd) def test_timescales(self): - + pytest.importorskip("pyoorb") + orbit = Orbit.from_horizons('Ceres') orbit['epoch'] = orbit['epoch'].tdb @@ -170,12 +168,13 @@ def test_timescales(self): assert kep_orbit['epoch'].scale == 'tdb' -@pytest.mark.skipif('pyoorb is None') @pytest.mark.remote_data class TestOOPropagate: def test_oo_propagate(self): """ test oo_propagate method""" + pytest.importorskip("pyoorb") + orbit = Orbit.from_horizons('Ceres') epoch = Time(Time.now().jd + 100, format='jd', scale='utc') diff --git a/sbpy/data/tests/test_phys_remote.py b/sbpy/data/tests/test_phys_remote.py index 4bc320ffd..f73524582 100644 --- a/sbpy/data/tests/test_phys_remote.py +++ b/sbpy/data/tests/test_phys_remote.py @@ -4,6 +4,7 @@ import astropy.units as u from sbpy.data import Phys +pytest.importorskip("astroquery") @pytest.mark.remote_data def test_from_sbdb(): diff --git a/sbpy/photometry/bandpass.py b/sbpy/photometry/bandpass.py index 27d0a3e74..11871170e 100644 --- a/sbpy/photometry/bandpass.py +++ b/sbpy/photometry/bandpass.py @@ -9,9 +9,10 @@ ] import os -from astropy.utils.data import get_pkg_data_filename - +from astropy.utils.data import get_pkg_data_path +from ..utils.decorators import requires +@requires("synphot") def bandpass(name): """Retrieve bandpass transmission spectrum from sbpy. @@ -113,10 +114,7 @@ def bandpass(name): """ - try: - import synphot - except ImportError: - raise ImportError('synphot is required.') + import synphot name2file = { '2mass j': '2mass-j-rsr.txt', @@ -156,7 +154,7 @@ def bandpass(name): 'ps1 z': 'nm', } - fn = get_pkg_data_filename(os.path.join( + fn = get_pkg_data_path(os.path.join( '..', 'photometry', 'data', name2file[name.lower()])) bp = synphot.SpectralElement.from_file( fn, wave_unit=wave_unit.get(name.lower(), 'AA')) diff --git a/sbpy/photometry/core.py b/sbpy/photometry/core.py index d25725e55..19d9f2b5c 100644 --- a/sbpy/photometry/core.py +++ b/sbpy/photometry/core.py @@ -9,9 +9,23 @@ __all__ = ['DiskIntegratedPhaseFunc', 'LinearPhaseFunc', 'InvalidPhaseFunctionWarning'] +__doctest_requires__ = { + ("DiskIntegratedPhaseFunc", + "DiskIntegratedPhaseFunc._phase_integral", + "DiskIntegratedPhaseFunc.from_obs", + "LinearPhaseFunc", + "LinearPhaseFunc._phase_integral" + ): ["scipy"], +} + from collections import OrderedDict import numpy as np -from scipy.integrate import quad + +try: + from scipy.integrate import quad +except ImportError: + quad = None + from astropy.modeling import (Fittable1DModel, Parameter) import astropy.units as u from astropy import log @@ -19,6 +33,7 @@ quantity_to_dataclass) from ..units import reflectance from ..exceptions import SbpyWarning +from ..utils.decorators import requires class InvalidPhaseFunctionWarning(SbpyWarning): @@ -650,6 +665,7 @@ def to_ref(self, eph, normalized=None, append_results=False, **kwargs): else: return out + @requires("scipy") def _phase_integral(self, integrator=quad): """Calculate phase integral with numerical integration diff --git a/sbpy/photometry/iau.py b/sbpy/photometry/iau.py index f4b0da8cd..c8356f78b 100644 --- a/sbpy/photometry/iau.py +++ b/sbpy/photometry/iau.py @@ -7,19 +7,22 @@ """ + +__all__ = ['HG', 'HG12', 'HG1G2', 'HG12_Pen16'] + +__doctest_requires__ = { + "HG*": ["scipy"], +} + import warnings from collections import OrderedDict import numpy as np -from scipy.integrate import quad from astropy.modeling import Parameter import astropy.units as u from .core import DiskIntegratedPhaseFunc, InvalidPhaseFunctionWarning from ..bib import cite -__all__ = ['HG', 'HG12', 'HG1G2', 'HG12_Pen16'] - - # define the bounds of various model parameters _hg_g_bounds = [-0.253, 1.194] _hg1g2_g1_bounds = [0, 1] diff --git a/sbpy/photometry/tests/test_bandpass.py b/sbpy/photometry/tests/test_bandpass.py index 3f8c63dec..684e27c39 100644 --- a/sbpy/photometry/tests/test_bandpass.py +++ b/sbpy/photometry/tests/test_bandpass.py @@ -1,11 +1,13 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst import sys -import mock +from unittest import mock import pytest import numpy as np -from ..bandpass import * +synphot = pytest.importorskip("synphot") + +from ..bandpass import * @pytest.mark.parametrize('name, avgwave', ( ('2mass j', 12410.52630476), @@ -41,7 +43,7 @@ def test_bandpass(name, avgwave): assert np.isclose(bp.avgwave().value, avgwave) +@mock.patch.dict(sys.modules, {'synphot': None}) def test_bandpass_synphot(): - with mock.patch.dict(sys.modules, {'synphot': None}): with pytest.raises(ImportError): bandpass('sdss u') diff --git a/sbpy/photometry/tests/test_core.py b/sbpy/photometry/tests/test_core.py index 80f602f2e..5f94749bd 100644 --- a/sbpy/photometry/tests/test_core.py +++ b/sbpy/photometry/tests/test_core.py @@ -133,6 +133,7 @@ def test_to_ref(self): linphase.to_ref(10 * u.deg) def test_props(self): + pytest.importorskip("scipy") linphase = LinearPhaseFunc(5 * u.mag, 2.29 * u.mag/u.rad, radius=300 * u.km, wfb='V') assert np.isclose(linphase.geomalb, 0.0487089) @@ -148,6 +149,7 @@ def test__distance_module(self): assert np.allclose(module, module_test) def test_fit(self): + pytest.importorskip("scipy") pha = np.linspace(0, 60, 100) * u.deg mag = LinearPhaseFunc(5 * u.mag, 0.04 * u.mag/u.deg)(pha) + \ (np.random.rand(100)*0.2-0.1) * u.mag diff --git a/sbpy/photometry/tests/test_iau.py b/sbpy/photometry/tests/test_iau.py index 395355803..be72bd668 100644 --- a/sbpy/photometry/tests/test_iau.py +++ b/sbpy/photometry/tests/test_iau.py @@ -61,6 +61,7 @@ def test_from_phys(self): m = HG.from_phys(phys) def test_to_phys(self): + pytest.importorskip("scipy") m = HG(3.34 * u.mag, 0.12) p = m.to_phys() assert isinstance(p, Phys) @@ -105,12 +106,14 @@ def test__check_unit(self): assert ceres._unit == 'mag' def test_props(self): + pytest.importorskip("scipy") ceres = HG(3.34 * u.mag, 0.12, radius=480 * u.km, wfb='V') assert np.isclose(ceres.geomalb, 0.0877745) assert np.isclose(ceres.bondalb, 0.03198069) assert np.isclose(ceres.phaseint, 0.3643505755292945) def test_from_obs(self): + pytest.importorskip("scipy") pha = [0., 6.31578947, 12.63157895, 18.94736842, 25.26315789, 31.57894737, 37.89473684, 44.21052632, 50.52631579, 56.84210526, 63.15789474, 69.47368421, 75.78947368, 82.10526316, 88.42105263, @@ -284,6 +287,7 @@ def test_props(self): assert np.isclose(themis.phaseint, 0.374152) def test_from_obs(self): + pytest.importorskip("scipy") pha = [0., 6.31578947, 12.63157895, 18.94736842, 25.26315789, 31.57894737, 37.89473684, 44.21052632, 50.52631579, 56.84210526, 63.15789474, 69.47368421, 75.78947368, 82.10526316, 88.42105263, @@ -383,6 +387,7 @@ def test_props(self): assert np.isclose(themis.oe_amp, 0.23412300750840437) def test_from_obs(self): + pytest.importorskip("scipy") pha = [0., 6.31578947, 12.63157895, 18.94736842, 25.26315789, 31.57894737, 37.89473684, 44.21052632, 50.52631579, 56.84210526, 63.15789474, 69.47368421, 75.78947368, 82.10526316, 88.42105263, @@ -476,6 +481,7 @@ def test_props(self): assert np.isclose(themis.phaseint, 0.38042683486452) def test_from_obs(self): + pytest.importorskip("scipy") pha = [0., 6.31578947, 12.63157895, 18.94736842, 25.26315789, 31.57894737, 37.89473684, 44.21052632, 50.52631579, 56.84210526, 63.15789474, 69.47368421, 75.78947368, 82.10526316, 88.42105263, diff --git a/sbpy/spectroscopy/core.py b/sbpy/spectroscopy/core.py index 0005f060e..acee3da74 100644 --- a/sbpy/spectroscopy/core.py +++ b/sbpy/spectroscopy/core.py @@ -6,28 +6,16 @@ """ import numpy as np -import astropy.constants as con import astropy.units as u -from astropy.time import Time -from astroquery.jplhorizons import Horizons, conf -from astropy import log -from ..bib import register -from ..data import Phys - - -try: - from synphot import SpectralElement -except ImportError: - class SpectralElement: - pass __all__ = ['Spectrum', 'SpectralModel', 'SpectralGradient'] __doctest_requires__ = { - "SpectralGradient": ["astropy>=5.3"], - "SpectralGradient.from_color": ["astropy>=5.3"], - "SpectralGradient.renormalize": ["astropy>=5.3"], + "SpectralGradient": ["astropy>=5.3", "synphot"], + "SpectralGradient.from_color": ["astropy>=5.3", "synphot"], + "SpectralGradient.to_color": ["synphot"], + "SpectralGradient.renormalize": ["astropy>=5.3", "synphot"], } diff --git a/sbpy/spectroscopy/sources.py b/sbpy/spectroscopy/sources.py index 868e069d8..9053bda16 100644 --- a/sbpy/spectroscopy/sources.py +++ b/sbpy/spectroscopy/sources.py @@ -16,6 +16,7 @@ from abc import ABC import astropy.units as u from astropy.utils.data import download_file, _is_url +from astropy.utils.decorators import deprecated try: import synphot @@ -26,7 +27,11 @@ class SpectralElement: pass + class BaseUnitlessSpectrum: + pass + from ..exceptions import SbpyException +from ..utils.decorators import requires __doctest_requires__ = { 'SpectralSource': ['synphot'], @@ -37,7 +42,7 @@ class SpectralElement: class SinglePointSpectrumError(SbpyException): """Single point provided, but multiple values expected.""" - +@deprecated("v0.4.1") class SynphotRequired(SbpyException): pass @@ -65,15 +70,13 @@ class SpectralSource(ABC): """ + @requires("synphot") def __init__(self, source, description=None): - if synphot is None: - raise SynphotRequired( - 'synphot required for {}.'.format(self.__class__.__name__)) - self._source = source self._description = description @classmethod + @requires("synphot") def from_array(cls, wave, fluxd, meta=None, **kwargs): """Create standard from arrays. @@ -93,9 +96,6 @@ def from_array(cls, wave, fluxd, meta=None, **kwargs): Passed to object initialization. """ - if synphot is None: - raise ImportError( - 'synphot required for {}.'.format(cls.__name__)) source = synphot.SourceSpectrum( synphot.Empirical1D, points=wave, lookup_table=fluxd, @@ -104,6 +104,7 @@ def from_array(cls, wave, fluxd, meta=None, **kwargs): return cls(source, **kwargs) @classmethod + @requires("synphot") def from_file(cls, filename, wave_unit=None, flux_unit=None, cache=True, **kwargs): """Load the source spectrum from a file. @@ -128,10 +129,6 @@ def from_file(cls, filename, wave_unit=None, flux_unit=None, """ - if synphot is None: - raise ImportError( - 'synphot required for {}.'.format(cls.__name__)) - if filename.lower().endswith(('.fits', '.fit', '.fz')): read_spec = synphot.specio.read_fits_spec else: @@ -504,6 +501,7 @@ class BlackbodySource(SpectralSource): """ + @requires("synphot") def __init__(self, T=None): super().__init__(None, description='πB(T)') @@ -529,14 +527,20 @@ class Reddening(BaseUnitlessSpectrum): ---------- S : `~SpectralGradient` The spectral gradient to redden. + """ + @u.quantity_input(S=u.percent / u.um) + @requires("synphot") def __init__(self, S): + if getattr(S, 'wave0', None) is None: raise ValueError("Normalization wavelength in `S` (.wave0) is " "required by not available.") + wv = [1, 2] * S.wave0 df = (S.wave0 * S).to('').value + super().__init__( synphot.Empirical1D, points=wv, lookup_table=[1, 1+df], fill_value=None) diff --git a/sbpy/spectroscopy/tests/test_sources.py b/sbpy/spectroscopy/tests/test_sources.py index 03ae8b153..4af11070b 100644 --- a/sbpy/spectroscopy/tests/test_sources.py +++ b/sbpy/spectroscopy/tests/test_sources.py @@ -5,10 +5,9 @@ import astropy.units as u from astropy.modeling.models import BlackBody import astropy.constants as const -import synphot -from .. import sources -from ..sources import (BlackbodySource, SpectralSource, SynphotRequired, - Reddening) +synphot = pytest.importorskip("synphot") + +from ..sources import (BlackbodySource, SpectralSource, Reddening) from ..core import SpectralGradient from ...photometry import bandpass from ...units import hundred_nm @@ -16,7 +15,6 @@ V = bandpass('johnson v') I = bandpass('cousins i') - class Star(SpectralSource): def __init__(self): super().__init__(synphot.SourceSpectrum( @@ -24,11 +22,6 @@ def __init__(self): class TestSpectralSource: - def test_init_error(self, monkeypatch): - with pytest.raises(SynphotRequired): - monkeypatch.setattr(sources, 'synphot', None) - Star() - @pytest.mark.parametrize('wfb, interpolate', ( ([V], False), ([1] * u.um, True), diff --git a/sbpy/spectroscopy/tests/test_specgrad.py b/sbpy/spectroscopy/tests/test_specgrad.py index ccd29e0fb..2af0e8c86 100644 --- a/sbpy/spectroscopy/tests/test_specgrad.py +++ b/sbpy/spectroscopy/tests/test_specgrad.py @@ -1,11 +1,20 @@ +import pytest import numpy as np import astropy.units as u import pytest from ...units import hundred_nm -from ...photometry import bandpass from ..core import SpectralGradient +try: + import synphot + from ...photometry import bandpass +except ModuleNotFoundError: + # do nothing function so that the TestSpectralGradient class can compile + synphot = None + def bandpass(bp): + pass +@pytest.mark.skipif("synphot is None") class TestSpectralGradient(): def test_new(self): S = SpectralGradient(100 / u.um, wave=(525, 575) * u.nm) diff --git a/sbpy/units/core.py b/sbpy/units/core.py index f579863ae..8bbbdaa3b 100644 --- a/sbpy/units/core.py +++ b/sbpy/units/core.py @@ -26,17 +26,20 @@ 'projected_size', ] +__doctest_requires__ = { + "spectral_density_vega": ["synphot"] +} + from warnings import warn import numpy as np import astropy.units as u import astropy.constants as const -from ..exceptions import SbpyWarning from ..calib import ( Vega, Sun, vega_fluxd, FilterLookupError, UndefinedSourceError ) from .. import data as sbd -from ..spectroscopy.sources import SinglePointSpectrumError, SynphotRequired -from ..exceptions import OptionalPackageUnavailable, SbpyWarning +from ..spectroscopy.sources import SinglePointSpectrumError +from ..exceptions import RequiredPackageUnavailable, OptionalPackageUnavailable, SbpyWarning VEGA = u.def_unit(['VEGA', 'VEGAflux'], @@ -152,7 +155,7 @@ def spectral_density_vega(wfb): lambda f_phys, fluxd0=fluxd0.value: f_phys / fluxd0, lambda f_vega, fluxd0=fluxd0.value: f_vega * fluxd0 )) - except SynphotRequired: + except RequiredPackageUnavailable: warn(OptionalPackageUnavailable( 'synphot is required for Vega-based magnitude conversions' ' with {}'.format(wfb))) diff --git a/sbpy/units/tests/test_core.py b/sbpy/units/tests/test_core.py index fca60334a..530b8ccb7 100644 --- a/sbpy/units/tests/test_core.py +++ b/sbpy/units/tests/test_core.py @@ -5,16 +5,14 @@ import numpy as np import astropy.units as u from astropy.io import ascii -from astropy.utils.data import get_pkg_data_filename -import synphot +from astropy.utils.data import get_pkg_data_path + from .. import core from ..core import * from ...photometry import bandpass from ...calib import (vega_spectrum, vega_fluxd, solar_fluxd, solar_spectrum, Sun, Vega) - - -JohnsonV = bandpass('Johnson V') +from ...exceptions import RequiredPackageUnavailable @pytest.mark.parametrize('unit,test', ( @@ -55,6 +53,9 @@ def test_spectral_density_vega_wf(wf, fluxd, to): Flux density at 5557.5 AA is from Bohlin 2014 (0.5% uncertainty). """ + + pytest.importorskip("synphot") + v = fluxd.to(to.unit, spectral_density_vega(wf)) assert v.unit == to.unit if to.unit in (VEGAmag, JMmag): @@ -86,7 +87,10 @@ def test_spectral_density_vega_bp(filename, fluxd, to, tol): agreement with 0.03 mag. """ - fn = get_pkg_data_filename(os.path.join( + + synphot = pytest.importorskip("synphot") + + fn = get_pkg_data_path(os.path.join( '..', '..', 'photometry', 'data', filename)) bp = synphot.SpectralElement.from_file(fn) @@ -98,19 +102,14 @@ def test_spectral_density_vega_bp(filename, fluxd, to, tol): assert np.isclose(v.value, to.value, rtol=tol) -def test_spectral_density_vega_synphot_import_fail(monkeypatch): - from ...calib import core as calib_core - monkeypatch.setattr(calib_core, 'synphot', None) - assert spectral_density_vega([1, 2, 3] * u.um) == [] - - def test_spectral_density_vega_undefinedsourceerror(): + pytest.importorskip("synphot") with vega_spectrum.set(Vega(None)): assert spectral_density_vega([1, 2, 3] * u.um) == [] @pytest.mark.parametrize('fluxd, wfb, f_sun, ref', ( - (3.4 * VEGAmag, JohnsonV, None, 0.02865984), + (3.4 * VEGAmag, "Johnson V", None, 0.02865984), (3.4 * VEGAmag, 5500 * u.AA, None, 0.02774623), (1.56644783e-09 * u.Unit('W/(m2 um)'), 'V', 1839.93273227 * u.Unit('W/(m2 um)'), 0.02865984), @@ -129,6 +128,9 @@ def test_reflectance_ref(fluxd, wfb, f_sun, ref): """ + pytest.importorskip("synphot") + + wfb = bandpass(wfb) if type(wfb) is str else wfb xsec = 6.648e5 * u.km**2 with vega_fluxd.set({'V': u.Quantity(3.589e-9, 'erg/(s cm2 AA)')}): @@ -139,7 +141,7 @@ def test_reflectance_ref(fluxd, wfb, f_sun, ref): @pytest.mark.parametrize('fluxd, wfb, f_sun, radius', ( - (3.4 * VEGAmag, JohnsonV, None, 460.01351274), + (3.4 * VEGAmag, "Johnson V", None, 460.01351274), (3.4 * VEGAmag, 5500 * u.AA, None, 452.62198065), (1.56644783e-09 * u.Unit('W/(m2 um)'), 'V', 1839.93273227 * u.Unit('W/(m2 um)'), 460.01351274), @@ -158,6 +160,10 @@ def test_reflectance_xsec(fluxd, wfb, f_sun, radius): """ + pytest.importorskip("synphot") + + wfb = bandpass(wfb) if type(wfb) is str else wfb + ref = 0.02865984 / u.sr with vega_fluxd.set({'V': u.Quantity(3.589e-9, 'erg/(s cm2 AA)')}): with solar_fluxd.set({wfb: f_sun}): @@ -179,12 +185,14 @@ def test_reflectance_spec(): 'data/hi05070405_9000036-avg-spec.txt', data from McLaughlin et al (2014). """ + pytest.importorskip("synphot") + ifov = 1e-5 * u.rad delta = 15828 * u.km rh = 1.5 * u.au # Tempel 1 spectrum, includes reference solar spectrum - fn = get_pkg_data_filename( + fn = get_pkg_data_path( os.path.join('data', 'hi05070405_9000036-avg-spec.txt')) t1 = ascii.read(fn) sun = Sun.from_array(t1['wave'] * u.um, diff --git a/setup.cfg b/setup.cfg index 9c8dbb5f7..dca773af7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,21 +27,27 @@ packages = find: zip_save = False python_requires = >=3.8 setup_requires = setuptools_scm + # keep requirements in synchronization with docs/install.rst install_requires = numpy>=1.18.0 astropy>=4.3 - ads>=0.12 - synphot>=1.1.1 - astroquery>=0.4.5 include_package_data = True [options.extras_require] -all = +recommended = + ads>=0.12 + astroquery>=0.4.5 scipy>=1.3 + synphot>=1.1.1 +all = + ads>=0.12 + astroquery>=0.4.5 ginga photutils pyyaml + scipy>=1.3 + synphot>=1.1.1 test = pytest>=7.0 pytest-astropy From 7115c65308db91aa5ad47d616f3d56679f163ced Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Mon, 21 Aug 2023 08:54:47 -0400 Subject: [PATCH 05/21] Address code style checks. --- sbpy/activity/gas/tests/test_core.py | 17 +++++++++-------- sbpy/activity/gas/tests/test_data.py | 14 ++++++++------ sbpy/activity/gas/tests/test_prodrate_remote.py | 1 + sbpy/activity/tests/test_dust.py | 2 +- sbpy/calib/core.py | 1 + sbpy/calib/tests/test_core.py | 2 ++ sbpy/calib/tests/test_vega.py | 3 +++ sbpy/data/tests/test_orbit.py | 2 +- sbpy/data/tests/test_orbit_remote.py | 2 +- sbpy/data/tests/test_phys_remote.py | 1 + sbpy/photometry/bandpass.py | 1 + sbpy/photometry/core.py | 2 +- sbpy/photometry/tests/test_bandpass.py | 1 + sbpy/spectroscopy/sources.py | 1 + sbpy/spectroscopy/tests/test_sources.py | 1 + sbpy/spectroscopy/tests/test_specgrad.py | 2 ++ sbpy/units/tests/test_core.py | 2 +- sbpy/utils/tests/test_decorators.py | 2 ++ 18 files changed, 38 insertions(+), 19 deletions(-) diff --git a/sbpy/activity/gas/tests/test_core.py b/sbpy/activity/gas/tests/test_core.py index d6599d335..4eba9afb7 100644 --- a/sbpy/activity/gas/tests/test_core.py +++ b/sbpy/activity/gas/tests/test_core.py @@ -18,6 +18,7 @@ ) from ....data import Phys + def test_photo_lengthscale(): gamma = photo_lengthscale("OH", "CS93") assert gamma == 1.6e5 * u.km @@ -99,7 +100,7 @@ def test_column_density_small_angular_aperture(self): Should be within 1% of ideal value. """ - + pytest.importorskip("scipy") Q = 1e28 / u.s @@ -117,7 +118,7 @@ def test_column_density(self): Test column density for aperture = lengthscale. """ - + pytest.importorskip("scipy") Q = 1e28 / u.s @@ -131,9 +132,9 @@ def test_column_density(self): def test_total_number_large_aperture(self): """Test column density for aperture >> lengthscale.""" - + pytest.importorskip("scipy") - + Q = 1 / u.s v = 1 * u.km / u.s rho = 1000 * u.km @@ -150,7 +151,7 @@ def test_total_number_circular_aperture_angular(self): Code initially from Quanzhi Ye. """ - + pytest.importorskip("scipy") Q = 1e25 / u.s @@ -249,7 +250,7 @@ def test_circular_integration_0step(self): parent only """ - + pytest.importorskip("scipy") # Nobs = 2.314348613550494e+27 @@ -286,7 +287,7 @@ def test_circular_integration_1step(self): def test_total_number_annulus(self): """Test column density for annular aperture.""" - + pytest.importorskip("scipy") Q = 1 / u.s @@ -355,7 +356,7 @@ def test_total_number_gaussian_ap(self): which is within 0.5% of the test value below """ - + pytest.importorskip("scipy") parent = 1.4e4 * u.km diff --git a/sbpy/activity/gas/tests/test_data.py b/sbpy/activity/gas/tests/test_data.py index 53411ae3c..41b0930ce 100644 --- a/sbpy/activity/gas/tests/test_data.py +++ b/sbpy/activity/gas/tests/test_data.py @@ -6,32 +6,34 @@ import astropy.units as u from .. import data + def patched_import_module(name): if name == "scipy": raise ModuleNotFoundError __import__(name) + class TestOHFluorescenceSA88: def test_linear_interpolation(self, monkeypatch): monkeypatch.setattr(importlib, "import_module", patched_import_module) - model = data.OHFluorescenceSA88('0-0') + model = data.OHFluorescenceSA88("0-0") LN = model(-1 * u.km / u.s) assert np.isclose(LN.value, 1.54e-15) def test_tau(self): - model = data.OHFluorescenceSA88('0-0') + model = data.OHFluorescenceSA88("0-0") assert np.isclose(model.tau[0].value, 2.87e5) def test_inversion(self): - model = data.OHFluorescenceSA88('0-0') + model = data.OHFluorescenceSA88("0-0") assert np.isclose(model.inversion[0], -0.304) def test_rdot_error(self): - model = data.OHFluorescenceSA88('0-0') + model = data.OHFluorescenceSA88("0-0") with pytest.raises(ValueError): model(-61 * u.km / u.s) def test_rh_error(self): - model = data.OHFluorescenceSA88('0-0') + model = data.OHFluorescenceSA88("0-0") with pytest.raises(ValueError): - model({'rdot': 1 * u.km / u.s, 'rh': 0.4 * u.au}) + model({"rdot": 1 * u.km / u.s, "rh": 0.4 * u.au}) diff --git a/sbpy/activity/gas/tests/test_prodrate_remote.py b/sbpy/activity/gas/tests/test_prodrate_remote.py index a23dcdd8b..c11f7d627 100644 --- a/sbpy/activity/gas/tests/test_prodrate_remote.py +++ b/sbpy/activity/gas/tests/test_prodrate_remote.py @@ -13,6 +13,7 @@ intensity_conversion, beta_factor, total_number, from_Haser) from ....data import Ephem, Phys + class MockPyradex: """ Class to be the mock return value of NonLTE.from_pyradex diff --git a/sbpy/activity/tests/test_dust.py b/sbpy/activity/tests/test_dust.py index a9627aa3f..f15b1a3d1 100644 --- a/sbpy/activity/tests/test_dust.py +++ b/sbpy/activity/tests/test_dust.py @@ -103,7 +103,7 @@ def test_from_fluxd(self, wfb, fluxd0, rho, rh, delta, S, afrho0, tol): synphot.units.convert_flux(6182, 11.97 * u.ABmag, u.STmag) """ - + pytest.importorskip("synphot") eph = Ephem.from_dict(dict(rh=rh * u.au, delta=delta * u.au)) diff --git a/sbpy/calib/core.py b/sbpy/calib/core.py index 65fededdc..e858a7ca0 100644 --- a/sbpy/calib/core.py +++ b/sbpy/calib/core.py @@ -43,6 +43,7 @@ from synphot import SpectralElement except ImportError: synphot = None + class SpectralElement: pass diff --git a/sbpy/calib/tests/test_core.py b/sbpy/calib/tests/test_core.py index dbd925881..5f52f4c72 100644 --- a/sbpy/calib/tests/test_core.py +++ b/sbpy/calib/tests/test_core.py @@ -14,6 +14,7 @@ class Star(SpectralStandard): pass + class TestSpectralStandard: def test_from_array(self): pytest.importorskip("synphot") @@ -264,6 +265,7 @@ def test_set_source(self): with solar_spectrum.set(source): assert solar_spectrum.get().description == 'dummy source' + class Test_vega_spectrum: def test_validate_str(self): pytest.importorskip("synphot") diff --git a/sbpy/calib/tests/test_vega.py b/sbpy/calib/tests/test_vega.py index 97caacd1a..9afbdff7a 100644 --- a/sbpy/calib/tests/test_vega.py +++ b/sbpy/calib/tests/test_vega.py @@ -10,14 +10,17 @@ pytest.importorskip("synphot") + def patched_import_module(name): if name == "synphot": raise ModuleNotFoundError __import__(name) + class Star(core.SpectralStandard): pass + class TestVega: def test___repr__(self): with vega_spectrum.set('Bohlin2014'): diff --git a/sbpy/data/tests/test_orbit.py b/sbpy/data/tests/test_orbit.py index a4260b241..15ab59fb9 100644 --- a/sbpy/data/tests/test_orbit.py +++ b/sbpy/data/tests/test_orbit.py @@ -140,7 +140,7 @@ class TestOOPropagate: def test_oo_propagate(self): """ test oo_propagate method""" - + pytest.importorskip("pyoorb") orbit = Orbit.from_dict(CERES) diff --git a/sbpy/data/tests/test_orbit_remote.py b/sbpy/data/tests/test_orbit_remote.py index dc79b511d..98a21902f 100644 --- a/sbpy/data/tests/test_orbit_remote.py +++ b/sbpy/data/tests/test_orbit_remote.py @@ -140,7 +140,7 @@ def test_oo_transform(self): def test_timescales(self): pytest.importorskip("pyoorb") - + orbit = Orbit.from_horizons('Ceres') orbit['epoch'] = orbit['epoch'].tdb diff --git a/sbpy/data/tests/test_phys_remote.py b/sbpy/data/tests/test_phys_remote.py index f73524582..2ae351ba1 100644 --- a/sbpy/data/tests/test_phys_remote.py +++ b/sbpy/data/tests/test_phys_remote.py @@ -6,6 +6,7 @@ pytest.importorskip("astroquery") + @pytest.mark.remote_data def test_from_sbdb(): """ test from_sbdb method""" diff --git a/sbpy/photometry/bandpass.py b/sbpy/photometry/bandpass.py index 11871170e..570a39727 100644 --- a/sbpy/photometry/bandpass.py +++ b/sbpy/photometry/bandpass.py @@ -12,6 +12,7 @@ from astropy.utils.data import get_pkg_data_path from ..utils.decorators import requires + @requires("synphot") def bandpass(name): """Retrieve bandpass transmission spectrum from sbpy. diff --git a/sbpy/photometry/core.py b/sbpy/photometry/core.py index 19d9f2b5c..9c01ecb8c 100644 --- a/sbpy/photometry/core.py +++ b/sbpy/photometry/core.py @@ -15,7 +15,7 @@ "DiskIntegratedPhaseFunc.from_obs", "LinearPhaseFunc", "LinearPhaseFunc._phase_integral" - ): ["scipy"], + ): ["scipy"], } from collections import OrderedDict diff --git a/sbpy/photometry/tests/test_bandpass.py b/sbpy/photometry/tests/test_bandpass.py index 684e27c39..820fef32e 100644 --- a/sbpy/photometry/tests/test_bandpass.py +++ b/sbpy/photometry/tests/test_bandpass.py @@ -9,6 +9,7 @@ from ..bandpass import * + @pytest.mark.parametrize('name, avgwave', ( ('2mass j', 12410.52630476), ('2mass h', 16513.66475736), diff --git a/sbpy/spectroscopy/sources.py b/sbpy/spectroscopy/sources.py index 9053bda16..3af51849e 100644 --- a/sbpy/spectroscopy/sources.py +++ b/sbpy/spectroscopy/sources.py @@ -42,6 +42,7 @@ class BaseUnitlessSpectrum: class SinglePointSpectrumError(SbpyException): """Single point provided, but multiple values expected.""" + @deprecated("v0.4.1") class SynphotRequired(SbpyException): pass diff --git a/sbpy/spectroscopy/tests/test_sources.py b/sbpy/spectroscopy/tests/test_sources.py index 4af11070b..9b6e6b9dc 100644 --- a/sbpy/spectroscopy/tests/test_sources.py +++ b/sbpy/spectroscopy/tests/test_sources.py @@ -15,6 +15,7 @@ V = bandpass('johnson v') I = bandpass('cousins i') + class Star(SpectralSource): def __init__(self): super().__init__(synphot.SourceSpectrum( diff --git a/sbpy/spectroscopy/tests/test_specgrad.py b/sbpy/spectroscopy/tests/test_specgrad.py index 2af0e8c86..8299d8ac8 100644 --- a/sbpy/spectroscopy/tests/test_specgrad.py +++ b/sbpy/spectroscopy/tests/test_specgrad.py @@ -11,9 +11,11 @@ except ModuleNotFoundError: # do nothing function so that the TestSpectralGradient class can compile synphot = None + def bandpass(bp): pass + @pytest.mark.skipif("synphot is None") class TestSpectralGradient(): def test_new(self): diff --git a/sbpy/units/tests/test_core.py b/sbpy/units/tests/test_core.py index 530b8ccb7..aba101b59 100644 --- a/sbpy/units/tests/test_core.py +++ b/sbpy/units/tests/test_core.py @@ -129,7 +129,7 @@ def test_reflectance_ref(fluxd, wfb, f_sun, ref): """ pytest.importorskip("synphot") - + wfb = bandpass(wfb) if type(wfb) is str else wfb xsec = 6.648e5 * u.km**2 diff --git a/sbpy/utils/tests/test_decorators.py b/sbpy/utils/tests/test_decorators.py index b64113550..1c0c676b2 100644 --- a/sbpy/utils/tests/test_decorators.py +++ b/sbpy/utils/tests/test_decorators.py @@ -3,6 +3,7 @@ from ..decorators import requires, optional from ...exceptions import RequiredPackageUnavailable, OptionalPackageUnavailable + def test_requires(): @requires("unavailable_package") def f(): @@ -11,6 +12,7 @@ def f(): with pytest.raises(RequiredPackageUnavailable): f() + def test_optional(): @optional("unavailable_package") def f(): From 3a27eaff40b910adfeb9553937d230c8fd4ac88e Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Mon, 21 Aug 2023 15:55:57 -0400 Subject: [PATCH 06/21] Update for testing with "all" dependencies --- sbpy/calib/core.py | 10 +- sbpy/calib/tests/test_core.py | 221 ++++++++++++++----------- sbpy/calib/tests/test_vega.py | 32 ++-- sbpy/data/ephem.py | 2 +- sbpy/data/orbit.py | 4 +- sbpy/photometry/tests/test_bandpass.py | 15 +- sbpy/spectroscopy/sources.py | 2 +- sbpy/units/tests/test_core.py | 15 +- 8 files changed, 175 insertions(+), 126 deletions(-) diff --git a/sbpy/calib/core.py b/sbpy/calib/core.py index e858a7ca0..93f62f0d1 100644 --- a/sbpy/calib/core.py +++ b/sbpy/calib/core.py @@ -36,7 +36,8 @@ from ..exceptions import SbpyException from .. import bib from . import solar_sources, vega_sources -from ..utils.decorators import optional, requires +from ..utils.decorators import requires +from ..utils import optional try: import synphot @@ -129,17 +130,16 @@ def from_builtin(cls, name): return cls.from_file(**parameters) @classmethod - @optional("synphot") def from_default(cls): """Initialize new spectral standard from current default. The spectrum will be ``None`` if `synphot` is not available. """ - if synphot is None: - standard = cls(None) - else: + if optional("synphot"): standard = cls._spectrum_state.get() + else: + standard = cls(None) return standard @classmethod diff --git a/sbpy/calib/tests/test_core.py b/sbpy/calib/tests/test_core.py index 5f52f4c72..38ca18fb6 100644 --- a/sbpy/calib/tests/test_core.py +++ b/sbpy/calib/tests/test_core.py @@ -18,8 +18,8 @@ class Star(SpectralStandard): class TestSpectralStandard: def test_from_array(self): pytest.importorskip("synphot") - w = u.Quantity(np.linspace(0.3, 1.0), 'um') - f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') + w = u.Quantity(np.linspace(0.3, 1.0), "um") + f = u.Quantity(np.ones(len(w)), "W/(m2 um)") s = Star.from_array(w, f) assert np.allclose(f.value, s(w, f.unit).value) @@ -27,33 +27,36 @@ def test_from_array(self): def test_description(self): pytest.importorskip("synphot") - s = Star(None, description='asdf') - assert s.description == 'asdf' + s = Star(None, description="asdf") + assert s.description == "asdf" def test_wave(self): synphot = pytest.importorskip("synphot") - w = u.Quantity(np.linspace(0.3, 1.0), 'um') - f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') - source = synphot.SourceSpectrum(synphot.Empirical1D, points=w, - lookup_table=f) + w = u.Quantity(np.linspace(0.3, 1.0), "um") + f = u.Quantity(np.ones(len(w)), "W/(m2 um)") + source = synphot.SourceSpectrum( + synphot.Empirical1D, points=w, lookup_table=f + ) s = Star(source) assert np.allclose(s.wave.to(w.unit).value, w.value) def test_fluxd(self): synphot = pytest.importorskip("synphot") - w = u.Quantity(np.linspace(0.3, 1.0), 'um') - f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') - source = synphot.SourceSpectrum(synphot.Empirical1D, points=w, - lookup_table=f) + w = u.Quantity(np.linspace(0.3, 1.0), "um") + f = u.Quantity(np.ones(len(w)), "W/(m2 um)") + source = synphot.SourceSpectrum( + synphot.Empirical1D, points=w, lookup_table=f + ) s = Star(source) assert np.allclose(s.fluxd.to(f.unit).value, f.value) def test_source(self): synphot = pytest.importorskip("synphot") - w = u.Quantity(np.linspace(0.3, 1.0), 'um') - f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') - source = synphot.SourceSpectrum(synphot.Empirical1D, points=w, - lookup_table=f) + w = u.Quantity(np.linspace(0.3, 1.0), "um") + f = u.Quantity(np.ones(len(w)), "W/(m2 um)") + source = synphot.SourceSpectrum( + synphot.Empirical1D, points=w, lookup_table=f + ) s = Star(source) f = s.source(w) assert s.source == source @@ -62,34 +65,33 @@ def test_source(self): def test_call_frequency(self): pytest.importorskip("synphot") - nu = u.Quantity(np.linspace(300, 1000), 'THz') - f = u.Quantity(0.5 * nu.value + 0.1, 'Jy') + nu = u.Quantity(np.linspace(300, 1000), "THz") + f = u.Quantity(0.5 * nu.value + 0.1, "Jy") s = Star.from_array(nu, f) nu = np.linspace(310, 999) * u.THz assert np.allclose(s(nu).value, 0.5 * nu.value + 0.1, rtol=0.002) def test_observe_units(self): pytest.importorskip("synphot") - w = u.Quantity(np.linspace(0.3, 1.0), 'um') - f = u.Quantity(np.ones(len(w)) * 0.35 / w.value, 'W/(m2 um)') + w = u.Quantity(np.linspace(0.3, 1.0), "um") + f = u.Quantity(np.ones(len(w)) * 0.35 / w.value, "W/(m2 um)") s = Star.from_array(w, f) w = [0.3, 0.35, 0.4] * u.um a = s.observe(w) - b = s.observe(w, unit='W/(m2 Hz)') + b = s.observe(w, unit="W/(m2 Hz)") c = s.observe(w, unit=sbu.VEGAmag) d = s.observe(w, unit=u.ABmag) + assert np.allclose(a.value, b.to(a.unit, u.spectral_density(w)).value) assert np.allclose( - a.value, b.to(a.unit, u.spectral_density(w)).value) - assert np.allclose( - a.value, c.to(a.unit, sbu.spectral_density_vega(w)).value) + a.value, c.to(a.unit, sbu.spectral_density_vega(w)).value + ) assert c.unit == sbu.VEGAmag - assert np.allclose( - a.value, d.to(a.unit, u.spectral_density(w)).value) + assert np.allclose(a.value, d.to(a.unit, u.spectral_density(w)).value) def test_observe_wavelength(self): pytest.importorskip("synphot") - w = u.Quantity(np.linspace(0.3, 1.0), 'um') - f = u.Quantity(np.ones(len(w)) * 0.35 / w.value, 'W/(m2 um)') + w = u.Quantity(np.linspace(0.3, 1.0), "um") + f = u.Quantity(np.ones(len(w)) * 0.35 / w.value, "W/(m2 um)") s = Star.from_array(w, f) w = [0.3, 0.35, 0.4] * u.um assert np.isclose(s.observe(w).value[1], 1.0, rtol=0.001) @@ -97,8 +99,8 @@ def test_observe_wavelength(self): def test_observe_frequency(self): pytest.importorskip("synphot") - nu = u.Quantity(np.linspace(300, 1000), 'THz') - f = u.Quantity(np.ones(len(nu)) * nu.value / 350, 'Jy') + nu = u.Quantity(np.linspace(300, 1000), "THz") + f = u.Quantity(np.ones(len(nu)) * nu.value / 350, "Jy") s = Star.from_array(nu, f) nu = [325, 350, 375] * u.THz assert np.isclose(s.observe(nu).value[1], 1.0, rtol=0.004) @@ -106,29 +108,37 @@ def test_observe_frequency(self): def test_observe_bandpass(self): synphot = pytest.importorskip("synphot") - w = u.Quantity(np.linspace(0.3, 1.0), 'um') - f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') + w = u.Quantity(np.linspace(0.3, 1.0), "um") + f = u.Quantity(np.ones(len(w)), "W/(m2 um)") s = Star.from_array(w, f) - bp = synphot.SpectralElement(synphot.Box1D, x_0=0.55 * u.um, - width=0.1 * u.um) + bp = synphot.SpectralElement( + synphot.Box1D, x_0=0.55 * u.um, width=0.1 * u.um + ) fluxd = s.observe(bp) assert np.allclose(fluxd.value, [1]) - bps = [synphot.SpectralElement(synphot.Box1D, x_0=0.55 * u.um, - width=0.1 * u.um), - synphot.SpectralElement(synphot.Box1D, x_0=0.65 * u.um, - width=0.1 * u.um)] - fluxd = s.observe(bps, unit='W/(m2 um)') + bps = [ + synphot.SpectralElement( + synphot.Box1D, x_0=0.55 * u.um, width=0.1 * u.um + ), + synphot.SpectralElement( + synphot.Box1D, x_0=0.65 * u.um, width=0.1 * u.um + ), + ] + fluxd = s.observe(bps, unit="W/(m2 um)") assert np.allclose(fluxd.value, [1, 1]) - lambda_eff, fluxd = s.observe_bandpass(bps, unit='W/(m2 um)') + lambda_eff, fluxd = s.observe_bandpass(bps, unit="W/(m2 um)") assert np.allclose(fluxd.value, [1, 1]) def test_observe_singlepointspectrumerror(self): pytest.importorskip("synphot") - w = u.Quantity(np.linspace(0.3, 1.0), 'um') - f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') + + from ...spectroscopy import sources + + w = u.Quantity(np.linspace(0.3, 1.0), "um") + f = u.Quantity(np.ones(len(w)), "W/(m2 um)") s = Star.from_array(w, f) with pytest.raises(sources.SinglePointSpectrumError): s.observe(1 * u.um) @@ -139,20 +149,21 @@ def test_observe_filter_name(self): pytest.importorskip("synphot") s = Star(None) s._fluxd_state = solar_fluxd - with solar_fluxd.set({'B': 0.327 * u.ABmag}): - fluxd = s.observe_filter_name('B', unit=u.ABmag)[-1] + with solar_fluxd.set({"B": 0.327 * u.ABmag}): + fluxd = s.observe_filter_name("B", unit=u.ABmag)[-1] assert fluxd.value == 0.327 with pytest.raises(FilterLookupError): - fluxd = s.observe_filter_name('B', unit=u.ABmag) + fluxd = s.observe_filter_name("B", unit=u.ABmag) def test_observe_bad_wfb(self): synphot = pytest.importorskip("synphot") - w = u.Quantity(np.linspace(0.3, 1.0), 'um') - f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') - source = synphot.SourceSpectrum(synphot.Empirical1D, points=w, - lookup_table=f) + w = u.Quantity(np.linspace(0.3, 1.0), "um") + f = u.Quantity(np.ones(len(w)), "W/(m2 um)") + source = synphot.SourceSpectrum( + synphot.Empirical1D, points=w, lookup_table=f + ) s = Star(source) with pytest.raises(TypeError): s.observe(np.arange(5)) @@ -160,31 +171,33 @@ def test_observe_bad_wfb(self): def test_bibcode(self): pytest.importorskip("synphot") - w = u.Quantity(np.linspace(0.3, 1.0), 'um') - f = u.Quantity(np.ones(len(w)) * 0.35 / w.value, 'W/(m2 um)') - s = Star.from_array(w, f, bibcode='asdf', description='fdsa') + w = u.Quantity(np.linspace(0.3, 1.0), "um") + f = u.Quantity(np.ones(len(w)) * 0.35 / w.value, "W/(m2 um)") + s = Star.from_array(w, f, bibcode="asdf", description="fdsa") with bib.Tracking(): s.source - assert 'asdf' in bib.show() + assert "asdf" in bib.show() bib.reset() def test_observe_vegamag(self): pytest.importorskip("synphot") - w = u.Quantity(np.linspace(0.3, 1.0), 'um') - f = u.Quantity(np.ones(len(w)), 'W/(m2 um)') + w = u.Quantity(np.linspace(0.3, 1.0), "um") + f = u.Quantity(np.ones(len(w)), "W/(m2 um)") s = Star.from_array(w, f) - V = bandpass('johnson v') + V = bandpass("johnson v") mag = s.observe(V, unit=sbu.VEGAmag) # -18.60 is -2.5 * log10(3636e-11) assert np.isclose(mag.value, -18.60, atol=0.02) @pytest.mark.parametrize( - 'wfb, test, atol', - ((('johnson v', 'cousins i'), 0.0140 * sbu.VEGAmag, 0.004), - ((600 * u.nm, 750 * u.nm), -0.2422 * u.ABmag, 0.001)) + "wfb, test, atol", + ( + (("johnson v", "cousins i"), 0.0140 * sbu.VEGAmag, 0.004), + ((600 * u.nm, 750 * u.nm), -0.2422 * u.ABmag, 0.001), + ), ) def test_color_index(self, wfb, test, atol): """Test color index. @@ -204,10 +217,11 @@ def test_color_index(self, wfb, test, atol): pytest.importorskip("synphot") - if type(wfb) is str: - wfb = bandpass(wfb) - w = u.Quantity(np.linspace(0.3, 1.0), 'um') - f = u.Quantity(np.ones(len(w)) * w.value**-3, 'W/(m2 um)') + if isinstance(wfb[0], str): + wfb = (bandpass(wfb[0]), bandpass(wfb[1])) + + w = u.Quantity(np.linspace(0.3, 1.0), "um") + f = u.Quantity(np.ones(len(w)) * w.value**-3, "W/(m2 um)") s = Star.from_array(w, f) eff_wave, ci = s.color_index(wfb, test.unit) assert np.isclose(ci.value, test.value, atol=atol) @@ -215,8 +229,8 @@ def test_color_index(self, wfb, test, atol): def test_color_index_typeerror(self): pytest.importorskip("synphot") - w = u.Quantity(np.linspace(0.3, 1.0), 'um') - f = u.Quantity(np.ones(len(w)) * w.value**-3, 'W/(m2 um)') + w = u.Quantity(np.linspace(0.3, 1.0), "um") + f = u.Quantity(np.ones(len(w)) * w.value**-3, "W/(m2 um)") s = Star.from_array(w, f) with pytest.raises(TypeError): s.color_index((None, None), u.ABmag) @@ -225,58 +239,64 @@ def test_color_index_typeerror(self): class Test_solar_spectrum: def test_validate_str(self): pytest.importorskip("synphot") - assert isinstance(solar_spectrum.validate('E490_2014'), Sun) + assert isinstance(solar_spectrum.validate("E490_2014"), Sun) def test_validate_Sun(self): pytest.importorskip("synphot") wave = [1, 2] * u.um fluxd = [1, 2] * u.Jy - sun = Sun.from_array(wave, fluxd, description='dummy source') + sun = Sun.from_array(wave, fluxd, description="dummy source") assert isinstance(solar_spectrum.validate(sun), Sun) def test_validate_error(self): with pytest.raises(TypeError): solar_spectrum.validate(1) - @pytest.mark.parametrize('name,source', ( - ('E490_2014', solar_sources.SolarSpectra.E490_2014), - ('E490_2014LR', solar_sources.SolarSpectra.E490_2014LR)) + @pytest.mark.parametrize( + "name,source", + ( + ("E490_2014", solar_sources.SolarSpectra.E490_2014), + ("E490_2014LR", solar_sources.SolarSpectra.E490_2014LR), + ), ) def test_set_string(self, name, source): pytest.importorskip("synphot") with solar_spectrum.set(name): - assert solar_spectrum.get().description == source['description'] + assert solar_spectrum.get().description == source["description"] @pytest.mark.remote_data - @pytest.mark.parametrize('name,source', ( - ('Kurucz1993', solar_sources.SolarSpectra.Kurucz1993), - ('Castelli1996', solar_sources.SolarSpectra.Castelli1996)) + @pytest.mark.parametrize( + "name,source", + ( + ("Kurucz1993", solar_sources.SolarSpectra.Kurucz1993), + ("Castelli1996", solar_sources.SolarSpectra.Castelli1996), + ), ) def test_set_string_remote(self, name, source): pytest.importorskip("synphot") with solar_spectrum.set(name): - assert solar_spectrum.get().description == source['description'] + assert solar_spectrum.get().description == source["description"] def test_set_source(self): pytest.importorskip("synphot") wave = [1, 2] * u.um fluxd = [1, 2] * u.Jy - source = Sun.from_array(wave, fluxd, description='dummy source') + source = Sun.from_array(wave, fluxd, description="dummy source") with solar_spectrum.set(source): - assert solar_spectrum.get().description == 'dummy source' + assert solar_spectrum.get().description == "dummy source" class Test_vega_spectrum: def test_validate_str(self): pytest.importorskip("synphot") # pytest.importorskip("synphot") - assert isinstance(vega_spectrum.validate('Bohlin2014'), Vega) + assert isinstance(vega_spectrum.validate("Bohlin2014"), Vega) def test_validate_Vega(self): pytest.importorskip("synphot") wave = [1, 2] * u.um fluxd = [1, 2] * u.Jy - vega = Vega.from_array(wave, fluxd, description='dummy source') + vega = Vega.from_array(wave, fluxd, description="dummy source") assert isinstance(vega_spectrum.validate(vega), Vega) def test_validate_error(self): @@ -285,39 +305,42 @@ def test_validate_error(self): def test_set_string(self): pytest.importorskip("synphot") - with vega_spectrum.set('Bohlin2014'): - assert vega_spectrum.get( - ).description == vega_sources.VegaSpectra.Bohlin2014['description'] + with vega_spectrum.set("Bohlin2014"): + assert ( + vega_spectrum.get().description + == vega_sources.VegaSpectra.Bohlin2014["description"] + ) def test_set_source(self): pytest.importorskip("synphot") wave = [1, 2] * u.um fluxd = [1, 2] * u.Jy - source = Vega.from_array(wave, fluxd, description='dummy source') + source = Vega.from_array(wave, fluxd, description="dummy source") with vega_spectrum.set(source): - assert vega_spectrum.get().description == 'dummy source' + assert vega_spectrum.get().description == "dummy source" class TestSolarFluxd: def test_willmer2018(self): - with solar_fluxd.set('Willmer2018'): + with solar_fluxd.set("Willmer2018"): filters = solar_fluxd.get() - assert np.isclose(filters['PS1 r'].value, - 10**(-0.4 * (21.1 - 26.66))) - assert filters['PS1 r'].unit == 'erg/(s cm2 AA)' - assert filters['PS1 r(lambda eff)'].value == 0.6156 - assert filters['PS1 r(lambda eff)'].unit == u.um - assert filters['PS1 r(lambda pivot)'].value == 0.6201 - assert filters['PS1 r(lambda pivot)'].unit == u.um + assert np.isclose( + filters["PS1 r"].value, 10 ** (-0.4 * (21.1 - 26.66)) + ) + assert filters["PS1 r"].unit == "erg/(s cm2 AA)" + assert filters["PS1 r(lambda eff)"].value == 0.6156 + assert filters["PS1 r(lambda eff)"].unit == u.um + assert filters["PS1 r(lambda pivot)"].value == 0.6201 + assert filters["PS1 r(lambda pivot)"].unit == u.um class TestVegaFluxd: def test_willmer2018(self): - with vega_fluxd.set('Willmer2018'): + with vega_fluxd.set("Willmer2018"): filters = vega_fluxd.get() - assert filters['PS1 r'].value == 2.53499e-09 - assert filters['PS1 r'].unit == 'erg/(s cm2 AA)' - assert filters['PS1 r(lambda eff)'].value == 0.6156 - assert filters['PS1 r(lambda eff)'].unit == u.um - assert filters['PS1 r(lambda pivot)'].value == 0.6201 - assert filters['PS1 r(lambda pivot)'].unit == u.um + assert filters["PS1 r"].value == 2.53499e-09 + assert filters["PS1 r"].unit == "erg/(s cm2 AA)" + assert filters["PS1 r(lambda eff)"].value == 0.6156 + assert filters["PS1 r(lambda eff)"].unit == u.um + assert filters["PS1 r(lambda pivot)"].value == 0.6201 + assert filters["PS1 r(lambda pivot)"].unit == u.um diff --git a/sbpy/calib/tests/test_vega.py b/sbpy/calib/tests/test_vega.py index 9afbdff7a..9942f97f3 100644 --- a/sbpy/calib/tests/test_vega.py +++ b/sbpy/calib/tests/test_vega.py @@ -8,14 +8,6 @@ from .. import core from .. import * -pytest.importorskip("synphot") - - -def patched_import_module(name): - if name == "synphot": - raise ModuleNotFoundError - __import__(name) - class Star(core.SpectralStandard): pass @@ -23,6 +15,7 @@ class Star(core.SpectralStandard): class TestVega: def test___repr__(self): + pytest.importorskip("synphot") with vega_spectrum.set('Bohlin2014'): assert (repr(Vega.from_default()) == '') @@ -34,42 +27,54 @@ def test___repr__(self): u.Quantity(3.44e-8, 'W/(m2 um)'), 1 * sbu.VEGA )) def test_call_wavelength(self, fluxd0): + pytest.importorskip("synphot") vega = Vega.from_default() fluxd = vega(5557.5 * u.AA, unit=fluxd0.unit) assert np.isclose(fluxd.value, fluxd0.value) - def test_source_error(self, monkeypatch): - monkeypatch.setattr(importlib, "import_module", patched_import_module) + def test_source_error(self): + # Only test when synphot is not available. + try: + import synphot + pytest.skip() + except ImportError: + pass vega = Vega.from_default() with pytest.raises(UndefinedSourceError): vega.source def test_from_builtin(self): + pytest.importorskip("synphot") vega = Vega.from_builtin('Bohlin2014') assert vega.description == vega_sources.VegaSpectra.Bohlin2014['description'] def test_from_builtin_unknown(self): + pytest.importorskip("synphot") with pytest.raises(UndefinedSourceError): Vega.from_builtin('not a vega spectrum') def test_from_default(self): + pytest.importorskip("synphot") with vega_spectrum.set('Bohlin2014'): vega = Vega.from_default() assert vega.description == vega_sources.VegaSpectra.Bohlin2014['description'] def test_call_single_wavelength(self): + pytest.importorskip("synphot") with vega_spectrum.set('Bohlin2014'): vega = Vega.from_default() f = vega(0.55 * u.um) assert np.isclose(f.value, 3.546923511485616e-08) # W/(m2 μm) def test_call_single_frequency(self): + pytest.importorskip("synphot") with vega_spectrum.set('Bohlin2014'): vega = Vega.from_default() f = vega(3e14 * u.Hz) assert np.isclose(f.value, 2129.13636259) # Jy def test_show_builtin(self, capsys): + pytest.importorskip("synphot") Vega.show_builtin() captured = capsys.readouterr() sources = inspect.getmembers( @@ -78,18 +83,21 @@ def test_show_builtin(self, capsys): assert k in captured.out def test_observe_vega_fluxd(self): + pytest.importorskip("synphot") with vega_fluxd.set({'V': 3631 * u.Jy}): vega = Vega(None) fluxd = vega.observe('V', unit='Jy') assert np.isclose(fluxd.value, 3631) def test_observe_vega_missing_lambda_pivot(self): + pytest.importorskip("synphot") with pytest.raises(u.UnitConversionError): with vega_fluxd.set({'filter1': 1 * u.Jy}): vega = Vega(None) fluxd = vega.observe(['filter1'], unit='W/(m2 um)') def test_observe_vega_list(self): + pytest.importorskip("synphot") with vega_fluxd.set({'filter1': 1 * u.Jy, 'filter2': 2 * u.Jy}): vega = Vega(None) fluxd = vega.observe(['filter1', 'filter2'], unit='Jy') @@ -103,6 +111,7 @@ def test_color_index_wavelength(self): V - I = -0.013 - 0.414 = -0.427 """ + pytest.importorskip("synphot") w = [5476, 7993] * u.AA vega = Vega.from_default() lambda_eff, ci = vega.color_index(w, u.ABmag) @@ -111,6 +120,7 @@ def test_color_index_wavelength(self): def test_color_index_frequency(self): """Check that frequency is correctly coverted to wavelength.""" + pytest.importorskip("synphot") w = [0.5476, 0.7993] * u.um f = w.to(u.Hz, u.spectral()) vega = Vega.from_default() @@ -118,6 +128,7 @@ def test_color_index_frequency(self): assert np.allclose(lambda_eff.value, w.value) def test_color_index_bandpass(self): + pytest.importorskip("synphot") """Compare to Willmer 2018.""" bp = (bandpass('johnson v'), bandpass('cousins i')) vega = Vega.from_default() @@ -128,6 +139,7 @@ def test_color_index_bandpass(self): assert np.isclose(ci.value, -0.427, atol=0.02) def test_color_index_filter(self): + pytest.importorskip("synphot") vega = Vega.from_default() with vega_fluxd.set({ 'V': -0.013 * u.ABmag, diff --git a/sbpy/data/ephem.py b/sbpy/data/ephem.py index 40a5699db..7d9b275eb 100644 --- a/sbpy/data/ephem.py +++ b/sbpy/data/ephem.py @@ -651,7 +651,7 @@ def from_miriade(cls, targetids, objtype='asteroid', return cls.from_table(all_eph) @classmethod - @requires("oorb") + @requires("pyoorb") @cite({'method': '2009M&PS...44.1853G', 'software': 'https://github.com/oorb/oorb'}) def from_oo(cls, orbit, epochs=None, location='500', scope='full', diff --git a/sbpy/data/orbit.py b/sbpy/data/orbit.py index 376a2a1cc..48f8634f3 100644 --- a/sbpy/data/orbit.py +++ b/sbpy/data/orbit.py @@ -474,7 +474,7 @@ def _from_oo_propagatation(oo_orbits, orbittype, timescale): return Orbit._from_oo(oo_orbits, orbittype, timescale) - @requires("oorb") + @requires("pyoorb") @cite({'method': '2009M&PS...44.1853G', 'software': 'https://github.com/oorb/oorb'}) def oo_transform(self, orbittype, ephfile='de430'): @@ -589,7 +589,7 @@ def oo_transform(self, orbittype, ephfile='de430'): return orbits - @requires("oorb") + @requires("pyoorb") @cite({'method': '2009M&PS...44.1853G', 'software': 'https://github.com/oorb/oorb'}) def oo_propagate(self, epochs, dynmodel='N', ephfile='de430'): diff --git a/sbpy/photometry/tests/test_bandpass.py b/sbpy/photometry/tests/test_bandpass.py index 820fef32e..46a7cab8d 100644 --- a/sbpy/photometry/tests/test_bandpass.py +++ b/sbpy/photometry/tests/test_bandpass.py @@ -4,10 +4,8 @@ from unittest import mock import pytest import numpy as np - -synphot = pytest.importorskip("synphot") - from ..bandpass import * +from ...exceptions import RequiredPackageUnavailable @pytest.mark.parametrize('name, avgwave', ( @@ -44,7 +42,12 @@ def test_bandpass(name, avgwave): assert np.isclose(bp.avgwave().value, avgwave) -@mock.patch.dict(sys.modules, {'synphot': None}) def test_bandpass_synphot(): - with pytest.raises(ImportError): - bandpass('sdss u') + # skip if synphot is available + try: + import synphot + pytest.skip() + except ImportError: + pass + with pytest.raises(RequiredPackageUnavailable): + bandpass('sdss u') diff --git a/sbpy/spectroscopy/sources.py b/sbpy/spectroscopy/sources.py index 3af51849e..c4947e727 100644 --- a/sbpy/spectroscopy/sources.py +++ b/sbpy/spectroscopy/sources.py @@ -43,7 +43,7 @@ class SinglePointSpectrumError(SbpyException): """Single point provided, but multiple values expected.""" -@deprecated("v0.4.1") +@deprecated("v0.5.0") class SynphotRequired(SbpyException): pass diff --git a/sbpy/units/tests/test_core.py b/sbpy/units/tests/test_core.py index aba101b59..c28a5c98a 100644 --- a/sbpy/units/tests/test_core.py +++ b/sbpy/units/tests/test_core.py @@ -130,7 +130,13 @@ def test_reflectance_ref(fluxd, wfb, f_sun, ref): pytest.importorskip("synphot") - wfb = bandpass(wfb) if type(wfb) is str else wfb + try: + # passes for "Johnson V", but fails for "V" band + # we will set "V" band to a specific value below + wfb = bandpass(wfb) if type(wfb) is str else wfb + except KeyError: + pass + xsec = 6.648e5 * u.km**2 with vega_fluxd.set({'V': u.Quantity(3.589e-9, 'erg/(s cm2 AA)')}): @@ -162,7 +168,12 @@ def test_reflectance_xsec(fluxd, wfb, f_sun, radius): pytest.importorskip("synphot") - wfb = bandpass(wfb) if type(wfb) is str else wfb + try: + # passes for "Johnson V", but fails for "V" band + # we will set "V" band to a specific value below + wfb = bandpass(wfb) if type(wfb) is str else wfb + except KeyError: + pass ref = 0.02865984 / u.sr with vega_fluxd.set({'V': u.Quantity(3.589e-9, 'erg/(s cm2 AA)')}): From 5612e3ceab1f5dd12ccd79adb9b38ecaea83168c Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Mon, 21 Aug 2023 18:54:18 -0400 Subject: [PATCH 07/21] Fix test_bandpass failures. --- sbpy/photometry/tests/test_bandpass.py | 67 ++++++++++++++------------ 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/sbpy/photometry/tests/test_bandpass.py b/sbpy/photometry/tests/test_bandpass.py index 46a7cab8d..3eaee01a4 100644 --- a/sbpy/photometry/tests/test_bandpass.py +++ b/sbpy/photometry/tests/test_bandpass.py @@ -1,43 +1,45 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst -import sys -from unittest import mock import pytest import numpy as np from ..bandpass import * from ...exceptions import RequiredPackageUnavailable -@pytest.mark.parametrize('name, avgwave', ( - ('2mass j', 12410.52630476), - ('2mass h', 16513.66475736), - ('2mass ks', 21656.32078498), - ('atlas c', 5408.72465833), - ('atlas o', 6866.26069039), - ('cousins r', 6499.91478190), - ('cousins i', 7884.10581303), - ('johnson u', 3598.54452094), - ('johnson b', 4385.9244053), - ('johnson v', 5490.55520036), - ('ps1 g', 4866.4578708), - ('ps1 r', 6214.623038), - ('ps1 i', 7544.570357), - ('ps1 w', 6389.3518241), - ('ps1 y', 9633.2481028), - ('ps1 z', 8679.46803), - ('sdss u', 3561.78873418), - ('sdss g', 4718.87224631), - ('sdss r', 6185.19447698), - ('sdss i', 7499.70417489), - ('sdss z', 8961.48833667), - ('wfc3 f438w', 4324.44438089), - ('wfc3 f606w', 5946.7429129), - ('wise w1', 34002.59750555), - ('wise w2', 46520.16384937), - ('wise w3', 128108.72187073), - ('wise w4', 223752.74423983), -)) +@pytest.mark.parametrize( + "name, avgwave", + ( + ("2mass j", 12410.52630476), + ("2mass h", 16513.66475736), + ("2mass ks", 21656.32078498), + ("atlas c", 5408.72465833), + ("atlas o", 6866.26069039), + ("cousins r", 6499.91478190), + ("cousins i", 7884.10581303), + ("johnson u", 3598.54452094), + ("johnson b", 4385.9244053), + ("johnson v", 5490.55520036), + ("ps1 g", 4866.4578708), + ("ps1 r", 6214.623038), + ("ps1 i", 7544.570357), + ("ps1 w", 6389.3518241), + ("ps1 y", 9633.2481028), + ("ps1 z", 8679.46803), + ("sdss u", 3561.78873418), + ("sdss g", 4718.87224631), + ("sdss r", 6185.19447698), + ("sdss i", 7499.70417489), + ("sdss z", 8961.48833667), + ("wfc3 f438w", 4324.44438089), + ("wfc3 f606w", 5946.7429129), + ("wise w1", 34002.59750555), + ("wise w2", 46520.16384937), + ("wise w3", 128108.72187073), + ("wise w4", 223752.74423983), + ), +) def test_bandpass(name, avgwave): + pytest.importorskip("synphot") bp = bandpass(name) assert np.isclose(bp.avgwave().value, avgwave) @@ -46,8 +48,9 @@ def test_bandpass_synphot(): # skip if synphot is available try: import synphot + pytest.skip() except ImportError: pass with pytest.raises(RequiredPackageUnavailable): - bandpass('sdss u') + bandpass("sdss u") From 792a02071d59d8e39a3167e8b78bfbc7760cb45f Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Mon, 21 Aug 2023 19:52:58 -0400 Subject: [PATCH 08/21] Fix indentation. --- docs/development/index.rst | 6 ++---- tox.ini | 4 +++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/development/index.rst b/docs/development/index.rst index a2bc5435e..977fb30b6 100644 --- a/docs/development/index.rst +++ b/docs/development/index.rst @@ -124,10 +124,8 @@ Technical requirements name list` list where it makes sense * consider using the sbpy function and decorator helpers to test for the presence of optional dependencies: - * `~sbpy.utils.requires` and `~sbpy.utils.decorators.requires` raise an - exception if a package cannot be imported. - * `~sbpy.utils.optional` and `~sbpy.utils.decorators.optional` warn the user - if a package cannot be imported. + * `~sbpy.utils.requires` and `~sbpy.utils.decorators.requires` raise an exception if a package cannot be imported. + * `~sbpy.utils.optional` and `~sbpy.utils.decorators.optional` warn the user if a package cannot be imported. * a CHANGELOG entry is required; also update the :doc:`/status` where applicable diff --git a/tox.ini b/tox.ini index 516870004..201c705de 100644 --- a/tox.ini +++ b/tox.ini @@ -92,7 +92,9 @@ commands = [testenv:build_docs] changedir = docs description = invoke sphinx-build to build the HTML docs -extras = docs +extras = + docs + all commands = pip freeze sphinx-build -W -b html . _build/html From 77ca6ecc5347ada42257f4e4ec4ede551696da92 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Mon, 21 Aug 2023 20:27:37 -0400 Subject: [PATCH 09/21] Measure coverage with alldeps --- .github/workflows/ci_tests.yml | 52 +++++++++++++++++----------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index f3e81a702..e54839e61 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -7,9 +7,9 @@ name: CI Tests on: push: branches: - - main + - main tags: - - '*' + - "*" pull_request: # branches: # only build on PRs against 'main' if you need to further limit when CI is run. # - main @@ -34,17 +34,17 @@ jobs: ARCH_ON_CI: "normal" IS_CRON: "false" submodules: false - coverage: '' + coverage: "" envs: | - name: Code style checks linux: codestyle - - name: Python 3.11 with minimal dependencies and full coverage + - name: Python 3.11 with minimal dependencies, measuring coverage linux: py311-test-cov coverage: codecov - - name: Python 3.10 with all optional dependencies - linux: py310-test-alldeps + - name: Python 3.10 with all optional dependencies, measuring coverage + linux: py310-test-alldeps-cov - name: Python 3.8 with oldest supported versions linux: py38-test-oldestdeps @@ -56,23 +56,23 @@ jobs: env: ARCH_ON_CI: "normal" steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set up python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - name: Set up gfortran on ${{ matrix.os }} - if: runner.os == 'macos' - run: | - echo `which gfortran-11` - sudo ln -sfn /usr/local/bin/gfortran-11 /usr/local/bin/gfortran - gfortran --version - - name: Install base dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox - - name: Test with tox - run: tox -e py310-test-alldeps + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Set up gfortran on ${{ matrix.os }} + if: runner.os == 'macos' + run: | + echo `which gfortran-11` + sudo ln -sfn /usr/local/bin/gfortran-11 /usr/local/bin/gfortran + gfortran --version + - name: Install base dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Test with tox + run: tox -e py310-test-alldeps From 06a7295429add9121760b1a3f3713cd7b2c9a979 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Tue, 22 Aug 2023 08:48:48 -0400 Subject: [PATCH 10/21] Try again with coverage reporting. --- .github/workflows/ci_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index e54839e61..1794d3633 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -45,10 +45,10 @@ jobs: - name: Python 3.10 with all optional dependencies, measuring coverage linux: py310-test-alldeps-cov + coverage: codecov - name: Python 3.8 with oldest supported versions linux: py38-test-oldestdeps - coverage: codecov macos-tests: name: Python 3.10 with all optional dependencies (MacOS) From 5fed27c00fc5e3b277be3c4855c98651c7acbd80 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Thu, 24 Aug 2023 10:13:20 -0400 Subject: [PATCH 11/21] Address reviews. * Mark additional doctests with required packages: synphot, scipy * Rename utils.requires and utils.optional to utils.required_packages and utils.optional_packages * Rename utils.decorators.optional to utils.decorators.optionally_uses * Prefer use of unittest.mock.patch to test optional behavior when a package is available to the testing environment --- CHANGES.rst | 23 +++++++++++++++-------- docs/development/index.rst | 4 ++-- docs/install.rst | 4 ++-- docs/sbpy/activity/dust.rst | 6 ++++++ docs/sbpy/activity/gas.rst | 6 ++++++ docs/sbpy/calib.rst | 13 +++++++++---- docs/sbpy/photometry.rst | 9 +++++++++ docs/sbpy/spectroscopy/index.rst | 4 +++- docs/sbpy/units.rst | 2 ++ sbpy/activity/dust.py | 8 +++----- sbpy/activity/gas/core.py | 24 ++++++++++++++++-------- sbpy/activity/gas/data/__init__.py | 4 ++-- sbpy/activity/gas/productionrate.py | 7 ++++--- sbpy/activity/gas/tests/test_core.py | 2 +- sbpy/activity/gas/tests/test_data.py | 20 ++++++++++---------- sbpy/bib/core.py | 10 +++------- sbpy/calib/core.py | 4 ++-- sbpy/calib/tests/test_vega.py | 8 ++------ sbpy/photometry/tests/test_bandpass.py | 9 ++------- sbpy/spectroscopy/sources.py | 2 +- sbpy/utils/core.py | 19 +++++++++++-------- sbpy/utils/decorators.py | 26 +++++++++++++++----------- sbpy/utils/tests/test_core.py | 12 ++++++------ sbpy/utils/tests/test_decorators.py | 24 +++++++++++++----------- 24 files changed, 145 insertions(+), 105 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b19ad279a..af6e40711 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,22 +1,29 @@ -0.4.1 (unreleased) +0.5.0 (unreleased) ================== - Revised required and optional packages: - - Only numpy and astropy are required; synphot, ads, and astroquery are now optional dependences. - - Created an option to install a recommended list of packages, e.g., ``pip install sbpy[recommended]``. + + - Only numpy and astropy are required; scipy, synphot, ads, and astroquery are + now optional dependences. + + - Created an option to install a recommended list of packages, e.g., ``pip + install sbpy[recommended]``. New Features ------------ +sbpy.utils +^^^^^^^^^^ + +- New `required_packages` and `optional_packages` functions to test for the + presence of required and optional packages. + sbpy.utils.decorators ^^^^^^^^^^^^^^^^^^^^^ -- Added the `requires` function decorator to test for the presence of optional - packages. - -- Added the `optional` function decorator to test for the presence of optional - packages. +- New `requires` and `optionally_uses` function decorators to simplify testing + for required and optional packages. API Changes diff --git a/docs/development/index.rst b/docs/development/index.rst index 977fb30b6..ed639b17a 100644 --- a/docs/development/index.rst +++ b/docs/development/index.rst @@ -124,8 +124,8 @@ Technical requirements name list` list where it makes sense * consider using the sbpy function and decorator helpers to test for the presence of optional dependencies: - * `~sbpy.utils.requires` and `~sbpy.utils.decorators.requires` raise an exception if a package cannot be imported. - * `~sbpy.utils.optional` and `~sbpy.utils.decorators.optional` warn the user if a package cannot be imported. + * `~sbpy.utils.required_packages` and `~@sbpy.utils.decorators.requires` raise an exception if a package cannot be imported. + * `~sbpy.utils.optional_packages` and `~@sbpy.utils.decorators.optionally_uses` warn the user if a package cannot be imported. * a CHANGELOG entry is required; also update the :doc:`/status` where applicable diff --git a/docs/install.rst b/docs/install.rst index 37f9070b9..14b0ca918 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -25,10 +25,10 @@ Optional dependencies rate calculations related to cometary activity (`~sbpy.activity.gas.NonLTE`). * `scipy `__: 1.3 or later, for numerical integrations in `sbpy.activity.gas` and `sbpy.photometry`, among others. **Recommended** * `synphot `__ 1.1.1 or later, for calibration with respect to the Sun and Vega, filtering spectra through bandpasses. **Recommended** -* `ginga `__ and `photutils - `__: To interactively enhance +* `ginga `__ : To interactively enhance images of comets with the `~sbpy.imageanalysis.CometaryEnhancement` Ginga plugin. +* `photutils `__: For centroiding within the Cometary Enhancements Ginga plugin. Using pip diff --git a/docs/sbpy/activity/dust.rst b/docs/sbpy/activity/dust.rst index 0106f384b..ef6518201 100644 --- a/docs/sbpy/activity/dust.rst +++ b/docs/sbpy/activity/dust.rst @@ -147,18 +147,24 @@ Phase angles and functions Phase angle was not used in the previous section. In the *Afρ* formalism, "albedo" includes the scattering phase function, and is more precisely written *A(θ)*, where *θ* is the phase angle. The default behavior for `Afrho` is to compute *A(θ)fρ* as opposed to *A(0°)fρ*. Returning to the A'Hearn et al. data, we scale *Afρ* to 0° from 3.3° phase using the :func:`~sbpy.activity.Afrho.to_phase` method: +.. doctest-requires:: scipy + >>> afrho = Afrho(6029.9 * u.cm) >>> print(afrho.to_phase(0 * u.deg, 3.3 * u.deg)) # doctest: +FLOAT_CMP 6886.825981017757 cm The default phase function is the Halley-Marcus composite phase function (:func:`~sbpy.activity.phase_HalleyMarcus`). Any function or callable object that accepts an angle as a `~astropy.units.Quantity` and returns a scalar value may be used: +.. doctest-requires:: scipy + >>> Phi = lambda phase: 10**(-0.016 / u.deg * phase.to('deg')) >>> print(afrho.to_phase(0 * u.deg, 3.3 * u.deg, Phi=Phi)) # doctest: +FLOAT_CMP 6809.419810008357 cm To correct an observed flux density for the phase function, use the ``phasecor`` option of :func:`~sbpy.activity.Afrho.to_fluxd` and :func:`~sbpy.activity.Afrho.from_fluxd` methods: +.. doctest-requires:: scipy + >>> flam = 10**-13.99 * u.Unit('erg/(s cm2 AA)') >>> aper = 27200 * u.km >>> eph = Ephem.from_dict({ diff --git a/docs/sbpy/activity/gas.rst b/docs/sbpy/activity/gas.rst index ca5e96a1a..e0ddf6494 100644 --- a/docs/sbpy/activity/gas.rst +++ b/docs/sbpy/activity/gas.rst @@ -83,6 +83,8 @@ daughter species. It is included with some calculation enhancements based on Newburn and Johnson (1978). With `~sbpy.activity.gas.Haser`, we may compute the column density and total number of molecules within an aperture: +.. doctest-requires:: scipy + >>> Q = 1e28 / u.s # production rate >>> v = 0.8 * u.km / u.s # expansion speed >>> parent = gas.photo_lengthscale('H2O') @@ -95,6 +97,8 @@ column density and total number of molecules within an aperture: The gas coma models work with sbpy's apertures: +.. doctest-requires:: scipy + >>> from sbpy.activity import AnnularAperture >>> ap = AnnularAperture((5000, 10000) * u.km) >>> print(coma.total_number(ap)) # doctest: +FLOAT_CMP @@ -126,6 +130,8 @@ number of molecules in an aperture. Parent and daughter data is provided via | daughter | v_photo | m/s | photodissociation velocity (v_R) | +------------------+-----------+------+-------------------------------------------------------+ +.. doctest-requires:: scipy + >>> from sbpy.data import Phys >>> water = Phys.from_dict({ ... 'tau_T': gas.photo_timescale('H2O') * 0.8, # approximate diff --git a/docs/sbpy/calib.rst b/docs/sbpy/calib.rst index 1faa7cbd5..1b3f824a4 100644 --- a/docs/sbpy/calib.rst +++ b/docs/sbpy/calib.rst @@ -103,7 +103,7 @@ The `~astropy.utils.state.ScienceState` objects `~sbpy.calib.solar_fluxd` and `~ >>> from sbpy.calib import Sun, solar_fluxd, vega_fluxd >>> import sbpy.units as sbu - .. doctest-requires:: astropy>=5.3 +.. doctest-requires:: astropy>=5.3 >>> from sbpy.calib import Sun, solar_fluxd, vega_fluxd >>> import sbpy.units as sbu @@ -184,13 +184,15 @@ When the requested spectral resolution is comparable to the spectral resolution Compare interpolation and rebinning for the E490 low-resolution solar spectrum, using the stored wavelengths of the spectrum. Initialize a `~sbpy.calib.Sun` object with the low-resolution spectrum. +.. doctest-requires:: synphot + >>> import numpy as np >>> from sbpy.calib import Sun >>> sun = Sun.from_builtin('E490_2014LR') Inspect a sub-set of the data for this example. -.. doctest-requires:: astropy>=5.3 +.. doctest-requires:: astropy>=5.3, synphot >>> wave = sun.wave[430:435] >>> S = sun.fluxd[430:435] @@ -202,23 +204,26 @@ Inspect a sub-set of the data for this example. Interpolate with observe() and compare to the original values. .. testsetup:: -.. doctest-requires:: astropy<5.3 +.. doctest-requires:: astropy<5.3, synphot >>> wave = sun.wave[430:435] >>> S = sun.fluxd[430:435] - >>> S_interp = sun.observe(wave, interpolate=True) >>> np.allclose(S.value, S_interp.value) True Re-bin with observe using the same wavelengths as band centers. +.. doctest-requires:: synphot + >>> S_rebin = sun.observe(wave) >>> np.allclose(S.value, S_rebin.value) False Inspect the differences. +.. doctest-requires:: synphot + >>> print((S_rebin - S) / (S_rebin + S) * 2) # doctest: +FLOAT_CMP [-0.00429693 0.00281266 -0.00227604 0.00412338 -0.00132301] diff --git a/docs/sbpy/photometry.rst b/docs/sbpy/photometry.rst index 2e2a7f5cd..995862bf8 100644 --- a/docs/sbpy/photometry.rst +++ b/docs/sbpy/photometry.rst @@ -60,6 +60,8 @@ management `with` syntax. Calculate geometric albedo, Bond albedo, and phase integral: +.. doctest-requires:: scipy + >>> import astropy.units as u >>> from sbpy.calib import solar_fluxd >>> solar_fluxd.set({'V': -26.77 * u.mag}) @@ -112,6 +114,8 @@ parameter of `~astropy.modeling.Model`. Some fitters, such as `astropy.modeling.LevMarLSQFitter`, do not support constrained fit via the `ineqcons` parameter, though. +.. doctest-requires:: scipy + >>> import numpy as np >>> import astropy.units as u >>> from astropy.modeling.fitting import SLSQPLSQFitter @@ -129,6 +133,7 @@ Alternatively, one may use the class method initialize a model directly from an `~sbpy.data.Obs` object by fitting the data contained therein. +.. doctest-requires:: scipy >>> # use class method .from_obs >>> from astropy.modeling.fitting import SLSQPLSQFitter >>> fitter = SLSQPLSQFitter() @@ -141,6 +146,8 @@ One can also initialize a model set from multiple columns in the input measurements. The columns to be fitted are specified by a keyward argument ``fields``. By default, the column ``'mag'`` will be fitted. +.. doctest-requires:: scipy + >>> # Initialize model set >>> model4 = HG(5.2 * u.mag, 0.18) >>> mag4 = model4(alpha) + (np.random.rand(20)*0.2 - 0.1) * u.mag @@ -154,6 +161,8 @@ Filter Bandpasses ----------------- A few filter bandpasses are included with `sbpy` for internal tests and your convenience. The function `~sbpy.photometry.bandpass` will return the filter transmission as a `~synphot.spectrum.SpectralElement` (requires `synphot`): +.. doctest-requires:: synphot + >>> from sbpy.photometry import bandpass >>> bp = bandpass('Cousins R') >>> print(bp.avgwave()) # doctest: +FLOAT_CMP diff --git a/docs/sbpy/spectroscopy/index.rst b/docs/sbpy/spectroscopy/index.rst index 7c78f957f..234543dd8 100644 --- a/docs/sbpy/spectroscopy/index.rst +++ b/docs/sbpy/spectroscopy/index.rst @@ -47,7 +47,7 @@ inverse length. For convenience, `sbpy` includes a Initialize a spectral gradient from a color index: -.. doctest-requires:: astropy>=5.3 +.. doctest-requires:: astropy>=5.3, synphot >>> w = (550, 650) * u.nm >>> SpectralGradient.from_color(w, 0.1 * u.mag) # doctest: +FLOAT_CMP @@ -58,6 +58,8 @@ one that carries flux density units such as `astropy.units.ABmag`. Convert spectral gradient (normalized to 550 nm) to a color index: +.. doctest-requires:: synphot + >>> S = SpectralGradient(10 * u.percent / hundred_nm, wave0=550 * u.nm) >>> S.to_color((500, 600) * u.nm) # doctest: +FLOAT_CMP diff --git a/docs/sbpy/units.rst b/docs/sbpy/units.rst index 1d676bdd6..84f16a2cc 100644 --- a/docs/sbpy/units.rst +++ b/docs/sbpy/units.rst @@ -60,6 +60,8 @@ Unit conversions between flux density and Vega-based magnitudes use the `astropy To use a bandpass, define and pass a `synphot.spectrum.SpectralElement`. A limited set of bandpasses are distributed with sbpy (see :ref:`filter-bandpasses`): +.. doctest-requires:: synphot + >>> from sbpy.units import VEGAmag, spectral_density_vega >>> from sbpy.photometry import bandpass >>> V = bandpass('Johnson V') diff --git a/sbpy/activity/dust.py b/sbpy/activity/dust.py index fa19283a5..62a74a649 100644 --- a/sbpy/activity/dust.py +++ b/sbpy/activity/dust.py @@ -15,22 +15,20 @@ "Efrho.to_fluxd": ["synphot"], } -from warnings import warn import abc import numpy as np import astropy.units as u try: - import scipy from scipy.interpolate import splrep, splev except ImportError: - scipy = None + pass from .. import bib from ..calib import Sun from ..spectroscopy import BlackbodySource -from ..utils import optional +from ..utils import optional_packages from .. import data as sbd from .. import units as sbu from ..spectroscopy.sources import SinglePointSpectrumError @@ -289,7 +287,7 @@ def phase_HalleyMarcus(phase): _phase = np.abs(u.Quantity(phase, "deg").value) - if optional("scipy"): + if optional_packages("scipy"): Phi = splev(_phase, splrep(th, ph)) else: Phi = np.interp(_phase, th, ph) diff --git a/sbpy/activity/gas/core.py b/sbpy/activity/gas/core.py index 716a625ab..a8e1f3b73 100644 --- a/sbpy/activity/gas/core.py +++ b/sbpy/activity/gas/core.py @@ -88,7 +88,8 @@ def photo_lengthscale(species, source=None): for k, v in sorted(data.items()): summary += "\n{} [{}]".format(k, ", ".join(v.keys())) - raise ValueError("Invalid species {}. Choose from:{}".format(species, summary)) + raise ValueError( + "Invalid species {}. Choose from:{}".format(species, summary)) gas = data[species] source = default_sources[species] if source is None else source @@ -163,7 +164,8 @@ def photo_timescale(species, source=None): for k, v in sorted(data.items()): summary += "\n{} [{}]".format(k, ", ".join(v.keys())) - raise ValueError("Invalid species {}. Choose from:{}".format(species, summary)) + raise ValueError( + "Invalid species {}. Choose from:{}".format(species, summary)) gas = data[species] source = default_sources[species] if source is None else source @@ -1275,11 +1277,13 @@ def _setup_calculations(self) -> None: # occupy) # NOTE: Equation (16) of Festou 1981 where alpha is the percent # destruction of molecules - parent_beta_r = -np.log(1.0 - self.model_params.parent_destruction_level) + parent_beta_r = - \ + np.log(1.0 - self.model_params.parent_destruction_level) parent_r = parent_beta_r * vp * self.parent.tau_T self.vmr.coma_radius = parent_r * u.m - fragment_beta_r = -np.log(1.0 - self.model_params.fragment_destruction_level) + fragment_beta_r = - \ + np.log(1.0 - self.model_params.fragment_destruction_level) # Calculate the time needed to hit a steady, permanent production of # fragments perm_flow_radius = self.vmr.coma_radius.value + ( @@ -1508,7 +1512,8 @@ def _fragment_sputter(self, r: np.float64, theta: np.float64) -> np.float64: # differential addition to the density integrating along dr, # similar to eq. (36) Festou 1981 - n_r = (p_extinction * f_extinction * q_r_eps) / (sep_dist**2 * v) + n_r = (p_extinction * f_extinction * + q_r_eps) / (sep_dist**2 * v) sputter += n_r * dr @@ -1564,7 +1569,8 @@ def _compute_fragment_density(self) -> None: # Equivalent to summing over j for sin(theta[j]) * # fragment_sputter[i][j] with numpy magic self.solid_angle_sputter = np.sin(thetas) * self.fragment_sputter - self.fast_voldens = 2.0 * np.pi * np.sum(self.solid_angle_sputter, axis=1) + self.fast_voldens = 2.0 * np.pi * \ + np.sum(self.solid_angle_sputter, axis=1) # Tag with proper units self.vmr.volume_density = self.fast_voldens / (u.m**3) @@ -1629,7 +1635,8 @@ def _column_density_at_rho(self, rho: np.float64) -> np.float64: def column_density_integrand(z): return self._volume_density(np.sqrt(z**2 + rhosq)) - c_dens = 2 * romberg(column_density_integrand, 0, z_max, rtol=0.0001, divmax=50) + c_dens = 2 * romberg(column_density_integrand, 0, + z_max, rtol=0.0001, divmax=50) # result is in 1/m^2 return c_dens @@ -1704,7 +1711,8 @@ def _calc_num_fragments_theory(self) -> np.float64: for i, t in enumerate(time_slices[:-1]): extinction_one = t / p_tau_T extinction_two = time_slices[i + 1] / p_tau_T - mult_factor = -np.e ** (-extinction_one) + np.e ** (-extinction_two) + mult_factor = -np.e ** (-extinction_one) + \ + np.e ** (-extinction_two) theory_total += self.production_at_time(t) * mult_factor return theory_total * alpha - edge_adjust diff --git a/sbpy/activity/gas/data/__init__.py b/sbpy/activity/gas/data/__init__.py index a9654ca66..4de4b7249 100644 --- a/sbpy/activity/gas/data/__init__.py +++ b/sbpy/activity/gas/data/__init__.py @@ -16,7 +16,7 @@ from .... import data as sbd from .... import bib -from ....utils import optional +from ....utils import optional_packages photo_lengthscale = { # (value, {key feature: ADS bibcode}) 'H2O': { @@ -122,7 +122,7 @@ def __init__(self, band): self._LN = u.Quantity(self.table5[k].data * self.scales[i], 'erg / s') - if optional("scipy"): + if optional_packages("scipy"): from scipy.interpolate import splrep self._tck = splrep(self.rdot.value, self.LN.value) self._interp = self._spline diff --git a/sbpy/activity/gas/productionrate.py b/sbpy/activity/gas/productionrate.py index 9efa46f03..188eae512 100644 --- a/sbpy/activity/gas/productionrate.py +++ b/sbpy/activity/gas/productionrate.py @@ -30,6 +30,7 @@ from ...data import Phys from ...exceptions import RequiredPackageUnavailable from ...utils.decorators import requires +from ...utils import required_packages __all__ = ['LTE', 'NonLTE', 'einstein_coeff', 'intensity_conversion', 'beta_factor', 'total_number', @@ -257,8 +258,8 @@ def beta_factor(mol_data, ephemobj): r = (orb['r']) if not isinstance(mol_data['mol_tag'][0], str): - if astroquery is None: - raise RequiredPackageUnavailable(f"mol_tag = {mol_data['mol_tag'][0]} requires astroquery") + required_packages( + "astroquery", f"mol_tag = {mol_data['mol_tag'][0]} requires astroquery") cat = JPLSpec.get_species_table() mol = cat[cat['TAG'] == mol_data['mol_tag'][0]] @@ -664,7 +665,7 @@ class NonLTE(): """ @staticmethod - @requires(pyradex, astroquery) + @requires("pyradex", "astroquery") def from_pyradex(integrated_flux, mol_data, line_width=1.0 * u.km / u.s, escapeProbGeom='lvg', iter=100, collider_density={'H2': 900*2.2}): diff --git a/sbpy/activity/gas/tests/test_core.py b/sbpy/activity/gas/tests/test_core.py index 4eba9afb7..1c5a97cb1 100644 --- a/sbpy/activity/gas/tests/test_core.py +++ b/sbpy/activity/gas/tests/test_core.py @@ -369,7 +369,7 @@ def test_total_number_gaussian_ap(self): assert np.isclose(N, 5.146824269306973e27, rtol=0.005) -@pytest.mark.skipif("scipy is None") +@pytest.mark.skipif(scipy is None) class TestVectorialModel: def test_small_vphoto(self): """ diff --git a/sbpy/activity/gas/tests/test_data.py b/sbpy/activity/gas/tests/test_data.py index 41b0930ce..d0867a627 100644 --- a/sbpy/activity/gas/tests/test_data.py +++ b/sbpy/activity/gas/tests/test_data.py @@ -1,24 +1,24 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst -import importlib +from unittest.mock import patch import pytest import numpy as np import astropy.units as u from .. import data -def patched_import_module(name): - if name == "scipy": - raise ModuleNotFoundError - __import__(name) - - class TestOHFluorescenceSA88: + def test_spline_interpolation(self): + pytest.importorskip("scipy") + model = data.OHFluorescenceSA88("0-0") + LN = model(-0.5 * u.km / u.s) + assert np.isclose(LN.value, 1.50307895e-15) + + @patch.dict("sys.modules", {"scipy": None}) def test_linear_interpolation(self, monkeypatch): - monkeypatch.setattr(importlib, "import_module", patched_import_module) model = data.OHFluorescenceSA88("0-0") - LN = model(-1 * u.km / u.s) - assert np.isclose(LN.value, 1.54e-15) + LN = model(-0.5 * u.km / u.s) + assert np.isclose(LN.value, 1.515e-15) def test_tau(self): model = data.OHFluorescenceSA88("0-0") diff --git a/sbpy/bib/core.py b/sbpy/bib/core.py index 4eb8dc8b4..950c7a25b 100644 --- a/sbpy/bib/core.py +++ b/sbpy/bib/core.py @@ -34,7 +34,7 @@ from astropy import log -from ..exceptions import RequiredPackageUnavailable +from ..utils.decorators import requires def register(task, citations): @@ -228,6 +228,7 @@ def show(filter=None): return output +@requires("ads") def to_text(filter=None): """Convert bibcodes to human readable text. @@ -247,9 +248,6 @@ def to_text(filter=None): """ - if ads is None: - raise RequiredPackageUnavailable("requires ads") - output = '' for task, ref in _filter(filter).items(): output += '{:s}:\n'.format(task) @@ -316,6 +314,7 @@ def to_text(filter=None): return output +@requires("ads") def _to_format(format, filter=None): """Convert bibcodes to a range of different output formats. @@ -339,9 +338,6 @@ def _to_format(format, filter=None): """ - if ads is None: - raise RequiredPackageUnavailable("requires ads") - output = '' for task, ref in _filter(filter).items(): with warnings.catch_warnings(): diff --git a/sbpy/calib/core.py b/sbpy/calib/core.py index 93f62f0d1..b403bf67b 100644 --- a/sbpy/calib/core.py +++ b/sbpy/calib/core.py @@ -37,7 +37,7 @@ from .. import bib from . import solar_sources, vega_sources from ..utils.decorators import requires -from ..utils import optional +from ..utils import optional_packages try: import synphot @@ -136,7 +136,7 @@ def from_default(cls): The spectrum will be ``None`` if `synphot` is not available. """ - if optional("synphot"): + if optional_packages("synphot"): standard = cls._spectrum_state.get() else: standard = cls(None) diff --git a/sbpy/calib/tests/test_vega.py b/sbpy/calib/tests/test_vega.py index 9942f97f3..2ab9c8258 100644 --- a/sbpy/calib/tests/test_vega.py +++ b/sbpy/calib/tests/test_vega.py @@ -1,5 +1,5 @@ import inspect -import importlib +from unittest.mock import patch import pytest import numpy as np import astropy.units as u @@ -32,13 +32,9 @@ def test_call_wavelength(self, fluxd0): fluxd = vega(5557.5 * u.AA, unit=fluxd0.unit) assert np.isclose(fluxd.value, fluxd0.value) + @patch.dict("sys.modules", {"synphot": None}) def test_source_error(self): # Only test when synphot is not available. - try: - import synphot - pytest.skip() - except ImportError: - pass vega = Vega.from_default() with pytest.raises(UndefinedSourceError): vega.source diff --git a/sbpy/photometry/tests/test_bandpass.py b/sbpy/photometry/tests/test_bandpass.py index 3eaee01a4..e239b4885 100644 --- a/sbpy/photometry/tests/test_bandpass.py +++ b/sbpy/photometry/tests/test_bandpass.py @@ -1,5 +1,6 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst +from unittest.mock import patch import pytest import numpy as np from ..bandpass import * @@ -44,13 +45,7 @@ def test_bandpass(name, avgwave): assert np.isclose(bp.avgwave().value, avgwave) +@patch.dict("sys.modules", {"synphot": None}) def test_bandpass_synphot(): - # skip if synphot is available - try: - import synphot - - pytest.skip() - except ImportError: - pass with pytest.raises(RequiredPackageUnavailable): bandpass("sdss u") diff --git a/sbpy/spectroscopy/sources.py b/sbpy/spectroscopy/sources.py index c4947e727..6de151b0f 100644 --- a/sbpy/spectroscopy/sources.py +++ b/sbpy/spectroscopy/sources.py @@ -484,7 +484,7 @@ def redden(self, S): red_spec._source = red_spec.source * r if red_spec.description is not None: red_spec._description = '{} reddened by {} at {}'.format( - red_spec.description, S, S.wave0) + red_spec.description, S, S.wave0) return red_spec diff --git a/sbpy/utils/core.py b/sbpy/utils/core.py index 39de5972a..56d64f9a8 100644 --- a/sbpy/utils/core.py +++ b/sbpy/utils/core.py @@ -6,16 +6,17 @@ """ -__all__ = ["requires", "optional"] +__all__ = ["required_packages", "optional_packages"] from importlib import import_module from warnings import warn from ..exceptions import RequiredPackageUnavailable, OptionalPackageUnavailable -def requires(*packages, message=None): +def required_packages(*packages, message=None): """Verifies the arguments are valid packages. + Parameters ---------- *modules : str @@ -34,8 +35,8 @@ def requires(*packages, message=None): Examples -------- - >>> from sbpy.utils import requires - >>> requires("unavailable_package") + >>> from sbpy.utils import required_packages + >>> required_packages("unavailable_package") Traceback (most recent call last): ... sbpy.exceptions.RequiredPackageUnavailable: `unavailable_package` is required. @@ -47,12 +48,14 @@ def requires(*packages, message=None): import_module(package) except ModuleNotFoundError as exc: _message = "" if message is None else " " + message - raise RequiredPackageUnavailable(f"`{package}` is required.{_message}") from None + raise RequiredPackageUnavailable( + f"`{package}` is required.{_message}") from None -def optional(*packages, message=None): +def optional_packages(*packages, message=None): """Decorator that warns if the arguments are not valid packages. + Parameters ---------- *modules : str @@ -78,8 +81,8 @@ def optional(*packages, message=None): Examples -------- - >>> from sbpy.utils import optional - >>> optional("unavailable_package") # doctest: +SKIP + >>> from sbpy.utils import optional_packages + >>> optional_packages("unavailable_package") # doctest: +SKIP OptionalPackageUnavailable: Optional package `unavailable_package` is unavailable. """ diff --git a/sbpy/utils/decorators.py b/sbpy/utils/decorators.py index 15f10ca59..0db9e7bd4 100644 --- a/sbpy/utils/decorators.py +++ b/sbpy/utils/decorators.py @@ -2,7 +2,7 @@ """Common sbpy method/function decorators.""" -__all__ = ["requires", "optional"] +__all__ = ["requires", "optionally_uses"] from functools import wraps from . import core @@ -12,9 +12,10 @@ def requires(*packages, message=None): """Decorator that verifies the arguments are valid packages. + Parameters ---------- - *modules : str + *packages : str The names of packages to test. @@ -42,12 +43,13 @@ def decorator(wrapped_function): function_name = ".".join( (wrapped_function.__module__, wrapped_function.__qualname__) ) - _message = ("" if message is None else f"{message} ") + f"({function_name})" + _message = ( + "" if message is None else f"{message} ") + f"({function_name})" @wraps(wrapped_function) def wrapper(*func_args, **func_kwargs): try: - core.requires(*packages, message=_message) + core.required_packages(*packages, message=_message) except RequiredPackageUnavailable as exc: # trim a couple levels of the traceback to clean up error messages raise exc.with_traceback(exc.__traceback__.tb_next.tb_next) @@ -58,12 +60,13 @@ def wrapper(*func_args, **func_kwargs): return decorator -def optional(*packages, message=None): - """Decorator that warns if the arguments are not valid packages. +def optionally_uses(*packages, message=None): + """Decorator that warns if the arguments are not valid modules. + Parameters ---------- - *modules : str + *packages : str The names of packages to test. message : str @@ -80,8 +83,8 @@ def optional(*packages, message=None): Examples -------- - >>> from sbpy.utils.decorators import requires - >>> @optional("unavailable_package") + >>> from sbpy.utils.decorators import optionally_uses + >>> @optionally_uses("unavailable_package") ... def f(): ... pass >>> f() # doctest: +SKIP @@ -96,11 +99,12 @@ def decorator(wrapped_function): function_name = ".".join( (wrapped_function.__module__, wrapped_function.__qualname__) ) - _message = ("" if message is None else f"{message} ") + f"({function_name})" + _message = ( + "" if message is None else f"{message} ") + f"({function_name})" @wraps(wrapped_function) def wrapper(*func_args, **func_kwargs): - core.optional(*packages, message=_message) + core.optional_packages(*packages, message=_message) return wrapped_function(*func_args, **func_kwargs) return wrapper diff --git a/sbpy/utils/tests/test_core.py b/sbpy/utils/tests/test_core.py index 7383a0b79..272b0cada 100644 --- a/sbpy/utils/tests/test_core.py +++ b/sbpy/utils/tests/test_core.py @@ -1,29 +1,29 @@ import pytest -from ..core import requires, optional +from ..core import required_packages, optional_packages from ...exceptions import RequiredPackageUnavailable, OptionalPackageUnavailable def test_requires(): with pytest.raises(RequiredPackageUnavailable): - requires("unavailable_package") + required_packages("unavailable_package") def test_requires_message(): try: message = "Because these integrations are tricky." - requires("unavailable_package", message=message) + required_packages("unavailable_package", message=message) except RequiredPackageUnavailable as exc: assert message in str(exc) def test_optional(): with pytest.warns(OptionalPackageUnavailable): - assert not optional("unavailable_package") + assert not optional_packages("unavailable_package") - assert optional("astropy") + assert optional_packages("astropy") def test_optional_message(): message = "Using linear interpolation." with pytest.warns(OptionalPackageUnavailable, match=message) as record: - optional("unavailable_package", message=message) + optional_packages("unavailable_package", message=message) diff --git a/sbpy/utils/tests/test_decorators.py b/sbpy/utils/tests/test_decorators.py index 1c0c676b2..1e9ae2f04 100644 --- a/sbpy/utils/tests/test_decorators.py +++ b/sbpy/utils/tests/test_decorators.py @@ -1,22 +1,24 @@ from warnings import catch_warnings import pytest -from ..decorators import requires, optional +from ..decorators import requires, optionally_uses from ...exceptions import RequiredPackageUnavailable, OptionalPackageUnavailable -def test_requires(): - @requires("unavailable_package") - def f(): - pass +@requires("unavailable_package") +def f_required(): + pass + + +@optionally_uses("unavailable_package") +def f_optional(): + pass + +def test_requires(): with pytest.raises(RequiredPackageUnavailable): - f() + f_required() def test_optional(): - @optional("unavailable_package") - def f(): - pass - with pytest.warns(OptionalPackageUnavailable): - f() + f_optional() From badb4b300ead5517da72caee78e1bd9fec54f3af Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Thu, 24 Aug 2023 12:00:01 -0400 Subject: [PATCH 12/21] Provide reasons for skipping tests with skipif --- sbpy/activity/gas/tests/test_core.py | 47 ++++++++++++++++-------- sbpy/spectroscopy/tests/test_specgrad.py | 2 +- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/sbpy/activity/gas/tests/test_core.py b/sbpy/activity/gas/tests/test_core.py index 1c5a97cb1..a7aa53a68 100644 --- a/sbpy/activity/gas/tests/test_core.py +++ b/sbpy/activity/gas/tests/test_core.py @@ -90,7 +90,8 @@ def test_column_density_small_aperture(self): parent = 1e4 * u.km N_avg = 2 * Haser(Q, v, parent).column_density(rho) ideal = Q / v / 2 / rho - assert np.isclose(N_avg.decompose().value, ideal.decompose().value, rtol=0.001) + assert np.isclose(N_avg.decompose().value, + ideal.decompose().value, rtol=0.001) def test_column_density_small_angular_aperture(self): """Test column density for angular aperture << lengthscale. @@ -109,9 +110,11 @@ def test_column_density_small_angular_aperture(self): eph = dict(delta=1 * u.au) parent = 1e4 * u.km N_avg = 2 * Haser(Q, v, parent).column_density(rho, eph) - rho_km = (rho * eph["delta"] * 725.24 * u.km / u.arcsec / u.au).to("km") + rho_km = (rho * eph["delta"] * 725.24 * + u.km / u.arcsec / u.au).to("km") ideal = Q / v / 2 / rho_km - assert np.isclose(N_avg.to_value("1/m2"), ideal.to_value("1/m2"), rtol=0.001) + assert np.isclose(N_avg.to_value("1/m2"), + ideal.to_value("1/m2"), rtol=0.001) def test_column_density(self): """ @@ -296,8 +299,10 @@ def test_total_number_annulus(self): parent = 10 * u.km N = Haser(Q, v, parent).total_number(aper) - N1 = Haser(Q, v, parent).total_number(core.CircularAperture(aper.dim[0])) - N2 = Haser(Q, v, parent).total_number(core.CircularAperture(aper.dim[1])) + N1 = Haser(Q, v, parent).total_number( + core.CircularAperture(aper.dim[0])) + N2 = Haser(Q, v, parent).total_number( + core.CircularAperture(aper.dim[1])) assert np.allclose(N, N2 - N1) @@ -369,7 +374,7 @@ def test_total_number_gaussian_ap(self): assert np.isclose(N, 5.146824269306973e27, rtol=0.005) -@pytest.mark.skipif(scipy is None) +@pytest.mark.skipif(scipy is None, reason="requires scipy") class TestVectorialModel: def test_small_vphoto(self): """ @@ -396,7 +401,8 @@ def test_small_vphoto(self): # Fragment molecule is OH, but v_photo is modified to be smaller than # v_outflow fragment = Phys.from_dict( - {"tau_T": photo_timescale("OH") * 0.93, "v_photo": 0.5 * u.km / u.s} + {"tau_T": photo_timescale("OH") * 0.93, + "v_photo": 0.5 * u.km / u.s} ) coma = VectorialModel( @@ -433,7 +439,8 @@ def q_t(t): ) # Fragment molecule is OH fragment = Phys.from_dict( - {"tau_T": photo_timescale("OH") * 0.93, "v_photo": 1.05 * u.km / u.s} + {"tau_T": photo_timescale("OH") * 0.93, + "v_photo": 1.05 * u.km / u.s} ) coma_steady = VectorialModel( @@ -477,7 +484,8 @@ def test_binned_production_one_element_list(self): ) # Fragment molecule is OH fragment = Phys.from_dict( - {"tau_T": photo_timescale("OH") * 0.93, "v_photo": 1.05 * u.km / u.s} + {"tau_T": photo_timescale("OH") * 0.93, + "v_photo": 1.05 * u.km / u.s} ) coma_binned = VectorialModel.binned_production( @@ -516,7 +524,8 @@ def test_binned_production_multi_element_list(self): ) # Fragment molecule is OH fragment = Phys.from_dict( - {"tau_T": photo_timescale("OH") * 0.93, "v_photo": 1.05 * u.km / u.s} + {"tau_T": photo_timescale("OH") * 0.93, + "v_photo": 1.05 * u.km / u.s} ) coma_binned = VectorialModel.binned_production( @@ -552,7 +561,8 @@ def test_grid_count(self): ) # Fragment molecule is OH fragment = Phys.from_dict( - {"tau_T": photo_timescale("OH") * 0.93, "v_photo": 1.05 * u.km / u.s} + {"tau_T": photo_timescale("OH") * 0.93, + "v_photo": 1.05 * u.km / u.s} ) coma = VectorialModel(base_q=base_q, parent=parent, fragment=fragment) @@ -580,7 +590,8 @@ def test_total_number_large_aperture(self): ) # Fragment molecule is OH fragment = Phys.from_dict( - {"tau_T": photo_timescale("OH") * 0.93, "v_photo": 1.05 * u.km / u.s} + {"tau_T": photo_timescale("OH") * 0.93, + "v_photo": 1.05 * u.km / u.s} ) coma = VectorialModel(base_q=base_q, parent=parent, fragment=fragment) @@ -625,7 +636,8 @@ def test_model_symmetry(self): ) # Fragment molecule is OH fragment = Phys.from_dict( - {"tau_T": photo_timescale("OH") * 0.93, "v_photo": 1.05 * u.km / u.s} + {"tau_T": photo_timescale("OH") * 0.93, + "v_photo": 1.05 * u.km / u.s} ) coma = VectorialModel( @@ -703,7 +715,8 @@ def test_festou92(self, rh, delta, flux, g, Q): ) # Fragment molecule is OH fragment = Phys.from_dict( - {"tau_T": 160000 * (rh / u.au) ** 2 * u.s, "v_photo": 1.05 * u.km / u.s} + {"tau_T": 160000 * (rh / u.au) ** 2 * u.s, + "v_photo": 1.05 * u.km / u.s} ) # https://pds.nasa.gov/ds-view/pds/viewInstrumentProfile.jsp?INSTRUMENT_ID=LWP&INSTRUMENT_HOST_ID=IUE @@ -770,7 +783,8 @@ def test_combi93(self, rh, delta, N, Q): # Fragment molecule is OH fragment = Phys.from_dict( - {"tau_T": 2.0e5 * (rh / u.au) ** 2 * u.s, "v_photo": 1.05 * u.km / u.s} + {"tau_T": 2.0e5 * (rh / u.au) ** 2 * u.s, + "v_photo": 1.05 * u.km / u.s} ) Q0 = 2e29 / u.s @@ -910,7 +924,8 @@ def test_vm_fortran(self): sigma0_rho = x[::2] * u.km sigma0 = x[1::2] / u.cm**2 # absolute tolerance: 3 significant figures - sigma0_atol = 1.1 * 10 ** (np.floor(np.log10(sigma0.value)) - 2) * sigma0.unit + sigma0_atol = 1.1 * \ + 10 ** (np.floor(np.log10(sigma0.value)) - 2) * sigma0.unit sigma0_atol_revised = sigma0_atol * 40 # evaluate the model diff --git a/sbpy/spectroscopy/tests/test_specgrad.py b/sbpy/spectroscopy/tests/test_specgrad.py index 8299d8ac8..9cc791ab5 100644 --- a/sbpy/spectroscopy/tests/test_specgrad.py +++ b/sbpy/spectroscopy/tests/test_specgrad.py @@ -16,7 +16,7 @@ def bandpass(bp): pass -@pytest.mark.skipif("synphot is None") +@pytest.mark.skipif(synphot is None, reason="requires synphot") class TestSpectralGradient(): def test_new(self): S = SpectralGradient(100 / u.um, wave=(525, 575) * u.nm) From 90f3699cbff7c7545bdab794fd1ace45741d3122 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Fri, 25 Aug 2023 11:52:34 -0400 Subject: [PATCH 13/21] Need blank line after doctest-requires --- docs/sbpy/photometry.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sbpy/photometry.rst b/docs/sbpy/photometry.rst index 995862bf8..56dc6f658 100644 --- a/docs/sbpy/photometry.rst +++ b/docs/sbpy/photometry.rst @@ -134,6 +134,7 @@ initialize a model directly from an `~sbpy.data.Obs` object by fitting the data contained therein. .. doctest-requires:: scipy + >>> # use class method .from_obs >>> from astropy.modeling.fitting import SLSQPLSQFitter >>> fitter = SLSQPLSQFitter() From 1f7f214f40d3d95cd2c19970b8d7f949dfd3a7b6 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Fri, 25 Aug 2023 12:04:52 -0400 Subject: [PATCH 14/21] Mark doctests needing astroquery. --- docs/sbpy/activity/gas.rst | 2 ++ docs/sbpy/data/ephem.rst | 12 ++++++++++-- docs/sbpy/data/obs.rst | 2 ++ docs/sbpy/data/orbit.rst | 15 +++++++++++++-- docs/sbpy/data/phys.rst | 10 +++++++--- docs/sbpy/photometry.rst | 7 +++++-- 6 files changed, 39 insertions(+), 9 deletions(-) diff --git a/docs/sbpy/activity/gas.rst b/docs/sbpy/activity/gas.rst index e0ddf6494..9a74d28fd 100644 --- a/docs/sbpy/activity/gas.rst +++ b/docs/sbpy/activity/gas.rst @@ -48,6 +48,8 @@ Some sources provide values for the quiet and active Sun (Huebner et al. 1992): With the :doc:`../bib`, the citation may be discovered: +.. doctest-requires:: ads + >>> from sbpy import bib >>> bib.reset() # clear any old citations >>> with bib.Tracking(): diff --git a/docs/sbpy/data/ephem.rst b/docs/sbpy/data/ephem.rst index 18eb84e36..0a5ac3bbc 100644 --- a/docs/sbpy/data/ephem.rst +++ b/docs/sbpy/data/ephem.rst @@ -48,6 +48,7 @@ In the above example a specific epoch was specified, but multiple epochs may be requested, or even a range of epochs. All time references are `~astropy.time.Time` objects. +.. doctest-requires:: astroquery .. doctest-remote-data:: >>> epochs = Time(['2022-06-04', '2023-06-04']) @@ -67,6 +68,7 @@ than a few hundred to prevent corruption of the query (see To specify a range of epochs: +.. doctest-requires:: astroquery .. doctest-remote-data:: >>> import astropy.units as u @@ -96,7 +98,8 @@ Mulitple targets An additional feature of `~sbpy.data.Ephem.from_horizons` is that you can automatically concatenate queries for a number of objects: -.. doctest-remote-data:: +.. doctest-requires:: astroquery +.. doctest-remote-data:: >>> epoch1 = Time('2018-08-03 14:20') >>> eph = Ephem.from_horizons(['Ceres', 'Pallas', 12893, '1983 SA'], @@ -130,6 +133,7 @@ codes `__ as above, or by using `~astropy.coordinates.EarthLocation` as shown in the following example: +.. doctest-requires:: astroquery .. doctest-remote-data:: >>> from astropy.coordinates import EarthLocation @@ -155,6 +159,7 @@ to `~sbpy.data.Ephem.from_horizons` are directly passed on to flexibility of the latter function. For example one may use the ``skip_daylight`` keyword argument: +.. doctest-requires:: astroquery .. doctest-remote-data:: >>> epoch1 = Time('2018-08-03 14:20', scale='utc') @@ -170,6 +175,7 @@ Or, a common option for periodic cometary targets is to limit orbit look-ups to the apparition closest to the epochs being queried (requires ``id_type='designation'``): +.. doctest-requires:: astroquery .. doctest-remote-data:: >>> eph = Ephem.from_horizons('2P') # doctest: +SKIP @@ -196,12 +202,13 @@ Offering similar functionality, the `~sbpy.data.Ephem.from_mpc` method will retrieve ephemerides from the `Minor Planet Center's Ephemeris Service `_: +.. doctest-requires:: astroquery .. doctest-remote-data:: >>> eph = Ephem.from_mpc('2P', location='568', ... epochs={'start': Time('2018-10-22'), ... 'stop': Time('2018-10-26'), - ... 'step': 1*u.day}) # doctest: +REMOTE_DATA + ... 'step': 1*u.day}) >>> eph # doctest: +SKIP Targetname Date ... Moon distance Moon altitude @@ -223,6 +230,7 @@ Finally, `~sbpy.data.Ephem.from_miriade` will retrieve ephemerides from the `Institut de Mécanique Céleste et de Calcul des Éphémérides `_: +.. doctest-requires:: astroquery .. doctest-remote-data:: >>> eph = Ephem.from_miriade('2P', objtype='comet', location='568', diff --git a/docs/sbpy/data/obs.rst b/docs/sbpy/data/obs.rst index 9f8be561c..1d20cfcf6 100644 --- a/docs/sbpy/data/obs.rst +++ b/docs/sbpy/data/obs.rst @@ -8,6 +8,7 @@ Observational Data Objects (`sbpy.data.Obs`) For instance, this class allows you to query observations reported to the Minor Planet Center for a given target via `astroquery.mpc.MPCClass.get_observations`: +.. doctest-requires:: astroquery .. doctest-remote-data:: >>> from sbpy.data import Obs @@ -37,6 +38,7 @@ function makes use of the query functions that are part of `~sbpy.data.Ephem` and allows you to pick a service from which you would like to obtain the data. +.. doctest-requires:: astroquery .. doctest-remote-data:: >>> data.field_names diff --git a/docs/sbpy/data/orbit.rst b/docs/sbpy/data/orbit.rst index 3c35a7bdd..3151ddf1e 100644 --- a/docs/sbpy/data/orbit.rst +++ b/docs/sbpy/data/orbit.rst @@ -9,8 +9,9 @@ Orbit Queries body osculating elements from the `JPL Horizons service `_: +.. doctest-requires:: astroquery .. doctest-remote-data:: - + >>> from sbpy.data import Orbit >>> from astropy.time import Time >>> epoch = Time('2018-05-14', scale='utc') @@ -30,7 +31,10 @@ scale of the desired epoch is not supported by the query function and hence converted to a scale that is supported (``tdb`` in this case). The following fields are available in this object: - >>> elem.field_names # doctest: +REMOTE_DATA +.. doctest-requires:: astroquery +.. doctest-remote-data:: + + >>> elem.field_names ['targetname', 'H', 'G', 'e', 'q', 'incl', 'Omega', 'w', 'n', 'M', 'nu', 'a', 'Q', 'P', 'epoch', 'Tp'] If ``epochs`` is not set, the osculating elements for the current @@ -40,6 +44,7 @@ epoch (current time) are queried. Similar to parameter on to that function. Furthermore, it is possible to query orbital elements for a number of targets: +.. doctest-requires:: astroquery .. doctest-remote-data:: >>> epoch = Time('2018-08-03 14:20', scale='tdb') @@ -59,6 +64,7 @@ Alternatively, orbital elements can also be queried from the `Minor Planet Center `_, although in this case only the most recent elements are accessible: +.. doctest-requires:: astroquery .. doctest-remote-data:: >>> elem = Orbit.from_mpc(['3552', '12893']) @@ -87,6 +93,7 @@ using `OpenOrb `_. In order to transform some current orbits to a state vector in cartesian coordinates, one could use the following code: +.. doctest-requires:: astroquery, oorb .. doctest-remote-data:: >>> elem = Orbit.from_horizons(['Ceres', 'Pallas', 'Vesta']) @@ -113,6 +120,7 @@ propagated to either as `~astropy.time.Time` object, or as float in terms of Julian date. The following example propagates the current orbit of Ceres back to year 2000: +.. doctest-requires:: astroquery, oorb .. doctest-remote-data:: >>> elem = Orbit.from_horizons('Ceres') @@ -140,6 +148,7 @@ parameter with respect to Jupiter is used in the dynamical classification of comets. The Tisserand parameter can be calculated by `~sbpy.Orbit.tisserand` as follows: +.. doctest-requires:: astroquery .. doctest-remote-data:: >>> epoch = Time(2449400.5, format='jd', scale='tdb') @@ -153,6 +162,7 @@ One can also specify the planet with respect to which the Tisserand parameter is calculated with optional parameter `planet`. It also allows multiple planet to be specified simultaneously: +.. doctest-requires:: astroquery .. doctest-remote-data:: >>> import numpy as np @@ -170,6 +180,7 @@ Drummond function (`Drummond 1991 >> comets = Orbit.from_horizons(['252P', 'P/2016 BA14'], diff --git a/docs/sbpy/data/phys.rst b/docs/sbpy/data/phys.rst index 60300e26b..c64bf9e41 100644 --- a/docs/sbpy/data/phys.rst +++ b/docs/sbpy/data/phys.rst @@ -13,8 +13,11 @@ including the use of `~astropy.units`. As an example, the following code will query the properties for a small number of asteroids: +.. doctest-requires:: astroquery +.. doctest-remote-data:: + >>> from sbpy.data import Phys - >>> phys = Phys.from_sbdb(['Ceres', '12893', '3552']) # doctest: +REMOTE_DATA + >>> phys = Phys.from_sbdb(['Ceres', '12893', '3552']) >>> phys['targetname', 'H', 'diameter'] # doctest: +SKIP targetname H diameter @@ -47,7 +50,8 @@ from `~sbpy.data.Phys.from_jplspec` include the following data: | Lower level energy in Joules | Degrees of freedom -.. doctest-skip:: +.. doctest-requires:: astroquery +.. doctest-remote-data:: >>> from sbpy.data.phys import Phys >>> import astropy.units as u @@ -55,7 +59,7 @@ from `~sbpy.data.Phys.from_jplspec` include the following data: >>> transition_freq = (230.53799 * u.GHz).to('MHz') >>> mol_tag = '^CO$' >>> mol_data = Phys.from_jplspec(temp_estimate, transition_freq, mol_tag) - >>> mol_data + >>> mol_data # doctest: +SKIP Transition frequency Temperature ... Degrees of freedom Molecule Identifier MHz K ... diff --git a/docs/sbpy/photometry.rst b/docs/sbpy/photometry.rst index 56dc6f658..fa05889b7 100644 --- a/docs/sbpy/photometry.rst +++ b/docs/sbpy/photometry.rst @@ -83,9 +83,12 @@ The model class can also be initialized by a subclass of ``sbpy``'s `~sbpy.data.DataClass`, such as `~sbpy.data.Phys`, as long as it contains the model parameters: +.. doctest-requires:: astroquery +.. doctest-remote-data:: + >>> from sbpy.data import Phys - >>> phys = Phys.from_sbdb('Ceres') # doctest: +REMOTE_DATA - >>> m = HG.from_phys(phys) # doctest: +REMOTE_DATA + >>> phys = Phys.from_sbdb('Ceres') + >>> m = HG.from_phys(phys) INFO: Model initialized for 1 Ceres (A801 AA). [sbpy.photometry.core] >>> print(m) # doctest: +SKIP Model: HG From 895a682b89739a663e4d346eaf83335ab0d46001 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Fri, 25 Aug 2023 15:40:26 -0400 Subject: [PATCH 15/21] Fix requires message and remove unsused imports. --- sbpy/activity/gas/productionrate.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sbpy/activity/gas/productionrate.py b/sbpy/activity/gas/productionrate.py index 188eae512..f2b508f9d 100644 --- a/sbpy/activity/gas/productionrate.py +++ b/sbpy/activity/gas/productionrate.py @@ -15,11 +15,10 @@ import astropy.units as u try: - import astroquery from astroquery.jplspec import JPLSpec from astroquery.lamda import Lamda except ImportError: - astroquery = None + pass try: import pyradex @@ -28,7 +27,6 @@ from ...bib import register from ...data import Phys -from ...exceptions import RequiredPackageUnavailable from ...utils.decorators import requires from ...utils import required_packages @@ -259,7 +257,7 @@ def beta_factor(mol_data, ephemobj): if not isinstance(mol_data['mol_tag'][0], str): required_packages( - "astroquery", f"mol_tag = {mol_data['mol_tag'][0]} requires astroquery") + "astroquery", message=f"mol_tag = {mol_data['mol_tag'][0]} requires astroquery") cat = JPLSpec.get_species_table() mol = cat[cat['TAG'] == mol_data['mol_tag'][0]] From e122da11d4fd50f8b947ecd44e53c2ba2b93df57 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Fri, 25 Aug 2023 15:56:33 -0400 Subject: [PATCH 16/21] Improve stability of remote test in Orbit docs. --- docs/sbpy/data/orbit.rst | 49 ++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/docs/sbpy/data/orbit.rst b/docs/sbpy/data/orbit.rst index 3151ddf1e..594dfa0eb 100644 --- a/docs/sbpy/data/orbit.rst +++ b/docs/sbpy/data/orbit.rst @@ -166,11 +166,21 @@ planet to be specified simultaneously: .. doctest-remote-data:: >>> import numpy as np - >>> chariklo = Orbit.from_horizons('chariklo', id_type='name') - >>> T = chariklo.tisserand(planet=['599', '699', '799', '899']) + >>> import astropy.units as u + >>> from astropy.time import Time + >>> chariklo = Orbit.from_dict({ + ... "e": 0.16778, + ... "a": 15.78694 * u.au, + ... "incl": 23.39153 * u.deg, + ... "Omega": 300.44770 * u.deg, + ... "w": 242.01787 * u.deg, + ... "n": 0.015713 * u.deg / u.day, + ... "M": 113.36375 * u.deg, + ... }) + >>> T = chariklo.tisserand(planet=['599', '699', '799', '899'], epoch=Time("2023-08-25")) >>> with np.printoptions(precision=3): ... print(T) # doctest: +FLOAT_CMP - [3.485 2.931 2.858 3.224] + [3.482 2.93 2.859 3.225] `~sbpy.Orbit` also provides a method to compare the orbits of two objects in terms of the "D-criterion" (`Jopek 1993 `_). The `~sbpy.Orbit.D_criterion` method @@ -183,16 +193,37 @@ D_criterion: .. doctest-requires:: astroquery .. doctest-remote-data:: - >>> comets = Orbit.from_horizons(['252P', 'P/2016 BA14'], - ... id_type='designation', closest_apparition=True) + >>> import numpy as np + >>> import astropy.units as u + >>> from astropy.time import Time + >>> + >>> comet_252P = Orbit.from_dict({ # P/2016 BA14 + ... "e": 0.67309, + ... "q": 0.99605 * u.au, + ... "incl": 10.42220 * u.deg, + ... "Omega": 190.94850 * u.deg, + ... "w": 343.31047 * u.deg, + ... "n": 0.18533 * u.deg / u.day, + ... "Tp": Time("2016-03-15 06:19:30", scale="tdb") + ... }) + >>> + >>> comet_460P = Orbit.from_dict({ # P/2016 BA14 + ... "e": 0.66625, + ... "q": 1.00858 * u.au, + ... "incl": 18.91867 * u.deg, + ... "Omega": 180.53368 * u.deg, + ... "w": 351.89672 * u.deg, + ... "n": 0.18761 * u.deg / u.day, + ... "Tp": Time("2016-03-15 12:24:19", scale="tdb") + ... }) >>> >>> # Southworth & Hawkins function - >>> D_SH = comets[0].D_criterion(comets[1]) + >>> D_SH = comet_252P.D_criterion(comet_460P) >>> # Drummond function - >>> D_D = comets[0].D_criterion(comets[1], version='d') + >>> D_D = comet_252P.D_criterion(comet_460P, version='d') >>> # hybrid function - >>> D_H = comets[0].D_criterion(comets[1], version='h') + >>> D_H = comet_252P.D_criterion(comet_460P, version='h') >>> print('D_SH = {:.4f}, D_D = {:.4f}, D_H = {:.4f}'. ... format(D_SH, D_D, D_H)) - D_SH = 0.1560, D_D = 0.0502, D_H = 0.1556 + D_SH = 0.1562, D_D = 0.0503, D_H = 0.1558 From ef83f1d3b323c7bf03aa0253fdb3a60e782580ae Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Fri, 25 Aug 2023 16:03:35 -0400 Subject: [PATCH 17/21] Add comment to justify doctest block. --- docs/sbpy/spectroscopy/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sbpy/spectroscopy/index.rst b/docs/sbpy/spectroscopy/index.rst index 234543dd8..728f3dba5 100644 --- a/docs/sbpy/spectroscopy/index.rst +++ b/docs/sbpy/spectroscopy/index.rst @@ -29,6 +29,8 @@ re-normalization to other wavelengths. inverse length. For convenience, `sbpy` includes a `~sbpy.units.hundred_nm` unit, which is equal to 100 nm: +.. These imports are needed for astropy versions < 5.3 because the next + doctest block only executes for versions >=5.3. .. testsetup:: .. doctest-requires:: astropy<5.3 From dfaffb397fd1f78c65c5feb43f1586880d33ca63 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Mon, 28 Aug 2023 09:48:22 -0400 Subject: [PATCH 18/21] Added note and work around for doctest-requires/doctest-remote-data bug. --- docs/sbpy/data/ephem.rst | 18 ++++++++++-------- docs/sbpy/data/obs.rst | 4 ++-- docs/sbpy/data/orbit.rst | 18 +++++++++--------- docs/sbpy/data/phys.rst | 4 ++-- docs/sbpy/photometry.rst | 2 +- setup.cfg | 12 ++++++++++++ 6 files changed, 36 insertions(+), 22 deletions(-) diff --git a/docs/sbpy/data/ephem.rst b/docs/sbpy/data/ephem.rst index 0a5ac3bbc..01eb52bc6 100644 --- a/docs/sbpy/data/ephem.rst +++ b/docs/sbpy/data/ephem.rst @@ -16,6 +16,7 @@ query for ephemerides of asteroid Ceres on a given date and for the position of Mauna Kea Observatory (IAU observatory code 568) from the `JPL Horizons service `_: +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> from sbpy.data import Ephem @@ -35,6 +36,7 @@ Mauna Kea Observatory (IAU observatory code 568) from the `JPL Horizons service The full column name list in the data table can be retrieved with the `~sbpy.data.DataClass.field_names` property: +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> eph.field_names @@ -48,7 +50,7 @@ In the above example a specific epoch was specified, but multiple epochs may be requested, or even a range of epochs. All time references are `~astropy.time.Time` objects. -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> epochs = Time(['2022-06-04', '2023-06-04']) @@ -68,7 +70,7 @@ than a few hundred to prevent corruption of the query (see To specify a range of epochs: -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> import astropy.units as u @@ -98,7 +100,7 @@ Mulitple targets An additional feature of `~sbpy.data.Ephem.from_horizons` is that you can automatically concatenate queries for a number of objects: -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> epoch1 = Time('2018-08-03 14:20') @@ -133,7 +135,7 @@ codes `__ as above, or by using `~astropy.coordinates.EarthLocation` as shown in the following example: -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> from astropy.coordinates import EarthLocation @@ -159,7 +161,7 @@ to `~sbpy.data.Ephem.from_horizons` are directly passed on to flexibility of the latter function. For example one may use the ``skip_daylight`` keyword argument: -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> epoch1 = Time('2018-08-03 14:20', scale='utc') @@ -175,7 +177,7 @@ Or, a common option for periodic cometary targets is to limit orbit look-ups to the apparition closest to the epochs being queried (requires ``id_type='designation'``): -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> eph = Ephem.from_horizons('2P') # doctest: +SKIP @@ -202,7 +204,7 @@ Offering similar functionality, the `~sbpy.data.Ephem.from_mpc` method will retrieve ephemerides from the `Minor Planet Center's Ephemeris Service `_: -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> eph = Ephem.from_mpc('2P', location='568', @@ -230,7 +232,7 @@ Finally, `~sbpy.data.Ephem.from_miriade` will retrieve ephemerides from the `Institut de Mécanique Céleste et de Calcul des Éphémérides `_: -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> eph = Ephem.from_miriade('2P', objtype='comet', location='568', diff --git a/docs/sbpy/data/obs.rst b/docs/sbpy/data/obs.rst index 1d20cfcf6..037abf598 100644 --- a/docs/sbpy/data/obs.rst +++ b/docs/sbpy/data/obs.rst @@ -8,7 +8,7 @@ Observational Data Objects (`sbpy.data.Obs`) For instance, this class allows you to query observations reported to the Minor Planet Center for a given target via `astroquery.mpc.MPCClass.get_observations`: -.. doctest-requires:: astroquery +... .. doctest-requires:: astroquery .. doctest-remote-data:: >>> from sbpy.data import Obs @@ -38,7 +38,7 @@ function makes use of the query functions that are part of `~sbpy.data.Ephem` and allows you to pick a service from which you would like to obtain the data. -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> data.field_names diff --git a/docs/sbpy/data/orbit.rst b/docs/sbpy/data/orbit.rst index 594dfa0eb..1d190cafc 100644 --- a/docs/sbpy/data/orbit.rst +++ b/docs/sbpy/data/orbit.rst @@ -9,7 +9,7 @@ Orbit Queries body osculating elements from the `JPL Horizons service `_: -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> from sbpy.data import Orbit @@ -31,7 +31,7 @@ scale of the desired epoch is not supported by the query function and hence converted to a scale that is supported (``tdb`` in this case). The following fields are available in this object: -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> elem.field_names @@ -44,7 +44,7 @@ epoch (current time) are queried. Similar to parameter on to that function. Furthermore, it is possible to query orbital elements for a number of targets: -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> epoch = Time('2018-08-03 14:20', scale='tdb') @@ -64,7 +64,7 @@ Alternatively, orbital elements can also be queried from the `Minor Planet Center `_, although in this case only the most recent elements are accessible: -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> elem = Orbit.from_mpc(['3552', '12893']) @@ -93,7 +93,7 @@ using `OpenOrb `_. In order to transform some current orbits to a state vector in cartesian coordinates, one could use the following code: -.. doctest-requires:: astroquery, oorb +.. .. doctest-requires:: astroquery, oorb .. doctest-remote-data:: >>> elem = Orbit.from_horizons(['Ceres', 'Pallas', 'Vesta']) @@ -120,7 +120,7 @@ propagated to either as `~astropy.time.Time` object, or as float in terms of Julian date. The following example propagates the current orbit of Ceres back to year 2000: -.. doctest-requires:: astroquery, oorb +.. .. doctest-requires:: astroquery, oorb .. doctest-remote-data:: >>> elem = Orbit.from_horizons('Ceres') @@ -148,7 +148,7 @@ parameter with respect to Jupiter is used in the dynamical classification of comets. The Tisserand parameter can be calculated by `~sbpy.Orbit.tisserand` as follows: -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> epoch = Time(2449400.5, format='jd', scale='tdb') @@ -162,7 +162,7 @@ One can also specify the planet with respect to which the Tisserand parameter is calculated with optional parameter `planet`. It also allows multiple planet to be specified simultaneously: -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> import numpy as np @@ -190,7 +190,7 @@ Drummond function (`Drummond 1991 >> import numpy as np diff --git a/docs/sbpy/data/phys.rst b/docs/sbpy/data/phys.rst index c64bf9e41..360ca49f6 100644 --- a/docs/sbpy/data/phys.rst +++ b/docs/sbpy/data/phys.rst @@ -13,7 +13,7 @@ including the use of `~astropy.units`. As an example, the following code will query the properties for a small number of asteroids: -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> from sbpy.data import Phys @@ -50,7 +50,7 @@ from `~sbpy.data.Phys.from_jplspec` include the following data: | Lower level energy in Joules | Degrees of freedom -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> from sbpy.data.phys import Phys diff --git a/docs/sbpy/photometry.rst b/docs/sbpy/photometry.rst index fa05889b7..2ddb69c1b 100644 --- a/docs/sbpy/photometry.rst +++ b/docs/sbpy/photometry.rst @@ -83,7 +83,7 @@ The model class can also be initialized by a subclass of ``sbpy``'s `~sbpy.data.DataClass`, such as `~sbpy.data.Phys`, as long as it contains the model parameters: -.. doctest-requires:: astroquery +.. .. doctest-requires:: astroquery .. doctest-remote-data:: >>> from sbpy.data import Phys diff --git a/setup.cfg b/setup.cfg index dca773af7..be159a21f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -85,6 +85,18 @@ filterwarnings = ignore:numpy\.ufunc size changed:RuntimeWarning ignore:numpy\.ndarray size changed:RuntimeWarning +# The following is to work around an issue building the documentation (see +# #383). The prefered approach is to use doctest-requires at the code-block +# level (e.g., this marks all orbit.rst tests as needed oorb, but only 2 of 9 +# blocks require it). Relevant doctest-requires directives have been commented +# out. +doctest_subpackage_requires = + docs/sbpy/photometry.rst = astroquery + docs/sbpy/data/ephem.rst = astroquery + docs/sbpy/data/obs.rst = astroquery + docs/sbpy/data/orbit.rst = astroquery,oorb + docs/sbpy/data/phys.rst = astroquery + # [coverage:run] # omit = # sbpy/_astropy_init* From 9e589d6876ad082f2807c09cbfca10e2b088931d Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Mon, 28 Aug 2023 11:17:07 -0400 Subject: [PATCH 19/21] Remove apparent debug statement. --- sbpy/data/orbit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sbpy/data/orbit.py b/sbpy/data/orbit.py index 48f8634f3..07a7adf70 100644 --- a/sbpy/data/orbit.py +++ b/sbpy/data/orbit.py @@ -721,7 +721,6 @@ def oo_propagate(self, epochs, dynmodel='N', ephfile='de430'): in_orbits=in_orbits._to_oo(), in_epoch=ooepoch, in_dynmodel=dynmodel) - print(oo_orbits, err) if err != 0: OpenOrbError('pyoorb failed with error code {:d}'.format(err)) From 91834125efc778d5763745a51834befea10d2980 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Thu, 9 Nov 2023 20:41:44 -0500 Subject: [PATCH 20/21] Remove commented code. --- sbpy/calib/tests/test_core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sbpy/calib/tests/test_core.py b/sbpy/calib/tests/test_core.py index 38ca18fb6..5d932607f 100644 --- a/sbpy/calib/tests/test_core.py +++ b/sbpy/calib/tests/test_core.py @@ -289,7 +289,6 @@ def test_set_source(self): class Test_vega_spectrum: def test_validate_str(self): pytest.importorskip("synphot") - # pytest.importorskip("synphot") assert isinstance(vega_spectrum.validate("Bohlin2014"), Vega) def test_validate_Vega(self): From e342b89525afaf84cc413e4849226d8dbe97e1e4 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Wed, 15 Nov 2023 10:54:10 -0500 Subject: [PATCH 21/21] Update requirements for remote-tests --- sbpy/bib/core.py | 4 ++++ sbpy/data/ephem.py | 3 ++- sbpy/data/orbit.py | 12 +++++++++--- sbpy/data/phys.py | 8 ++++++-- sbpy/data/tests/test_obs_remote.py | 2 ++ sbpy/data/tests/test_orbit_remote.py | 2 +- sbpy/photometry/core.py | 1 + sbpy/photometry/tests/test_iau.py | 4 ++++ sbpy/thermal/core.py | 4 ++++ 9 files changed, 33 insertions(+), 7 deletions(-) diff --git a/sbpy/bib/core.py b/sbpy/bib/core.py index 950c7a25b..aa5eb68c4 100644 --- a/sbpy/bib/core.py +++ b/sbpy/bib/core.py @@ -22,6 +22,10 @@ 'to_mnras' ] +__doctest_requires__ = { + "cite": ["ads"], +} + import warnings from functools import wraps from collections import OrderedDict, defaultdict diff --git a/sbpy/data/ephem.py b/sbpy/data/ephem.py index 7d9b275eb..2328cd3db 100644 --- a/sbpy/data/ephem.py +++ b/sbpy/data/ephem.py @@ -42,7 +42,8 @@ __doctest_requires__ = { - ('Ephem.from_oo',): ['pyoorb'] + ("Ephem.from_oo",): ["pyoorb"], + ("Ephem.from_horizons", "Ephem.from_miriade", "Ephem.from_mpc"): ["astroquery"], } diff --git a/sbpy/data/orbit.py b/sbpy/data/orbit.py index 07a7adf70..1278bbe58 100644 --- a/sbpy/data/orbit.py +++ b/sbpy/data/orbit.py @@ -7,7 +7,16 @@ Class for querying, manipulating, integrating, and fitting orbital elements. created on June 04, 2017 + """ + +__all__ = ['Orbit', 'OrbitError', 'OpenOrbError'] + +__doctest_requires__ = { + ("Orbit.oo_propagate", "Orbit.oo_transform"): ["pyoorb", "astroquery"], + ("Orbit.D_criterion", "Orbit.tisserand", "Orbit.from_horizons", "Orbit.from_mpc"): ["astroquery"], +} + import os import itertools from warnings import warn @@ -35,9 +44,6 @@ from ..utils.decorators import requires -__all__ = ['Orbit', 'OrbitError', 'OpenOrbError'] - - class OrbitError(SbpyException): """Generic Error used in sbpy.data.orbit""" pass diff --git a/sbpy/data/phys.py b/sbpy/data/phys.py index 6063d6875..89715d2ee 100644 --- a/sbpy/data/phys.py +++ b/sbpy/data/phys.py @@ -9,6 +9,12 @@ created on June 04, 2017 """ +__all__ = ["Phys"] + +__doctest_requires__ = { + "Phys.from_sbdb": ["astroquery"], +} + from collections import OrderedDict import numpy as np @@ -26,8 +32,6 @@ from ..exceptions import SbpyException from ..utils.decorators import requires -__all__ = ['Phys'] - class JPLSpecQueryFailed(SbpyException): ''' diff --git a/sbpy/data/tests/test_obs_remote.py b/sbpy/data/tests/test_obs_remote.py index 3740e9c52..8359b01ab 100644 --- a/sbpy/data/tests/test_obs_remote.py +++ b/sbpy/data/tests/test_obs_remote.py @@ -9,6 +9,8 @@ from ... import bib from ..core import QueryError +pytest.importorskip("astroquery") + @pytest.mark.remote_data class TestObsfromMPC: diff --git a/sbpy/data/tests/test_orbit_remote.py b/sbpy/data/tests/test_orbit_remote.py index 98a21902f..5f9e7757a 100644 --- a/sbpy/data/tests/test_orbit_remote.py +++ b/sbpy/data/tests/test_orbit_remote.py @@ -100,7 +100,7 @@ def test_single(self): assert len(a) == 1 def test_multiple(self): - a = Orbit.from_mpc(['1P', '2P', '3P']) + a = Orbit.from_mpc(["1P", "2P", "4P"]) assert len(a) == 3 def test_break(self): diff --git a/sbpy/photometry/core.py b/sbpy/photometry/core.py index 9c01ecb8c..8ed46b335 100644 --- a/sbpy/photometry/core.py +++ b/sbpy/photometry/core.py @@ -16,6 +16,7 @@ "LinearPhaseFunc", "LinearPhaseFunc._phase_integral" ): ["scipy"], + ("DiskIntegratedPhaseFunc.from_phys", "DiskIntegratedPhaseFunc.to_phys"): ["astroquery"], } from collections import OrderedDict diff --git a/sbpy/photometry/tests/test_iau.py b/sbpy/photometry/tests/test_iau.py index be72bd668..b85551074 100644 --- a/sbpy/photometry/tests/test_iau.py +++ b/sbpy/photometry/tests/test_iau.py @@ -44,6 +44,8 @@ def test_init(self): @pytest.mark.remote_data def test_from_phys(self): + pytest.importorskip("astroquery") + # test initialization from `sbpy.data.DataClass` phys = Phys.from_sbdb('Ceres') m = HG.from_phys(phys) @@ -241,6 +243,8 @@ def test_init(self): @pytest.mark.remote_data def test_from_phys(self): + pytest.importorskip("astroquery") + # initialization with Phys, will cause exception because G1, G2 are # not generally unavailable. phys = Phys.from_sbdb(['Ceres']) diff --git a/sbpy/thermal/core.py b/sbpy/thermal/core.py index 08fed1494..1807b75d6 100644 --- a/sbpy/thermal/core.py +++ b/sbpy/thermal/core.py @@ -7,6 +7,10 @@ __all__ = ['ThermalClass', 'STM', 'FRM', 'NEATM'] +__doctest_requires__ = { + "ThermalClass.flux": "astroquery" +} + class ThermalClass():