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
« 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#
13from __future__ import annotations
15"""Utilities relating to introspection in python."""
17__all__ = [
18 "get_class_of",
19 "get_full_type_name",
20 "get_instance_of",
21 "get_caller_name",
22 "find_outside_stacklevel",
23]
25import builtins
26import inspect
27import types
28from typing import Any, Type, Union
30from .doImport import doImport, doImportType
33def get_full_type_name(cls: Any) -> str:
34 """Return full type name of the supplied entity.
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`.
42 Returns
43 -------
44 name : `str`
45 Full name of type.
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__
66 real_name = cls.__module__ + "." + cls.__qualname__
68 # Remove components with leading underscores
69 cleaned_name = ".".join(c for c in real_name.split(".") if not c.startswith("_"))
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
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
84 return cleaned_name
87def get_class_of(typeOrName: Union[Type, str]) -> Type:
88 """Given the type name or a type, return the python type.
90 If a type name is given, an attempt will be made to import the type.
92 Parameters
93 ----------
94 typeOrName : `str` or Python class
95 A string describing the Python class to load or a Python type.
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.
103 Notes
104 -----
105 This is a thin wrapper around `~lsst.utils.doImport`.
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
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.
124 If a type name is given, an attempt will be made to import the type.
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.
135 Returns
136 -------
137 instance : `object`
138 Instance of the requested type, instantiated with the provided
139 parameters.
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)
150def get_caller_name(stacklevel: int = 2) -> str:
151 """Get the name of the caller method.
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.
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.
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.
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]
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)
195def find_outside_stacklevel(module_name: str) -> int:
196 """Find the stacklevel for outside of the given module.
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.
202 Parameters
203 ----------
204 module_name : `str`
205 The name of the module to base the stack level calculation upon.
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
234 return stacklevel