diff --git a/pyproject.toml b/pyproject.toml index 6f4ebd5..5e6ba06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,23 +65,7 @@ indent-width = 4 [tool.ruff.lint] fixable = ['ALL'] ignore = [ - 'FA100', #Add `from __future__ import annotations` to simplify `typing.Optional` - 'UP022', # Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE` - 'SIM115', # Use a context manager for opening files 'UP031', #Use format specifiers instead of percent format - 'C408', # Unnecessary `dict()` call (rewrite as a literal) - 'C417' # Unnecessary `map()` usage (rewrite using a generator expression) ] -# [tool.pylint] -# max-line-length = 88 -# disable = [ -# "C0103", # (invalid-name) -# "C0114", # (missing-module-docstring) -# "C0115", # (missing-class-docstring) -# "C0116", # (missing-function-docstring) -# "R0903", # (too-few-public-methods) -# "R0913", # (too-many-arguments) -# "W0105", # (pointless-string-statement) -# ] diff --git a/src/wavinfo/__main__.py b/src/wavinfo/__main__.py index 43001a4..2a1f3d8 100644 --- a/src/wavinfo/__main__.py +++ b/src/wavinfo/__main__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime import importlib.metadata import json @@ -8,7 +10,6 @@ from cmd import Cmd from enum import Enum from optparse import OptionParser from shlex import split -from typing import Dict, List, Union from . import WavInfoReader @@ -30,8 +31,8 @@ class MissingDataError(RuntimeError): class MetaBrowser(Cmd): prompt = "(wavinfo) " - metadata: Union[List, Dict] - path: List[str] + metadata: list | dict + path: list[str] def preloop(self) -> None: self.path = [] @@ -39,7 +40,7 @@ class MetaBrowser(Cmd): @property def cwd(self): - root: List | Dict = self.metadata + root: list | dict = self.metadata for key in self.path: if isinstance(root, list): root = root[int(key)] diff --git a/src/wavinfo/rf64_parser.py b/src/wavinfo/rf64_parser.py index 92d3937..c8d249e 100644 --- a/src/wavinfo/rf64_parser.py +++ b/src/wavinfo/rf64_parser.py @@ -1,14 +1,14 @@ -import struct +from __future__ import annotations -# from collections import namedtuple -from typing import Dict, NamedTuple +import struct +from typing import NamedTuple from . import riff_parser class RF64Context(NamedTuple): sample_count: int - bigchunk_table: Dict[str, int] + bigchunk_table: dict[str, int] def parse_rf64(stream, signature=b"RF64") -> RF64Context: diff --git a/src/wavinfo/riff_parser.py b/src/wavinfo/riff_parser.py index ab317b3..08e5a3d 100644 --- a/src/wavinfo/riff_parser.py +++ b/src/wavinfo/riff_parser.py @@ -1,6 +1,7 @@ -# from optparse import Option +from __future__ import annotations + import struct -from typing import List, NamedTuple, Optional, Union +from typing import NamedTuple from .rf64_parser import RF64Context, parse_rf64 @@ -13,14 +14,14 @@ class WavInfoEOFError(EOFError): class ListChunkDescriptor(NamedTuple): signature: bytes - children: List[Union["ChunkDescriptor", "ListChunkDescriptor"]] + children: list[ChunkDescriptor | ListChunkDescriptor] class ChunkDescriptor(NamedTuple): ident: bytes start: int length: int - rf64_context: Optional[RF64Context] + rf64_context: RF64Context | None def read_data(self, from_stream) -> bytes: from_stream.seek(self.start) diff --git a/src/wavinfo/wave_adm_reader.py b/src/wavinfo/wave_adm_reader.py index a37db7f..aa3d3d4 100644 --- a/src/wavinfo/wave_adm_reader.py +++ b/src/wavinfo/wave_adm_reader.py @@ -1,11 +1,11 @@ """ ADM Reader """ +from __future__ import annotations from collections import namedtuple from io import BytesIO from struct import calcsize, unpack, unpack_from -from typing import Optional from lxml import etree as ET @@ -57,7 +57,7 @@ class WavADMReader: Read the ADM `audioProgramme` data structure and some of its reference properties. """ - ret_dict = dict() + ret_dict = {} nsmap = self.axml.getroot().nsmap @@ -71,7 +71,7 @@ class WavADMReader: ret_dict["contents"] = [] for content_ref in program.findall("audioContentIDRef", namespaces=nsmap): - content_dict = dict() + content_dict = {} content_dict["content_id"] = cid = content_ref.text content = afext.find( "audioContent[@audioContentID='%s']" % cid, namespaces=nsmap @@ -80,7 +80,7 @@ class WavADMReader: content_dict["objects"] = [] for object_ref in content.findall("audioObjectIDRef", namespaces=nsmap): - object_dict = dict() + object_dict = {} object_dict["object_id"] = oid = object_ref.text object = afext.find( "audioObject[@audioObjectID='%s']" % oid, namespaces=nsmap @@ -101,7 +101,7 @@ class WavADMReader: return ret_dict - def track_info(self, index) -> Optional[dict]: + def track_info(self, index) -> dict | None: """ Information about a track in the WAV file. @@ -206,7 +206,7 @@ class WavADMReader: rd.update(self.track_info(channel_uid_rec.track_index)) return rd - return dict( - channel_entries=list(map(lambda z: make_entry(z), self.channel_uids)), - programme=self.programme(), - ) + return { + 'channel_entries': [make_entry(z) for z in self.channel_uids], + 'programme': self.programme(), + } diff --git a/src/wavinfo/wave_bext_reader.py b/src/wavinfo/wave_bext_reader.py index 56e7112..7520b07 100644 --- a/src/wavinfo/wave_bext_reader.py +++ b/src/wavinfo/wave_bext_reader.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import struct # from .umid_parser import UMIDParser -from typing import Optional class WavBextReader: @@ -58,22 +59,22 @@ class WavBextReader: #: SMPTE 330M UMID of this audio file, 64 bytes are allocated though #: the UMID may only be 32 bytes long. - self.umid: Optional[bytes] = None + self.umid: bytes | None = None #: EBU R128 Integrated loudness, in LUFS. - self.loudness_value: Optional[float] = None + self.loudness_value: float | None = None #: EBU R128 Loudness range, in LUFS. - self.loudness_range: Optional[float] = None + self.loudness_range: float | None = None #: True peak level, in dBFS TP - self.max_true_peak: Optional[float] = None + self.max_true_peak: float | None = None #: EBU R128 Maximum momentary loudness, in LUFS - self.max_momentary_loudness: Optional[float] = None + self.max_momentary_loudness: float | None = None #: EBU R128 Maximum short-term loudness, in LUFS. - self.max_shortterm_loudness: Optional[float] = None + self.max_shortterm_loudness: float | None = None if self.version > 0: self.umid = unpacked[7] diff --git a/src/wavinfo/wave_cues_reader.py b/src/wavinfo/wave_cues_reader.py index fbe51b5..4938a39 100644 --- a/src/wavinfo/wave_cues_reader.py +++ b/src/wavinfo/wave_cues_reader.py @@ -8,9 +8,11 @@ IBM Corporation and Microsoft Corporation https://www.aelius.com/njh/wavemetatools/doc/riffmci.pdf """ +from __future__ import annotations + from dataclasses import dataclass from struct import calcsize, unpack -from typing import Any, Dict, Generator, List, NamedTuple, Optional, Tuple +from typing import Any, Generator, NamedTuple from .riff_parser import ChunkDescriptor @@ -120,7 +122,7 @@ class CueEntry(NamedTuple): return calcsize(cls.Format) @classmethod - def read(cls, data: bytes) -> "CueEntry": + def read(cls, data: bytes) -> CueEntry: assert len(data) == cls.format_size(), ( f"cue data size incorrect, expected {calcsize(cls.Format)} " "found {len(data)}" @@ -176,13 +178,15 @@ class RangeLabel(NamedTuple): parsed = unpack(leader_struct_fmt, data[0 : calcsize(leader_struct_fmt)]) text_data = data[calcsize(leader_struct_fmt) :] + purpose_str = parsed[2].decode('ascii') + if data[6] != 0: fallback_encoding = f"cp{data[6]}" return cls( name=parsed[0], length=parsed[1], - purpose=parsed[2], + purpose=purpose_str, country=parsed[3], language=parsed[4], dialect=parsed[5], @@ -194,27 +198,27 @@ class RangeLabel(NamedTuple): @dataclass class WavCuesReader: #: Every ``cue`` entry in the file - cues: List[CueEntry] + cues: list[CueEntry] #: Every ``labl`` in the file - labels: List[LabelEntry] + labels: list[LabelEntry] #: Every ``ltxt`` in the file - ranges: List[RangeLabel] + ranges: list[RangeLabel] #: Every ``note`` in the file - notes: List[NoteEntry] + notes: list[NoteEntry] @classmethod def read_all( cls, f, - cues: Optional[ChunkDescriptor], - labls: List[ChunkDescriptor], - ltxts: List[ChunkDescriptor], - notes: List[ChunkDescriptor], + cues: ChunkDescriptor | None, + labls: list[ChunkDescriptor], + ltxts: list[ChunkDescriptor], + notes: list[ChunkDescriptor], fallback_encoding: str, - ) -> "WavCuesReader": + ) -> WavCuesReader: cue_list = [] if cues is not None: cues_data = cues.read_data(f) @@ -249,7 +253,7 @@ class WavCuesReader: cues=cue_list, labels=label_list, ranges=range_list, notes=note_list ) - def each_cue(self) -> Generator[Tuple[int, int], None, None]: + def each_cue(self) -> Generator[tuple[int, int], None, None]: """ Iterate through each cue. @@ -258,7 +262,7 @@ class WavCuesReader: for cue in self.cues: yield (cue.name, cue.sample_offset) - def label_and_note(self, cue_ident: int) -> Tuple[Optional[str], Optional[str]]: + def label_and_note(self, cue_ident: int) -> tuple[str | None, str | None]: """ Get the label and note (extended comment) for a cue. @@ -272,7 +276,7 @@ class WavCuesReader: note = next((n.text for n in self.notes if n.name == cue_ident), None) return (label, note) - def range(self, cue_ident: int) -> Optional[int]: + def range(self, cue_ident: int) -> int | None: """ Get the length of the time range for a cue, if it has one. @@ -281,11 +285,11 @@ class WavCuesReader: """ return next((r.length for r in self.ranges if r.name == cue_ident), None) - def to_dict(self) -> Dict[str, Any]: - retval = dict() + def to_dict(self) -> dict[str, Any]: + retval = {} for n, t in self.each_cue(): - retval[n] = dict() + retval[n] = {} retval[n]["frame"] = t label, note = self.label_and_note(n) r = self.range(n) diff --git a/src/wavinfo/wave_dbmd_reader.py b/src/wavinfo/wave_dbmd_reader.py index a25563f..2cb473a 100644 --- a/src/wavinfo/wave_dbmd_reader.py +++ b/src/wavinfo/wave_dbmd_reader.py @@ -7,11 +7,13 @@ Unless otherwise stated, all ยง references here are to .. _EBU Tech 3285 Supplement 6: https://tech.ebu.ch/docs/tech/tech3285s6.pdf """ +from __future__ import annotations + from dataclasses import asdict, dataclass from enum import Enum, IntEnum from io import BytesIO from struct import unpack -from typing import Any, List, Tuple, Union +from typing import Any class SegmentType(IntEnum): @@ -339,8 +341,10 @@ class DolbyDigitalPlusMetadata: @staticmethod def load(buffer: bytes): - assert len(buffer) == 96, "Dolby Digital Plus segment incorrect size, " - "expected 96 got %i" % len(buffer) + assert len(buffer) == 96, ( + "Dolby Digital Plus segment incorrect size, " + "expected 96 got %i" % len(buffer) + ) def program_id(b) -> int: return b @@ -409,7 +413,7 @@ class DolbyDigitalPlusMetadata: return DolbyDigitalPlusMetadata.RFCompressionProfile(b) def dynrng1(b): - DolbyDigitalPlusMetadata.RFCompressionProfile(b) + return DolbyDigitalPlusMetadata.RFCompressionProfile(b) def ddplus_reserved3(_): pass @@ -504,7 +508,7 @@ class DolbyAtmosMetadata: NOT_INDICATED = 0x04 tool_name: str - tool_version: Tuple[int, int, int] + tool_version: tuple[int, int, int] warp_mode: WarpMode SEGMENT_LENGTH = 248 @@ -555,8 +559,8 @@ class DolbyAtmosSupplementalMetadata: NOT_INDICATED = 0x04 object_count: int - render_modes: List["DolbyAtmosSupplementalMetadata.BinauralRenderMode"] - trim_modes: List[int] + render_modes: list[DolbyAtmosSupplementalMetadata.BinauralRenderMode] + trim_modes: list[int] MAGIC = 0xF8726FBD TRIM_CONFIG_COUNT = 9 @@ -603,9 +607,9 @@ class WavDolbyMetadataReader: #: indicating if the segment's checksum was valid, and the #: segment's parsed dataclass (or a `bytes` array if it was #: not recognized). - segment_list: List[Tuple[Union[SegmentType, int], bool, Any]] + segment_list: list[tuple[SegmentType | int, bool, Any]] - version: Tuple[int, int, int, int] + version: tuple[int, int, int, int] @staticmethod def segment_checksum(bs: bytes, size: int): @@ -654,7 +658,7 @@ class WavDolbyMetadataReader: (stype, checksum == expected_checksum, segment) ) - def dolby_digital_plus(self) -> List[DolbyDigitalPlusMetadata]: + def dolby_digital_plus(self) -> list[DolbyDigitalPlusMetadata]: """ Every valid Dolby Digital Plus metadata segment in the file. """ @@ -664,7 +668,7 @@ class WavDolbyMetadataReader: if x[0] == SegmentType.DolbyDigitalPlus and x[1] ] - def dolby_atmos(self) -> List[DolbyAtmosMetadata]: + def dolby_atmos(self) -> list[DolbyAtmosMetadata]: """ Every valid Dolby Atmos metadata segment in the file. """ @@ -672,7 +676,7 @@ class WavDolbyMetadataReader: x[2] for x in self.segment_list if x[0] == SegmentType.DolbyAtmos and x[1] ] - def dolby_atmos_supplemental(self) -> List[DolbyAtmosSupplementalMetadata]: + def dolby_atmos_supplemental(self) -> list[DolbyAtmosSupplementalMetadata]: """ Every valid Dolby Atmos Supplemental metadata segment in the file. """ @@ -683,8 +687,8 @@ class WavDolbyMetadataReader: ] def to_dict(self) -> dict: - ddp = map(lambda x: asdict(x), self.dolby_digital_plus()) - atmos = map(lambda x: asdict(x), self.dolby_atmos()) + ddp = [asdict(x) for x in self.dolby_digital_plus()] + atmos = [asdict(x) for x in self.dolby_atmos()] # atmos_sup = map(lambda x: asdict(x), self.dolby_atmos_supplemental()) - return dict(dolby_digital_plus=list(ddp), dolby_atmos=list(atmos)) + return {"dolby_digital_plus": ddp, "dolby_atmos": atmos} diff --git a/src/wavinfo/wave_info_reader.py b/src/wavinfo/wave_info_reader.py index 9a9679d..b8a22ee 100644 --- a/src/wavinfo/wave_info_reader.py +++ b/src/wavinfo/wave_info_reader.py @@ -1,4 +1,4 @@ -from typing import Optional +from __future__ import annotations from .riff_parser import ListChunkDescriptor, parse_chunk @@ -22,40 +22,40 @@ class WavInfoChunkReader: ) #: 'ICOP' Copyright - self.copyright: Optional[str] = self._get_field(f, b"ICOP") + self.copyright: str | None = self._get_field(f, b"ICOP") #: 'IPRD' Product - self.product: Optional[str] = self._get_field(f, b"IPRD") - self.album: Optional[str] = self.product + self.product: str | None = self._get_field(f, b"IPRD") + self.album: str | None = self.product #: 'IGNR' Genre - self.genre: Optional[str] = self._get_field(f, b"IGNR") + self.genre: str | None = self._get_field(f, b"IGNR") #: 'ISBJ' Subject - self.subject: Optional[str] = self._get_field(f, b"ISBJ") + self.subject: str | None = self._get_field(f, b"ISBJ") #: 'IART' Artist, composer, author - self.artist: Optional[str] = self._get_field(f, b"IART") + self.artist: str | None = self._get_field(f, b"IART") #: 'ICMT' Comment - self.comment: Optional[str] = self._get_field(f, b"ICMT") + self.comment: str | None = self._get_field(f, b"ICMT") #: 'ISFT' Software, encoding application - self.software: Optional[str] = self._get_field(f, b"ISFT") + self.software: str | None = self._get_field(f, b"ISFT") #: 'ICRD' Created date - self.created_date: Optional[str] = self._get_field(f, b"ICRD") + self.created_date: str | None = self._get_field(f, b"ICRD") #: 'IENG' Engineer - self.engineer: Optional[str] = self._get_field(f, b"IENG") + self.engineer: str | None = self._get_field(f, b"IENG") #: 'ITCH' Technician - self.technician: Optional[str] = self._get_field(f, b"ITCH") + self.technician: str | None = self._get_field(f, b"ITCH") #: 'IKEY' Keywords, keyword list - self.keywords: Optional[str] = self._get_field(f, b"IKEY") + self.keywords: str | None = self._get_field(f, b"IKEY") #: 'INAM' Name, title - self.title: Optional[str] = self._get_field(f, b"INAM") + self.title: str | None = self._get_field(f, b"INAM") #: 'ISRC' Source - self.source: Optional[str] = self._get_field(f, b"ISRC") + self.source: str | None = self._get_field(f, b"ISRC") #: 'TAPE' Tape - self.tape: Optional[str] = self._get_field(f, b"TAPE") + self.tape: str | None = self._get_field(f, b"TAPE") #: 'IARL' Archival Location - self.archival_location: Optional[str] = self._get_field(f, b"IARL") + self.archival_location: str | None = self._get_field(f, b"IARL") #: 'ICSM' Commissioned - self.commissioned: Optional[str] = self._get_field(f, b"ICMS") + self.commissioned: str | None = self._get_field(f, b"ICMS") - def _get_field(self, f, field_ident) -> Optional[str]: + def _get_field(self, f, field_ident) -> str | None: search = next( ( (chunk.start, chunk.length) diff --git a/tests/test_rf64.py b/tests/test_rf64.py index abca12e..9046ea8 100644 --- a/tests/test_rf64.py +++ b/tests/test_rf64.py @@ -15,8 +15,6 @@ class TestRf64(TestCase): def test_open(self): for path in glob("tests/test_files/rf64/*.wav.gz"): - gz = gzip.open(path) - wav_info = wavinfo.WavInfoReader(gz) - - self.assertIsNotNone(wav_info) - # self.assertIsNotNone(wav_info.bext) + with gzip.open(path) as gz: + wav_info = wavinfo.WavInfoReader(gz) + self.assertIsNotNone(wav_info) diff --git a/tests/utils.py b/tests/utils.py index 0e0f847..e08eff0 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,14 +1,13 @@ import json import os.path import subprocess -from subprocess import PIPE FFPROBE = "ffprobe" def ffprobe(path): arguments = [FFPROBE, "-of", "json", "-show_format", "-show_streams", path] - process = subprocess.run(arguments, stdin=None, stdout=PIPE, stderr=PIPE, check=True) + process = subprocess.run(arguments, check=True, capture_output=True) if process.returncode == 0: output_str = process.stdout.decode("utf-8") return json.loads(output_str)