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
« 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."""
14from __future__ import annotations
16__all__ = [
17 "get_class_of",
18 "get_full_type_name",
19 "get_instance_of",
20 "get_caller_name",
21 "find_outside_stacklevel",
22]
24import builtins
25import inspect
26import types
27from typing import Any
29from .doImport import doImport, doImportType
32def get_full_type_name(cls: Any) -> str:
33 """Return full type name of the supplied entity.
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`.
41 Returns
42 -------
43 name : `str`
44 Full name of type.
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__
65 real_name = cls.__module__ + "." + cls.__qualname__
67 # Remove components with leading underscores
68 cleaned_name = ".".join(c for c in real_name.split(".") if not c.startswith("_"))
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
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
83 return cleaned_name
86def get_class_of(typeOrName: type | str | types.ModuleType) -> type:
87 """Given the type name or a type, return the python type.
89 If a type name is given, an attempt will be made to import the type.
91 Parameters
92 ----------
93 typeOrName : `str` or Python class
94 A string describing the Python class to load or a Python type.
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.
102 Notes
103 -----
104 This is a thin wrapper around `~lsst.utils.doImport`.
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
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.
123 If a type name is given, an attempt will be made to import the type.
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.
134 Returns
135 -------
136 instance : `object`
137 Instance of the requested type, instantiated with the provided
138 parameters.
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)
149def get_caller_name(stacklevel: int = 2) -> str:
150 """Get the name of the caller method.
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.
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.
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.
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]
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)
194def find_outside_stacklevel(module_name: str) -> int:
195 """Find the stacklevel for outside of the given module.
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.
201 Parameters
202 ----------
203 module_name : `str`
204 The name of the module to base the stack level calculation upon.
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
233 return stacklevel