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

68 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-21 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 

13from __future__ import annotations 

14 

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

16 

17__all__ = [ 

18 "get_class_of", 

19 "get_full_type_name", 

20 "get_instance_of", 

21 "get_caller_name", 

22 "find_outside_stacklevel", 

23] 

24 

25import builtins 

26import inspect 

27import types 

28from typing import Any, Type, Union 

29 

30from .doImport import doImport, doImportType 

31 

32 

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

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

35 

36 Parameters 

37 ---------- 

38 cls : `type` or `object` 

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

40 or a `type`. 

41 

42 Returns 

43 ------- 

44 name : `str` 

45 Full name of type. 

46 

47 Notes 

48 ----- 

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

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

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

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

53 entity will be hoisted into the parent namespace. 

54 """ 

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

56 # to a name. 

57 if isinstance(cls, types.ModuleType): 

58 return cls.__name__ 

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

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

61 cls = type(cls) 

62 if hasattr(builtins, cls.__qualname__): 

63 # Special case builtins such as str and dict 

64 return cls.__qualname__ 

65 

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

67 

68 # Remove components with leading underscores 

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

70 

71 # Consistency check 

72 if real_name != cleaned_name: 

73 try: 

74 test = doImport(cleaned_name) 

75 except Exception: 

76 # Could not import anything so return the real name 

77 return real_name 

78 

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

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

81 if test is not cls: 

82 return real_name 

83 

84 return cleaned_name 

85 

86 

87def get_class_of(typeOrName: Union[Type, str]) -> Type: 

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

89 

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

91 

92 Parameters 

93 ---------- 

94 typeOrName : `str` or Python class 

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

96 

97 Returns 

98 ------- 

99 type_ : `type` 

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

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

102 

103 Notes 

104 ----- 

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

106 

107 Raises 

108 ------ 

109 TypeError 

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

111 """ 

112 if isinstance(typeOrName, str): 

113 cls = doImportType(typeOrName) 

114 else: 

115 cls = typeOrName 

116 if isinstance(cls, types.ModuleType): 

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

118 return cls 

119 

120 

121def get_instance_of(typeOrName: Union[Type, str], *args: Any, **kwargs: Any) -> Any: 

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

123 

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

125 

126 Parameters 

127 ---------- 

128 typeOrName : `str` or Python class 

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

130 args : `tuple` 

131 Positional arguments to use pass to the object constructor. 

132 **kwargs 

133 Keyword arguments to pass to object constructor. 

134 

135 Returns 

136 ------- 

137 instance : `object` 

138 Instance of the requested type, instantiated with the provided 

139 parameters. 

140 

141 Raises 

142 ------ 

143 TypeError 

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

145 """ 

146 cls = get_class_of(typeOrName) 

147 return cls(*args, **kwargs) 

148 

149 

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

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

152 

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

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

155 associated separator. 

156 

157 Parameters 

158 ---------- 

159 stacklevel : `int` 

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

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

162 

163 Returns 

164 ------- 

165 name : `str` 

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

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

168 

169 Notes 

170 ----- 

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

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

173 """ 

174 stack = inspect.stack() 

175 start = 0 + stacklevel 

176 if len(stack) < start + 1: 

177 return "" 

178 parentframe = stack[start][0] 

179 

180 name = [] 

181 module = inspect.getmodule(parentframe) 

182 if module: 

183 name.append(module.__name__) 

184 # add class name, if any 

185 if "self" in parentframe.f_locals: 

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

187 elif "cls" in parentframe.f_locals: 

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

189 codename = parentframe.f_code.co_name 

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

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

192 return ".".join(name) 

193 

194 

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

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

197 

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

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

200 come from external code and not this package. 

201 

202 Parameters 

203 ---------- 

204 module_name : `str` 

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

206 

207 Returns 

208 ------- 

209 stacklevel : `int` 

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

211 given module. 

212 """ 

213 this_module = "lsst.utils" 

214 stacklevel = -1 

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

216 module = inspect.getmodule(s.frame) 

217 # Stack frames sometimes hang around so explicitly delete. 

218 del s 

219 if module is None: 

220 continue 

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

222 # Should not include this function unless explicitly requested. 

223 continue 

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

225 # 0 will be this function. 

226 # 1 will be the caller 

227 # and so does not need adjustment. 

228 stacklevel = i 

229 break 

230 else: 

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

232 stacklevel = i 

233 

234 return stacklevel