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