Coverage for tests / test_extensions.py: 34%
74 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 08:50 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 08:50 +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.
12import pickle
13import unittest
14from collections.abc import Mapping
15from typing import Any
17from astro_metadata_translator import (
18 ObservationInfo,
19 PropertyDefinition,
20 StubTranslator,
21 makeObservationInfo,
22)
24"""Test that extensions to the core set of properties works"""
26FOO = "bar"
27NUMBER = 12345
30class DummyTranslator(StubTranslator):
31 """Test translation class that supports extensions."""
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 }
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"
51 def to_ext_number(self) -> int:
52 """Return the combination on my luggage."""
53 return NUMBER
56class ExtensionsTestCase(unittest.TestCase):
57 """Test support for extended properties."""
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)
66 def assert_observation_info(self, obsinfo: ObservationInfo) -> None:
67 """Check that the `ObservationInfo` is as expected.
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")
80 def test_basic(self) -> None:
81 """Test construction of extended ObservationInfo."""
82 # Behaves like the original
83 self.assert_observation_info(self.obsinfo)
85 copy = makeObservationInfo(translator_class=DummyTranslator, ext_foo=FOO, ext_number=NUMBER)
86 self.assertEqual(copy, self.obsinfo)
88 with self.assertRaises(AttributeError):
89 # Variable is read-only
90 self.obsinfo.ext_foo = "something completely different"
92 with self.assertRaises(KeyError):
93 # Can't specify extension value without declaring extensions
94 makeObservationInfo(foo="foobar")
96 with self.assertRaises(TypeError):
97 # Type checking is applied, like in the original
98 makeObservationInfo(extensions=DummyTranslator.extensions, ext_foo=98765)
100 with self.assertRaises(TypeError):
101 # Type checking is applied, like in the original
102 makeObservationInfo(extensions=DummyTranslator.extensions, ext_number="42")
104 with self.assertRaises(KeyError):
105 makeObservationInfo(translator_class=DummyTranslator, ext_foo=FOO, ext_number2=NUMBER)
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)
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)
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)
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)
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)
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)
154 with self.assertRaises(ValueError):
155 makeObservationInfo(
156 translator_class=DummyTranslator, ext_foo=FOO, ext_number=NUMBER, extensions={}
157 )
160if __name__ == "__main__":
161 unittest.main()