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 "safeMakeDir",
31 "Singleton",
32 "stripIfNotNone",
33 "transactional",
34)
36import errno
37import os
38import builtins
39import functools
40from typing import (
41 Any,
42 Callable,
43 Dict,
44 Iterable,
45 Iterator,
46 Mapping,
47 Optional,
48 Type,
49 Union,
50)
52from lsst.utils import doImport
55def safeMakeDir(directory: str) -> None:
56 """Make a directory in a manner avoiding race conditions"""
57 if directory != "" and not os.path.exists(directory):
58 try:
59 os.makedirs(directory)
60 except OSError as e:
61 # Don't fail if directory exists due to race
62 if e.errno != errno.EEXIST:
63 raise e
66def iterable(a: Any) -> Iterable[Any]:
67 """Make input iterable.
69 There are three cases, when the input is:
71 - iterable, but not a `str` or Mapping -> iterate over elements
72 (e.g. ``[i for i in a]``)
73 - a `str` -> return single element iterable (e.g. ``[a]``)
74 - a Mapping -> return single element iterable
75 - not iterable -> return single elment iterable (e.g. ``[a]``).
77 Parameters
78 ----------
79 a : iterable or `str` or not iterable
80 Argument to be converted to an iterable.
82 Returns
83 -------
84 i : `generator`
85 Iterable version of the input value.
86 """
87 if isinstance(a, str):
88 yield a
89 return
90 if isinstance(a, Mapping):
91 yield a
92 return
93 try:
94 yield from a
95 except Exception:
96 yield a
99def allSlots(self: Any) -> Iterator[str]:
100 """
101 Return combined ``__slots__`` for all classes in objects mro.
103 Parameters
104 ----------
105 self : `object`
106 Instance to be inspected.
108 Returns
109 -------
110 slots : `itertools.chain`
111 All the slots as an iterable.
112 """
113 from itertools import chain
114 return chain.from_iterable(getattr(cls, "__slots__", []) for cls in self.__class__.__mro__)
117def getFullTypeName(cls: Any) -> str:
118 """Return full type name of the supplied entity.
120 Parameters
121 ----------
122 cls : `type` or `object`
123 Entity from which to obtain the full name. Can be an instance
124 or a `type`.
126 Returns
127 -------
128 name : `str`
129 Full name of type.
131 Notes
132 -----
133 Builtins are returned without the ``builtins`` specifier included. This
134 allows `str` to be returned as "str" rather than "builtins.str". Any
135 parts of the path that start with a leading underscore are removed
136 on the assumption that they are an implementation detail and the
137 entity will be hoisted into the parent namespace.
138 """
139 # If we have an instance we need to convert to a type
140 if not hasattr(cls, "__qualname__"): 140 ↛ 141line 140 didn't jump to line 141, because the condition on line 140 was never true
141 cls = type(cls)
142 if hasattr(builtins, cls.__qualname__):
143 # Special case builtins such as str and dict
144 return cls.__qualname__
146 real_name = cls.__module__ + "." + cls.__qualname__
148 # Remove components with leading underscores
149 cleaned_name = ".".join(c for c in real_name.split(".") if not c.startswith("_"))
151 # Consistency check
152 if real_name != cleaned_name: 152 ↛ 153line 152 didn't jump to line 153, because the condition on line 152 was never true
153 try:
154 test = doImport(cleaned_name)
155 except Exception:
156 # Could not import anything so return the real name
157 return real_name
159 # The thing we imported should match the class we started with
160 # despite the clean up. If it does not we return the real name
161 if test is not cls:
162 return real_name
164 return cleaned_name
167def getClassOf(typeOrName: Union[Type, str]) -> Type:
168 """Given the type name or a type, return the python type.
170 If a type name is given, an attempt will be made to import the type.
172 Parameters
173 ----------
174 typeOrName : `str` or Python class
175 A string describing the Python class to load or a Python type.
177 Returns
178 -------
179 type_ : `type`
180 Directly returns the Python type if a type was provided, else
181 tries to import the given string and returns the resulting type.
183 Notes
184 -----
185 This is a thin wrapper around `~lsst.utils.doImport`.
186 """
187 if isinstance(typeOrName, str):
188 cls = doImport(typeOrName)
189 else:
190 cls = typeOrName
191 return cls
194def getInstanceOf(typeOrName: Union[Type, str], *args: Any, **kwargs: Any) -> Any:
195 """Given the type name or a type, instantiate an object of that type.
197 If a type name is given, an attempt will be made to import the type.
199 Parameters
200 ----------
201 typeOrName : `str` or Python class
202 A string describing the Python class to load or a Python type.
203 args : `tuple`
204 Positional arguments to use pass to the object constructor.
205 kwargs : `dict`
206 Keyword arguments to pass to object constructor.
208 Returns
209 -------
210 instance : `object`
211 Instance of the requested type, instantiated with the provided
212 parameters.
213 """
214 cls = getClassOf(typeOrName)
215 return cls(*args, **kwargs)
218class Singleton(type):
219 """Metaclass to convert a class to a Singleton.
221 If this metaclass is used the constructor for the singleton class must
222 take no arguments. This is because a singleton class will only accept
223 the arguments the first time an instance is instantiated.
224 Therefore since you do not know if the constructor has been called yet it
225 is safer to always call it with no arguments and then call a method to
226 adjust state of the singleton.
227 """
229 _instances: Dict[Type, Any] = {}
231 # Signature is intentionally not substitutable for type.__call__ (no *args,
232 # **kwargs) to require classes that use this metaclass to have no
233 # constructor arguments.
234 def __call__(cls) -> Any: # type: ignore
235 if cls not in cls._instances:
236 cls._instances[cls] = super(Singleton, cls).__call__()
237 return cls._instances[cls]
240def transactional(func: Callable) -> Callable:
241 """Decorator that wraps a method and makes it transactional.
243 This depends on the class also defining a `transaction` method
244 that takes no arguments and acts as a context manager.
245 """
246 @functools.wraps(func)
247 def inner(self: Any, *args: Any, **kwargs: Any) -> Any:
248 with self.transaction():
249 return func(self, *args, **kwargs)
250 return inner
253def stripIfNotNone(s: Optional[str]) -> Optional[str]:
254 """Strip leading and trailing whitespace if the given object is not None.
256 Parameters
257 ----------
258 s : `str`, optional
259 Input string.
261 Returns
262 -------
263 r : `str` or `None`
264 A string with leading and trailing whitespace stripped if `s` is not
265 `None`, or `None` if `s` is `None`.
266 """
267 if s is not None:
268 s = s.strip()
269 return s
272def immutable(cls: Type) -> Type:
273 """A class decorator that simulates a simple form of immutability for
274 the decorated class.
276 A class decorated as `immutable` may only set each of its attributes once
277 (by convention, in ``__new__``); any attempts to set an already-set
278 attribute will raise `AttributeError`.
280 Because this behavior interferes with the default implementation for
281 the ``pickle`` and ``copy`` modules, `immutable` provides implementations
282 of ``__getstate__`` and ``__setstate__`` that override this behavior.
283 Immutable classes can them implement pickle/copy via ``__getnewargs__``
284 only (other approaches such as ``__reduce__`` and ``__deepcopy__`` may
285 also be used).
286 """
287 def __setattr__(self: Any, name: str, value: Any) -> None: # noqa: N807
288 if hasattr(self, name):
289 raise AttributeError(f"{cls.__name__} instances are immutable.")
290 object.__setattr__(self, name, value)
291 # mypy says the variable here has signature (str, Any) i.e. no "self";
292 # I think it's just confused by descriptor stuff.
293 cls.__setattr__ = __setattr__ # type: ignore
295 def __getstate__(self: Any) -> dict: # noqa: N807
296 # Disable default state-setting when unpickled.
297 return {}
298 cls.__getstate__ = __getstate__
300 def __setstate__(self: Any, state: Any) -> None: # noqa: N807
301 # Disable default state-setting when copied.
302 # Sadly what works for pickle doesn't work for copy.
303 assert not state
304 cls.__setstate__ = __setstate__
305 return cls