diff --git a/pyrightconfig.json b/pyrightconfig.json index a78bcb0..f9c8df6 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,9 +1,10 @@ { - "pythonVersion": "3.9", + "pythonVersion": "3.10", "exclude": [ "**/node_modules", "**/__pycache__", "**/.venv", - "tests/" + "tests/", + "**/.*" ] } diff --git a/soundfile.py b/soundfile.py index 3d75b4e..8f6ac89 100644 --- a/soundfile.py +++ b/soundfile.py @@ -13,28 +13,24 @@ import os as _os import sys as _sys import threading as _threading -import numpy.typing -from os import SEEK_SET, SEEK_CUR, SEEK_END +from collections.abc import Generator from ctypes.util import find_library as _find_library -from typing import Any, BinaryIO, Dict, Generator, Optional, Tuple, Union -from typing_extensions import TypeAlias, Self, Final +from os import SEEK_CUR, SEEK_END, SEEK_SET +from typing import Any, BinaryIO, Final, Literal, TypeAlias, overload + +import numpy +from typing_extensions import Self + from _soundfile import ffi as _ffi -try: - _unicode = unicode # type: ignore # doesn't exist in Python 3.x -except NameError: - _unicode = str - -# Type aliases for specific types -if _sys.version_info >= (3, 9): - FileDescriptorOrPath: TypeAlias = Union[str, int, BinaryIO, _os.PathLike[Any]] -else: - FileDescriptorOrPath: TypeAlias = Union[str, int, BinaryIO, _os.PathLike] -AudioData: TypeAlias = numpy.typing.NDArray[Any] +FileDescriptorOrPath: TypeAlias = str | int | BinaryIO | _os.PathLike[Any] +AudioData: TypeAlias = numpy.ndarray[tuple[int, ...], numpy.dtype[numpy.float32 | numpy.float64 | numpy.int32 | numpy.int16]] +AudioData_2d: TypeAlias = numpy.ndarray[tuple[int, int], numpy.dtype[numpy.float32 | numpy.float64 | numpy.int32 | numpy.int16]] +dtype_str: TypeAlias = Literal['float64', 'float32', 'int32', 'int16'] _snd: Any _ffi: Any -_str_types: Final[Dict[str, int]] = { +_str_types: Final[dict[str, int]] = { 'title': 0x01, 'copyright': 0x02, 'software': 0x03, @@ -47,7 +43,7 @@ 'genre': 0x10, } -_formats: Final[Dict[str, int]] = { +_formats: Final[dict[str, int]] = { 'WAV': 0x010000, # Microsoft WAV format (little endian default). 'AIFF': 0x020000, # Apple/SGI AIFF format (big endian). 'AU': 0x030000, # Sun/NeXT AU format (big endian). @@ -76,7 +72,7 @@ 'MP3': 0x230000, # MPEG-1/2 audio stream } -_subtypes: Final[Dict[str, int]] = { +_subtypes: Final[dict[str, int]] = { 'PCM_S8': 0x0001, # Signed 8 bit data 'PCM_16': 0x0002, # Signed 16 bit data 'PCM_24': 0x0003, # Signed 24 bit data @@ -113,7 +109,7 @@ 'MPEG_LAYER_III': 0x0082, # MPEG-2 Audio Layer III. } -_endians: Final[Dict[str, int]] = { +_endians: Final[dict[str, int]] = { 'FILE': 0x00000000, # Default file endian-ness. 'LITTLE': 0x10000000, # Force little endian-ness. 'BIG': 0x20000000, # Force big endian-ness. @@ -121,7 +117,7 @@ } # libsndfile doesn't specify default subtypes, these are somehow arbitrary: -_default_subtypes: Final[Dict[str, str]] = { +_default_subtypes: Final[dict[str, str]] = { 'WAV': 'PCM_16', 'AIFF': 'PCM_16', 'AU': 'PCM_16', @@ -150,14 +146,14 @@ 'MP3': 'MPEG_LAYER_III', } -_ffi_types: Final[Dict[str, str]] = { +_ffi_types: Final[dict[str, str]] = { 'float64': 'double', 'float32': 'float', 'int32': 'int', 'int16': 'short' } -_bitrate_modes: Final[Dict[str, int]] = { +_bitrate_modes: Final[dict[str, int]] = { 'CONSTANT': 0, 'AVERAGE': 1, 'VARIABLE': 2, @@ -170,6 +166,7 @@ elif _sys.platform == 'win32': from platform import architecture as _architecture from platform import machine as _machine + # this check can not be completed correctly: for x64 binaries running on # arm64 Windows report the same values as arm64 binaries. For now, neither # numpy nor cffi are available for arm64, so we can safely assume we're @@ -228,13 +225,20 @@ __libsndfile_version__ = __libsndfile_version__[len('libsndfile-'):] - -def read(file: FileDescriptorOrPath, frames: int = -1, start: int = 0, stop: Optional[int] = None, - dtype: str = 'float64', always_2d: bool = False, - fill_value: Optional[float] = None, out: Optional[AudioData] = None, - samplerate: Optional[int] = None, channels: Optional[int] = None, - format: Optional[str] = None, subtype: Optional[str] = None, - endian: Optional[str] = None, closefd: bool = True) -> Tuple[AudioData, int]: +@overload +def read(file: FileDescriptorOrPath, frames: int = -1, start: int = 0, stop: int | None = None, dtype: dtype_str = 'float64', + *, always_2d: Literal[True], fill_value: float | None = None, out: AudioData_2d | None = None, + samplerate: int | None = None, channels: int | None = None, format: str | None = None, subtype: str | None = None, + endian: str | None = None, closefd: bool = True) -> tuple[AudioData_2d, int]:... +@overload +def read(file: FileDescriptorOrPath, frames: int = -1, start: int = 0, stop: int | None = None, dtype: dtype_str = 'float64', + always_2d: bool = False, fill_value: float | None = None, out: AudioData | None = None, + samplerate: int | None = None, channels: int | None = None, format: str | None = None, subtype: str | None = None, + endian: str | None = None, closefd: bool = True) -> tuple[AudioData, int]:... +def read(file: FileDescriptorOrPath, frames: int = -1, start: int = 0, stop: int | None = None, dtype: dtype_str = 'float64', + always_2d: bool = False, fill_value: float | None = None, out: AudioData | None = None, + samplerate: int | None = None, channels: int | None = None, format: str | None = None, subtype: str | None = None, + endian: str | None = None, closefd: bool = True) -> tuple[AudioData | AudioData_2d, int]: """Provide audio data from a sound file as NumPy array. @@ -327,11 +331,11 @@ def read(file: FileDescriptorOrPath, frames: int = -1, start: int = 0, stop: Opt -def write(file: FileDescriptorOrPath, data: AudioData, samplerate: int, - subtype: Optional[str] = None, endian: Optional[str] = None, - format: Optional[str] = None, closefd: bool = True, - compression_level: Optional[float] = None, - bitrate_mode: Optional[str] = None) -> None: +def write(file: FileDescriptorOrPath, data: AudioData, samplerate: int, + subtype: str | None = None, endian: str | None = None, + format: str | None = None, closefd: bool = True, + compression_level: float | None = None, + bitrate_mode: str | None = None) -> None: """Write data to a sound file. .. note:: If *file* exists, it will be truncated and overwritten! @@ -386,15 +390,32 @@ def write(file: FileDescriptorOrPath, data: AudioData, samplerate: int, compression_level, bitrate_mode) as f: f.write(data) - -def blocks(file: FileDescriptorOrPath, blocksize: Optional[int] = None, - overlap: int = 0, frames: int = -1, start: int = 0, - stop: Optional[int] = None, dtype: str = 'float64', - always_2d: bool = False, fill_value: Optional[float] = None, - out: Optional[AudioData] = None, samplerate: Optional[int] = None, - channels: Optional[int] = None, format: Optional[str] = None, - subtype: Optional[str] = None, endian: Optional[str] = None, - closefd: bool = True) -> Generator[AudioData, None, None]: +@overload +def blocks(file: FileDescriptorOrPath, blocksize: int | None = None, + overlap: int = 0, frames: int = -1, start: int = 0, + stop: int | None = None, dtype: dtype_str = 'float64', + *, always_2d: Literal[True], fill_value: float | None = None, + out: AudioData | None = None, samplerate: int | None = None, + channels: int | None = None, format: str | None = None, + subtype: str | None = None, endian: str | None = None, + closefd: bool = True) -> Generator[AudioData_2d, None, None]: ... +@overload +def blocks(file: FileDescriptorOrPath, blocksize: int | None = None, + overlap: int = 0, frames: int = -1, start: int = 0, + stop: int | None = None, dtype: dtype_str = 'float64', + always_2d: bool = False, fill_value: float | None = None, + out: AudioData | None = None, samplerate: int | None = None, + channels: int | None = None, format: str | None = None, + subtype: str | None = None, endian: str | None = None, + closefd: bool = True) -> Generator[AudioData, None, None]: ... +def blocks(file: FileDescriptorOrPath, blocksize: int | None = None, + overlap: int = 0, frames: int = -1, start: int = 0, + stop: int | None = None, dtype: dtype_str = 'float64', + always_2d: bool = False, fill_value: float | None = None, + out: AudioData | None = None, samplerate: int | None = None, + channels: int | None = None, format: str | None = None, + subtype: str | None = None, endian: str | None = None, + closefd: bool = True) -> Generator[AudioData, None, None] | Generator[AudioData_2d, None, None]: """Return a generator for block-wise reading. By default, iteration starts at the beginning and stops at the end @@ -445,41 +466,40 @@ def blocks(file: FileDescriptorOrPath, blocksize: Optional[int] = None, with SoundFile(file, 'r', samplerate, channels, subtype, endian, format, closefd) as f: frames = f._prepare_read(start, stop, frames) - for block in f.blocks(blocksize, overlap, frames, dtype, always_2d, fill_value, out): - yield block + yield from f.blocks(blocksize, overlap, frames, dtype, always_2d, fill_value, out) -class _SoundFileInfo(object): +class _SoundFileInfo: """Information about a SoundFile""" def __init__(self, file, verbose): - self.verbose = verbose + self.verbose: bool = verbose with SoundFile(file) as f: - self.name = f.name - self.samplerate = f.samplerate - self.channels = f.channels - self.frames = f.frames - self.duration = float(self.frames)/f.samplerate - self.format = f.format - self.subtype = f.subtype - self.endian = f.endian - self.format_info = f.format_info - self.subtype_info = f.subtype_info - self.sections = f.sections - self.extra_info = f.extra_info + self.name: str | int | Any = f.name + self.samplerate: int = f.samplerate + self.channels: int = f.channels + self.frames: int = f.frames + self.duration: float = float(self.frames)/f.samplerate + self.format: str = f.format + self.subtype: str = f.subtype + self.endian: str = f.endian + self.format_info: str = f.format_info + self.subtype_info: str = f.subtype_info + self.sections: int = f.sections + self.extra_info: str = f.extra_info @property def _duration_str(self): hours, rest = divmod(self.duration, 3600) minutes, seconds = divmod(rest, 60) if hours >= 1: - duration = "{0:.0g}:{1:02.0g}:{2:05.3f} h".format(hours, minutes, seconds) + duration = f"{hours:.0g}:{minutes:02.0g}:{seconds:05.3f} h" elif minutes >= 1: - duration = "{0:02.0g}:{1:05.3f} min".format(minutes, seconds) + duration = f"{minutes:02.0g}:{seconds:05.3f} min" elif seconds <= 1: - duration = "{0:d} samples".format(self.frames) + duration = f"{self.frames:d} samples" else: - duration = "{0:.3f} s".format(seconds) + duration = f"{seconds:.3f} s" return duration def __repr__(self): @@ -512,7 +532,7 @@ def info(file: FileDescriptorOrPath, verbose: bool = False) -> _SoundFileInfo: return _SoundFileInfo(file, verbose) -def available_formats() -> Dict[str, str]: +def available_formats() -> dict[str, str]: """Return a dictionary of available major formats. Examples @@ -533,7 +553,7 @@ def available_formats() -> Dict[str, str]: _snd.SFC_GET_FORMAT_MAJOR)) -def available_subtypes(format: Optional[str] = None) -> Dict[str, str]: +def available_subtypes(format: str | None = None) -> dict[str, str]: """Return a dictionary of available subtypes. Parameters @@ -552,12 +572,12 @@ def available_subtypes(format: Optional[str] = None) -> Dict[str, str]: """ subtypes = _available_formats_helper(_snd.SFC_GET_FORMAT_SUBTYPE_COUNT, _snd.SFC_GET_FORMAT_SUBTYPE) - return dict((subtype, name) for subtype, name in subtypes - if format is None or check_format(format, subtype)) + return {subtype: name for subtype, name in subtypes + if format is None or check_format(format, subtype)} -def check_format(format: str, subtype: Optional[str] = None, - endian: Optional[str] = None) -> bool: +def check_format(format: str, subtype: str | None = None, + endian: str | None = None) -> bool: """Check if the combination of format/subtype/endian is valid. Examples @@ -575,7 +595,7 @@ def check_format(format: str, subtype: Optional[str] = None, return False -def default_subtype(format: str) -> Optional[str]: +def default_subtype(format: str) -> str | None: """Return the default subtype for a given format. Examples @@ -591,7 +611,7 @@ def default_subtype(format: str) -> Optional[str]: return _default_subtypes.get(format.upper()) -class SoundFile(object): +class SoundFile: """A sound file. For more documentation see the __init__() docstring (which is also @@ -599,12 +619,12 @@ class SoundFile(object): """ - def __init__(self, file: FileDescriptorOrPath, mode: Optional[str] = 'r', - samplerate: Optional[int] = None, channels: Optional[int] = None, - subtype: Optional[str] = None, endian: Optional[str] = None, - format: Optional[str] = None, closefd: bool = True, - compression_level: Optional[float] = None, - bitrate_mode: Optional[str] = None) -> None: + def __init__(self, file: FileDescriptorOrPath, mode: str | None = 'r', + samplerate: int | None = None, channels: int | None = None, + subtype: str | None = None, endian: str | None = None, + format: str | None = None, closefd: bool = True, + compression_level: float | None = None, + bitrate_mode: str | None = None) -> None: """Open a sound file. If a file is opened with `mode` ``'r'`` (the default) or @@ -679,7 +699,7 @@ def __init__(self, file: FileDescriptorOrPath, mode: Optional[str] = 'r', (highest compression level). See `libsndfile document `__. bitrate_mode : {'CONSTANT', 'AVERAGE', 'VARIABLE'}, optional - The bitrate mode on 'write()'. + The bitrate mode on 'write()'. See `libsndfile document `__. Examples @@ -722,7 +742,7 @@ def __init__(self, file: FileDescriptorOrPath, mode: Optional[str] = 'r', self.seek(0) _snd.sf_command(self._file, _snd.SFC_SET_CLIPPING, _ffi.NULL, _snd.SF_TRUE) - + # set compression setting if self._compression_level is not None: # needs to be called before set_bitrate_mode @@ -769,7 +789,7 @@ def __init__(self, file: FileDescriptorOrPath, mode: Optional[str] = 'r', """The bitrate mode on 'write()'""" @property - def extra_info(self): + def extra_info(self) -> str: """Retrieve the log string generated when opening the file.""" info = _ffi.new("char[]", 2**14) _snd.sf_command(self._file, _snd.SFC_GET_LOG_INFO, @@ -780,9 +800,9 @@ def extra_info(self): _file = None def __repr__(self) -> str: - compression_setting = (", compression_level={0}".format(self.compression_level) + compression_setting = (f", compression_level={self.compression_level}" if self.compression_level is not None else "") - compression_setting += (", bitrate_mode='{0}'".format(self.bitrate_mode) + compression_setting += (f", bitrate_mode='{self.bitrate_mode}'" if self.bitrate_mode is not None else "") return ("SoundFile({0.name!r}, mode={0.mode!r}, " "samplerate={0.samplerate}, channels={0.channels}, " @@ -816,7 +836,7 @@ def __getattr__(self, name: str) -> Any: return _ffi.string(data).decode('utf-8', 'replace') if data else "" else: raise AttributeError( - "'SoundFile' object has no attribute {0!r}".format(name)) + f"'SoundFile' object has no attribute {name!r}") def __len__(self) -> int: # Note: This is deprecated and will be removed at some point, @@ -883,9 +903,17 @@ def tell(self) -> int: return self.seek(0, SEEK_CUR) - def read(self, frames: int = -1, dtype: str = 'float64', - always_2d: bool = False, fill_value: Optional[float] = None, - out: Optional[AudioData] = None) -> AudioData: + @overload + def read(self, frames: int = -1, dtype: dtype_str = 'float64', + *, always_2d: Literal[True], fill_value: float | None = None, + out: AudioData_2d | None = None) -> AudioData_2d:... + @overload + def read(self, frames: int = -1, dtype: dtype_str = 'float64', + always_2d: bool = False, fill_value: float | None = None, + out: AudioData | None = None) -> AudioData:... + def read(self, frames: int = -1, dtype: dtype_str = 'float64', + always_2d: bool = False, fill_value: float | None = None, + out: AudioData | None = None) -> AudioData | AudioData_2d: """Read from the file and return data as NumPy array. Reads the given number of frames in the given data format @@ -979,7 +1007,7 @@ def read(self, frames: int = -1, dtype: str = 'float64', return out - def buffer_read(self, frames: int = -1, dtype: Optional[str] = None) -> memoryview: + def buffer_read(self, frames: int = -1, dtype: dtype_str | None = None) -> memoryview: """Read from the file and return data as buffer object. Reads the given number of *frames* in the given data format @@ -1014,7 +1042,7 @@ def buffer_read(self, frames: int = -1, dtype: Optional[str] = None) -> memoryvi assert read_frames == frames return _ffi.buffer(cdata) - def buffer_read_into(self, buffer: Union[bytearray, memoryview, Any], dtype: str) -> int: + def buffer_read_into(self, buffer: bytearray | memoryview | Any, dtype: dtype_str) -> int: """Read from the file into a given buffer object. Fills the given *buffer* with frames in the given data format @@ -1101,7 +1129,7 @@ def write(self, data: AudioData) -> None: assert written == len(data) self._update_frames(written) - def buffer_write(self, data: Any, dtype: str) -> None: + def buffer_write(self, data: bytes, dtype: dtype_str) -> None: """Write audio data from a buffer/bytes object to the file. Writes the contents of *data* to the file at the current @@ -1128,10 +1156,20 @@ def buffer_write(self, data: Any, dtype: str) -> None: assert written == frames self._update_frames(written) - def blocks(self, blocksize: Optional[int] = None, overlap: int = 0, - frames: int = -1, dtype: str = 'float64', - always_2d: bool = False, fill_value: Optional[float] = None, - out: Optional[AudioData] = None) -> Generator[AudioData, None, None]: + @overload + def blocks(self, blocksize: int | None = None, overlap: int = 0, + frames: int = -1, dtype: dtype_str = 'float64', + *, always_2d: Literal[True], fill_value: float | None = None, + out: AudioData | None = None) -> Generator[AudioData_2d, None, None]: ... + @overload + def blocks(self, blocksize: int | None = None, overlap: int = 0, + frames: int = -1, dtype: dtype_str = 'float64', + always_2d: bool = False, fill_value: float | None = None, + out: AudioData | None = None) -> Generator[AudioData, None, None]: ... + def blocks(self, blocksize: int | None = None, overlap: int = 0, + frames: int = -1, dtype: dtype_str = 'float64', + always_2d: bool = False, fill_value: float | None = None, + out: AudioData | None = None) -> Generator[AudioData, None, None] | Generator[AudioData_2d, None, None]: """Return a generator for block-wise reading. By default, the generator yields blocks of the given @@ -1223,7 +1261,7 @@ def blocks(self, blocksize: Optional[int] = None, overlap: int = 0, yield np.copy(block) if copy_out else block frames -= toread - def truncate(self, frames: Optional[int] = None) -> None: + def truncate(self, frames: int | None = None) -> None: """Truncate the file to a given number of frames. After this command, the read/write position will be at the new @@ -1279,15 +1317,15 @@ def close(self) -> None: def _open(self, file, mode_int, closefd): """Call the appropriate sf_open*() function from libsndfile.""" - if isinstance(file, (_unicode, bytes)): + if isinstance(file, (str, bytes)): if _os.path.isfile(file): if 'x' in self.mode: - raise OSError("File exists: {0!r}".format(self.name)) + raise OSError(f"File exists: {self.name!r}") elif set(self.mode).issuperset('w+'): # truncate the file, because SFM_RDWR doesn't: _os.close(_os.open(file, _os.O_WRONLY | _os.O_TRUNC)) openfunction = _snd.sf_open - if isinstance(file, _unicode): + if isinstance(file, str): if _sys.platform == 'win32': openfunction = _snd.sf_wchar_open else: @@ -1418,16 +1456,16 @@ def _check_dtype(self, dtype): try: return _ffi_types[dtype] except KeyError: - raise ValueError("dtype must be one of {0!r} and not {1!r}".format( + raise ValueError("dtype must be one of {!r} and not {!r}".format( sorted(_ffi_types.keys()), dtype)) def _array_io(self, action, array, frames): """Check array and call low-level IO function.""" if array.ndim not in (1,2): - raise ValueError("Invalid shape: {0!r} ({1})".format(array.shape, "0 dimensions not supported" if array.ndim < 1 else "too many dimensions")) + raise ValueError("Invalid shape: {!r} ({})".format(array.shape, "0 dimensions not supported" if array.ndim < 1 else "too many dimensions")) array_channels = 1 if array.ndim == 1 else array.shape[1] if array_channels != self.channels: - raise ValueError("Invalid shape: {0!r} (Expected {1} channels, got {2})".format(array.shape, self.channels, array_channels)) + raise ValueError(f"Invalid shape: {array.shape!r} (Expected {self.channels} channels, got {array_channels})") if not array.flags.c_contiguous: raise ValueError("Data must be C-contiguous") ctype = self._check_dtype(array.dtype.name) @@ -1474,7 +1512,7 @@ def _prepare_read(self, start, stop, frames): self.seek(start, SEEK_SET) return frames - def copy_metadata(self): + def copy_metadata(self) -> dict[str, str]: """Get all metadata present in this SoundFile Returns @@ -1491,7 +1529,7 @@ def copy_metadata(self): if data: strs[strtype] = _ffi.string(data).decode('utf-8', 'replace') return strs - + def _set_bitrate_mode(self, bitrate_mode): """Call libsndfile's set bitrate mode function.""" assert bitrate_mode in _bitrate_modes @@ -1503,7 +1541,7 @@ def _set_bitrate_mode(self, bitrate_mode): err = _snd.sf_error(self._file) raise LibsndfileError(err, f"Error set bitrate mode {bitrate_mode}") - + def _set_compression_level(self, compression_level): """Call libsndfile's set compression level function.""" if not (0 <= compression_level <= 1): @@ -1530,21 +1568,21 @@ def _format_int(format, subtype, endian): subtype = default_subtype(format) if subtype is None: raise TypeError( - "No default subtype for major format {0!r}".format(format)) - elif not isinstance(subtype, (_unicode, str)): - raise TypeError("Invalid subtype: {0!r}".format(subtype)) + f"No default subtype for major format {format!r}") + elif not isinstance(subtype, str): + raise TypeError(f"Invalid subtype: {subtype!r}") try: result |= _subtypes[subtype.upper()] except KeyError: - raise ValueError("Unknown subtype: {0!r}".format(subtype)) + raise ValueError(f"Unknown subtype: {subtype!r}") if endian is None: endian = 'FILE' - elif not isinstance(endian, (_unicode, str)): - raise TypeError("Invalid endian-ness: {0!r}".format(endian)) + elif not isinstance(endian, str): + raise TypeError(f"Invalid endian-ness: {endian!r}") try: result |= _endians[endian.upper()] except KeyError: - raise ValueError("Unknown endian-ness: {0!r}".format(endian)) + raise ValueError(f"Unknown endian-ness: {endian!r}") info = _ffi.new("SF_INFO*") info.format = result @@ -1557,11 +1595,11 @@ def _format_int(format, subtype, endian): def _check_mode(mode): """Check if mode is valid and return its integer representation.""" - if not isinstance(mode, (_unicode, str)): - raise TypeError("Invalid mode: {0!r}".format(mode)) + if not isinstance(mode, str): + raise TypeError(f"Invalid mode: {mode!r}") mode_set = set(mode) if mode_set.difference('xrwb+') or len(mode) > len(mode_set): - raise ValueError("Invalid mode: {0!r}".format(mode)) + raise ValueError(f"Invalid mode: {mode!r}") if len(mode_set.intersection('xrw')) != 1: raise ValueError("mode must contain exactly one of 'xrw'") @@ -1580,7 +1618,7 @@ def _create_info_struct(file, mode, samplerate, channels, original_format = format if format is None: format = _get_format_from_filename(file, mode) - assert isinstance(format, (_unicode, str)) + assert isinstance(format, str) else: _check_format(format) @@ -1621,7 +1659,7 @@ def _get_format_from_filename(file, mode): pass if format.upper() not in _formats and 'r' not in mode: raise TypeError("No format specified and unable to get format from " - "file extension: {0!r}".format(file)) + "file extension: {!r}".format(file)) return format @@ -1656,12 +1694,12 @@ def _available_formats_helper(count_flag, format_flag): def _check_format(format_str): """Check if `format_str` is valid and return format ID.""" - if not isinstance(format_str, (_unicode, str)): - raise TypeError("Invalid format: {0!r}".format(format_str)) + if not isinstance(format_str, str): + raise TypeError(f"Invalid format: {format_str!r}") try: format_int = _formats[format_str.upper()] except KeyError: - raise ValueError("Unknown format: {0!r}".format(format_str)) + raise ValueError(f"Unknown format: {format_str!r}") return format_int