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

57 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-19 11:17 +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]: 

139 ... 

140 

141 

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

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

144 

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

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

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

148 

149 Parameters 

150 ---------- 

151 default_base : `str`, optional 

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

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

154 

155 Returns 

156 ------- 

157 dir : `str` 

158 Name of the new temporary directory. 

159 """ 

160 base = default_base 

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

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

163 base = os.environ[envvar] 

164 break 

165 return tempfile.mkdtemp(dir=base) 

166 

167 

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

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

170 unable to. 

171 

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

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

174 

175 Parameters 

176 ---------- 

177 root : `str`, optional 

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

179 """ 

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

181 shutil.rmtree(root, ignore_errors=True) 

182 

183 

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

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

186 access files in it. 

187 

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

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

190 

191 Parameters 

192 ---------- 

193 directory_path : `str` or `bytes` 

194 Path to the directory that will be made writeable. 

195 """ 

196 current_mode = os.stat(directory_path).st_mode 

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

198 if current_mode != desired_mode: 

199 os.chmod(directory_path, desired_mode)