Coverage for python/lsst/utils/introspection.py: 15%
53 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-01 02:29 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-01 02:29 -0700
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__ = ["get_class_of", "get_full_type_name", "get_instance_of", "get_caller_name"]
19import builtins
20import inspect
21import types
22from typing import Any, Type, Union
24from .doImport import doImport, doImportType
27def get_full_type_name(cls: Any) -> str:
28 """Return full type name of the supplied entity.
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`.
36 Returns
37 -------
38 name : `str`
39 Full name of type.
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__
60 real_name = cls.__module__ + "." + cls.__qualname__
62 # Remove components with leading underscores
63 cleaned_name = ".".join(c for c in real_name.split(".") if not c.startswith("_"))
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
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
78 return cleaned_name
81def get_class_of(typeOrName: Union[Type, str]) -> Type:
82 """Given the type name or a type, return the python type.
84 If a type name is given, an attempt will be made to import the type.
86 Parameters
87 ----------
88 typeOrName : `str` or Python class
89 A string describing the Python class to load or a Python type.
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.
97 Notes
98 -----
99 This is a thin wrapper around `~lsst.utils.doImport`.
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
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.
118 If a type name is given, an attempt will be made to import the type.
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.
129 Returns
130 -------
131 instance : `object`
132 Instance of the requested type, instantiated with the provided
133 parameters.
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)
144def get_caller_name(stacklevel: int = 2) -> str:
145 """Get the name of the caller method.
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.
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.
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.
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]
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)