safer 4.10.0__tar.gz → 4.11.0__tar.gz
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: safer
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.11.0
|
|
4
4
|
Summary: 🧿 A safer writer for files and streams 🧿
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Tom Ritchford
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.9
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Requires-Dist: black (>=23.12.0,<24.0.0)
|
|
15
16
|
Description-Content-Type: text/markdown
|
|
16
17
|
|
|
17
18
|
# 🧿 `safer`: A safer writer 🧿
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
[tool.black]
|
|
2
|
-
line-length =
|
|
2
|
+
line-length = 88
|
|
3
3
|
skip-string-normalization = true
|
|
4
|
-
target-version = [
|
|
4
|
+
target-version = ["py38"]
|
|
5
5
|
|
|
6
6
|
[tool.doks]
|
|
7
7
|
auto = true
|
|
8
8
|
|
|
9
9
|
[tool.poetry]
|
|
10
10
|
name = "safer"
|
|
11
|
-
version = "4.
|
|
11
|
+
version = "4.11.0"
|
|
12
12
|
description = "🧿 A safer writer for files and streams 🧿"
|
|
13
13
|
authors = ["Tom Ritchford <tom@swirly.com>"]
|
|
14
14
|
license = "MIT"
|
|
@@ -16,6 +16,7 @@ readme = "README.md"
|
|
|
16
16
|
|
|
17
17
|
[tool.poetry.dependencies]
|
|
18
18
|
python = ">=3.8"
|
|
19
|
+
black = "^23.12.0"
|
|
19
20
|
|
|
20
21
|
[tool.poetry.group.dev.dependencies]
|
|
21
22
|
coverage = "*"
|
|
@@ -25,7 +26,18 @@ readme-renderer = "*"
|
|
|
25
26
|
tdir = "*"
|
|
26
27
|
toml = "*"
|
|
27
28
|
pyyaml = "*"
|
|
29
|
+
isort = "*"
|
|
30
|
+
ruff = "*"
|
|
31
|
+
mypy = "*"
|
|
28
32
|
|
|
33
|
+
[tool.coverage.run]
|
|
34
|
+
branch = true
|
|
35
|
+
source = ["safer"]
|
|
36
|
+
|
|
37
|
+
[tool.coverage.report]
|
|
38
|
+
fail_under = "90"
|
|
39
|
+
skip_covered = true
|
|
40
|
+
exclude_lines = ["pragma: no cover", "if False:", "if __name__ == .__main__.:", "raise NotImplementedError"]
|
|
29
41
|
[build-system]
|
|
30
42
|
requires = ["poetry-core"]
|
|
31
43
|
build-backend = "poetry.core.masonry.api"
|
|
@@ -144,8 +144,6 @@ With `safer`
|
|
|
144
144
|
print(item)
|
|
145
145
|
# Either the whole file is written, or nothing
|
|
146
146
|
"""
|
|
147
|
-
from pathlib import Path
|
|
148
|
-
from typing import Callable, IO, Optional, Union
|
|
149
147
|
import contextlib
|
|
150
148
|
import functools
|
|
151
149
|
import io
|
|
@@ -155,6 +153,8 @@ import shutil
|
|
|
155
153
|
import sys
|
|
156
154
|
import tempfile
|
|
157
155
|
import traceback
|
|
156
|
+
from pathlib import Path
|
|
157
|
+
from typing import IO, Callable, Optional, Union
|
|
158
158
|
|
|
159
159
|
# There's an edge case in #23 I can't yet fix, so I fail
|
|
160
160
|
# deliberately
|
|
@@ -221,16 +221,20 @@ def writer(
|
|
|
221
221
|
enabled: If `enabled` is falsey, the stream is returned unchanged
|
|
222
222
|
"""
|
|
223
223
|
if isinstance(stream, (str, Path)):
|
|
224
|
-
mode = 'wb' if is_binary else 'w'
|
|
225
224
|
return open(
|
|
226
|
-
stream,
|
|
227
|
-
|
|
225
|
+
stream,
|
|
226
|
+
'wb' if is_binary else 'w',
|
|
227
|
+
delete_failures=delete_failures,
|
|
228
|
+
dry_run=dry_run,
|
|
229
|
+
enabled=enabled,
|
|
228
230
|
)
|
|
229
231
|
|
|
230
232
|
stream = stream or sys.stdout
|
|
231
233
|
if not enabled:
|
|
232
234
|
return stream
|
|
233
235
|
|
|
236
|
+
write: Optional[Callable]
|
|
237
|
+
|
|
234
238
|
if callable(dry_run):
|
|
235
239
|
write, dry_run = dry_run, True
|
|
236
240
|
|
|
@@ -244,6 +248,7 @@ def writer(
|
|
|
244
248
|
def write(v):
|
|
245
249
|
with stream:
|
|
246
250
|
stream.write(v)
|
|
251
|
+
|
|
247
252
|
else:
|
|
248
253
|
write = getattr(stream, 'write', None)
|
|
249
254
|
|
|
@@ -282,6 +287,8 @@ def writer(
|
|
|
282
287
|
else:
|
|
283
288
|
raise ValueError('Stream is not a file, a socket, or callable')
|
|
284
289
|
|
|
290
|
+
closer: _StreamCloser
|
|
291
|
+
|
|
285
292
|
if temp_file:
|
|
286
293
|
closer = _FileStreamCloser(
|
|
287
294
|
write,
|
|
@@ -303,7 +310,7 @@ def writer(
|
|
|
303
310
|
def open(
|
|
304
311
|
name: Union[Path, str],
|
|
305
312
|
mode: str = 'r',
|
|
306
|
-
buffering:
|
|
313
|
+
buffering: int = -1,
|
|
307
314
|
encoding: Optional[str] = None,
|
|
308
315
|
errors: Optional[str] = None,
|
|
309
316
|
newline: Optional[str] = None,
|
|
@@ -312,7 +319,7 @@ def open(
|
|
|
312
319
|
make_parents: bool = False,
|
|
313
320
|
delete_failures: bool = True,
|
|
314
321
|
temp_file: bool = False,
|
|
315
|
-
dry_run: bool = False,
|
|
322
|
+
dry_run: Union[bool, Callable] = False,
|
|
316
323
|
enabled: bool = True,
|
|
317
324
|
) -> IO:
|
|
318
325
|
"""
|
|
@@ -323,17 +330,19 @@ def open(
|
|
|
323
330
|
if there is an exception.
|
|
324
331
|
|
|
325
332
|
temp_file: If `temp_file` is truthy, write to a disk file and use
|
|
326
|
-
|
|
333
|
+
os.replace() at the end, otherwise cache the writes in memory.
|
|
327
334
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
335
|
+
If `temp_file` is a string, use it as the name of the temporary
|
|
336
|
+
file, otherwise select one in the same directory as the target
|
|
337
|
+
file, or in the system tempfile for streams that aren't files.
|
|
331
338
|
|
|
332
|
-
dry_run:
|
|
333
|
-
If dry_run is
|
|
339
|
+
dry_run: If `dry_run` is truthy, the file is not written at all
|
|
340
|
+
If `dry_run` is also callable, the results are passed to `dry_run()`
|
|
341
|
+
rather than being written.
|
|
334
342
|
|
|
335
343
|
enabled:
|
|
336
|
-
If `enabled` is falsey,
|
|
344
|
+
If `enabled` is falsey, safer is entirely bypassed, and
|
|
345
|
+
built-in `open()` is used instead.
|
|
337
346
|
|
|
338
347
|
The remaining arguments are the same as for built-in `open()`.
|
|
339
348
|
|
|
@@ -367,9 +376,7 @@ def open(
|
|
|
367
376
|
is_read = 'r' in mode and not is_copy
|
|
368
377
|
is_binary = 'b' in mode
|
|
369
378
|
|
|
370
|
-
kwargs = dict(
|
|
371
|
-
encoding=encoding, errors=errors, newline=newline, opener=opener
|
|
372
|
-
)
|
|
379
|
+
kwargs = dict(encoding=encoding, errors=errors, newline=newline, opener=opener)
|
|
373
380
|
|
|
374
381
|
if isinstance(name, Path):
|
|
375
382
|
name = str(name)
|
|
@@ -427,7 +434,7 @@ def open(
|
|
|
427
434
|
buffering = io.DEFAULT_BUFFER_SIZE
|
|
428
435
|
|
|
429
436
|
closer = _FileRenameCloser(
|
|
430
|
-
name, temp_file, delete_failures, parent, dry_run
|
|
437
|
+
name, temp_file, delete_failures, parent, dry_run, is_binary
|
|
431
438
|
)
|
|
432
439
|
|
|
433
440
|
if is_copy and os.path.exists(name):
|
|
@@ -630,11 +637,13 @@ class _FileRenameCloser(_FileCloser):
|
|
|
630
637
|
target_file,
|
|
631
638
|
temp_file,
|
|
632
639
|
delete_failures,
|
|
633
|
-
parent
|
|
634
|
-
dry_run
|
|
640
|
+
parent,
|
|
641
|
+
dry_run,
|
|
642
|
+
is_binary,
|
|
635
643
|
):
|
|
636
644
|
self.target_file = target_file
|
|
637
645
|
self.dry_run = dry_run
|
|
646
|
+
self.is_binary = is_binary
|
|
638
647
|
super().__init__(temp_file, delete_failures, parent)
|
|
639
648
|
|
|
640
649
|
def _success(self):
|
|
@@ -645,6 +654,10 @@ class _FileRenameCloser(_FileCloser):
|
|
|
645
654
|
os.chmod(self.temp_file, 0o100644)
|
|
646
655
|
os.replace(self.temp_file, self.target_file)
|
|
647
656
|
|
|
657
|
+
elif callable(self.dry_run):
|
|
658
|
+
with open(self.temp_file, 'rb' if self.is_binary else 'r') as fp:
|
|
659
|
+
self.dry_run(fp.read())
|
|
660
|
+
|
|
648
661
|
|
|
649
662
|
class _StreamCloser(_Closer):
|
|
650
663
|
def __init__(self, write, close_on_exit):
|
|
File without changes
|
|
File without changes
|