diff --git a/pyproject.toml b/pyproject.toml index 2759369..3de1026 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dependencies = [ [dependency-groups] dev = [ "pytest>=8.3.5", - "ruff>=0.14.0", + "ruff>=0.16.8", ] doc = [ "sphinx>=7.1.2", @@ -64,6 +64,11 @@ indent-width = 4 [tool.ruff.lint] fixable = ['ALL'] +ignore = [ + '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 +] # [tool.pylint] # max-line-length = 88 diff --git a/src/wavinfo/wave_ixml_reader.py b/src/wavinfo/wave_ixml_reader.py index 4d74f9f..1b66c59 100644 --- a/src/wavinfo/wave_ixml_reader.py +++ b/src/wavinfo/wave_ixml_reader.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import io from enum import IntEnum # from collections import namedtuple -from typing import NamedTuple, Optional +from typing import NamedTuple from lxml import etree as ET @@ -76,7 +78,7 @@ class SteinbergMetadata: self.parsed = xml.find(self.Steinberg_xpath) @property - def audio_speaker_arrangement(self) -> Optional[AudioSpeakerArrangement]: + def audio_speaker_arrangement(self) -> AudioSpeakerArrangement | None: """ `AudioSpeakerArrangement` property """ @@ -87,7 +89,7 @@ class SteinbergMetadata: return type(self).AudioSpeakerArrangement(int(val.text)) @property - def sample_format_size(self) -> Optional[int]: + def sample_format_size(self) -> int | None: """ AudioSampleFormatSize """ @@ -96,7 +98,7 @@ class SteinbergMetadata: return int(val.text) @property - def media_company(self) -> Optional[str]: + def media_company(self) -> str | None: """ MediaCompany """ @@ -105,7 +107,7 @@ class SteinbergMetadata: return val.text @property - def media_drop_frames(self) -> Optional[bool]: + def media_drop_frames(self) -> bool | None: """ MediaDropFrames """ @@ -114,7 +116,7 @@ class SteinbergMetadata: return val.text == "1" @property - def media_duration(self) -> Optional[float]: + def media_duration(self) -> float | None: """ MediaDuration """ @@ -122,33 +124,29 @@ class SteinbergMetadata: if val is not None: return float(val.text) - @property - def media_start_time(self) -> Optional[float]: - """ - MediaStartTime - """ - pass + # @property + # def media_start_time(self) -> float | None: + # """ + # MediaStartTime + # """ - @property - def media_track_title(self) -> Optional[str]: - """ - MediaTrackTitle - """ - pass + # @property + # def media_track_title(self) -> str | None: + # """ + # MediaTrackTitle + # """ - @property - def program_name(self) -> Optional[str]: - """ - ProgramName - """ - pass + # @property + # def program_name(self) -> str | None: + # """ + # ProgramName + # """ - @property - def program_version(self) -> Optional[str]: - """ - ProgramVersion - """ - pass + # @property + # def program_version(self) -> str | None: + # """ + # ProgramVersion + # """ class WavIXMLFormat: @@ -166,7 +164,7 @@ class WavIXMLFormat: parser = ET.XMLParser(recover=True) self.parsed: ET.ElementTree = ET.parse(xml_bytes, parser=parser) - def _get_text_value(self, xpath) -> Optional[str]: + def _get_text_value(self, xpath) -> str | None: e = self.parsed.find("./" + xpath) if e is not None: return e.text @@ -200,35 +198,35 @@ class WavIXMLFormat: ) @property - def project(self) -> Optional[str]: + def project(self) -> str | None: """ The project/film name entered for the recording. """ return self._get_text_value("PROJECT") @property - def scene(self) -> Optional[str]: + def scene(self) -> str | None: """ Scene/slate. """ return self._get_text_value("SCENE") @property - def take(self) -> Optional[str]: + def take(self) -> str | None: """ Take number. """ return self._get_text_value("TAKE") @property - def tape(self) -> Optional[str]: + def tape(self) -> str | None: """ Tape name. """ return self._get_text_value("TAPE") @property - def family_uid(self) -> Optional[str]: + def family_uid(self) -> str | None: """ The globally-unique ID for this file family. This may be in the format of a GUID, or an EBU Rec 9 source identifier, or some other dumb @@ -237,14 +235,14 @@ class WavIXMLFormat: return self._get_text_value("FILE_SET/FAMILY_UID") @property - def family_name(self) -> Optional[str]: + def family_name(self) -> str | None: """ The name of this file's file family. """ return self._get_text_value("FILE_SET/FAMILY_NAME") @property - def steinberg(self) -> Optional[SteinbergMetadata]: + def steinberg(self) -> SteinbergMetadata | None: """ Steinberg vendor iXML metadata if present. """ @@ -254,12 +252,12 @@ class WavIXMLFormat: return None def to_dict(self): - return dict( - track_list=list(map(lambda x: x._asdict(), self.track_list)), - project=self.project, - scene=self.scene, - take=self.take, - tape=self.tape, - family_uid=self.family_uid, - family_name=self.family_name, - ) + return { + "track_list": [x._asdict() for x in self.track_list], + "project": self.project, + "scene": self.scene, + "take": self.take, + "tape": self.tape, + "family_uid": self.family_uid, + "family_name": self.family_name, + } diff --git a/src/wavinfo/wave_reader.py b/src/wavinfo/wave_reader.py index bccc6e5..3691464 100644 --- a/src/wavinfo/wave_reader.py +++ b/src/wavinfo/wave_reader.py @@ -1,8 +1,9 @@ -# -*- coding: utf-8 -*- +from __future__ import annotations + import os import pathlib import struct -from typing import Any, Generator, NamedTuple, Optional +from typing import Any, Generator, NamedTuple from .riff_parser import ChunkDescriptor, ListChunkDescriptor, parse_chunk from .wave_adm_reader import WavADMReader @@ -58,31 +59,31 @@ class WavInfoReader: self.bext_encoding = bext_encoding #: Wave audio data format. - self.fmt: Optional[WavAudioFormat] = None + self.fmt: WavAudioFormat | None = None #: Statistics of the `data` section. - self.data: Optional[WavDataDescriptor] = None + self.data: WavDataDescriptor | None = None #: Broadcast-Wave metadata. - self.bext: Optional[WavBextReader] = None + self.bext: WavBextReader | None = None #: iXML metadata. - self.ixml: Optional[WavIXMLFormat] = None + self.ixml: WavIXMLFormat | None = None #: ADM Audio Definiton Model metadata. - self.adm: Optional[WavADMReader] = None + self.adm: WavADMReader | None = None #: Dolby bitstream metadata. - self.dolby: Optional[WavDolbyMetadataReader] = None + self.dolby: WavDolbyMetadataReader | None = None #: RIFF INFO metadata. - self.info: Optional[WavInfoChunkReader] = None + self.info: WavInfoChunkReader | None = None #: RIFF cues markers, labels, and notes. - self.cues: Optional[WavCuesReader] = None + self.cues: WavCuesReader | None = None #: Sampler `smpl` metadata - self.smpl: Optional[WavSmplReader] = None + self.smpl: WavSmplReader | None = None if hasattr(path, "read"): self.get_wav_info(path) @@ -97,8 +98,8 @@ class WavInfoReader: self.path = absolute_path - with open(path, "rb") as path: - self.get_wav_info(path) + with open(path, "rb") as file: + self.get_wav_info(file) def get_wav_info(self, wavfile): chunks = parse_chunk(wavfile) @@ -117,9 +118,7 @@ class WavInfoReader: self.smpl = self._get_sampler_loops(wavfile) self.data = self._describe_data() - def _find_chunk_data( - self, ident, from_stream, default_none=False - ) -> Optional[bytes]: + def _find_chunk_data(self, ident, from_stream, default_none=False) -> bytes | None: top_chunks = ( chunk for chunk in self.main_list @@ -130,7 +129,7 @@ class WavInfoReader: return chunk_descriptor.read_data(from_stream) if chunk_descriptor else None - def _find_list_chunk(self, signature) -> Optional[ListChunkDescriptor]: + def _find_list_chunk(self, signature) -> ListChunkDescriptor | None: top_chunks = ( chunk for chunk in self.main_list @@ -261,10 +260,8 @@ class WavInfoReader: if self.__getattribute__(scope) else {} ) - for key in mdict.keys(): + for key in mdict: yield scope, key, mdict[key] def __repr__(self): - return "WavInfoReader({}, {}, {})".format( - self.path, self.info_encoding, self.bext_encoding - ) + return f"WavInfoReader({self.path}, {self.info_encoding}, {self.bext_encoding})" diff --git a/src/wavinfo/wave_smpl_reader.py b/src/wavinfo/wave_smpl_reader.py index 772141d..59f9d49 100644 --- a/src/wavinfo/wave_smpl_reader.py +++ b/src/wavinfo/wave_smpl_reader.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import struct -from typing import List, NamedTuple, Tuple +from typing import NamedTuple class WaveSmplLoop(NamedTuple): @@ -69,13 +71,13 @@ class WavSmplReader: #: The SMPTE offset to apply, as a tuple of four ints representing #: hh, mm, ss, ff - self.smpte_offset: Tuple[int, int, int, int] = unpacked_data[6:10] + self.smpte_offset: tuple[int, int, int, int] = unpacked_data[6:10] loop_count = unpacked_data[10] sampler_udata_length = unpacked_data[11] #: List of loops in the file. - self.sample_loops: List[WaveSmplLoop] = [] + self.sample_loops: list[WaveSmplLoop] = [] loop_buffer = smpl_data[header_size : header_size + loop_size * loop_count] diff --git a/src/wavinfo/wavfind.py b/src/wavinfo/wavfind.py index ca64042..4181f3e 100644 --- a/src/wavinfo/wavfind.py +++ b/src/wavinfo/wavfind.py @@ -1,5 +1,8 @@ -""" """ +""" +wavfind.py +Under construction! Do Not Use! +""" import sys from optparse import OptionGroup, OptionParser @@ -21,7 +24,7 @@ def main(): primaries.add_option("--desc", help="Search descriptions", metavar="DESC") - (options, args) = parser.parse_args(sys.argv) + (_options, _args) = parser.parse_args(sys.argv) if __name__ == "__main__": diff --git a/tests/test_adm.py b/tests/test_adm.py index 5a4c279..7180c7a 100644 --- a/tests/test_adm.py +++ b/tests/test_adm.py @@ -42,12 +42,14 @@ class TestADMWave(TestCase): adm = info.adm assert adm is not None t1 = adm.track_info(0) - self.assertTrue("channel_format_name" in t1.keys()) + assert t1 is not None + self.assertTrue("channel_format_name" in t1) self.assertEqual("RoomCentricLeft", t1["channel_format_name"]) - self.assertTrue("pack_format_name" in t1.keys()) + self.assertTrue("pack_format_name" in t1) self.assertEqual("AtmosCustomPackFormat1", t1["pack_format_name"]) t10 = adm.track_info(10) - self.assertTrue("content_name" in t10.keys()) + assert t10 is not None + self.assertTrue("content_name" in t10) self.assertEqual("Dialog", t10["content_name"]) diff --git a/tests/test_main.py b/tests/test_main.py index 0fedd32..683c3ba 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -9,24 +9,15 @@ from wavinfo.__main__ import main class MainTest(unittest.TestCase): def test_empty_argv(self): with patch.object(sys, "argv", []): - try: - main() - except Exception as e: - self.fail(f"main() throwing an exception: {e}") + main() def test_a_file(self): for path in glob.glob("tests/test_files/**/*.wav"): with patch.object(sys, "argv", ["TEST", path]): - try: - main() - except Exception as e: - self.fail(f"main() throwing an exception: {e}") + main() def test_ixml(self): with patch.object( sys, "argv", ["TEST", "--ixml", "tests/test_files/sounddevices/A101_1.WAV"] ): - try: - main() - except Exception as e: - self.fail(f"main() throwing an exception: {e}") + main() diff --git a/tests/test_walk.py b/tests/test_walk.py index 58d0438..41d9661 100644 --- a/tests/test_walk.py +++ b/tests/test_walk.py @@ -11,12 +11,10 @@ class TestWalk(unittest.TestCase): tested_data, tested_format = False, False for scope, key, value in info.walk(): - if scope == "fmt": - if key == "channel_count": + if scope == "fmt" and key == "channel_count": tested_format = True self.assertEqual(value, 2) - if scope == "data": - if key == "frame_count": + if scope == "data" and key == "frame_count": tested_data = True self.assertEqual(value, 144140) @@ -26,11 +24,11 @@ class TestWalk(unittest.TestCase): for file in glob.glob("tests/test_files/**/*.wav"): info = wavinfo.WavInfoReader(file) - try: - for _, _, _ in info.walk(): - pass - except Exception as e: - self.fail(f"Failed to walk metadata in file {file}: {e}") + # try: + for _, _, _ in info.walk(): + pass + # except Exception as e: + # self.fail(f"Failed to walk metadata in file {file}: {e}") if __name__ == "__main__": diff --git a/tests/test_wave_parsing.py b/tests/test_wave_parsing.py index fbeec15..1804a84 100644 --- a/tests/test_wave_parsing.py +++ b/tests/test_wave_parsing.py @@ -14,7 +14,7 @@ class TestWaveInfo(TestCase): info = wavinfo.WavInfoReader(wav_file) self.assertEqual( info.__repr__(), - "WavInfoReader({}, latin_1, ascii)".format(os.path.abspath(wav_file)), + f"WavInfoReader({os.path.abspath(wav_file)}, latin_1, ascii)", ) self.assertIsNotNone(info) diff --git a/tests/utils.py b/tests/utils.py index 70f2b2d..0e0f847 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,6 @@ import json import os.path import subprocess -import sys from subprocess import PIPE FFPROBE = "ffprobe" @@ -9,23 +8,12 @@ FFPROBE = "ffprobe" def ffprobe(path): arguments = [FFPROBE, "-of", "json", "-show_format", "-show_streams", path] - if int(sys.version[0]) < 3: - process = subprocess.Popen(arguments, stdout=PIPE) - process.wait() - if process.returncode == 0: - output = process.communicate()[0] - if output: - output_str = output.decode("utf-8") - return json.loads(output_str) - else: - return None + process = subprocess.run(arguments, stdin=None, stdout=PIPE, stderr=PIPE, check=True) + if process.returncode == 0: + output_str = process.stdout.decode("utf-8") + return json.loads(output_str) else: - process = subprocess.run(arguments, stdin=None, stdout=PIPE, stderr=PIPE) - if process.returncode == 0: - output_str = process.stdout.decode("utf-8") - return json.loads(output_str) - else: - return None + return None def all_files():