From 3e7bd325ee5146a93cac6fc76c65ddf9a9f579c7 Mon Sep 17 00:00:00 2001 From: Tom Ritchford Date: Sat, 2 Nov 2024 13:51:47 +0100 Subject: [PATCH 1/3] Close file handles if close_on_exit even if safer.writer() fails --- safer/__init__.py | 114 ++++++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 54 deletions(-) diff --git a/safer/__init__.py b/safer/__init__.py index 846f91c..afa6801 100644 --- a/safer/__init__.py +++ b/safer/__init__.py @@ -163,7 +163,7 @@ def writer( stream: t.Union[t.Callable, None, t.IO, Path, str] = None, - is_binary: t.Optional[bool] = None, + is_binary: t.Union[bool, None] = None, close_on_exit: bool = False, temp_file: bool = False, chunk_size: int = 0x100000, @@ -233,76 +233,82 @@ def writer( write: t.Optional[t.Callable] - if callable(dry_run): - write, dry_run = dry_run, True + if close_on_exit and stream in (sys.stdout, sys.stderr): + raise ValueError('You cannot close stdout or stderr') - elif dry_run: - write = len + if dry_run: + close_on_exit = False - elif close_on_exit and hasattr(stream, 'write'): - if temp_file and BUG_MESSAGE: - raise NotImplementedError(BUG_MESSAGE) + try: + if callable(dry_run): + write, dry_run = dry_run, True - def write(v): - with stream: - stream.write(v) + elif dry_run: + write = len - else: - write = getattr(stream, 'write', None) + elif close_on_exit and hasattr(stream, 'write'): + if temp_file and BUG_MESSAGE: + raise NotImplementedError(BUG_MESSAGE) - send = getattr(stream, 'send', None) - mode = getattr(stream, 'mode', None) + def write(v): + with stream: + stream.write(v) - if dry_run: - close_on_exit = False + else: + write = getattr(stream, 'write', None) - if close_on_exit and stream in (sys.stdout, sys.stderr): - raise ValueError('You cannot close stdout or stderr') + send = getattr(stream, 'send', None) + mode = getattr(stream, 'mode', None) - if write and mode: - if not set('w+a').intersection(mode): - raise ValueError('Stream mode "%s" is not a write mode' % mode) + if write and mode: + if not set('w+a').intersection(mode): + raise ValueError('Stream mode "%s" is not a write mode' % mode) - binary_mode = 'b' in mode - if is_binary is not None and is_binary is not binary_mode: - raise ValueError('is_binary is inconsistent with the file stream') + binary_mode = 'b' in mode + if is_binary is not None and is_binary is not binary_mode: + raise ValueError('is_binary is inconsistent with the file stream') - is_binary = binary_mode + is_binary = binary_mode - elif dry_run: - pass + elif dry_run: + pass - elif send and hasattr(stream, 'recv'): # It looks like a socket: - if not (is_binary is None or is_binary is True): - raise ValueError('is_binary=False is inconsistent with a socket') + elif send and hasattr(stream, 'recv'): # It looks like a socket: + if not (is_binary is None or is_binary is True): + raise ValueError('is_binary=False is inconsistent with a socket') - write = send - is_binary = True + write = send + is_binary = True - elif callable(stream): - write = stream + elif callable(stream): + write = stream - else: - raise ValueError('Stream is not a file, a socket, or callable') - - closer: _StreamCloser - - if temp_file: - closer = _FileStreamCloser( - write, - close_on_exit, - is_binary, - temp_file, - chunk_size, - delete_failures, - ) - else: - closer = _MemoryStreamCloser(write, close_on_exit, is_binary) + else: + raise ValueError('Stream is not a file, a socket, or callable') + + closer: _StreamCloser + + if temp_file: + closer = _FileStreamCloser( + write, + close_on_exit, + is_binary, + temp_file, + chunk_size, + delete_failures, + ) + else: + closer = _MemoryStreamCloser(write, close_on_exit, is_binary) + + if send is write: + closer.fp.send = write - if send is write: - closer.fp.send = write + return closer.fp - return closer.fp + except Exception: + if close_on_exit: + getattr(stream, 'close', lambda: None)() + raise # There's an edge case in #23 I can't yet fix, so I fail From 9aef72bf6cbc5847256fd45a146b0ee80fe72c61 Mon Sep 17 00:00:00 2001 From: Tom Ritchford Date: Sat, 2 Nov 2024 13:57:28 +0100 Subject: [PATCH 2/3] Use future type annotations --- safer/__init__.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/safer/__init__.py b/safer/__init__.py index afa6801..9a70799 100644 --- a/safer/__init__.py +++ b/safer/__init__.py @@ -147,6 +147,8 @@ # Either the whole file is written, or nothing """ +from __future__ import annotations + import contextlib import functools import io @@ -162,15 +164,15 @@ def writer( - stream: t.Union[t.Callable, None, t.IO, Path, str] = None, - is_binary: t.Union[bool, None] = None, + stream: t.Callable | None | t.IO | Path | str = None, + is_binary: bool | None = None, close_on_exit: bool = False, temp_file: bool = False, chunk_size: int = 0x100000, delete_failures: bool = True, - dry_run: t.Union[bool, t.Callable] = False, + dry_run: bool | t.Callable = False, enabled: bool = True, -) -> t.Union[t.Callable, t.IO]: +) -> t.Callable | t.IO: """ Write safely to file streams, sockets and callables. @@ -231,7 +233,7 @@ def writer( if not enabled: return stream - write: t.Optional[t.Callable] + write: t.Callable | None if close_on_exit and stream in (sys.stdout, sys.stderr): raise ValueError('You cannot close stdout or stderr') @@ -317,18 +319,18 @@ def write(v): def open( - name: t.Union[Path, str], + name: Path | str, mode: str = 'r', buffering: int = -1, - encoding: t.Optional[str] = None, - errors: t.Optional[str] = None, - newline: t.Optional[str] = None, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, closefd: bool = True, - opener: t.Optional[t.Callable] = None, + opener: t.Callable | None = None, make_parents: bool = False, delete_failures: bool = True, temp_file: bool = False, - dry_run: t.Union[bool, t.Callable] = False, + dry_run: bool | t.Callable = False, enabled: bool = True, ) -> t.IO: """ @@ -453,8 +455,8 @@ def simple_write(value): def closer( - stream: t.IO, is_binary: t.Optional[bool] = None, close_on_exit: bool = True, **kwds -) -> t.Union[t.Callable, t.IO]: + stream: t.IO, is_binary: bool | None = None, close_on_exit: bool = True, **kwds +) -> t.Callable | t.IO: """ Like `safer.writer()` but with `close_on_exit=True` by default @@ -466,7 +468,7 @@ def closer( def dump( obj, - stream: t.Union[t.Callable, None, t.IO, Path, str] = None, + stream: t.Callable | None | t.IO | Path | str = None, dump: t.Any = None, **kwargs, ) -> t.Any: @@ -539,8 +541,8 @@ def _get_dumper(dump: t.Any) -> t.Callable: @contextlib.contextmanager def printer( - name: t.Union[Path, str], mode: str = 'w', *args, **kwargs -) -> t.Generator[t.Callable, None, None]: + name: Path | str, mode: str = 'w', *args, **kwargs +) -> t.Iterator[t.Callable]: """ A context manager that yields a function that prints to the opened file, only writing to the original file at the exit of the context, From f61c82470b31b7e123733a5a1782fec7c94a8a66 Mon Sep 17 00:00:00 2001 From: Tom Ritchford Date: Sun, 3 Nov 2024 17:39:44 +0100 Subject: [PATCH 3/3] Get rid of string interpolation --- safer/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/safer/__init__.py b/safer/__init__.py index 9a70799..8756822 100644 --- a/safer/__init__.py +++ b/safer/__init__.py @@ -264,7 +264,7 @@ def write(v): if write and mode: if not set('w+a').intersection(mode): - raise ValueError('Stream mode "%s" is not a write mode' % mode) + raise ValueError(f'Stream mode "{mode}" is not a write mode') binary_mode = 'b' in mode if is_binary is not None and is_binary is not binary_mode: @@ -393,7 +393,7 @@ def open( name = str(name) if not isinstance(name, str): - raise TypeError('`name` must be string, not %s' % type(name).__name__) + raise TypeError(f'`name` must be string, not {type(name).__name__}') name = os.path.realpath(name) parent = os.path.dirname(os.path.abspath(name)) @@ -439,7 +439,7 @@ def simple_write(value): raise ValueError("binary mode doesn't take an errors argument") if 'x' in mode and os.path.exists(name): - raise FileExistsError("File exists: '%s'" % name) + raise FileExistsError(f"File exists: '{name}'") if buffering == -1: buffering = io.DEFAULT_BUFFER_SIZE