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

57 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-30 11:34 +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 yield None 

115 

116 

117class TransactionProtocol(Protocol): 

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

119 

120 @contextlib.contextmanager 

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

122 ... 

123 

124 

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

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

127 

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

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

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

131 

132 Parameters 

133 ---------- 

134 default_base : `str`, optional 

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

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

137 

138 Returns 

139 ------- 

140 dir : `str` 

141 Name of the new temporary directory. 

142 """ 

143 base = default_base 

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

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

146 base = os.environ[envvar] 

147 break 

148 return tempfile.mkdtemp(dir=base) 

149 

150 

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

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

153 unable to. 

154 

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

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

157 

158 Parameters 

159 ---------- 

160 root : `str`, optional 

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

162 """ 

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

164 shutil.rmtree(root, ignore_errors=True) 

165 

166 

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

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

169 access files in it. Alters the directory permissions by adding the 

170 owner-write and owner-traverse permission bits if they aren't already set 

171 

172 Parameters 

173 ---------- 

174 directory_path : `str` or `bytes` 

175 Path to the directory that will be made writeable 

176 """ 

177 current_mode = os.stat(directory_path).st_mode 

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

179 if current_mode != desired_mode: 

180 os.chmod(directory_path, desired_mode)