Repository: feder-observatory/stellarphot

Issue:
data_unit example for Camera contains errors
In particular, it lists one of the options as "electrons" but that is not a valid unit -- "electron" is the correct name. Same error for "counts".

Found by @Tanner728 

Discussion:

Patch:
diff --git a/stellarphot/settings/models.py b/stellarphot/settings/models.py
index db043c2..d64f9aa 100644
--- a/stellarphot/settings/models.py
+++ b/stellarphot/settings/models.py
@@ -252,23 +252,27 @@ class Camera(BaseModelWithTableRep):
         NonEmptyStr,
         Field(
             description="Name of the camera",
-            examples=["SBIG ST-8300M", "ZWO ASI1600"],
+            examples=["SBIG FakeCam", "ZWO NadaCam", "CG16m"],
         ),
     ]
     data_unit: UnitType = Field(
-        description="units of the data", examples=["adu", "counts", "DN", "electrons"]
+        description="units of the data", examples=["adu", "DN", "count"]
     )
     gain: QuantityType = Field(
         description="unit should be consistent with data and read noise",
-        examples=["1.0 electron / adu"],
+        examples=["1.5 electron / adu", "1.0 electron / DN", "1.0 photon / count"],
     )
     read_noise: QuantityType = Field(
         description="unit should be consistent with dark current",
-        examples=["10.0 electron"],
+        examples=["10.0 electron", "10.0 electron", "10.0 photon"],
     )
     dark_current: QuantityType = Field(
         description="unit consistent with read noise, per unit time",
-        examples=["0.01 electron / second"],
+        examples=[
+            "0.01 electron / second",
+            "0.01 electron / second",
+            "0.01 photon / second",
+        ],
     )
     pixel_scale: Annotated[
         QuantityType,
@@ -279,7 +283,7 @@ class Camera(BaseModelWithTableRep):
         QuantityType,
         Field(
             description="maximum data value while performing photometry",
-            examples=["50000 adu"],
+            examples=["50000 adu", "50000 DN", "50000 count"],
             gt=0,
         ),
     ]
@@ -505,10 +509,12 @@ class Observatory(BaseModelWithTableRep):
     above example, but the example is given in part to show how to use sexagesimal
     notation.
 
-
     """
 
-    name: Annotated[NonEmptyStr, Field(description="Name of the observatory")]
+    name: Annotated[
+        NonEmptyStr,
+        Field(description="Name of the observatory", examples=["My Observatory"]),
+    ]
     latitude: Annotated[
         Latitude,
         _UnitQuantTypePydanticAnnotation,
@@ -532,7 +538,6 @@ class Observatory(BaseModelWithTableRep):
             examples=[
                 "-96.7678",
                 "-96d46m04.08s",
-                "263.2322",
                 "263.2322 degree",
                 "263d13m55.92s",
             ],
@@ -543,7 +548,7 @@ class Observatory(BaseModelWithTableRep):
         WithPhysicalType("length"),
         Field(
             description="Elevation of the observatory",
-            examples=["1000 m", "1 km", "3.241e-14 pc"],
+            examples=["1000 m", "1 km", "3.241e-14 pc", "1e12 nm"],
         ),
     ]
     AAVSO_code: Annotated[str | None, Field(description="AAVSO code for observer")] = (
@@ -753,8 +758,12 @@ class PassbandMapEntry(BaseModel):
 
     """
 
-    your_filter_name: Annotated[str, Field(description="Instrumental Filter Name")]
-    aavso_filter_name: Annotated[AAVSOFilters, Field(title="AAVSO Filter Name")]
+    your_filter_name: Annotated[
+        str, Field(description="Instrumental Filter Name", examples=["Sloan r"])
+    ]
+    aavso_filter_name: Annotated[
+        AAVSOFilters, Field(title="AAVSO Filter Name", default="SR")
+    ]
 
 
 class PassbandMap(BaseModelWithTableRep):


Test patch:
diff --git a/stellarphot/settings/tests/test_models.py b/stellarphot/settings/tests/test_models.py
index a580347..85f599b 100644
--- a/stellarphot/settings/tests/test_models.py
+++ b/stellarphot/settings/tests/test_models.py
@@ -1,8 +1,9 @@
 import json
+import re
 
 import astropy.units as u
 import pytest
-from astropy.coordinates import EarthLocation, SkyCoord
+from astropy.coordinates import EarthLocation, Latitude, Longitude, SkyCoord
 from astropy.table import Table
 from astropy.time import Time
 from pydantic import ValidationError
@@ -257,6 +258,83 @@ class TestModelsWithName:
         assert model(**settings).name == "π"
 
 
+# Only include models here that have examples that should be tested
+@pytest.mark.parametrize(
+    "model,settings",
+    [
+        [Camera, TEST_CAMERA_VALUES.copy()],
+        [Observatory, DEFAULT_OBSERVATORY_SETTINGS.copy()],
+    ],
+)
+class TestModelExamples:
+    """ "
+    Test that you can make a valid model from the examples. The assumption is that
+    all of the first choices in the examples make a valid model, all of the second
+    choices make a valid model, etc.
+
+    The purpose for including this test is that users may use the examples as guidance
+    so we should make sure the guidance isn't nonsense.
+    """
+
+    def test_example(self, model, settings):
+        # Get the model's fields so that we can get their examples. fields is dict
+        # with the field names as keys and the field objects as values.
+        fields = model.model_fields
+
+        examples = {k: f.examples for k, f in fields.items()}
+        example_lengths = set(len(e) for e in examples.values() if e is not None)
+
+        # We can't handle more than two different example lengths in an unambiguous way,
+        # so we raise an error if we have more than two.
+        if len(example_lengths) > 2:
+            raise ValueError(f"Too many different example lengths for {model.__name__}")
+        elif min(example_lengths) > 1 and len(example_lengths) == 2:
+            raise ValueError(
+                "Must have the same number of examples for all fields "
+                "or one example for some fields and the same number for "
+                "the rest."
+            )
+        max_len = max(example_lengths)
+        for k in examples.keys():
+            if examples[k] is None:
+                examples[k] = [None] * max_len
+            elif len(examples[k]) == 1:
+                examples[k] = examples[k] * max_len
+
+        for i in range(max_len):
+            settings = {k: examples[k][i] for k in examples.keys()}
+
+            mod = model(**settings)
+
+            # Really need to compare some fields as
+            # latitude/longitude/quantities/numbers but don't want to hard code that
+            # here.
+            for k, v in settings.items():
+                model_value = getattr(mod, k)
+
+                # For some foolish reason Observatory allows the latitude and longitude
+                # to be entered as floats, which we assume are intended to have unit of
+                # degrees. Test and handle that case...
+                print(k, v)
+                if k.lower() in ["latitude", "longitude"] and re.match(
+                    r"[+-]?\d+\.\d+$", v
+                ):
+                    v = v + " degree"
+
+                # Also, latitude and longitude are not Quantity, so handle that too
+                if k == "latitude":
+                    v = Latitude(v)
+                elif k == "longitude":
+                    v = Longitude(v)
+
+                if isinstance(model_value, u.Quantity):
+                    assert model_value == u.Quantity(v)
+                elif isinstance(model_value, u.UnitBase):
+                    assert model_value == u.Unit(v)
+                else:
+                    assert model_value == v
+
+
 def test_camera_unitscheck():
     # Check that the units are checked properly