Coverage for python/lsst/daf/butler/core/utils.py : 39%

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 daf_butler.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21from __future__ import annotations
23__all__ = (
24 "allSlots",
25 "getClassOf",
26 "getFullTypeName",
27 "getInstanceOf",
28 "immutable",
29 "iterable",
30 "Singleton",
31 "stripIfNotNone",
32 "transactional",
33)
35import builtins
36import functools
37from typing import (
38 Any,
39 Callable,
40 Dict,
41 Iterable,
42 Iterator,
43 Mapping,
44 Optional,
45 Type,
46 Union,
47)
49from lsst.utils import doImport
52def iterable(a: Any) -> Iterable[Any]:
53 """Make input iterable.
55 There are three cases, when the input is:
57 - iterable, but not a `str` or Mapping -> iterate over elements
58 (e.g. ``[i for i in a]``)
59 - a `str` -> return single element iterable (e.g. ``[a]``)
60 - a Mapping -> return single element iterable
61 - not iterable -> return single elment iterable (e.g. ``[a]``).
63 Parameters
64 ----------
65 a : iterable or `str` or not iterable
66 Argument to be converted to an iterable.
68 Returns
69 -------
70 i : `generator`
71 Iterable version of the input value.
72 """
73 if isinstance(a, str):
74 yield a
75 return
76 if isinstance(a, Mapping):
77 yield a
78 return
79 try:
80 yield from a
81 except Exception:
82 yield a
85def allSlots(self: Any) -> Iterator[str]:
86 """
87 Return combined ``__slots__`` for all classes in objects mro.
89 Parameters
90 ----------
91 self : `object`
92 Instance to be inspected.
94 Returns
95 -------
96 slots : `itertools.chain`
97 All the slots as an iterable.
98 """
99 from itertools import chain
100 return chain.from_iterable(getattr(cls, "__slots__", []) for cls in self.__class__.__mro__)
103def getFullTypeName(cls: Any) -> str:
104 """Return full type name of the supplied entity.
106 Parameters
107 ----------
108 cls : `type` or `object`
109 Entity from which to obtain the full name. Can be an instance
110 or a `type`.
112 Returns
113 -------
114 name : `str`
115 Full name of type.
117 Notes
118 -----
119 Builtins are returned without the ``builtins`` specifier included. This
120 allows `str` to be returned as "str" rather than "builtins.str".
121 """
122 # If we have an instance we need to convert to a type
123 if not hasattr(cls, "__qualname__"): 123 ↛ 124line 123 didn't jump to line 124, because the condition on line 123 was never true
124 cls = type(cls)
125 if hasattr(builtins, cls.__qualname__): 125 ↛ 127line 125 didn't jump to line 127, because the condition on line 125 was never true
126 # Special case builtins such as str and dict
127 return cls.__qualname__
128 return cls.__module__ + "." + cls.__qualname__
131def getClassOf(typeOrName: Union[Type, str]) -> Type:
132 """Given the type name or a type, return the python type.
134 If a type name is given, an attempt will be made to import the type.
136 Parameters
137 ----------
138 typeOrName : `str` or Python class
139 A string describing the Python class to load or a Python type.
141 Returns
142 -------
143 type_ : `type`
144 Directly returns the Python type if a type was provided, else
145 tries to import the given string and returns the resulting type.
147 Notes
148 -----
149 This is a thin wrapper around `~lsst.utils.doImport`.
150 """
151 if isinstance(typeOrName, str):
152 cls = doImport(typeOrName)
153 else:
154 cls = typeOrName
155 return cls
158def getInstanceOf(typeOrName: Union[Type, str], *args: Any, **kwargs: Any) -> Any:
159 """Given the type name or a type, instantiate an object of that type.
161 If a type name is given, an attempt will be made to import the type.
163 Parameters
164 ----------
165 typeOrName : `str` or Python class
166 A string describing the Python class to load or a Python type.
167 args : `tuple`
168 Positional arguments to use pass to the object constructor.
169 kwargs : `dict`
170 Keyword arguments to pass to object constructor.
172 Returns
173 -------
174 instance : `object`
175 Instance of the requested type, instantiated with the provided
176 parameters.
177 """
178 cls = getClassOf(typeOrName)
179 return cls(*args, **kwargs)
182class Singleton(type):
183 """Metaclass to convert a class to a Singleton.
185 If this metaclass is used the constructor for the singleton class must
186 take no arguments. This is because a singleton class will only accept
187 the arguments the first time an instance is instantiated.
188 Therefore since you do not know if the constructor has been called yet it
189 is safer to always call it with no arguments and then call a method to
190 adjust state of the singleton.
191 """
193 _instances: Dict[Type, Any] = {}
195 # Signature is intentionally not substitutable for type.__call__ (no *args,
196 # **kwargs) to require classes that use this metaclass to have no
197 # constructor arguments.
198 def __call__(cls) -> Any: # type: ignore
199 if cls not in cls._instances:
200 cls._instances[cls] = super(Singleton, cls).__call__()
201 return cls._instances[cls]
204def transactional(func: Callable) -> Callable:
205 """Decorator that wraps a method and makes it transactional.
207 This depends on the class also defining a `transaction` method
208 that takes no arguments and acts as a context manager.
209 """
210 @functools.wraps(func)
211 def inner(self: Any, *args: Any, **kwargs: Any) -> Any:
212 with self.transaction():
213 return func(self, *args, **kwargs)
214 return inner
217def stripIfNotNone(s: Optional[str]) -> Optional[str]:
218 """Strip leading and trailing whitespace if the given object is not None.
220 Parameters
221 ----------
222 s : `str`, optional
223 Input string.
225 Returns
226 -------
227 r : `str` or `None`
228 A string with leading and trailing whitespace stripped if `s` is not
229 `None`, or `None` if `s` is `None`.
230 """
231 if s is not None:
232 s = s.strip()
233 return s
236def immutable(cls: Type) -> Type:
237 """A class decorator that simulates a simple form of immutability for
238 the decorated class.
240 A class decorated as `immutable` may only set each of its attributes once
241 (by convention, in ``__new__``); any attempts to set an already-set
242 attribute will raise `AttributeError`.
244 Because this behavior interferes with the default implementation for
245 the ``pickle`` and ``copy`` modules, `immutable` provides implementations
246 of ``__getstate__`` and ``__setstate__`` that override this behavior.
247 Immutable classes can them implement pickle/copy via ``__getnewargs__``
248 only (other approaches such as ``__reduce__`` and ``__deepcopy__`` may
249 also be used).
250 """
251 def __setattr__(self: Any, name: str, value: Any) -> None: # noqa: N807
252 if hasattr(self, name):
253 raise AttributeError(f"{cls.__name__} instances are immutable.")
254 object.__setattr__(self, name, value)
255 # mypy says the variable here has signature (str, Any) i.e. no "self";
256 # I think it's just confused by descriptor stuff.
257 cls.__setattr__ = __setattr__ # type: ignore
259 def __getstate__(self: Any) -> dict: # noqa: N807
260 # Disable default state-setting when unpickled.
261 return {}
262 cls.__getstate__ = __getstate__
264 def __setstate__(self: Any, state: Any) -> None: # noqa: N807
265 # Disable default state-setting when copied.
266 # Sadly what works for pickle doesn't work for copy.
267 assert not state
268 cls.__setstate__ = __setstate__
269 return cls