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

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
40import re
41from typing import (
42 Any,
43 Callable,
44 Dict,
45 Iterable,
46 Iterator,
47 List,
48 Mapping,
49 Optional,
50 Type,
51 TypeVar,
52 Union,
53)
55from lsst.utils import doImport
58def safeMakeDir(directory: str) -> None:
59 """Make a directory in a manner avoiding race conditions"""
60 if directory != "" and not os.path.exists(directory):
61 try:
62 os.makedirs(directory)
63 except OSError as e:
64 # Don't fail if directory exists due to race
65 if e.errno != errno.EEXIST:
66 raise e
69def iterable(a: Any) -> Iterable[Any]:
70 """Make input iterable.
72 There are three cases, when the input is:
74 - iterable, but not a `str` or Mapping -> iterate over elements
75 (e.g. ``[i for i in a]``)
76 - a `str` -> return single element iterable (e.g. ``[a]``)
77 - a Mapping -> return single element iterable
78 - not iterable -> return single elment iterable (e.g. ``[a]``).
80 Parameters
81 ----------
82 a : iterable or `str` or not iterable
83 Argument to be converted to an iterable.
85 Returns
86 -------
87 i : `generator`
88 Iterable version of the input value.
89 """
90 if isinstance(a, str):
91 yield a
92 return
93 if isinstance(a, Mapping):
94 yield a
95 return
96 try:
97 yield from a
98 except Exception:
99 yield a
102def allSlots(self: Any) -> Iterator[str]:
103 """
104 Return combined ``__slots__`` for all classes in objects mro.
106 Parameters
107 ----------
108 self : `object`
109 Instance to be inspected.
111 Returns
112 -------
113 slots : `itertools.chain`
114 All the slots as an iterable.
115 """
116 from itertools import chain
117 return chain.from_iterable(getattr(cls, "__slots__", []) for cls in self.__class__.__mro__)
120def getFullTypeName(cls: Any) -> str:
121 """Return full type name of the supplied entity.
123 Parameters
124 ----------
125 cls : `type` or `object`
126 Entity from which to obtain the full name. Can be an instance
127 or a `type`.
129 Returns
130 -------
131 name : `str`
132 Full name of type.
134 Notes
135 -----
136 Builtins are returned without the ``builtins`` specifier included. This
137 allows `str` to be returned as "str" rather than "builtins.str". Any
138 parts of the path that start with a leading underscore are removed
139 on the assumption that they are an implementation detail and the
140 entity will be hoisted into the parent namespace.
141 """
142 # If we have an instance we need to convert to a type
143 if not hasattr(cls, "__qualname__"): 143 ↛ 144line 143 didn't jump to line 144, because the condition on line 143 was never true
144 cls = type(cls)
145 if hasattr(builtins, cls.__qualname__):
146 # Special case builtins such as str and dict
147 return cls.__qualname__
149 real_name = cls.__module__ + "." + cls.__qualname__
151 # Remove components with leading underscores
152 cleaned_name = ".".join(c for c in real_name.split(".") if not c.startswith("_"))
154 # Consistency check
155 if real_name != cleaned_name: 155 ↛ 156line 155 didn't jump to line 156, because the condition on line 155 was never true
156 try:
157 test = doImport(cleaned_name)
158 except Exception:
159 # Could not import anything so return the real name
160 return real_name
162 # The thing we imported should match the class we started with
163 # despite the clean up. If it does not we return the real name
164 if test is not cls:
165 return real_name
167 return cleaned_name
170def getClassOf(typeOrName: Union[Type, str]) -> Type:
171 """Given the type name or a type, return the python type.
173 If a type name is given, an attempt will be made to import the type.
175 Parameters
176 ----------
177 typeOrName : `str` or Python class
178 A string describing the Python class to load or a Python type.
180 Returns
181 -------
182 type_ : `type`
183 Directly returns the Python type if a type was provided, else
184 tries to import the given string and returns the resulting type.
186 Notes
187 -----
188 This is a thin wrapper around `~lsst.utils.doImport`.
189 """
190 if isinstance(typeOrName, str):
191 cls = doImport(typeOrName)
192 else:
193 cls = typeOrName
194 return cls
197def getInstanceOf(typeOrName: Union[Type, str], *args: Any, **kwargs: Any) -> Any:
198 """Given the type name or a type, instantiate an object of that type.
200 If a type name is given, an attempt will be made to import the type.
202 Parameters
203 ----------
204 typeOrName : `str` or Python class
205 A string describing the Python class to load or a Python type.
206 args : `tuple`
207 Positional arguments to use pass to the object constructor.
208 kwargs : `dict`
209 Keyword arguments to pass to object constructor.
211 Returns
212 -------
213 instance : `object`
214 Instance of the requested type, instantiated with the provided
215 parameters.
216 """
217 cls = getClassOf(typeOrName)
218 return cls(*args, **kwargs)
221class Singleton(type):
222 """Metaclass to convert a class to a Singleton.
224 If this metaclass is used the constructor for the singleton class must
225 take no arguments. This is because a singleton class will only accept
226 the arguments the first time an instance is instantiated.
227 Therefore since you do not know if the constructor has been called yet it
228 is safer to always call it with no arguments and then call a method to
229 adjust state of the singleton.
230 """
232 _instances: Dict[Type, Any] = {}
234 # Signature is intentionally not substitutable for type.__call__ (no *args,
235 # **kwargs) to require classes that use this metaclass to have no
236 # constructor arguments.
237 def __call__(cls) -> Any: # type: ignore
238 if cls not in cls._instances:
239 cls._instances[cls] = super(Singleton, cls).__call__()
240 return cls._instances[cls]
243F = TypeVar("F", bound=Callable)
246def transactional(func: F) -> F:
247 """Decorator that wraps a method and makes it transactional.
249 This depends on the class also defining a `transaction` method
250 that takes no arguments and acts as a context manager.
251 """
252 @functools.wraps(func)
253 def inner(self: Any, *args: Any, **kwargs: Any) -> Any:
254 with self.transaction():
255 return func(self, *args, **kwargs)
256 return inner # type: ignore
259def stripIfNotNone(s: Optional[str]) -> Optional[str]:
260 """Strip leading and trailing whitespace if the given object is not None.
262 Parameters
263 ----------
264 s : `str`, optional
265 Input string.
267 Returns
268 -------
269 r : `str` or `None`
270 A string with leading and trailing whitespace stripped if `s` is not
271 `None`, or `None` if `s` is `None`.
272 """
273 if s is not None:
274 s = s.strip()
275 return s
278def immutable(cls: Type) -> Type:
279 """A class decorator that simulates a simple form of immutability for
280 the decorated class.
282 A class decorated as `immutable` may only set each of its attributes once
283 (by convention, in ``__new__``); any attempts to set an already-set
284 attribute will raise `AttributeError`.
286 Because this behavior interferes with the default implementation for
287 the ``pickle`` and ``copy`` modules, `immutable` provides implementations
288 of ``__getstate__`` and ``__setstate__`` that override this behavior.
289 Immutable classes can them implement pickle/copy via ``__getnewargs__``
290 only (other approaches such as ``__reduce__`` and ``__deepcopy__`` may
291 also be used).
292 """
293 def __setattr__(self: Any, name: str, value: Any) -> None: # noqa: N807
294 if hasattr(self, name):
295 raise AttributeError(f"{cls.__name__} instances are immutable.")
296 object.__setattr__(self, name, value)
297 # mypy says the variable here has signature (str, Any) i.e. no "self";
298 # I think it's just confused by descriptor stuff.
299 cls.__setattr__ = __setattr__ # type: ignore
301 def __getstate__(self: Any) -> dict: # noqa: N807
302 # Disable default state-setting when unpickled.
303 return {}
304 cls.__getstate__ = __getstate__
306 def __setstate__(self: Any, state: Any) -> None: # noqa: N807
307 # Disable default state-setting when copied.
308 # Sadly what works for pickle doesn't work for copy.
309 assert not state
310 cls.__setstate__ = __setstate__
311 return cls
314def findFileResources(values: Iterable[str], regex: Optional[str] = None) -> List[str]:
315 """Get the files from a list of values. If a value is a file it is added to
316 the list of returned files. If a value is a directory, all the files in
317 the directory (recursively) that match the regex will be returned.
319 Parameters
320 ----------
321 values : iterable [`str`]
322 The files to return and directories in which to look for files to
323 return.
324 regex : `str`
325 The regex to use when searching for files within directories. Optional,
326 by default returns all the found files.
328 Returns
329 -------
330 resources: `list` [`str`]
331 The passed-in files and files found in passed-in directories.
332 """
333 fileRegex = None if regex is None else re.compile(regex)
334 resources = []
336 # Find all the files of interest
337 for location in values:
338 if os.path.isdir(location):
339 for root, dirs, files in os.walk(location):
340 for name in files:
341 path = os.path.join(root, name)
342 if os.path.isfile(path) and (fileRegex is None or fileRegex.search(name)):
343 resources.append(path)
344 else:
345 resources.append(location)
346 return resources