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

53 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-02 06:11 -0800

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__ = ["get_class_of", "get_full_type_name", "get_instance_of", "get_caller_name"] 

18 

19import builtins 

20import inspect 

21import types 

22from typing import Any, Type, Union 

23 

24from .doImport import doImport, doImportType 

25 

26 

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

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

29 

30 Parameters 

31 ---------- 

32 cls : `type` or `object` 

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

34 or a `type`. 

35 

36 Returns 

37 ------- 

38 name : `str` 

39 Full name of type. 

40 

41 Notes 

42 ----- 

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

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

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

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

47 entity will be hoisted into the parent namespace. 

48 """ 

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

50 # to a name. 

51 if isinstance(cls, types.ModuleType): 

52 return cls.__name__ 

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

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

55 cls = type(cls) 

56 if hasattr(builtins, cls.__qualname__): 

57 # Special case builtins such as str and dict 

58 return cls.__qualname__ 

59 

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

61 

62 # Remove components with leading underscores 

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

64 

65 # Consistency check 

66 if real_name != cleaned_name: 

67 try: 

68 test = doImport(cleaned_name) 

69 except Exception: 

70 # Could not import anything so return the real name 

71 return real_name 

72 

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

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

75 if test is not cls: 

76 return real_name 

77 

78 return cleaned_name 

79 

80 

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

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

83 

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

85 

86 Parameters 

87 ---------- 

88 typeOrName : `str` or Python class 

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

90 

91 Returns 

92 ------- 

93 type_ : `type` 

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

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

96 

97 Notes 

98 ----- 

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

100 

101 Raises 

102 ------ 

103 TypeError 

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

105 """ 

106 if isinstance(typeOrName, str): 

107 cls = doImportType(typeOrName) 

108 else: 

109 cls = typeOrName 

110 if isinstance(cls, types.ModuleType): 

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

112 return cls 

113 

114 

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

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

117 

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

119 

120 Parameters 

121 ---------- 

122 typeOrName : `str` or Python class 

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

124 args : `tuple` 

125 Positional arguments to use pass to the object constructor. 

126 **kwargs 

127 Keyword arguments to pass to object constructor. 

128 

129 Returns 

130 ------- 

131 instance : `object` 

132 Instance of the requested type, instantiated with the provided 

133 parameters. 

134 

135 Raises 

136 ------ 

137 TypeError 

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

139 """ 

140 cls = get_class_of(typeOrName) 

141 return cls(*args, **kwargs) 

142 

143 

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

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

146 

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

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

149 associated separator. 

150 

151 Parameters 

152 ---------- 

153 stacklevel : `int` 

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

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

156 

157 Returns 

158 ------- 

159 name : `str` 

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

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

162 

163 Notes 

164 ----- 

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

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

167 """ 

168 stack = inspect.stack() 

169 start = 0 + stacklevel 

170 if len(stack) < start + 1: 

171 return "" 

172 parentframe = stack[start][0] 

173 

174 name = [] 

175 module = inspect.getmodule(parentframe) 

176 if module: 

177 name.append(module.__name__) 

178 # add class name, if any 

179 if "self" in parentframe.f_locals: 

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

181 elif "cls" in parentframe.f_locals: 

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

183 codename = parentframe.f_code.co_name 

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

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

186 return ".".join(name)