Coverage for python/lsst/resources/utils.py: 39%

56 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-03-13 09:59 +0000

1# This file is part of lsst-resources. 

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 

12from __future__ import annotations 

13 

14__all__ = ("os2posix", "posix2os", "NoTransaction", "TransactionProtocol") 

15 

16import contextlib 

17import logging 

18import os 

19import posixpath 

20import shutil 

21import stat 

22import tempfile 

23from collections.abc import Callable, Iterator 

24from pathlib import Path, PurePath, PurePosixPath 

25from typing import Any, Protocol 

26 

27# Determine if the path separator for the OS looks like POSIX 

28IS_POSIX = os.sep == posixpath.sep 

29 

30# Root path for this operating system. This can use getcwd which 

31# can fail in some situations so in the default case assume that 

32# posix means posix and only determine explicitly in the non-posix case. 

33OS_ROOT_PATH = posixpath.sep if IS_POSIX else Path().resolve().root 

34 

35log = logging.getLogger(__name__) 

36 

37 

38def os2posix(ospath: str) -> str: 

39 """Convert a local path description to a POSIX path description. 

40 

41 Parameters 

42 ---------- 

43 ospath : `str` 

44 Path using the local path separator. 

45 

46 Returns 

47 ------- 

48 posix : `str` 

49 Path using POSIX path separator. 

50 """ 

51 if IS_POSIX: 

52 return ospath 

53 

54 posix = PurePath(ospath).as_posix() 

55 

56 # PurePath strips trailing "/" from paths such that you can no 

57 # longer tell if a path is meant to be referring to a directory 

58 # Try to fix this. 

59 if ospath.endswith(os.sep) and not posix.endswith(posixpath.sep): 

60 posix += posixpath.sep 

61 

62 return posix 

63 

64 

65def posix2os(posix: PurePath | str) -> str: 

66 """Convert a POSIX path description to a local path description. 

67 

68 Parameters 

69 ---------- 

70 posix : `str`, `~pathlib.PurePath` 

71 Path using the POSIX path separator. 

72 

73 Returns 

74 ------- 

75 ospath : `str` 

76 Path using OS path separator. 

77 """ 

78 if IS_POSIX: 

79 return str(posix) 

80 

81 posixPath = PurePosixPath(posix) 

82 paths = list(posixPath.parts) 

83 

84 # Have to convert the root directory after splitting 

85 if paths[0] == posixPath.root: 

86 paths[0] = OS_ROOT_PATH 

87 

88 # Trailing "/" is stripped so we need to add back an empty path 

89 # for consistency 

90 if str(posix).endswith(posixpath.sep): 

91 paths.append("") 

92 

93 return os.path.join(*paths) 

94 

95 

96class NoTransaction: 

97 """A simple emulation of the 

98 `~lsst.daf.butler.core.datastore.DatastoreTransaction` class. 

99 

100 Notes 

101 ----- 

102 Does nothing. Used as a fallback in the absence of an explicit transaction 

103 class. 

104 """ 

105 

106 def __init__(self) -> None: 

107 return 

108 

109 @contextlib.contextmanager 

110 def undoWith(self, name: str, undoFunc: Callable, *args: Any, **kwargs: Any) -> Iterator[None]: 

111 """No-op context manager to replace 

112 `~lsst.daf.butler.core.datastore.DatastoreTransaction`. 

113 

114 Parameters 

115 ---------- 

116 name : `str` 

117 The name of this undo request. 

118 undoFunc : `~collections.abc.Callable` 

119 Function to call if there is an exception. Not used. 

120 *args : `~typing.Any` 

121 Parameters to pass to ``undoFunc``. 

122 **kwargs : `~typing.Any` 

123 Keyword parameters to pass to ``undoFunc``. 

124 

125 Yields 

126 ------ 

127 `None` 

128 Context manager returns nothing since transactions are disabled 

129 by definition. 

130 """ 

131 yield None 

132 

133 

134class TransactionProtocol(Protocol): 

135 """Protocol for type checking transaction interface.""" 

136 

137 @contextlib.contextmanager 

138 def undoWith(self, name: str, undoFunc: Callable, *args: Any, **kwargs: Any) -> Iterator[None]: ... 138 ↛ exitline 138 didn't return from function 'undoWith'

139 

140 

141def makeTestTempDir(default_base: str | None = None) -> str: 

142 """Create a temporary directory for test usage. 

143 

144 The directory will be created within ``LSST_RESOURCES_TEST_TMP`` if that 

145 environment variable is set, falling back to ``LSST_RESOURCES_TMPDIR`` 

146 amd then ``default_base`` if none are set. 

147 

148 Parameters 

149 ---------- 

150 default_base : `str`, optional 

151 Default parent directory. Will use system default if no environment 

152 variables are set and base is set to `None`. 

153 

154 Returns 

155 ------- 

156 dir : `str` 

157 Name of the new temporary directory. 

158 """ 

159 base = default_base 

160 for envvar in ("LSST_RESOURCES_TEST_TMP", "LSST_RESOURCES_TMPDIR"): 

161 if envvar in os.environ and os.environ[envvar]: 

162 base = os.environ[envvar] 

163 break 

164 return tempfile.mkdtemp(dir=base) 

165 

166 

167def removeTestTempDir(root: str | None) -> None: 

168 """Attempt to remove a temporary test directory, but do not raise if 

169 unable to. 

170 

171 Unlike `tempfile.TemporaryDirectory`, this passes ``ignore_errors=True`` 

172 to ``shutil.rmtree`` at close, making it safe to use on NFS. 

173 

174 Parameters 

175 ---------- 

176 root : `str`, optional 

177 Name of the directory to be removed. If `None`, nothing will be done. 

178 """ 

179 if root is not None and os.path.exists(root): 

180 shutil.rmtree(root, ignore_errors=True) 

181 

182 

183def ensure_directory_is_writeable(directory_path: str | bytes) -> None: 

184 """Given the path to a directory, ensures that we are able to write it and 

185 access files in it. 

186 

187 Alters the directory permissions by adding the owner-write and 

188 owner-traverse permission bits if they aren't already set 

189 

190 Parameters 

191 ---------- 

192 directory_path : `str` or `bytes` 

193 Path to the directory that will be made writeable. 

194 """ 

195 current_mode = os.stat(directory_path).st_mode 

196 desired_mode = current_mode | stat.S_IWUSR | stat.S_IXUSR 

197 if current_mode != desired_mode: 

198 os.chmod(directory_path, desired_mode)