Coverage for tests / test_introspection.py: 16%
140 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-14 23:31 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-14 23:31 +0000
1# This file is part of utils.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import sys
23import unittest
24from collections import Counter
26# Classes and functions to use in tests.
27import lsst.utils
28from lsst.utils import doImport
29from lsst.utils._packaging import getPackageDir
30from lsst.utils.introspection import (
31 find_outside_stacklevel,
32 get_caller_name,
33 get_class_of,
34 get_full_type_name,
35 get_instance_of,
36 take_object_census,
37 trace_object_references,
38)
41class GetCallerNameTestCase(unittest.TestCase):
42 """Test get_caller_name.
44 Warning: due to the different ways this can be run
45 (e.g. directly or py.test), the module name can be one of two different
46 things.
47 """
49 def test_free_function(self):
50 def test_func():
51 return get_caller_name(1)
53 result = test_func()
54 self.assertEqual(result, f"{__name__}.test_func")
56 def test_instance_method(self):
57 class TestClass:
58 def run(self):
59 return get_caller_name(1)
61 tc = TestClass()
62 result = tc.run()
63 self.assertEqual(result, f"{__name__}.TestClass.run")
65 def test_class_method(self):
66 class TestClass:
67 @classmethod
68 def run(cls):
69 return get_caller_name(1)
71 tc = TestClass()
72 result = tc.run()
73 self.assertEqual(result, f"{__name__}.TestClass.run")
75 def test_skip(self):
76 def test_func(stacklevel):
77 return get_caller_name(stacklevel)
79 result = test_func(2)
80 self.assertEqual(result, f"{__name__}.GetCallerNameTestCase.test_skip")
82 result = test_func(2000000) # use a large number to avoid details of how the test is run
83 self.assertEqual(result, "")
86class TestInstropection(unittest.TestCase):
87 """Tests for lsst.utils.introspection."""
89 maxDiff = None
91 def testTypeNames(self):
92 # Check types and also an object
93 tests = [
94 (getPackageDir, "lsst.utils.getPackageDir"), # underscore filtered out
95 (int, "int"),
96 (0, "int"),
97 ("", "str"),
98 (doImport, "lsst.utils.doImport.doImport"), # no underscore
99 (Counter, "collections.Counter"),
100 (Counter(), "collections.Counter"),
101 (lsst.utils, "lsst.utils"),
102 ]
104 for item, typeName in tests:
105 self.assertEqual(get_full_type_name(item), typeName)
107 def testUnderscores(self):
108 # Underscores are filtered out unless they can't be, either
109 # because __init__.py did not import it or there is a clash with
110 # the non-underscore version.
111 for test_name in (
112 "import_test.two._four.simple.Simple",
113 "import_test.two._four.clash.Simple",
114 "import_test.two.clash.Simple",
115 ):
116 test_cls = get_class_of(test_name)
117 self.assertTrue(test_cls.true())
118 full = get_full_type_name(test_cls)
119 self.assertEqual(full, test_name)
121 def testGetClassOf(self):
122 tests = [(doImport, "lsst.utils.doImport"), (Counter, "collections.Counter")]
124 for test in tests:
125 ref_type = test[0]
126 for t in test:
127 c = get_class_of(t)
128 self.assertIs(c, ref_type)
130 def testGetInstanceOf(self):
131 c = get_instance_of("collections.Counter", "abcdeab")
132 self.assertIsInstance(c, Counter)
133 self.assertEqual(c["a"], 2)
134 with self.assertRaises(TypeError) as cm:
135 get_instance_of(lsst.utils)
136 self.assertIn("lsst.utils", str(cm.exception))
138 def test_stacklevel(self):
139 level = find_outside_stacklevel("lsst.utils")
140 self.assertEqual(level, 1)
142 info = {}
143 level = find_outside_stacklevel("lsst.utils", stack_info=info)
144 self.assertIn("test_introspection.py", info["filename"])
146 c = doImport("import_test.two.three.success.Container")
147 with self.assertWarns(Warning) as cm:
148 level = c.level()
149 self.assertTrue(cm.filename.endswith("test_introspection.py"))
150 self.assertEqual(level, 2)
151 with self.assertWarns(Warning) as cm:
152 level = c.indirect_level()
153 self.assertTrue(cm.filename.endswith("test_introspection.py"))
154 self.assertEqual(level, 3)
156 # Test with additional options.
157 with self.assertWarns(Warning) as cm:
158 level = c.indirect_level(allow_methods={"indirect_level"})
159 self.assertEqual(level, 2)
160 self.assertTrue(cm.filename.endswith("success.py"))
162 # Adjust test on python 3.10.
163 allow_methods = {"import_test.two.three.success.Container.level"}
164 stacklevel = 1
165 if sys.version_info < (3, 11, 0):
166 # python 3.10 does not support "." syntax and will filter it out.
167 allow_methods.add("indirect_level")
168 stacklevel = 2
169 with self.assertWarns(FutureWarning) as cm:
170 level = c.indirect_level(allow_methods=allow_methods)
171 self.assertEqual(level, stacklevel)
172 self.assertTrue(cm.filename.endswith("success.py"))
174 def test_take_object_census(self):
175 # Full output cannot be validated, because it depends on the global
176 # state of the test process.
177 class DummyClass:
178 pass
180 dummy = DummyClass() # noqa: F841, unused variable
182 counts = take_object_census()
183 self.assertIsInstance(counts, Counter)
184 self.assertEqual(counts[DummyClass], 1)
186 def test_trace_object_references_simple(self):
187 class RefTester:
188 pass
190 obj1 = RefTester()
191 obj2 = RefTester()
192 mapping = {"2": obj2}
194 trace, complete = trace_object_references(RefTester) # max_level = 10
195 self.assertTrue(complete)
196 self.assertEqual(len(trace), 2)
197 # The local namespace is *not* counted as a referring object.
198 self.assertEqual(set(trace[0]), {obj1, obj2})
199 self.assertEqual(list(trace[1]), [mapping])
201 def test_trace_object_references_atlimit(self):
202 """Test that completion is detected when trace ends *just* at
203 the limit.
204 """
206 class RefTester:
207 pass
209 obj1 = RefTester()
210 obj2 = RefTester()
211 mapping = {"2": obj2}
213 trace, complete = trace_object_references(RefTester, max_level=1)
214 self.assertTrue(complete)
215 self.assertEqual(len(trace), 2)
216 # The local namespace is *not* counted as a referring object.
217 self.assertEqual(set(trace[0]), {obj1, obj2})
218 self.assertEqual(list(trace[1]), [mapping])
220 def test_trace_object_references_cyclic(self):
221 class RefTester:
222 pass
224 obj1 = RefTester()
225 obj2 = RefTester()
226 mapping = {"2": obj2}
227 cyclic = {"back": mapping}
228 mapping["forth"] = cyclic
230 trace, complete = trace_object_references(RefTester, max_level=3)
231 self.assertFalse(complete)
232 self.assertEqual(len(trace), 4)
233 # The local namespace is *not* counted as a referring object.
234 self.assertEqual(set(trace[0]), {obj1, obj2})
235 self.assertEqual(list(trace[1]), [mapping])
236 self.assertEqual(list(trace[2]), [cyclic])
237 self.assertEqual(list(trace[3]), [mapping])
239 def test_trace_object_references_null(self):
240 class UnusedClass:
241 pass
243 trace, complete = trace_object_references(UnusedClass)
244 self.assertTrue(complete)
245 self.assertEqual(len(trace), 1)
246 self.assertEqual(list(trace[0]), [])
249if __name__ == "__main__":
250 unittest.main()