Coverage for tests / test_extensions.py: 34%

74 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-30 08:43 +0000

1# This file is part of astro_metadata_translator. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the LICENSE file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12import pickle 

13import unittest 

14from collections.abc import Mapping 

15from typing import Any 

16 

17from astro_metadata_translator import ( 

18 ObservationInfo, 

19 PropertyDefinition, 

20 StubTranslator, 

21 makeObservationInfo, 

22) 

23 

24"""Test that extensions to the core set of properties works""" 

25 

26FOO = "bar" 

27NUMBER = 12345 

28 

29 

30class DummyTranslator(StubTranslator): 

31 """Test translation class that supports extensions.""" 

32 

33 name = "dummy" 

34 supported_instrument = "dummy" 

35 extensions = dict( 

36 number=PropertyDefinition("A number", "int", int), 

37 foo=PropertyDefinition("A string", "str", str), 

38 ) 

39 _const_map = { 

40 "ext_foo": FOO, 

41 "detector_name": "detA", 

42 } 

43 _trivial_map = { 

44 "instrument": "INSTRUME", 

45 } 

46 

47 @classmethod 

48 def can_translate(cls, header: Mapping[str, Any], filename: str | None = None) -> bool: 

49 return "INSTRUME" in header and header["INSTRUME"] == "dummy" 

50 

51 def to_ext_number(self) -> int: 

52 """Return the combination on my luggage.""" 

53 return NUMBER 

54 

55 

56class ExtensionsTestCase(unittest.TestCase): 

57 """Test support for extended properties.""" 

58 

59 def setUp(self) -> None: 

60 self.header = dict(INSTRUME="dummy") 

61 # The test translator is incomplete so it will issue warnings 

62 # about missing translations. Catch them. 

63 with self.assertWarns(UserWarning): 

64 self.obsinfo = ObservationInfo(self.header) 

65 

66 def assert_observation_info(self, obsinfo: ObservationInfo) -> None: 

67 """Check that the `ObservationInfo` is as expected. 

68 

69 Parameters 

70 ---------- 

71 obsinfo : `ObservationInfo` 

72 Information to check. 

73 """ 

74 self.assertIsInstance(obsinfo, ObservationInfo) 

75 self.assertEqual(obsinfo.ext_foo, FOO) 

76 self.assertEqual(obsinfo.ext_number, NUMBER) 

77 self.assertEqual(obsinfo.instrument, "dummy") 

78 self.assertEqual(obsinfo.detector_name, "detA") 

79 

80 def test_basic(self) -> None: 

81 """Test construction of extended ObservationInfo.""" 

82 # Behaves like the original 

83 self.assert_observation_info(self.obsinfo) 

84 

85 copy = makeObservationInfo(translator_class=DummyTranslator, ext_foo=FOO, ext_number=NUMBER) 

86 self.assertEqual(copy, self.obsinfo) 

87 

88 with self.assertRaises(AttributeError): 

89 # Variable is read-only 

90 self.obsinfo.ext_foo = "something completely different" 

91 

92 with self.assertRaises(KeyError): 

93 # Can't specify extension value without declaring extensions 

94 makeObservationInfo(foo="foobar") 

95 

96 with self.assertRaises(TypeError): 

97 # Type checking is applied, like in the original 

98 makeObservationInfo(extensions=DummyTranslator.extensions, ext_foo=98765) 

99 

100 with self.assertRaises(TypeError): 

101 # Type checking is applied, like in the original 

102 makeObservationInfo(extensions=DummyTranslator.extensions, ext_number="42") 

103 

104 with self.assertRaises(KeyError): 

105 makeObservationInfo(translator_class=DummyTranslator, ext_foo=FOO, ext_number2=NUMBER) 

106 

107 # Make sure that explicit None is accepted. 

108 new = makeObservationInfo(translator_class=DummyTranslator, ext_foo=FOO, ext_number=None) 

109 self.assertEqual(new.ext_foo, FOO) 

110 self.assertIsNone(new.ext_number) 

111 

112 def test_pickle(self) -> None: 

113 """Test that pickling works on ObservationInfo with extensions.""" 

114 obsinfo = pickle.loads(pickle.dumps(self.obsinfo)) 

115 self.assert_observation_info(obsinfo) 

116 

117 def test_simple(self) -> None: 

118 """Test that simple representation works.""" 

119 simple = self.obsinfo.to_simple() 

120 self.assertIn("ext_foo", simple) 

121 self.assertIn("ext_number", simple) 

122 obsinfo = ObservationInfo.from_simple(simple) 

123 self.assert_observation_info(obsinfo) 

124 

125 def test_json(self) -> None: 

126 """Test that JSON representation works.""" 

127 json = self.obsinfo.to_json() 

128 self.assertIn("ext_foo", json) 

129 self.assertIn("ext_number", json) 

130 obsinfo = ObservationInfo.from_json(json) 

131 self.assert_observation_info(obsinfo) 

132 

133 def test_pydantic_extensions(self) -> None: 

134 """Test that pydantic model APIs work.""" 

135 jstr = self.obsinfo.model_dump_json() 

136 from_j = ObservationInfo.model_validate_json(jstr) 

137 self.assert_observation_info(from_j) 

138 self.assertEqual(from_j, self.obsinfo) 

139 

140 # Check serialization of a copy to make sure copying propagates 

141 # the extension class. 

142 copy = makeObservationInfo( 

143 translator_class=DummyTranslator, 

144 ext_foo=FOO, 

145 ext_number=NUMBER, 

146 instrument="dummy", 

147 detector_name="detA", 

148 ) 

149 jstr = copy.model_dump_json() 

150 from_j = ObservationInfo.model_validate_json(jstr) 

151 self.assert_observation_info(from_j) 

152 self.assertEqual(from_j, copy) 

153 

154 with self.assertRaises(ValueError): 

155 makeObservationInfo( 

156 translator_class=DummyTranslator, ext_foo=FOO, ext_number=NUMBER, extensions={} 

157 ) 

158 

159 

160if __name__ == "__main__": 

161 unittest.main()