Skip to content

Commit

Permalink
Merge pull request #263 from mwcraig/add-pydantic-tests-pre-upgrade
Browse files Browse the repository at this point in the history
Add more tests of our pydantic models prior to upgrading to pydantic 2
  • Loading branch information
mwcraig authored Jan 24, 2024
2 parents 6fb8001 + 5b9afb7 commit bbd2474
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 14 deletions.
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,8 @@ filterwarnings = [
'ignore:metadata \{.+\} was set from the constructor:DeprecationWarning',
# Generated from batman
'ignore:Conversion of an array with ndim > 0 to a scalar:DeprecationWarning',
# ipywidgets or ipyautoui generates this warning...
'ignore:Passing unrecognized arguments to super:DeprecationWarning',
# pandas will require pyarrow at some point, which is good to know, I guess...
'ignore:[.\n]*Pyarrow will become a required dependency of pandas[.\n]*:DeprecationWarning',
]
1 change: 1 addition & 0 deletions stellarphot/gui_tools/fits_opener.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,4 @@ def set_file(self, file, directory=None):

self._fc.reset(directory, file)
self._fc._apply_selection()
self._fc._value = self._fc.selected
5 changes: 4 additions & 1 deletion stellarphot/gui_tools/seeing_profile_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,9 @@ def show_event(
x = int(np.floor(event.data_x))
y = int(np.floor(event.data_y))

rad_prof = CenterAndProfile(data, (x, y), profile_radius=profile_size)
rad_prof = CenterAndProfile(
data, (x, y), profile_radius=profile_size, cutout_size=profile_size
)

try:
try: # Remove previous marker
Expand Down Expand Up @@ -387,6 +389,7 @@ def show_event(
radius=aperture_radius,
gap=default_gap,
annulus_width=default_annulus_width,
fwhm=rad_prof.FWHM,
)
update_aperture_settings = True
else:
Expand Down
83 changes: 83 additions & 0 deletions stellarphot/gui_tools/tests/test_comparison_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import warnings

import ipywidgets as ipw
import numpy as np
import pytest
from astropy.io import fits
from astropy.nddata import CCDData
from astropy.utils.data import get_pkg_data_filename
from astropy.utils.exceptions import AstropyWarning
from astropy.wcs import WCS
from astropy.wcs.wcs import FITSFixedWarning

from stellarphot.gui_tools import comparison_functions as cf

CCD_SHAPE = [2048, 3073]


def test_comparison_object_creation():
# This test simply makes sure we can create the object
comparison_widget = cf.ComparisonViewer()
assert isinstance(comparison_widget.box, ipw.Box)


@pytest.mark.remote_data
def test_comparison_properties(tmp_path):
# Test that we can load a file...
wcs_file = get_pkg_data_filename("../../tests/data/sample_wcs_ey_uma.fits")
with fits.open(wcs_file) as hdulist:
with warnings.catch_warnings():
# Ignore the warning about the WCS having a different number of
# axes than the (non-existent) image.
warnings.filterwarnings(
"ignore",
message="The WCS transformation has more",
category=FITSFixedWarning,
)
wcs = WCS(hdulist[0].header)
wcs.pixel_shape = list(reversed(CCD_SHAPE))
ccd = CCDData(data=np.zeros(CCD_SHAPE), wcs=wcs, unit="adu")
# Set the object name to the star in this field, EY UMa
ccd.header["object"] = "EY UMa"
file_name = "test.fits"
ccd.write(tmp_path / file_name, overwrite=True)
with warnings.catch_warnings():
# Ignore the warning about the WCS having non-standard keywords (the SIP
# distortion parameters).
warnings.filterwarnings(
"ignore",
message="Some non-standard WCS keywords were excluded",
category=AstropyWarning,
)
comparison_widget = cf.ComparisonViewer(directory=tmp_path, file=str(file_name))

# Check that we have some variables in this field of view, which contains the
# variable star EY UMa
assert len(comparison_widget.variables) > 0

# Check that we have APASS stars too
table = comparison_widget.generate_table()
assert "APASS comparison" in table["marker name"]

# Check that is we show labels then the label names we expect show up in
# the astrowidgets marker table.
comparison_widget.show_labels()

# Supress a warning about the default marker set containing no stars
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore",
message="Marker set named.*is empty",
category=UserWarning,
)
label_markers = comparison_widget.iw.get_markers(
marker_name=comparison_widget._label_name
)
# There should be a label for each star, so check that the number of labels matches
# the length of the table of stars excluding the labels.
table = comparison_widget.generate_table()

# Drop table entries that are labels
table = table[table["marker name"] != comparison_widget._label_name]

assert len(label_markers) == len(table)
78 changes: 78 additions & 0 deletions stellarphot/gui_tools/tests/test_seeing_profile.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import warnings
from collections import namedtuple

import ipywidgets as ipw
import matplotlib
from astropy.nddata import CCDData
from astrowidgets import ImageWidget
from photutils.datasets import make_gaussian_sources_image, make_noise_image

from stellarphot.gui_tools import seeing_profile_functions as spf
from stellarphot.photometry.tests.test_profiles import RANDOM_SEED, SHAPE, STARS
from stellarphot.settings import Camera, PhotometryApertures
from stellarphot.settings.tests.test_models import (
TEST_CAMERA_VALUES,
)


def test_keybindings():
Expand Down Expand Up @@ -31,3 +43,69 @@ def simple_bindmap(bindmap):
assert "Nonekp_+" in bound_keys
# Yes, the line below is correct...
assert new_bindings[bound_keys["Nonekp_left"]]["name"] == "pan_right"


def test_seeing_profile_object_creation():
# This test simply makes sure we can create the object
profile_widget = spf.SeeingProfileWidget()
assert isinstance(profile_widget.box, ipw.Box)


def test_seeing_profile_properties(tmp_path):
# Here we make a seeing profile then load an image.
profile_widget = spf.SeeingProfileWidget(camera=Camera(**TEST_CAMERA_VALUES))

# Make a fits file
image = make_gaussian_sources_image(SHAPE, STARS) + make_noise_image(
SHAPE, mean=10, stddev=100, seed=RANDOM_SEED
)

ccd = CCDData(image, unit="adu")
ccd.header["exposure"] = 30.0
ccd.header["object"] = "test"
file_name = tmp_path / "test.fits"
ccd.write(file_name)

# Load the file
profile_widget.fits_file.set_file("test.fits", tmp_path)
profile_widget.load_fits()

# Check that a couple of properties are set correctly
assert profile_widget.object_name == "test"
assert profile_widget.exposure == 30.0

# Check that the photometry apertures have defaulted to the
# default values in the model.
assert profile_widget.aperture_settings.value == PhotometryApertures().dict()

# Get the event handler that updates plots
handler = profile_widget._make_show_event()

# Make a mock event object
Event = namedtuple("Event", ["data_x", "data_y"])
star_loc_x, star_loc_y = STARS["x_mean"][0], STARS["y_mean"][0]
# Sending a mock event will generate plots that we don't want to see
# so set the matplotlib backend to a non-interactive one
matplotlib.use("agg")
# matplotlib generates a warning that we are using a non-interactive backend
# so filter that warning out for the remainder of the test. There are at least
# a couple of times we generate this warning as values are changed.
with warnings.catch_warnings():
warnings.simplefilter("ignore")
handler(profile_widget.iw, Event(star_loc_x, star_loc_y))

# The FWHM should be close to 9.6
assert 9 < profile_widget.aperture_settings.value["fwhm"] < 10

# Get a copy of the current aperture settings
phot_aps = dict(profile_widget.aperture_settings.value)
new_radius = phot_aps["radius"] - 2
# Change the radius by directly setting the value of the widget that holds
# the value. That ends up being nested fairly deeply...
profile_widget.aperture_settings.autowidget.children[0].children[
0
].value = new_radius

# Make sure the settings are updated
phot_aps["radius"] = new_radius
assert profile_widget.aperture_settings.value == phot_aps
3 changes: 2 additions & 1 deletion stellarphot/settings/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ class PhotometryApertures(BaseModel):
radius: conint(ge=1) = Field(autoui=CustomBoundedIntTex, default=1)
gap: conint(ge=1) = Field(autoui=CustomBoundedIntTex, default=1)
annulus_width: conint(ge=1) = Field(autoui=CustomBoundedIntTex, default=1)
fwhm: confloat(gt=0)
# Disable the UI element by default because it is often calculate from an image
fwhm: confloat(gt=0) = Field(disabled=True, default=1.0)

class Config:
validate_assignment = True
Expand Down
91 changes: 79 additions & 12 deletions stellarphot/settings/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import json

import astropy.units as u
import pytest
from astropy.coordinates import SkyCoord
from astropy.time import Time
from pydantic import ValidationError

from stellarphot.settings import ui_generator
from stellarphot.settings.models import Camera, Exoplanet, PhotometryApertures

DEFAULT_APERTURE_SETTINGS = dict(radius=5, gap=10, annulus_width=15, fwhm=3.2)
Expand All @@ -19,6 +22,18 @@
)


DEFAULT_EXOPLANET_SETTINGS = dict(
epoch=Time(0, format="jd"),
period=0 * u.min,
identifier="a planet",
coordinate=SkyCoord(
ra="00:00:00.00", dec="+00:00:00.0", frame="icrs", unit=("hour", "degree")
),
depth=0,
duration=0 * u.min,
)


def test_camera_attributes():
# Check that the attributes are set properly
c = Camera(
Expand Down Expand Up @@ -142,6 +157,70 @@ def test_create_aperture_settings_correctly():
)


# Right now Exoplanet doesn't have a schema, so don't test it. Will
# fix after the pydantic 2 transition.
# [Exoplanet, DEFAULT_EXOPLANET_SETTINGS]
@pytest.mark.parametrize(
"class_, defaults",
(
[PhotometryApertures, DEFAULT_APERTURE_SETTINGS],
[Camera, TEST_CAMERA_VALUES],
),
)
def test_aperture_settings_ui_generation(class_, defaults):
# Check a few things about the UI generation:
# 1) The UI is generated
# 2) The UI model matches our input
# 3) The UI widgets contains the titles we expect
#

# 1) The UI is generated from the class
ui = ui_generator(class_)

print(f"{class_=}")
print(f"{defaults=}")
# 2) The UI model matches our input
# Set the ui values to the defaults -- the value needs to be whatever would
# go into a **widget** though, not a **model**. It is easiest to create
# a model and then use its dict() method to get the widget values.
values_dict_as_strings = json.loads(class_(**defaults).json())
print(f"{values_dict_as_strings=}")
ui.value = values_dict_as_strings
print(f"{ui.value=}")
assert class_(**ui.value).dict() == defaults

# 3) The UI widgets contains the titles generated from pydantic.
# Pydantic generically is supposed to generate titles from the field names,
# replacing "_" with " " and capitalizing the first letter.
#
# In fact, ipyautoui pre-pydantic-2 seems to either use the field name,
# the space-replaced name, or a name with the underscore just removed,
# not replaced by a space.
# Hopefully that improves in future versions, but for now we'll just
# check that the titles are present in the labels.
# We'll ignore the case but need to replace the underscores
pydantic_titles = {
f: [f.replace("_", " "), f.replace("_", "")] for f in defaults.keys()
}
# pydantic_titles = defaults.keys()
title_present = []
print(f"{ui.di_labels=}")
for title in pydantic_titles.keys():
for label in ui.di_labels.values():
present = (
title.lower() in label.lower()
or pydantic_titles[title][0].lower() in label.lower()
or pydantic_titles[title][1].lower() in label.lower()
)
if present:
title_present.append(present)
break
else:
title_present.append(False)

assert all(title_present)


@pytest.mark.parametrize("bad_one", ["radius", "gap", "annulus_width"])
def test_create_invalid_values(bad_one):
# Check that individual values that are bad raise an error
Expand All @@ -151,18 +230,6 @@ def test_create_invalid_values(bad_one):
PhotometryApertures(**bad_settings)


DEFAULT_EXOPLANET_SETTINGS = dict(
epoch=Time(0, format="jd"),
period=0 * u.min,
identifier="a planet",
coordinate=SkyCoord(
ra="00:00:00.00", dec="+00:00:00.0", frame="icrs", unit=("hour", "degree")
),
depth=0,
duration=0 * u.min,
)


def test_create_exoplanet_correctly():
planet = Exoplanet(**DEFAULT_EXOPLANET_SETTINGS)
print(planet)
Expand Down

0 comments on commit bbd2474

Please sign in to comment.