Coverage for pycommons / io / temp.py: 100%

44 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-11 03:04 +0000

1""" 

2Automatically deleted temporary files and directories. 

3 

4This module provides two classes, :func:`temp_dir` for temporary directories 

5and :func:`temp_file` for temporary files. Both of them implement the 

6:class:`typing.ContextManager` protocol and will be deleted when going out 

7of scope. 

8""" 

9from os import close as osclose 

10from tempfile import mkdtemp, mkstemp 

11from typing import Final 

12 

13from pycommons.io.path import Path, delete_path, directory_path 

14 

15 

16class TempPath(Path): 

17 """A path to a temp file or directory for use in a `with` statement.""" 

18 

19 #: is the directory or file open? 

20 __is_open: bool 

21 

22 def __new__(cls, value: str): # noqa 

23 """ 

24 Construct the temporary path. 

25 

26 :param value: the string value of the path 

27 """ 

28 ret = super().__new__(cls, value) 

29 ret.__is_open = True # noqa:SLF001 

30 return ret 

31 

32 def __enter__(self) -> "TempPath": 

33 """ 

34 Nothing, just exists for `with`. 

35 

36 >>> te = temp_dir() 

37 >>> with te: 

38 ... pass 

39 >>> try: 

40 ... with te: # does not work, already closed 

41 ... pass 

42 ... except ValueError as ve: 

43 ... print(str(ve)[:14]) 

44 Temporary path 

45 """ 

46 if not self.__is_open: 

47 raise ValueError(f"Temporary path {self!r} already closed.") 

48 return self 

49 

50 def __exit__(self, exception_type, _, __) -> bool: 

51 """ 

52 Delete the temporary directory and everything in it. 

53 

54 :param exception_type: ignored 

55 :returns: `True` to suppress an exception, `False` to rethrow it 

56 

57 >>> with temp_dir() as td: 

58 ... f = td.resolve_inside("a") 

59 ... f.ensure_file_exists() # False, file did not yet exist 

60 ... f.enforce_file() 

61 ... f.is_file() # True, because it does now 

62 ... d = td.resolve_inside("b") 

63 ... d.is_dir() # False, does not exist 

64 ... d.ensure_dir_exists() 

65 ... d.is_dir() # True, now it does 

66 False 

67 True 

68 False 

69 True 

70 >>> f.is_file() # False, because it no longer does 

71 False 

72 >>> d.is_dir() # False, because it no longer exists 

73 False 

74 """ 

75 opn: Final[bool] = self.__is_open 

76 self.__is_open = False 

77 if opn: 

78 delete_path(self) 

79 return exception_type is None 

80 

81 

82def temp_dir(directory: str | None = None) -> TempPath: 

83 """ 

84 Create the temporary directory. 

85 

86 :param directory: an optional root directory 

87 :raises TypeError: if `directory` is not `None` but also no `str` 

88 

89 >>> with temp_dir() as td: 

90 ... pass 

91 >>> try: 

92 ... with temp_dir(1): 

93 ... pass 

94 ... except TypeError as te: 

95 ... print(te) 

96 descriptor '__len__' requires a 'str' object but received a 'int' 

97 >>> from os.path import dirname 

98 >>> with temp_dir(dirname(__file__)) as td: 

99 ... pass 

100 """ 

101 return TempPath(mkdtemp( 

102 dir=None if directory is None else directory_path(directory))) 

103 

104 

105def temp_file(directory: str | None = None, 

106 prefix: str | None = None, 

107 suffix: str | None = None) -> TempPath: 

108 r""" 

109 Create a temporary file that will be deleted when going out of scope. 

110 

111 :param directory: a root directory or `TempDir` instance 

112 :param prefix: an optional prefix 

113 :param suffix: an optional suffix, e.g., `.txt` 

114 :raises TypeError: if any of the parameters does not fulfill the type 

115 contract 

116 :raises ValueError: if the `prefix` or `suffix` are specified, but are 

117 empty strings, or if `directory` does not identify an existing 

118 directory although not being `None` 

119 

120 >>> with temp_file() as tf: 

121 ... tf.is_file() 

122 ... p = Path(tf) 

123 ... p.is_file() 

124 True 

125 True 

126 >>> p.is_file() 

127 False 

128 

129 >>> try: 

130 ... temp_file(1) 

131 ... except TypeError as te: 

132 ... print(te) 

133 descriptor '__len__' requires a 'str' object but received a 'int' 

134 

135 >>> try: 

136 ... temp_file("") 

137 ... except ValueError as ve: 

138 ... print(ve) 

139 Path must not be empty. 

140 

141 >>> try: 

142 ... temp_file(None, 1) 

143 ... except TypeError as te: 

144 ... print(te) 

145 descriptor 'strip' for 'str' objects doesn't apply to a 'int' object 

146 

147 >>> try: 

148 ... temp_file(None, None, 1) 

149 ... except TypeError as te: 

150 ... print(te) 

151 descriptor 'strip' for 'str' objects doesn't apply to a 'int' object 

152 

153 >>> try: 

154 ... temp_file(None, "") 

155 ... except ValueError as ve: 

156 ... print(ve) 

157 Stripped prefix cannot be empty if specified. 

158 

159 >>> try: 

160 ... temp_file(None, None, "") 

161 ... except ValueError as ve: 

162 ... print(ve) 

163 Stripped suffix cannot be empty if specified. 

164 

165 >>> try: 

166 ... temp_file(None, None, "bla.") 

167 ... except ValueError as ve: 

168 ... print(ve) 

169 Stripped suffix must not end with '.', but 'bla.' does. 

170 

171 >>> try: 

172 ... temp_file(None, None, "bl/a") 

173 ... except ValueError as ve: 

174 ... print(ve) 

175 Suffix must contain neither '/' nor '\', but 'bl/a' does. 

176 

177 >>> try: 

178 ... temp_file(None, None, "b\\la") 

179 ... except ValueError as ve: 

180 ... print(ve) 

181 Suffix must contain neither '/' nor '\', but 'b\\la' does. 

182 

183 >>> try: 

184 ... temp_file(None, "bl/a", None) 

185 ... except ValueError as ve: 

186 ... print(ve) 

187 Prefix must contain neither '/' nor '\', but 'bl/a' does. 

188 

189 >>> try: 

190 ... temp_file(None, "b\\la", None) 

191 ... except ValueError as ve: 

192 ... print(ve) 

193 Prefix must contain neither '/' nor '\', but 'b\\la' does. 

194 

195 >>> from os.path import dirname 

196 >>> from pycommons.io.path import file_path 

197 >>> bd = directory_path(dirname(__file__)) 

198 >>> with temp_file(bd) as tf: 

199 ... bd.enforce_contains(tf) 

200 ... bd in tf 

201 ... p = file_path(str(f"{tf}")) 

202 True 

203 >>> p.is_file() 

204 False 

205 

206 >>> from os.path import basename 

207 >>> with temp_file(None, "pre") as tf: 

208 ... "pre" in tf 

209 ... bd.contains(tf) 

210 ... basename(tf).startswith("pre") 

211 ... p = file_path(str(f"{tf}")) 

212 True 

213 False 

214 True 

215 >>> p.is_file() 

216 False 

217 

218 >>> with temp_file(bd, "pre") as tf: 

219 ... "pre" in tf 

220 ... bd.contains(tf) 

221 ... basename(tf).startswith("pre") 

222 ... p = file_path(str(f"{tf}")) 

223 True 

224 True 

225 True 

226 >>> p.is_file() 

227 False 

228 

229 >>> with temp_file(bd, None, "suf") as tf: 

230 ... "suf" in tf 

231 ... bd.contains(tf) 

232 ... tf.endswith("suf") 

233 ... p = file_path(str(f"{tf}")) 

234 True 

235 True 

236 True 

237 >>> p.is_file() 

238 False 

239 

240 >>> with temp_file(None, None, "suf") as tf: 

241 ... "suf" in tf 

242 ... tf.endswith("suf") 

243 ... bd.contains(tf) 

244 ... p = file_path(str(f"{tf}")) 

245 True 

246 True 

247 False 

248 >>> p.is_file() 

249 False 

250 

251 >>> with temp_file(None, "pref", "suf") as tf: 

252 ... tf.index("pref") < tf.index("suf") 

253 ... tf.endswith("suf") 

254 ... basename(tf).startswith("pref") 

255 ... bd.contains(tf) 

256 ... p = file_path(str(f"{tf}")) 

257 True 

258 True 

259 True 

260 False 

261 >>> p.is_file() 

262 False 

263 

264 >>> with temp_file(bd, "pref", "suf") as tf: 

265 ... tf.index("pref") < tf.index("suf") 

266 ... tf.endswith("suf") 

267 ... basename(tf).startswith("pref") 

268 ... bd.contains(tf) 

269 ... p = file_path(str(f"{tf}")) 

270 True 

271 True 

272 True 

273 True 

274 >>> p.is_file() 

275 False 

276 """ 

277 if prefix is not None: 

278 prefix = str.strip(prefix) 

279 if str.__len__(prefix) == 0: 

280 raise ValueError( 

281 "Stripped prefix cannot be empty if specified.") 

282 if ("/" in prefix) or ("\\" in prefix): 

283 raise ValueError("Prefix must contain neither '/' nor" 

284 f" '\\', but {prefix!r} does.") 

285 

286 if suffix is not None: 

287 suffix = str.strip(suffix) 

288 if str.__len__(suffix) == 0: 

289 raise ValueError( 

290 "Stripped suffix cannot be empty if specified.") 

291 if suffix.endswith("."): 

292 raise ValueError("Stripped suffix must not end " 

293 f"with '.', but {suffix!r} does.") 

294 if ("/" in suffix) or ("\\" in suffix): 

295 raise ValueError("Suffix must contain neither '/' nor" 

296 f" '\\', but {suffix!r} does.") 

297 

298 if directory is not None: 

299 base_dir = directory_path(directory) 

300 base_dir.enforce_dir() 

301 else: 

302 base_dir = None 

303 

304 (handle, path) = mkstemp( 

305 suffix=suffix, prefix=prefix, 

306 dir=None if base_dir is None else directory_path(base_dir)) 

307 osclose(handle) 

308 return TempPath(path)