Coverage for tests / test_introspection.py: 16%

140 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-17 08:43 +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/>. 

21 

22import sys 

23import unittest 

24from collections import Counter 

25 

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) 

39 

40 

41class GetCallerNameTestCase(unittest.TestCase): 

42 """Test get_caller_name. 

43 

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 """ 

48 

49 def test_free_function(self): 

50 def test_func(): 

51 return get_caller_name(1) 

52 

53 result = test_func() 

54 self.assertEqual(result, f"{__name__}.test_func") 

55 

56 def test_instance_method(self): 

57 class TestClass: 

58 def run(self): 

59 return get_caller_name(1) 

60 

61 tc = TestClass() 

62 result = tc.run() 

63 self.assertEqual(result, f"{__name__}.TestClass.run") 

64 

65 def test_class_method(self): 

66 class TestClass: 

67 @classmethod 

68 def run(cls): 

69 return get_caller_name(1) 

70 

71 tc = TestClass() 

72 result = tc.run() 

73 self.assertEqual(result, f"{__name__}.TestClass.run") 

74 

75 def test_skip(self): 

76 def test_func(stacklevel): 

77 return get_caller_name(stacklevel) 

78 

79 result = test_func(2) 

80 self.assertEqual(result, f"{__name__}.GetCallerNameTestCase.test_skip") 

81 

82 result = test_func(2000000) # use a large number to avoid details of how the test is run 

83 self.assertEqual(result, "") 

84 

85 

86class TestInstropection(unittest.TestCase): 

87 """Tests for lsst.utils.introspection.""" 

88 

89 maxDiff = None 

90 

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 ] 

103 

104 for item, typeName in tests: 

105 self.assertEqual(get_full_type_name(item), typeName) 

106 

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) 

120 

121 def testGetClassOf(self): 

122 tests = [(doImport, "lsst.utils.doImport"), (Counter, "collections.Counter")] 

123 

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) 

129 

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)) 

137 

138 def test_stacklevel(self): 

139 level = find_outside_stacklevel("lsst.utils") 

140 self.assertEqual(level, 1) 

141 

142 info = {} 

143 level = find_outside_stacklevel("lsst.utils", stack_info=info) 

144 self.assertIn("test_introspection.py", info["filename"]) 

145 

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) 

155 

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")) 

161 

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")) 

173 

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 

179 

180 dummy = DummyClass() # noqa: F841, unused variable 

181 

182 counts = take_object_census() 

183 self.assertIsInstance(counts, Counter) 

184 self.assertEqual(counts[DummyClass], 1) 

185 

186 def test_trace_object_references_simple(self): 

187 class RefTester: 

188 pass 

189 

190 obj1 = RefTester() 

191 obj2 = RefTester() 

192 mapping = {"2": obj2} 

193 

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]) 

200 

201 def test_trace_object_references_atlimit(self): 

202 """Test that completion is detected when trace ends *just* at 

203 the limit. 

204 """ 

205 

206 class RefTester: 

207 pass 

208 

209 obj1 = RefTester() 

210 obj2 = RefTester() 

211 mapping = {"2": obj2} 

212 

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]) 

219 

220 def test_trace_object_references_cyclic(self): 

221 class RefTester: 

222 pass 

223 

224 obj1 = RefTester() 

225 obj2 = RefTester() 

226 mapping = {"2": obj2} 

227 cyclic = {"back": mapping} 

228 mapping["forth"] = cyclic 

229 

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]) 

238 

239 def test_trace_object_references_null(self): 

240 class UnusedClass: 

241 pass 

242 

243 trace, complete = trace_object_references(UnusedClass) 

244 self.assertTrue(complete) 

245 self.assertEqual(len(trace), 1) 

246 self.assertEqual(list(trace[0]), []) 

247 

248 

249if __name__ == "__main__": 

250 unittest.main()