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

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 (
23 Any,
24 Union,
25 Type,
26)
28from .doImport import doImportType, doImport
31def get_full_type_name(cls: Any) -> str:
32 """Return full type name of the supplied entity.
34 Parameters
35 ----------
36 cls : `type` or `object`
37 Entity from which to obtain the full name. Can be an instance
38 or a `type`.
40 Returns
41 -------
42 name : `str`
43 Full name of type.
45 Notes
46 -----
47 Builtins are returned without the ``builtins`` specifier included. This
48 allows `str` to be returned as "str" rather than "builtins.str". Any
49 parts of the path that start with a leading underscore are removed
50 on the assumption that they are an implementation detail and the
51 entity will be hoisted into the parent namespace.
52 """
53 # If we have a module that needs to be converted directly
54 # to a name.
55 if isinstance(cls, types.ModuleType):
56 return cls.__name__
57 # If we have an instance we need to convert to a type
58 if not hasattr(cls, "__qualname__"):
59 cls = type(cls)
60 if hasattr(builtins, cls.__qualname__):
61 # Special case builtins such as str and dict
62 return cls.__qualname__
64 real_name = cls.__module__ + "." + cls.__qualname__
66 # Remove components with leading underscores
67 cleaned_name = ".".join(c for c in real_name.split(".") if not c.startswith("_"))
69 # Consistency check
70 if real_name != cleaned_name:
71 try:
72 test = doImport(cleaned_name)
73 except Exception:
74 # Could not import anything so return the real name
75 return real_name
77 # The thing we imported should match the class we started with
78 # despite the clean up. If it does not we return the real name
79 if test is not cls:
80 return real_name
82 return cleaned_name
85def get_class_of(typeOrName: Union[Type, str]) -> Type:
86 """Given the type name or a type, return the python type.
88 If a type name is given, an attempt will be made to import the type.
90 Parameters
91 ----------
92 typeOrName : `str` or Python class
93 A string describing the Python class to load or a Python type.
95 Returns
96 -------
97 type_ : `type`
98 Directly returns the Python type if a type was provided, else
99 tries to import the given string and returns the resulting type.
101 Notes
102 -----
103 This is a thin wrapper around `~lsst.utils.doImport`.
105 Raises
106 ------
107 TypeError
108 Raised if a module is imported rather than a type.
109 """
110 if isinstance(typeOrName, str):
111 cls = doImportType(typeOrName)
112 else:
113 cls = typeOrName
114 if isinstance(cls, types.ModuleType):
115 raise TypeError(f"Can not get class of module {get_full_type_name(typeOrName)}")
116 return cls
119def get_instance_of(typeOrName: Union[Type, str], *args: Any, **kwargs: Any) -> Any:
120 """Given the type name or a type, instantiate an object of that type.
122 If a type name is given, an attempt will be made to import the type.
124 Parameters
125 ----------
126 typeOrName : `str` or Python class
127 A string describing the Python class to load or a Python type.
128 args : `tuple`
129 Positional arguments to use pass to the object constructor.
130 **kwargs
131 Keyword arguments to pass to object constructor.
133 Returns
134 -------
135 instance : `object`
136 Instance of the requested type, instantiated with the provided
137 parameters.
139 Raises
140 ------
141 TypeError
142 Raised if a module is imported rather than a type.
143 """
144 cls = get_class_of(typeOrName)
145 return cls(*args, **kwargs)
148def get_caller_name(stacklevel: int = 2) -> str:
149 """Get the name of the caller method.
151 Any item that cannot be determined (or is not relevant, e.g. a free
152 function has no class) is silently omitted, along with an
153 associated separator.
155 Parameters
156 ----------
157 stacklevel : `int`
158 How many levels of stack to skip while getting caller name;
159 1 means "who calls me", 2 means "who calls my caller", etc.
161 Returns
162 -------
163 name : `str`
164 Name of the caller as a string in the form ``module.class.method``.
165 An empty string is returned if ``stacklevel`` exceeds the stack height.
167 Notes
168 -----
169 Adapted from http://stackoverflow.com/a/9812105
170 by adding support to get the class from ``parentframe.f_locals['cls']``
171 """
172 stack = inspect.stack()
173 start = 0 + stacklevel
174 if len(stack) < start + 1:
175 return ''
176 parentframe = stack[start][0]
178 name = []
179 module = inspect.getmodule(parentframe)
180 if module:
181 name.append(module.__name__)
182 # add class name, if any
183 if 'self' in parentframe.f_locals:
184 name.append(type(parentframe.f_locals['self']).__name__)
185 elif 'cls' in parentframe.f_locals:
186 name.append(parentframe.f_locals['cls'].__name__)
187 codename = parentframe.f_code.co_name
188 if codename != '<module>': # top level usually
189 name.append(codename) # function or a method
190 return ".".join(name)