Coverage for python/lsst/utils/introspection.py: 13%

68 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-08 09:53 +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# Use of this source code is governed by a 3-clause BSD-style 

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

11# 

12"""Utilities relating to introspection in python.""" 

13 

14from __future__ import annotations 

15 

16__all__ = [ 

17 "get_class_of", 

18 "get_full_type_name", 

19 "get_instance_of", 

20 "get_caller_name", 

21 "find_outside_stacklevel", 

22] 

23 

24import builtins 

25import inspect 

26import types 

27from typing import Any 

28 

29from .doImport import doImport, doImportType 

30 

31 

32def get_full_type_name(cls: Any) -> str: 

33 """Return full type name of the supplied entity. 

34 

35 Parameters 

36 ---------- 

37 cls : `type` or `object` 

38 Entity from which to obtain the full name. Can be an instance 

39 or a `type`. 

40 

41 Returns 

42 ------- 

43 name : `str` 

44 Full name of type. 

45 

46 Notes 

47 ----- 

48 Builtins are returned without the ``builtins`` specifier included. This 

49 allows `str` to be returned as "str" rather than "builtins.str". Any 

50 parts of the path that start with a leading underscore are removed 

51 on the assumption that they are an implementation detail and the 

52 entity will be hoisted into the parent namespace. 

53 """ 

54 # If we have a module that needs to be converted directly 

55 # to a name. 

56 if isinstance(cls, types.ModuleType): 

57 return cls.__name__ 

58 # If we have an instance we need to convert to a type 

59 if not hasattr(cls, "__qualname__"): 

60 cls = type(cls) 

61 if hasattr(builtins, cls.__qualname__): 

62 # Special case builtins such as str and dict 

63 return cls.__qualname__ 

64 

65 real_name = cls.__module__ + "." + cls.__qualname__ 

66 

67 # Remove components with leading underscores 

68 cleaned_name = ".".join(c for c in real_name.split(".") if not c.startswith("_")) 

69 

70 # Consistency check 

71 if real_name != cleaned_name: 

72 try: 

73 test = doImport(cleaned_name) 

74 except Exception: 

75 # Could not import anything so return the real name 

76 return real_name 

77 

78 # The thing we imported should match the class we started with 

79 # despite the clean up. If it does not we return the real name 

80 if test is not cls: 

81 return real_name 

82 

83 return cleaned_name 

84 

85 

86def get_class_of(typeOrName: type | str | types.ModuleType) -> type: 

87 """Given the type name or a type, return the python type. 

88 

89 If a type name is given, an attempt will be made to import the type. 

90 

91 Parameters 

92 ---------- 

93 typeOrName : `str` or Python class 

94 A string describing the Python class to load or a Python type. 

95 

96 Returns 

97 ------- 

98 type_ : `type` 

99 Directly returns the Python type if a type was provided, else 

100 tries to import the given string and returns the resulting type. 

101 

102 Notes 

103 ----- 

104 This is a thin wrapper around `~lsst.utils.doImport`. 

105 

106 Raises 

107 ------ 

108 TypeError 

109 Raised if a module is imported rather than a type. 

110 """ 

111 if isinstance(typeOrName, str): 

112 cls = doImportType(typeOrName) 

113 else: 

114 if isinstance(typeOrName, types.ModuleType): 

115 raise TypeError(f"Can not get class of module {get_full_type_name(typeOrName)}") 

116 cls = typeOrName 

117 return cls 

118 

119 

120def get_instance_of(typeOrName: type | str, *args: Any, **kwargs: Any) -> Any: 

121 """Given the type name or a type, instantiate an object of that type. 

122 

123 If a type name is given, an attempt will be made to import the type. 

124 

125 Parameters 

126 ---------- 

127 typeOrName : `str` or Python class 

128 A string describing the Python class to load or a Python type. 

129 args : `tuple` 

130 Positional arguments to use pass to the object constructor. 

131 **kwargs 

132 Keyword arguments to pass to object constructor. 

133 

134 Returns 

135 ------- 

136 instance : `object` 

137 Instance of the requested type, instantiated with the provided 

138 parameters. 

139 

140 Raises 

141 ------ 

142 TypeError 

143 Raised if a module is imported rather than a type. 

144 """ 

145 cls = get_class_of(typeOrName) 

146 return cls(*args, **kwargs) 

147 

148 

149def get_caller_name(stacklevel: int = 2) -> str: 

150 """Get the name of the caller method. 

151 

152 Any item that cannot be determined (or is not relevant, e.g. a free 

153 function has no class) is silently omitted, along with an 

154 associated separator. 

155 

156 Parameters 

157 ---------- 

158 stacklevel : `int` 

159 How many levels of stack to skip while getting caller name; 

160 1 means "who calls me", 2 means "who calls my caller", etc. 

161 

162 Returns 

163 ------- 

164 name : `str` 

165 Name of the caller as a string in the form ``module.class.method``. 

166 An empty string is returned if ``stacklevel`` exceeds the stack height. 

167 

168 Notes 

169 ----- 

170 Adapted from http://stackoverflow.com/a/9812105 

171 by adding support to get the class from ``parentframe.f_locals['cls']`` 

172 """ 

173 stack = inspect.stack() 

174 start = 0 + stacklevel 

175 if len(stack) < start + 1: 

176 return "" 

177 parentframe = stack[start][0] 

178 

179 name = [] 

180 module = inspect.getmodule(parentframe) 

181 if module: 

182 name.append(module.__name__) 

183 # add class name, if any 

184 if "self" in parentframe.f_locals: 

185 name.append(type(parentframe.f_locals["self"]).__name__) 

186 elif "cls" in parentframe.f_locals: 

187 name.append(parentframe.f_locals["cls"].__name__) 

188 codename = parentframe.f_code.co_name 

189 if codename != "<module>": # top level usually 

190 name.append(codename) # function or a method 

191 return ".".join(name) 

192 

193 

194def find_outside_stacklevel(module_name: str) -> int: 

195 """Find the stacklevel for outside of the given module. 

196 

197 This can be used to determine the stacklevel parameter that should be 

198 passed to log messages or warnings in order to make them appear to 

199 come from external code and not this package. 

200 

201 Parameters 

202 ---------- 

203 module_name : `str` 

204 The name of the module to base the stack level calculation upon. 

205 

206 Returns 

207 ------- 

208 stacklevel : `int` 

209 The stacklevel to use matching the first stack frame outside of the 

210 given module. 

211 """ 

212 this_module = "lsst.utils" 

213 stacklevel = -1 

214 for i, s in enumerate(inspect.stack()): 

215 module = inspect.getmodule(s.frame) 

216 # Stack frames sometimes hang around so explicitly delete. 

217 del s 

218 if module is None: 

219 continue 

220 if module_name != this_module and module.__name__.startswith(this_module): 

221 # Should not include this function unless explicitly requested. 

222 continue 

223 if not module.__name__.startswith(module_name): 

224 # 0 will be this function. 

225 # 1 will be the caller 

226 # and so does not need adjustment. 

227 stacklevel = i 

228 break 

229 else: 

230 # The top can't be inside the module. 

231 stacklevel = i 

232 

233 return stacklevel