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
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 03:04 +0000
1"""
2Automatically deleted temporary files and directories.
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
13from pycommons.io.path import Path, delete_path, directory_path
16class TempPath(Path):
17 """A path to a temp file or directory for use in a `with` statement."""
19 #: is the directory or file open?
20 __is_open: bool
22 def __new__(cls, value: str): # noqa
23 """
24 Construct the temporary path.
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
32 def __enter__(self) -> "TempPath":
33 """
34 Nothing, just exists for `with`.
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
50 def __exit__(self, exception_type, _, __) -> bool:
51 """
52 Delete the temporary directory and everything in it.
54 :param exception_type: ignored
55 :returns: `True` to suppress an exception, `False` to rethrow it
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
82def temp_dir(directory: str | None = None) -> TempPath:
83 """
84 Create the temporary directory.
86 :param directory: an optional root directory
87 :raises TypeError: if `directory` is not `None` but also no `str`
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)))
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.
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`
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
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'
135 >>> try:
136 ... temp_file("")
137 ... except ValueError as ve:
138 ... print(ve)
139 Path must not be empty.
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
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
153 >>> try:
154 ... temp_file(None, "")
155 ... except ValueError as ve:
156 ... print(ve)
157 Stripped prefix cannot be empty if specified.
159 >>> try:
160 ... temp_file(None, None, "")
161 ... except ValueError as ve:
162 ... print(ve)
163 Stripped suffix cannot be empty if specified.
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.
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.
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.
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.
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.
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
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
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
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
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
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
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.")
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.")
298 if directory is not None:
299 base_dir = directory_path(directory)
300 base_dir.enforce_dir()
301 else:
302 base_dir = None
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)