Skip to content

Commit f155abc

Browse files
committed
Add centralized input validation helpers and consistent error handling
Implement input validation helpers in pydeepskylog/validation.py to reduce code duplication. Refactor modules to use validation helpers for numeric, positive, range, and sequence checks.
1 parent 45ac6d9 commit f155abc

4 files changed

Lines changed: 87 additions & 156 deletions

File tree

pydeepskylog/contrast_reserve.py

Lines changed: 28 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
from typing import Optional, Tuple, List
44
from pydeepskylog.config import ContrastReserveConfig
55
from pydeepskylog.exceptions import InvalidParameterError
6+
from pydeepskylog.validation import (
7+
validate_number, validate_positive, validate_sequence
8+
)
69

710
def surface_brightness(magnitude: float, object_diameter1: float, object_diameter2: float) -> float:
811
"""
@@ -16,24 +19,10 @@ def surface_brightness(magnitude: float, object_diameter1: float, object_diamete
1619
# Validate inputs
1720
logger: logging = logging.getLogger(__name__)
1821

19-
if not isinstance(magnitude, (int, float)):
20-
logger.error(f"The magnitude parameter is not a number")
21-
raise InvalidParameterError("Magnitude must be a number")
22-
if not isinstance(object_diameter1, (int, float)):
23-
logger.error(f"The object diameter 1 parameter is not a number")
24-
raise InvalidParameterError("Object diameter 1 must be a number")
25-
if not isinstance(object_diameter2, (int, float)):
26-
logger.error(f"The object diameter 2 parameter is not a number")
27-
raise InvalidParameterError("Object diameter 2 must be a number")
28-
29-
# Check for positive diameters
30-
if object_diameter1 <= 0:
31-
logger.error(f"The object diameter 1 parameter is not positive: {object_diameter1}")
32-
raise InvalidParameterError("Object diameter 1 must be positive")
33-
if object_diameter2 <= 0:
34-
logger.error(f"The object diameter 2 parameter is not positive: {object_diameter2}")
35-
raise InvalidParameterError("Object diameter 2 must be positive")
36-
22+
validate_number(magnitude, "Magnitude")
23+
validate_positive(object_diameter1, "Object diameter 1")
24+
validate_positive(object_diameter2, "Object diameter 2")
25+
3726
return magnitude + (2.5 * math.log10(2827.0 * (object_diameter1 / 60) * (object_diameter2 / 60)))
3827

3928

@@ -57,50 +46,13 @@ def validate_contrast_reserve_inputs(
5746
# Validate required numeric inputs
5847
logger: logging = logging.getLogger(__name__)
5948

60-
if not isinstance(sqm, (int, float)):
61-
logger.error(f"The sqm parameter is not a number")
62-
raise InvalidParameterError("SQM must be a number")
63-
if not isinstance(telescope_diameter, (int, float)):
64-
logger.error(f"The telescope diameter parameter is not a number")
65-
raise InvalidParameterError("Telescope diameter must be a number")
66-
if not isinstance(magnification, (int, float)):
67-
logger.error(f"The magnification parameter is not a number")
68-
raise InvalidParameterError("Magnification must be a number")
69-
70-
# Check for positive values
71-
if telescope_diameter <= 0:
72-
logger.error(f"The telescope diameter parameter is not positive: {telescope_diameter}")
73-
raise InvalidParameterError("Telescope diameter must be positive")
74-
if magnification <= 0:
75-
logger.error(f"The magnification parameter is not positive: {magnification}")
76-
raise InvalidParameterError("Magnification must be positive")
77-
78-
# Validate surf_brightness if provided
79-
if surf_brightness is not None and not isinstance(surf_brightness, (int, float)):
80-
logger.error(f"The surface brightness parameter is not a number: {surf_brightness}")
81-
raise InvalidParameterError("Surface brightness must be a number or None")
82-
83-
# Validate magnitude if provided and needed
84-
if surf_brightness is None and magnitude is not None:
85-
if not isinstance(magnitude, (int, float)):
86-
logger.error(f"The magnitude parameter is not a number: {magnitude}")
87-
raise InvalidParameterError("Magnitude must be a number or None")
88-
89-
# Validate object diameters if provided
90-
if object_diameter1 is not None and not isinstance(object_diameter1, (int, float)):
91-
logger.error(f"The object diameter parameter is not a number: {object_diameter1}")
92-
raise InvalidParameterError("Object diameter 1 must be a number or None")
93-
if object_diameter2 is not None and not isinstance(object_diameter2, (int, float)):
94-
logger.error(f"The object diameter parameter is not a number: {object_diameter2}")
95-
raise InvalidParameterError("Object diameter 2 must be a number or None")
96-
97-
# Check for positive diameters if provided
98-
if object_diameter1 is not None and object_diameter1 <= 0:
99-
logger.error(f"The object diameter 1 parameter is not positive: {object_diameter1}")
100-
raise InvalidParameterError("Object diameter 1 must be positive")
101-
if object_diameter2 is not None and object_diameter2 <= 0:
102-
logger.error(f"The object diameter 2 parameter is not positive: {object_diameter2}")
103-
raise InvalidParameterError("Object diameter 2 must be positive")
49+
validate_number(sqm, "SQM")
50+
validate_positive(telescope_diameter, "Telescope diameter")
51+
validate_positive(magnification, "Magnification")
52+
validate_number(surf_brightness, "Surface brightness", allow_none=True)
53+
validate_number(magnitude, "Magnitude", allow_none=True)
54+
validate_positive(object_diameter1, "Object diameter 1", allow_none=True)
55+
validate_positive(object_diameter2, "Object diameter 2", allow_none=True)
10456

10557

10658
def calculate_initial_parameters(
@@ -122,7 +74,7 @@ def calculate_initial_parameters(
12274

12375
# Minimum useful magnification
12476
sbb1 = sqm - (5 * math.log10(2.833 * aperture_in_inches))
125-
77+
# Validate objectdiameters
12678
if object_diameter1 is None or object_diameter2 is None:
12779
# If the object diameters are not given, we cannot calculate the contrast reserve
12880
logger.error("Cannot calculate contrast reserve, missing object diameters")
@@ -339,59 +291,32 @@ def optimal_detection_magnification(
339291
# Validate required numeric inputs
340292
logger: logging = logging.getLogger(__name__)
341293

342-
if not isinstance(sqm, (int, float)):
343-
logger.error(f"The sqm parameter is not a number: {sqm}")
344-
raise InvalidParameterError("SQM must be a number")
345-
if not isinstance(telescope_diameter, (int, float)):
346-
logger.error(f"The telescope diameter parameter is not a number: {telescope_diameter}")
347-
raise InvalidParameterError("Telescope diameter must be a number")
348-
349-
# Check for positive telescope diameter
350-
if telescope_diameter <= 0:
351-
logger.error(f"The telescope diameter parameter is not positive: {telescope_diameter}")
352-
raise InvalidParameterError("Telescope diameter must be positive")
353-
294+
validate_number(sqm, "SQM")
295+
validate_positive(telescope_diameter, "Telescope diameter")
296+
354297
# Validate surf_brightness if provided
355-
if surf_brightness is not None and not isinstance(surf_brightness, (int, float)):
356-
logger.error(f"The surface brightness parameter is not a number: {surf_brightness}")
357-
raise InvalidParameterError("Surface brightness must be a number or None")
358-
298+
if surf_brightness is not None:
299+
validate_number(surf_brightness, "Surface brightness")
300+
359301
# Validate magnitude if provided and needed
360302
if surf_brightness is None and magnitude is not None:
361-
if not isinstance(magnitude, (int, float)):
362-
logger.error(f"The magnitude parameter is not a number: {magnitude}")
363-
raise InvalidParameterError("Magnitude must be a number or None")
364-
303+
validate_number(magnitude, "Magnitude")
304+
365305
# Validate object diameters if provided
366306
if object_diameter1 is not None and not isinstance(object_diameter1, (int, float)):
367-
logger.error(f"The object diameter 1 parameter is not a number: {object_diameter1}")
368-
raise InvalidParameterError("Object diameter 1 must be a number or None")
307+
validate_positive(object_diameter1, "Object diameter 1")
369308
if object_diameter2 is not None and not isinstance(object_diameter2, (int, float)):
370-
logger.error(f"The object diameter 2 parameter is not a number: {object_diameter2}")
371-
raise InvalidParameterError("Object diameter 2 must be a number or None")
372-
373-
# Check for positive diameters if provided
374-
if object_diameter1 is not None and object_diameter1 <= 0:
375-
logger.error(f"The object diameter 1 parameter is not positive: {object_diameter1}")
376-
raise InvalidParameterError("Object diameter 1 must be positive")
377-
if object_diameter2 is not None and object_diameter2 <= 0:
378-
logger.error(f"The object diameter 2 parameter is not positive: {object_diameter2}")
379-
raise InvalidParameterError("Object diameter 2 must be positive")
380-
309+
validate_positive(object_diameter2, "Object diameter 2")
310+
381311
# Validate magnifications list
382312
if not isinstance(magnifications, list):
383313
logger.error("Magnifications parameter is not a list")
384314
raise InvalidParameterError("Magnifications must be a list")
385315

386316
# Validate each magnification in the list
387317
for mag in magnifications:
388-
if not isinstance(mag, (int, float)):
389-
logger.error(f"Each magnification must be a number: {mag}")
390-
raise InvalidParameterError("Each magnification must be a number")
391-
if mag <= 0:
392-
logger.error(f"Each magnification must be positive: {mag}")
393-
raise InvalidParameterError("Each magnification must be positive")
394-
318+
validate_positive(mag, "Each magnification")
319+
395320
best_contrast = -999
396321
best_x = 0
397322

pydeepskylog/magnitude.py

Lines changed: 16 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import math
22
import logging
33
from pydeepskylog.exceptions import InvalidParameterError
4+
from pydeepskylog.validation import validate_in_range, validate_number
45

56
logger: logging = logging.getLogger(__name__)
67

@@ -19,15 +20,10 @@ def nelm_to_sqm(nelm: float, fst_offset:float=0.0) -> float:
1920
2021
:return: The SQM value
2122
"""
22-
if not isinstance(nelm, (int, float)):
23-
logger.error("nelm must be an int or float")
24-
raise InvalidParameterError("NELM must be a number")
25-
if not isinstance(fst_offset, (int, float)):
26-
logger.error("fst_offset must be an int or float")
27-
raise InvalidParameterError("fst_offset must be a number")
28-
if nelm + fst_offset < 0 or nelm + fst_offset > 6.7:
29-
logger.error("NELM must be between 0 and 6.7")
30-
raise InvalidParameterError("NELM must be between 0 and 6.7")
23+
validate_number(fst_offset, "fst_offset", allow_none=False)
24+
validate_number(nelm, "nelm", allow_none=False)
25+
validate_in_range(nelm + fst_offset, "NELM + fst_offset", 0, 6.7)
26+
3127
try:
3228
exponent = 1.586 - (nelm + fst_offset) / 5.0
3329
base = math.pow(10, exponent) - 1.0
@@ -51,13 +47,8 @@ def nelm_to_bortle(nelm: float) -> int:
5147
:param nelm: The Naked Eye Limiting Magnitude
5248
:return: The Bortle scale value (1 - 9)
5349
"""
54-
if not isinstance(nelm, (int, float)):
55-
logger.error("NELM must be an int or float")
56-
raise InvalidParameterError("NELM must be a number")
50+
validate_in_range(nelm, "NELM", 0, 6.7)
5751

58-
if nelm < 0 or nelm > 6.7:
59-
logger.error("NELM must be between 0 and 6.7")
60-
raise InvalidParameterError("NELM must be between 0 and 6.7")
6152
if nelm < 3.6:
6253
return 9
6354
elif nelm < 3.9:
@@ -87,12 +78,8 @@ def sqm_to_bortle(sqm: float) -> int:
8778
:param sqm: The Sky Quality Meter value
8879
:return: The Bortle scale value (1 - 9)
8980
"""
90-
if not isinstance(sqm, (int, float)):
91-
logger.error("SQM must be an int or float")
92-
raise InvalidParameterError("SQM must be a number")
93-
if sqm < 0 or sqm > 22:
94-
logger.error("SQM must be between 0 and 22")
95-
raise InvalidParameterError("SQM must be between 0 and 22")
81+
validate_in_range(sqm, "SQM", 0, 22)
82+
9683
if sqm <= 17.5:
9784
return 9
9885
elif sqm <= 18.0:
@@ -125,15 +112,9 @@ def sqm_to_nelm(sqm: float, fst_offset: float=0.0) -> float:
125112
:param fst_offset: The offset between the real Nelm and the Nelm for the observer
126113
:return: The Naked Eye Limiting Magnitude
127114
"""
128-
if not isinstance(sqm, (int, float)):
129-
logger.error("SQM must be an int or float")
130-
raise InvalidParameterError("SQM must be a number")
131-
if not isinstance(fst_offset, (int, float)):
132-
logger.error("fst_offset must be an int or float")
133-
raise InvalidParameterError("fst_offset must be a number")
134-
if sqm < 0 or sqm > 22:
135-
logger.error("SQM must be between 0 and 22")
136-
raise InvalidParameterError("SQM must be between 0 and 22")
115+
validate_in_range(sqm, "SQM", 0, 22)
116+
validate_number(fst_offset, "fst_offset", allow_none=False)
117+
137118
try:
138119
base = 1 + math.pow(10, 4.316 - sqm / 5.0)
139120
if base <= 0:
@@ -157,15 +138,9 @@ def bortle_to_nelm(bortle: int, fst_offset: float=0.0) -> float:
157138
:param fst_offset: The offset between the real Nelm and the Nelm for the observer
158139
:return: The NELM value
159140
"""
160-
if not isinstance(bortle, int):
161-
logger.error("bortle must be an int or float")
162-
raise InvalidParameterError("Bortle must be an integer")
163-
if not 1 <= bortle <= 9:
164-
logger.error("bortle must be between 1 and 9")
165-
raise InvalidParameterError("Bortle must be between 1 and 9")
166-
if not isinstance(fst_offset, (int, float)):
167-
logger.error("fst_offset must be an int or float")
168-
raise InvalidParameterError("fst_offset must be a number")
141+
validate_in_range(bortle, "Bortle", 1, 9)
142+
validate_number(fst_offset, "fst_offset", allow_none=False)
143+
169144
# Lookup dictionary mapping Bortle scale to NELM values
170145
bortle_nelm_map = {
171146
1: 6.6,
@@ -191,12 +166,8 @@ def bortle_to_sqm(bortle: int) -> float:
191166
:param bortle: The bortle scale
192167
:return: The SQM value
193168
"""
194-
if not isinstance(bortle, int):
195-
logger.error("bortle must be an int or float")
196-
raise InvalidParameterError("Bortle must be an integer")
197-
if not 1 <= bortle <= 9:
198-
logger.error("bortle must be between 1 and 9")
199-
raise InvalidParameterError("Bortle must be between 1 and 9")
169+
validate_in_range(bortle, "bortle", 1, 9)
170+
200171
# Lookup dictionary mapping Bortle scale to SQM values
201172
bortle_sqm_map = {
202173
1: 21.85,

pydeepskylog/tests/test_contrast_reserve.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,22 +179,22 @@ def test_contrast_reserve(self):
179179
# Test non-numeric surf_brightness
180180
with self.assertRaises(InvalidParameterError) as context:
181181
pds.contrast_reserve(sqm, diameter, 66, "invalid", 9.2, 540, 138)
182-
self.assertEqual(str(context.exception), "Surface brightness must be a number or None")
182+
self.assertEqual(str(context.exception), "Surface brightness must be a number")
183183

184184
# Test non-numeric magnitude when surf_brightness is None
185185
with self.assertRaises(InvalidParameterError) as context:
186186
pds.contrast_reserve(sqm, diameter, 66, None, "invalid", 540, 138)
187-
self.assertEqual(str(context.exception), "Magnitude must be a number or None")
187+
self.assertEqual(str(context.exception), "Magnitude must be a number")
188188

189189
# Test non-numeric object_diameter1
190190
with self.assertRaises(InvalidParameterError) as context:
191191
pds.contrast_reserve(sqm, diameter, 66, None, 9.2, "invalid", 138)
192-
self.assertEqual(str(context.exception), "Object diameter 1 must be a number or None")
192+
self.assertEqual(str(context.exception), "Object diameter 1 must be a number")
193193

194194
# Test non-numeric object_diameter2
195195
with self.assertRaises(InvalidParameterError) as context:
196196
pds.contrast_reserve(sqm, diameter, 66, None, 9.2, 540, "invalid")
197-
self.assertEqual(str(context.exception), "Object diameter 2 must be a number or None")
197+
self.assertEqual(str(context.exception), "Object diameter 2 must be a number")
198198

199199
# Test non-positive object_diameter1
200200
with self.assertRaises(InvalidParameterError) as context:
@@ -297,22 +297,22 @@ def test_best_magnification(self):
297297
# Test non-numeric surf_brightness
298298
with self.assertRaises(InvalidParameterError) as context:
299299
pds.optimal_detection_magnification(sqm, diameter, "invalid", 11, 600, 600, available_magnifications)
300-
self.assertEqual(str(context.exception), "Surface brightness must be a number or None")
300+
self.assertEqual(str(context.exception), "Surface brightness must be a number")
301301

302302
# Test non-numeric magnitude when surf_brightness is None
303303
with self.assertRaises(InvalidParameterError) as context:
304304
pds.optimal_detection_magnification(sqm, diameter, None, "invalid", 600, 600, available_magnifications)
305-
self.assertEqual(str(context.exception), "Magnitude must be a number or None")
305+
self.assertEqual(str(context.exception), "Magnitude must be a number")
306306

307307
# Test non-numeric object_diameter1
308308
with self.assertRaises(InvalidParameterError) as context:
309309
pds.optimal_detection_magnification(sqm, diameter, None, 11, "invalid", 600, available_magnifications)
310-
self.assertEqual(str(context.exception), "Object diameter 1 must be a number or None")
310+
self.assertEqual(str(context.exception), "Object diameter 1 must be a number")
311311

312312
# Test non-numeric object_diameter2
313313
with self.assertRaises(InvalidParameterError) as context:
314314
pds.optimal_detection_magnification(sqm, diameter, None, 11, 600, "invalid", available_magnifications)
315-
self.assertEqual(str(context.exception), "Object diameter 2 must be a number or None")
315+
self.assertEqual(str(context.exception), "Object diameter 2 must be a number")
316316

317317
# Test non-positive object_diameter1
318318
with self.assertRaises(InvalidParameterError) as context:

pydeepskylog/validation.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import logging
2+
from typing import Any, Optional, Sequence
3+
from pydeepskylog.exceptions import InvalidParameterError
4+
5+
logger = logging.getLogger(__name__)
6+
7+
def validate_number(value: Any, name: str, allow_none: bool = False) -> None:
8+
if value is None and allow_none:
9+
return
10+
if not isinstance(value, (int, float)):
11+
logger.error(f"{name} must be a number")
12+
raise InvalidParameterError(f"{name} must be a number")
13+
14+
def validate_positive(value: Any, name: str, allow_none: bool = False) -> None:
15+
validate_number(value, name, allow_none)
16+
if value is None and allow_none:
17+
return
18+
if value <= 0:
19+
logger.error(f"{name} must be positive")
20+
raise InvalidParameterError(f"{name} must be positive")
21+
22+
def validate_in_range(value: Any, name: str, min_value: float, max_value: float) -> None:
23+
validate_number(value, name)
24+
if not (min_value <= value <= max_value):
25+
logger.error(f"{name} must be between {min_value} and {max_value}")
26+
raise InvalidParameterError(f"{name} must be between {min_value} and {max_value}")
27+
28+
def validate_sequence(seq: Any, name: str, item_type: type = float) -> None:
29+
if not isinstance(seq, Sequence):
30+
logger.error(f"{name} must be a sequence")
31+
raise InvalidParameterError(f"{name} must be a sequence")
32+
for item in seq:
33+
if not isinstance(item, item_type):
34+
logger.error(f"Each item in {name} must be of type {item_type.__name__}")
35+
raise InvalidParameterError(f"Each item in {name} must be of type {item_type.__name__}")

0 commit comments

Comments
 (0)