safer 4.12.3__py3-none-any.whl → 5.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
safer/__init__.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
# 🧿 `safer`: A safer writer 🧿
|
|
1
|
+
"""# 🧿 `safer`: A safer writer 🧿
|
|
3
2
|
|
|
4
3
|
Avoid partial writes or corruption!
|
|
5
4
|
|
|
@@ -42,6 +41,10 @@ https://pypi.org/project/atomicwrites/
|
|
|
42
41
|
It also has a useful `dry_run` setting to let you test your code without
|
|
43
42
|
actually overwriting the target file.
|
|
44
43
|
|
|
44
|
+
NOTE: Just like plain old `open`, if a file that is already opened for writing
|
|
45
|
+
is opened again before the first write has completed, the results are
|
|
46
|
+
unpredictable: so don't do it!
|
|
47
|
+
|
|
45
48
|
* `safer.writer()` wraps an existing writer, socket or stream and writes a
|
|
46
49
|
whole response or nothing
|
|
47
50
|
|
|
@@ -67,7 +70,6 @@ writes the data to a temporary file on disk, which is moved over using
|
|
|
67
70
|
does not work on Windows. (In fact, it's unclear if any of this works on
|
|
68
71
|
Windows, but that certainly won't. Windows developer solicted!)
|
|
69
72
|
|
|
70
|
-
|
|
71
73
|
### Example: `safer.writer()`
|
|
72
74
|
|
|
73
75
|
`safer.writer()` wraps an existing stream - a writer, socket, or callback -
|
|
@@ -143,7 +145,10 @@ With `safer`
|
|
|
143
145
|
for item in items:
|
|
144
146
|
print(item)
|
|
145
147
|
# Either the whole file is written, or nothing
|
|
148
|
+
|
|
146
149
|
"""
|
|
150
|
+
from __future__ import annotations
|
|
151
|
+
|
|
147
152
|
import contextlib
|
|
148
153
|
import functools
|
|
149
154
|
import io
|
|
@@ -159,15 +164,15 @@ __all__ = 'writer', 'open', 'closer', 'dump', 'printer'
|
|
|
159
164
|
|
|
160
165
|
|
|
161
166
|
def writer(
|
|
162
|
-
stream: t.
|
|
163
|
-
is_binary:
|
|
167
|
+
stream: t.Callable | None | t.IO | Path | str = None,
|
|
168
|
+
is_binary: bool | None = None,
|
|
164
169
|
close_on_exit: bool = False,
|
|
165
170
|
temp_file: bool = False,
|
|
166
171
|
chunk_size: int = 0x100000,
|
|
167
172
|
delete_failures: bool = True,
|
|
168
|
-
dry_run:
|
|
173
|
+
dry_run: bool | t.Callable = False,
|
|
169
174
|
enabled: bool = True,
|
|
170
|
-
) -> t.
|
|
175
|
+
) -> t.Callable | t.IO:
|
|
171
176
|
"""
|
|
172
177
|
Write safely to file streams, sockets and callables.
|
|
173
178
|
|
|
@@ -228,78 +233,84 @@ def writer(
|
|
|
228
233
|
if not enabled:
|
|
229
234
|
return stream
|
|
230
235
|
|
|
231
|
-
write: t.
|
|
236
|
+
write: t.Callable | None
|
|
232
237
|
|
|
233
|
-
if
|
|
234
|
-
|
|
238
|
+
if close_on_exit and stream in (sys.stdout, sys.stderr):
|
|
239
|
+
raise ValueError('You cannot close stdout or stderr')
|
|
235
240
|
|
|
236
|
-
|
|
237
|
-
|
|
241
|
+
if dry_run:
|
|
242
|
+
close_on_exit = False
|
|
238
243
|
|
|
239
|
-
|
|
240
|
-
if
|
|
241
|
-
|
|
244
|
+
try:
|
|
245
|
+
if callable(dry_run):
|
|
246
|
+
write, dry_run = dry_run, True
|
|
242
247
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
stream.write(v)
|
|
248
|
+
elif dry_run:
|
|
249
|
+
write = len
|
|
246
250
|
|
|
247
|
-
|
|
248
|
-
|
|
251
|
+
elif close_on_exit and hasattr(stream, 'write'):
|
|
252
|
+
if temp_file and BUG_MESSAGE:
|
|
253
|
+
raise NotImplementedError(BUG_MESSAGE)
|
|
249
254
|
|
|
250
|
-
|
|
251
|
-
|
|
255
|
+
def write(v):
|
|
256
|
+
with stream:
|
|
257
|
+
stream.write(v)
|
|
252
258
|
|
|
253
|
-
|
|
254
|
-
|
|
259
|
+
else:
|
|
260
|
+
write = getattr(stream, 'write', None)
|
|
255
261
|
|
|
256
|
-
|
|
257
|
-
|
|
262
|
+
send = getattr(stream, 'send', None)
|
|
263
|
+
mode = getattr(stream, 'mode', None)
|
|
258
264
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
265
|
+
if write and mode:
|
|
266
|
+
if not set('w+a').intersection(mode):
|
|
267
|
+
raise ValueError(f'Stream mode "{mode}" is not a write mode')
|
|
262
268
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
269
|
+
binary_mode = 'b' in mode
|
|
270
|
+
if is_binary is not None and is_binary is not binary_mode:
|
|
271
|
+
raise ValueError('is_binary is inconsistent with the file stream')
|
|
266
272
|
|
|
267
|
-
|
|
273
|
+
is_binary = binary_mode
|
|
268
274
|
|
|
269
|
-
|
|
270
|
-
|
|
275
|
+
elif dry_run:
|
|
276
|
+
pass
|
|
271
277
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
278
|
+
elif send and hasattr(stream, 'recv'): # It looks like a socket:
|
|
279
|
+
if not (is_binary is None or is_binary is True):
|
|
280
|
+
raise ValueError('is_binary=False is inconsistent with a socket')
|
|
275
281
|
|
|
276
|
-
|
|
277
|
-
|
|
282
|
+
write = send
|
|
283
|
+
is_binary = True
|
|
278
284
|
|
|
279
|
-
|
|
280
|
-
|
|
285
|
+
elif callable(stream):
|
|
286
|
+
write = stream
|
|
281
287
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
288
|
+
else:
|
|
289
|
+
raise ValueError('Stream is not a file, a socket, or callable')
|
|
290
|
+
|
|
291
|
+
closer: _StreamCloser
|
|
292
|
+
|
|
293
|
+
if temp_file:
|
|
294
|
+
closer = _FileStreamCloser(
|
|
295
|
+
write,
|
|
296
|
+
close_on_exit,
|
|
297
|
+
is_binary,
|
|
298
|
+
temp_file,
|
|
299
|
+
chunk_size,
|
|
300
|
+
delete_failures,
|
|
301
|
+
)
|
|
302
|
+
else:
|
|
303
|
+
closer = _MemoryStreamCloser(write, close_on_exit, is_binary)
|
|
304
|
+
|
|
305
|
+
if send is write:
|
|
306
|
+
closer.fp.send = write
|
|
298
307
|
|
|
299
|
-
|
|
300
|
-
closer.fp.send = write
|
|
308
|
+
return closer.fp
|
|
301
309
|
|
|
302
|
-
|
|
310
|
+
except Exception:
|
|
311
|
+
if close_on_exit:
|
|
312
|
+
getattr(stream, 'close', lambda: None)()
|
|
313
|
+
raise
|
|
303
314
|
|
|
304
315
|
|
|
305
316
|
# There's an edge case in #23 I can't yet fix, so I fail
|
|
@@ -308,18 +319,18 @@ BUG_MESSAGE = 'Sorry, safer.writer fails if temp_file (#23)'
|
|
|
308
319
|
|
|
309
320
|
|
|
310
321
|
def open(
|
|
311
|
-
name:
|
|
322
|
+
name: Path | str,
|
|
312
323
|
mode: str = 'r',
|
|
313
324
|
buffering: int = -1,
|
|
314
|
-
encoding:
|
|
315
|
-
errors:
|
|
316
|
-
newline:
|
|
325
|
+
encoding: str | None = None,
|
|
326
|
+
errors: str | None = None,
|
|
327
|
+
newline: str | None = None,
|
|
317
328
|
closefd: bool = True,
|
|
318
|
-
opener: t.
|
|
329
|
+
opener: t.Callable | None = None,
|
|
319
330
|
make_parents: bool = False,
|
|
320
331
|
delete_failures: bool = True,
|
|
321
332
|
temp_file: bool = False,
|
|
322
|
-
dry_run:
|
|
333
|
+
dry_run: bool | t.Callable = False,
|
|
323
334
|
enabled: bool = True,
|
|
324
335
|
) -> t.IO:
|
|
325
336
|
"""
|
|
@@ -382,7 +393,7 @@ def open(
|
|
|
382
393
|
name = str(name)
|
|
383
394
|
|
|
384
395
|
if not isinstance(name, str):
|
|
385
|
-
raise TypeError('`name` must be string, not
|
|
396
|
+
raise TypeError(f'`name` must be string, not {type(name).__name__}')
|
|
386
397
|
|
|
387
398
|
name = os.path.realpath(name)
|
|
388
399
|
parent = os.path.dirname(os.path.abspath(name))
|
|
@@ -428,7 +439,7 @@ def open(
|
|
|
428
439
|
raise ValueError("binary mode doesn't take an errors argument")
|
|
429
440
|
|
|
430
441
|
if 'x' in mode and os.path.exists(name):
|
|
431
|
-
raise FileExistsError("File exists: '
|
|
442
|
+
raise FileExistsError(f"File exists: '{name}'")
|
|
432
443
|
|
|
433
444
|
if buffering == -1:
|
|
434
445
|
buffering = io.DEFAULT_BUFFER_SIZE
|
|
@@ -444,8 +455,8 @@ def open(
|
|
|
444
455
|
|
|
445
456
|
|
|
446
457
|
def closer(
|
|
447
|
-
stream: t.IO, is_binary:
|
|
448
|
-
) -> t.
|
|
458
|
+
stream: t.IO, is_binary: bool | None = None, close_on_exit: bool = True, **kwds
|
|
459
|
+
) -> t.Callable | t.IO:
|
|
449
460
|
"""
|
|
450
461
|
Like `safer.writer()` but with `close_on_exit=True` by default
|
|
451
462
|
|
|
@@ -457,7 +468,7 @@ def closer(
|
|
|
457
468
|
|
|
458
469
|
def dump(
|
|
459
470
|
obj,
|
|
460
|
-
stream: t.
|
|
471
|
+
stream: t.Callable | None | t.IO | Path | str = None,
|
|
461
472
|
dump: t.Any = None,
|
|
462
473
|
**kwargs,
|
|
463
474
|
) -> t.Any:
|
|
@@ -530,8 +541,8 @@ def _get_dumper(dump: t.Any) -> t.Callable:
|
|
|
530
541
|
|
|
531
542
|
@contextlib.contextmanager
|
|
532
543
|
def printer(
|
|
533
|
-
name:
|
|
534
|
-
) -> t.
|
|
544
|
+
name: Path | str, mode: str = 'w', *args, **kwargs
|
|
545
|
+
) -> t.Iterator[t.Callable]:
|
|
535
546
|
"""
|
|
536
547
|
A context manager that yields a function that prints to the opened file,
|
|
537
548
|
only writing to the original file at the exit of the context,
|
|
@@ -657,14 +668,16 @@ class _FileRenameCloser(_FileCloser):
|
|
|
657
668
|
self.target_file = target_file
|
|
658
669
|
self.dry_run = dry_run
|
|
659
670
|
self.is_binary = is_binary
|
|
671
|
+
if temp_file is True:
|
|
672
|
+
parent, file = os.path.split(target_file)
|
|
673
|
+
temp_file = os.path.join(parent, f'.{file}.tmp-safer')
|
|
674
|
+
|
|
660
675
|
super().__init__(temp_file, delete_failures, parent)
|
|
661
676
|
|
|
662
677
|
def _success(self):
|
|
663
678
|
if not self.dry_run:
|
|
664
679
|
if os.path.exists(self.target_file):
|
|
665
680
|
shutil.copymode(self.target_file, self.temp_file)
|
|
666
|
-
else:
|
|
667
|
-
os.chmod(self.temp_file, 0o100644)
|
|
668
681
|
os.replace(self.temp_file, self.target_file)
|
|
669
682
|
|
|
670
683
|
elif callable(self.dry_run):
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
safer/__init__.py,sha256=gol1HEWHBCBlrotKRuS2mSln5GAyv6PWdML09x9GZI8,22692
|
|
2
|
+
safer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
safer-5.1.0.dist-info/LICENSE,sha256=YrPqlE_MughiZSHUT2iVoduqlqmStMop9EEwCTdlzBw,1067
|
|
4
|
+
safer-5.1.0.dist-info/METADATA,sha256=TiGbpkpfSotKcUdbSdeR8kz27YdpbD1EroL_D52Zi0M,5449
|
|
5
|
+
safer-5.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
6
|
+
safer-5.1.0.dist-info/RECORD,,
|
safer-4.12.3.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
safer/__init__.py,sha256=pHsZGbnFFGcOsoR3mqlFksHYTUEDFiHEsYz0emW-nmE,22192
|
|
2
|
-
safer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
safer-4.12.3.dist-info/LICENSE,sha256=YrPqlE_MughiZSHUT2iVoduqlqmStMop9EEwCTdlzBw,1067
|
|
4
|
-
safer-4.12.3.dist-info/METADATA,sha256=H_QnHGAiyLOoMJ1FY0ZRjrNADF01l586Ohf9rTz5yUE,5450
|
|
5
|
-
safer-4.12.3.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
6
|
-
safer-4.12.3.dist-info/RECORD,,
|
|
File without changes
|