diff --git a/docs/conf.py b/docs/conf.py index 5a4b5d7..5432b8f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python3 # noqa: EXE001 # # pyttsx3 documentation build configuration file, created by # sphinx-quickstart on Sun Jun 25 11:19:31 2017. diff --git a/example/__init__.py b/example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/main.py b/example/main.py index 65e3417..12ab5da 100644 --- a/example/main.py +++ b/example/main.py @@ -11,18 +11,14 @@ # VOLUME -volume = engine.getProperty( - "volume" -) # getting to know current volume level (min=0 and max=1) +volume = engine.getProperty("volume") # getting to know current volume level (min=0 and max=1) print(volume) # printing current volume level engine.setProperty("volume", 1.0) # setting up volume level between 0 and 1 # VOICE voices = engine.getProperty("voices") # getting details of current voice # engine.setProperty('voice', voices[0].id) #changing index, changes voices. 0 for male -engine.setProperty( - "voice", voices[1].id -) # changing index, changes voices. 1 for female +engine.setProperty("voice", voices[1].id) # changing index, changes voices. 1 for female # PITCH pitch = engine.getProperty("pitch") # Get current pitch value diff --git a/pyproject.toml b/pyproject.toml index 067231a..b2d8ec2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,5 +54,81 @@ packages = [ "pyttsx3", "pyttsx3.drivers" ] include-package-data = false [tool.ruff] +line-length = 100 target-version = "py39" -line-length = 88 + +[tool.ruff.lint] +select = [ + "AIR", # Airflow + "ASYNC", # flake8-async + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "C90", # McCabe cyclomatic complexity + "DJ", # flake8-django + "DTZ", # flake8-datetimez + "E", # pycodestyle + "EM", # flake8-errmsg + "EXE", # flake8-executable + "F", # Pyflakes + "FA", # flake8-future-annotations + "FLY", # flynt + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "INP", # flake8-no-pep420 + "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "NPY", # NumPy-specific rules + "PD", # pandas-vet + "PERF", # Perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # Pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "PYI", # flake8-pyi + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "SIM", # flake8-simplify + "SLOT", # flake8-slots + "T10", # flake8-debugger + "TCH", # flake8-type-checking + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 + # "A", # flake8-builtins + # "ANN", # flake8-annotations + # "ARG", # flake8-unused-arguments + # "COM", # flake8-commas + # "CPY", # flake8-copyright + # "D", # pydocstyle + # "DOC", # pydoclint + # "ERA", # eradicate + # "FAST", # FastAPI + # "FBT", # flake8-boolean-trap + # "FIX", # flake8-fixme + # "N", # pep8-naming + # "Q", # flake8-quotes + # "S", # flake8-bandit + # "SLF", # flake8-self + # "T20", # flake8-print + # "TD", # flake8-todos + # "TRY", # tryceratops +] +ignore = [ + "B904", # raise-without-from-inside-except + "BLE001", # blind-except + "D212", # Multi-line docstring summary should start at the first line + "ISC001", # implicit-str-concat conflicts with ruff format + "S101", # assert +] +[tool.ruff.lint.per-file-ignores] +"pyttsx3/drivers/_espeak.py" = ["E101", "E501"] + +[tool.ruff.lint.pylint] +allow-magic-value-types = ["int", "str"] diff --git a/pyttsx3/drivers/_espeak.py b/pyttsx3/drivers/_espeak.py index 1c93c9c..b5b0570 100644 --- a/pyttsx3/drivers/_espeak.py +++ b/pyttsx3/drivers/_espeak.py @@ -31,8 +31,8 @@ def cfunc(name, dll, result, *args): def load_library() -> bool: - global dll - paths = [ + global dll # noqa: PLW0603 + paths = ( # macOS paths "/opt/homebrew/lib/libespeak-ng.1.dylib", "/usr/local/lib/libespeak-ng.1.dylib", @@ -44,13 +44,13 @@ def load_library() -> bool: # Windows paths r"C:\Program Files\eSpeak NG\libespeak-ng.dll", r"C:\Program Files (x86)\eSpeak NG\libespeak-ng.dll", - ] + ) for path in paths: try: dll = cdll.LoadLibrary(path) return True - except Exception: + except Exception: # noqa: PERF203 continue # Try the next path return False @@ -74,11 +74,11 @@ def load_library() -> bool: class numberORname(Union): - _fields_ = [("number", c_int), ("name", c_char_p)] + _fields_ = (("number", c_int), ("name", c_char_p)) class EVENT(Structure): - _fields_ = [ + _fields_ = ( ("type", c_int), ("unique_identifier", c_uint), ("text_position", c_int), @@ -87,7 +87,7 @@ class EVENT(Structure): ("sample", c_int), ("user_data", c_void_p), ("id", numberORname), - ] + ) AUDIO_OUTPUT_PLAYBACK = 0 @@ -126,7 +126,7 @@ class EVENT(Structure): def SetSynthCallback(cb) -> None: - global SynthCallback + global SynthCallback # noqa: PLW0603 SynthCallback = t_espeak_callback(cb) cSetSynthCallback(SynthCallback) @@ -155,14 +155,12 @@ def SetSynthCallback(cb) -> None: t_UriCallback = CFUNCTYPE(c_int, c_int, c_char_p, c_char_p) -cSetUriCallback = cfunc( - "espeak_SetUriCallback", dll, None, ("UriCallback", t_UriCallback, 1) -) +cSetUriCallback = cfunc("espeak_SetUriCallback", dll, None, ("UriCallback", t_UriCallback, 1)) UriCallback = None def SetUriCallback(cb) -> None: - global UriCallback + global UriCallback # noqa: PLW0603 UriCallback = t_UriCallback(UriCallback) cSetUriCallback(UriCallback) @@ -202,7 +200,7 @@ def SetUriCallback(cb) -> None: POS_SENTENCE = 3 -def Synth( +def Synth( # noqa: PLR0913 text, position=0, position_type=POS_CHARACTER, @@ -384,9 +382,7 @@ def Synth_Mark(text, index_mark, end_position=0, flags=CHARS_AUTO) -> None: GetParameter.__doc__ = """current=0 Returns the default value of the specified parameter. current=1 Returns the current value of the specified parameter, as set by SetParameter()""" -SetPunctuationList = cfunc( - "espeak_SetPunctuationList", dll, c_int, ("punctlist", c_wchar, 1) -) +SetPunctuationList = cfunc("espeak_SetPunctuationList", dll, c_int, ("punctlist", c_wchar, 1)) SetPunctuationList.__doc__ = """Specified a list of punctuation characters whose names are to be spoken when the value of the Punctuation parameter is set to "some". @@ -419,7 +415,7 @@ def Synth_Mark(text, index_mark, end_position=0, flags=CHARS_AUTO) -> None: class VOICE(Structure): - _fields_ = [ + _fields_ = ( ("name", c_char_p), ("languages", c_char_p), ("identifier", c_char_p), @@ -429,14 +425,12 @@ class VOICE(Structure): ("xx1", c_ubyte), ("score", c_int), ("spare", c_void_p), - ] + ) def __repr__(self) -> str: """Print the fields.""" - res = [] - for field in self._fields_: - res.append(f"{field[0]}={getattr(self, field[0])!r}") - return self.__class__.__name__ + "(" + ",".join(res) + ")" + res = ",".join(f"{field[0]}={getattr(self, field[0])!r}" for field in self._fields_) + return f"{self.__class__.__name__}({res})" cListVoices = cfunc( diff --git a/pyttsx3/drivers/avspeech.py b/pyttsx3/drivers/avspeech.py index e993d52..5e5800b 100644 --- a/pyttsx3/drivers/avspeech.py +++ b/pyttsx3/drivers/avspeech.py @@ -28,7 +28,7 @@ from collections.abc import Iterator -def buildDriver(proxy): # noqa: N802, ANN001, ANN201 +def buildDriver(proxy): """Build an AVSpeech driver instance.""" driver = AVSpeechDriver.alloc().init() driver.setProxy(proxy) diff --git a/pyttsx3/drivers/espeak.py b/pyttsx3/drivers/espeak.py index d2554c1..85d317f 100644 --- a/pyttsx3/drivers/espeak.py +++ b/pyttsx3/drivers/espeak.py @@ -1,16 +1,17 @@ import ctypes +import logging import os import platform import subprocess import time import wave from tempfile import NamedTemporaryFile -import logging if platform.system() == "Windows": import winsound -from ..voice import Voice +from pyttsx3.voice import Voice + from . import _espeak @@ -31,7 +32,8 @@ def __init__(self, proxy): # so just keep it alive and init once rate = _espeak.Initialize(_espeak.AUDIO_OUTPUT_RETRIEVAL, 1000) if rate == -1: - raise RuntimeError("could not initialize espeak") + msg = "could not initialize espeak" + raise RuntimeError(msg) current_voice = _espeak.GetCurrentVoice() if current_voice and current_voice.contents.name: EspeakDriver._defaultVoice = current_voice.contents.name.decode("utf-8") @@ -85,9 +87,7 @@ def getProperty(name: str): if v.languages: try: language_code_bytes = v.languages[1:] - language_code = language_code_bytes.decode( - "utf-8", errors="ignore" - ) + language_code = language_code_bytes.decode("utf-8", errors="ignore") kwargs["languages"] = [language_code] except UnicodeDecodeError: kwargs["languages"] = ["Unknown"] @@ -107,33 +107,35 @@ def getProperty(name: str): return _espeak.GetParameter(_espeak.VOLUME) / 100.0 if name == "pitch": return _espeak.GetParameter(_espeak.PITCH) - raise KeyError("unknown property %s" % name) + msg = f"unknown property {name}" + raise KeyError(msg) @staticmethod - def setProperty(name: str, value): + def setProperty(name: str, value): # noqa: C901,PLR0912 if name == "voice": if value is None: return try: utf8Value = str(value).encode("utf-8") - logging.debug(f"Attempting to set voice to: {value}") + logging.debug(f"Attempting to set voice to: {value}") # noqa: G004 result = _espeak.SetVoiceByName(utf8Value) if result == 0: # EE_OK is 0 - logging.debug(f"Successfully set voice to: {value}") + logging.debug(f"Successfully set voice to: {value}") # noqa: G004 elif result == 1: # EE_BUFFER_FULL - raise ValueError( - f"SetVoiceByName failed: EE_BUFFER_FULL while setting voice to {value}" - ) + msg = f"SetVoiceByName failed: EE_BUFFER_FULL while setting voice to {value}" + raise ValueError(msg) elif result == 2: # EE_INTERNAL_ERROR - raise ValueError( - f"SetVoiceByName failed: EE_INTERNAL_ERROR while setting voice to {value}" - ) + msg = f"SetVoiceByName failed: EE_INTERNAL_ERROR while setting voice to {value}" + raise ValueError(msg) else: - raise ValueError( - f"SetVoiceByName failed with unknown return code {result} for voice: {value}" + msg = ( + "SetVoiceByName " + f"ailed with unknown return code {result} for voice: {value}" ) + raise ValueError(msg) except ctypes.ArgumentError as e: - raise ValueError(f"Invalid voice name: {value}, error: {e}") + msg = f"Invalid voice name: {value}, error: {e}" + raise ValueError(msg) elif name == "rate": try: _espeak.SetParameter(_espeak.RATE, value, 0) @@ -150,7 +152,8 @@ def setProperty(name: str, value): except TypeError as e: raise ValueError(str(e)) else: - raise KeyError("unknown property %s" % name) + msg = f"unknown property {name}" + raise KeyError(msg) def save_to_file(self, text, filename): """ @@ -165,15 +168,16 @@ def _start_synthesis(self, text): self._speaking = True self._data_buffer = b"" # Ensure buffer is cleared before starting try: - _espeak.Synth( - str(text).encode("utf-8"), flags=_espeak.ENDPAUSE | _espeak.CHARS_UTF8 - ) + _espeak.Synth(str(text).encode("utf-8"), flags=_espeak.ENDPAUSE | _espeak.CHARS_UTF8) except Exception as e: self._proxy.setBusy(False) self._proxy.notify("error", exception=e) raise - def _onSynth(self, wav, numsamples, events): + def _onSynth(self, wav, numsamples, events): # noqa: C901,PLR0912,PLR0915 + """ + TODO: Refactor this function because it is too complex by several measures. + """ if not self._speaking: return 0 @@ -208,12 +212,11 @@ def _onSynth(self, wav, numsamples, events): f.writeframes(self._data_buffer) print(f"Audio saved to {self._save_file}") except Exception as e: - raise RuntimeError(f"Error saving WAV file: {e}") + msg = f"Error saving WAV file: {e}" + raise RuntimeError(msg) else: try: - with NamedTemporaryFile( - suffix=".wav", delete=False - ) as temp_wav: + with NamedTemporaryFile(suffix=".wav", delete=False) as temp_wav: with wave.open(temp_wav, "wb") as f: f.setnchannels(1) # Mono f.setsampwidth(2) # 16-bit samples @@ -232,7 +235,7 @@ def _onSynth(self, wav, numsamples, events): winsound.PlaySound(temp_wav_name, winsound.SND_FILENAME) # Remove the file after playback - os.remove(temp_wav_name) + os.remove(temp_wav_name) # noqa: PTH107 except Exception as e: print(f"Playback error: {e}") @@ -248,9 +251,7 @@ def _onSynth(self, wav, numsamples, events): # Accumulate audio data if available if numsamples > 0: - self._data_buffer += ctypes.string_at( - wav, numsamples * ctypes.sizeof(ctypes.c_short) - ) + self._data_buffer += ctypes.string_at(wav, numsamples * ctypes.sizeof(ctypes.c_short)) return 0 diff --git a/pyttsx3/drivers/nsss.py b/pyttsx3/drivers/nsss.py index 6dca9ac..dc0b5e6 100644 --- a/pyttsx3/drivers/nsss.py +++ b/pyttsx3/drivers/nsss.py @@ -60,7 +60,7 @@ def onPumpFirst_(self, timer) -> None: self._proxy.setBusy(False) def startLoop(self) -> None: - # https://github.com/ronaldoussoren/pyobjc/blob/master/pyobjc-framework-Cocoa/Lib/PyObjCTools/AppHelper.py#L243C25-L243C25 # noqa + # https://github.com/ronaldoussoren/pyobjc/blob/master/pyobjc-framework-Cocoa/Lib/PyObjCTools/AppHelper.py#L243C25-L243C25 # noqa: E501 NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_( 0.0, self, "onPumpFirst:", None, False ) @@ -69,9 +69,7 @@ def startLoop(self) -> None: PyObjCAppHelperRunLoopStopper.addRunLoopStopper_toRunLoop_(stopper, runLoop) while stopper.shouldRun(): nextfire = runLoop.limitDateForMode_(NSDefaultRunLoopMode) - soon = NSDate.dateWithTimeIntervalSinceNow_( - 0 - ) # maxTimeout in runConsoleEventLoop + soon = NSDate.dateWithTimeIntervalSinceNow_(0) # maxTimeout in runConsoleEventLoop if nextfire is not None: nextfire = soon.earlierDate_(nextfire) if not runLoop.runMode_beforeDate_(NSDefaultRunLoopMode, nextfire): @@ -163,10 +161,11 @@ def speechSynthesizer_didFinishSpeaking_(self, tts, success) -> None: self._proxy.setBusy(False) def speechSynthesizer_willSpeakWord_ofString_(self, tts, rng, text) -> None: - if self._current_text: - current_word = self._current_text[rng.location : rng.location + rng.length] - else: - current_word = "Unknown" + current_word = ( + self._current_text[rng.location : rng.location + rng.length] + if self._current_text + else "Unknown" + ) self._proxy.notify( "started-word", name=current_word, location=rng.location, length=rng.length diff --git a/pyttsx3/drivers/sapi5.py b/pyttsx3/drivers/sapi5.py index 21d8a41..67e1f81 100644 --- a/pyttsx3/drivers/sapi5.py +++ b/pyttsx3/drivers/sapi5.py @@ -11,11 +11,11 @@ from comtypes.gen import SpeechLib # noinspection PyUnresolvedReferences +import locale import math import os import time import weakref -import locale import pythoncom @@ -85,7 +85,7 @@ def stop(self) -> None: self._tts.Speak("", 3) def save_to_file(self, text, filename) -> None: - cwd = os.getcwd() + cwd = os.getcwd() # noqa: PTH109 stream = comtypes.client.CreateObject("SAPI.SPFileStream") stream.Open(filename, SpeechLib.SSFMCreateForWrite) temp_stream = self._tts.AudioOutputStream @@ -118,9 +118,7 @@ def _toVoice(attr): age = age_attr if age_attr in {"Child", "Teen", "Adult", "Senior"} else None # Create and return the Voice object with additional attributes - return Voice( - id=voice_id, name=voice_name, languages=languages, gender=gender, age=age - ) + return Voice(id=voice_id, name=voice_name, languages=languages, gender=gender, age=age) def _tokenFromId(self, id_): tokens = self._tts.GetVoices() @@ -199,9 +197,7 @@ def setDriver(self, driver) -> None: self._driver = driver def _ISpeechVoiceEvents_StartStream(self, stream_number, stream_position) -> None: - self._driver._proxy.notify( - "started-word", location=stream_number, length=stream_position - ) + self._driver._proxy.notify("started-word", location=stream_number, length=stream_position) def _ISpeechVoiceEvents_EndStream(self, stream_number, stream_position) -> None: d = self._driver @@ -212,14 +208,11 @@ def _ISpeechVoiceEvents_EndStream(self, stream_number, stream_position) -> None: d._proxy.setBusy(False) d.endLoop() # hangs if you dont have this - def _ISpeechVoiceEvents_Word( - self, stream_number, stream_position, char, length - ) -> None: - if current_text := self._driver._current_text: - current_word = current_text[char : char + length] - else: - current_word = "Unknown" - - self._driver._proxy.notify( - "started-word", name=current_word, location=char, length=length + def _ISpeechVoiceEvents_Word(self, stream_number, stream_position, char, length) -> None: + current_word = ( + current_text[char : char + length] + if (current_text := self._driver._current_text) + else "Unknown" ) + + self._driver._proxy.notify("started-word", name=current_word, location=char, length=length) diff --git a/pyttsx3/engine.py b/pyttsx3/engine.py index 1d75be8..b77c4a7 100644 --- a/pyttsx3/engine.py +++ b/pyttsx3/engine.py @@ -83,7 +83,7 @@ def _notify(self, topic: str, **kwargs) -> None: for cb in self._connects.get(topic, []): try: cb(**kwargs) - except Exception: + except Exception: # noqa: PERF203 if self._debug: traceback.print_exc() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_engines.py b/tests/test_engines.py index fe21de3..7b8bff1 100644 --- a/tests/test_engines.py +++ b/tests/test_engines.py @@ -60,7 +60,7 @@ def test_espeak_voices(driver_name) -> None: # Define the expected English voice IDs (excluding Caribbean for now as not in some envs # Linux eSpeak-NG v1.50 has 7 English voices, # macOS eSpeak-NG v1.51 and Windows eSpeak-NG v1.52-dev have 8 English voices. - english_voice_ids = [ + english_voice_ids = ( "gmw/en", # Great Britain "gmw/en-GB-scotland", # Scotland "gmw/en-GB-x-gbclan", # Lancaster @@ -68,7 +68,7 @@ def test_espeak_voices(driver_name) -> None: "gmw/en-GB-x-rp", # Received Pronunciation "gmw/en-US", # America "gmw/en-US-nyc", # America, New York City - ] + ) for voice_id in english_voice_ids: target_voice = next((v for v in voices if v.id == voice_id), None) @@ -84,7 +84,8 @@ def test_espeak_voices(driver_name) -> None: print(f"Current voice ID: {current_voice}") if current_voice != target_voice.id: print( - f"Voice change mismatch. Expected: {target_voice.id}, Got: {current_voice}. Skipping." + "Voice change mismatch. " + f"Expected: {target_voice.id}, Got: {current_voice}. Skipping." ) continue @@ -108,21 +109,18 @@ def test_apple_nsss_voices(driver_name) -> None: voice = engine.getProperty("voice") # On macOS v14.x, the default nsss voice is com.apple.voice.compact.en-US.Samantha. # ON macOS v15.x, the default nsss voice is "" - assert ( - voice in {"", "com.apple.voice.compact.en-US.Samantha"} - ), "Expected default voice to be com.apple.voice.compact.en-US.Samantha on macOS and iOS" + if voice: + assert ( + voice == "com.apple.voice.compact.en-US.Samantha" + ), "Expected default voice to be com.apple.voice.compact.en-US.Samantha on macOS and iOS" voices = engine.getProperty("voices") # On macOS v13.x or v14.x, nsss has 143 voices. # On macOS v15.x, nsss has 176 voices print(f"On macOS v{macos_version}, {engine} has {len(voices) = } voices.") assert len(voices) in {176, 143}, "Expected 176 or 143 voices on macOS and iOS" # print("\n".join(voice.id for voice in voices)) - en_us_voices = [ - voice for voice in voices if voice.id.startswith("com.apple.eloquence.en-US.") - ] - assert ( - len(en_us_voices) == 8 - ), "Expected 8 com.apple.eloquence.en-US voices on macOS and iOS" + en_us_voices = [voice for voice in voices if voice.id.startswith("com.apple.eloquence.en-US.")] + assert len(en_us_voices) == 8, "Expected 8 com.apple.eloquence.en-US voices on macOS and iOS" names = [] for _voice in en_us_voices: engine.setProperty("voice", _voice.id) diff --git a/tests/test_pyttsx3.py b/tests/test_pyttsx3.py index c4350ab..1b706fb 100644 --- a/tests/test_pyttsx3.py +++ b/tests/test_pyttsx3.py @@ -2,13 +2,16 @@ import sys import wave -from pathlib import Path +from typing import TYPE_CHECKING from unittest import mock import pytest import pyttsx3 +if TYPE_CHECKING: + from pathlib import Path + quick_brown_fox = "The quick brown fox jumped over the lazy dog." @@ -27,9 +30,7 @@ def test_engine_name(engine) -> None: assert repr(engine) == f"pyttsx3.engine.Engine('{expected}', debug=False)" -@pytest.mark.skipif( - sys.platform == "win32", reason="TODO: Fix this test to pass on Windows" -) +@pytest.mark.skipif(sys.platform == "win32", reason="TODO: Fix this test to pass on Windows") def test_speaking_text(engine) -> None: engine.say("Sally sells seashells by the seashore.") engine.say(quick_brown_fox) @@ -37,9 +38,7 @@ def test_speaking_text(engine) -> None: engine.runAndWait() -@pytest.mark.skipif( - sys.platform not in ("darwin", "ios"), reason="Testing only on macOS and iOS" -) +@pytest.mark.skipif(sys.platform not in ("darwin", "ios"), reason="Testing only on macOS and iOS") def test_apple_avspeech_voices(engine): import platform @@ -64,12 +63,8 @@ def test_apple_avspeech_voices(engine): print(f"On macOS v{macos_version}, {engine} has {len(voices) = } voices.") assert len(voices) in (176, 143), "Expected 176 or 143 voices on macOS and iOS" # print("\n".join(voice.id for voice in voices)) - en_us_voices = [ - voice for voice in voices if voice.id.startswith("com.apple.eloquence.en-US.") - ] - assert ( - len(en_us_voices) == 8 - ), "Expected 8 com.apple.eloquence.en-US voices on macOS and iOS" + en_us_voices = [voice for voice in voices if voice.id.startswith("com.apple.eloquence.en-US.")] + assert len(en_us_voices) == 8, "Expected 8 com.apple.eloquence.en-US voices on macOS and iOS" names = [] for _voice in en_us_voices: engine.setProperty("voice", _voice.id) @@ -83,9 +78,7 @@ def test_apple_avspeech_voices(engine): engine.setProperty("voice", voice) # Reset voice to original value -@pytest.mark.skipif( - sys.platform not in ("darwin", "ios"), reason="Testing only on macOS and iOS" -) +@pytest.mark.skipif(sys.platform not in ("darwin", "ios"), reason="Testing only on macOS and iOS") def test_apple_nsss_voices(engine): import platform @@ -97,8 +90,9 @@ def test_apple_nsss_voices(engine): voice = engine.getProperty("voice") # On macOS v14.x, the default nsss voice is com.apple.voice.compact.en-US.Samantha. # ON macOS v15.x, the default nsss voice is "" - assert ( - voice in ("", "com.apple.voice.compact.en-US.Samantha") + assert voice in ( + "", + "com.apple.voice.compact.en-US.Samantha", ), "Expected default voice to be com.apple.voice.compact.en-US.Samantha on macOS and iOS" voices = engine.getProperty("voices") # On macOS v14.x, nsss has 143 voices. @@ -106,12 +100,8 @@ def test_apple_nsss_voices(engine): print(f"On macOS v{macos_version}, {engine} has {len(voices) = } voices.") assert len(voices) in (176, 143), "Expected 176 or 143 voices on macOS and iOS" # print("\n".join(voice.id for voice in voices)) - en_us_voices = [ - voice for voice in voices if voice.id.startswith("com.apple.eloquence.en-US.") - ] - assert ( - len(en_us_voices) == 8 - ), "Expected 8 com.apple.eloquence.en-US voices on macOS and iOS" + en_us_voices = [voice for voice in voices if voice.id.startswith("com.apple.eloquence.en-US.")] + assert len(en_us_voices) == 8, "Expected 8 com.apple.eloquence.en-US voices on macOS and iOS" names = [] for _voice in en_us_voices: engine.setProperty("voice", _voice.id) @@ -151,9 +141,7 @@ def test_saving_to_file(engine, tmp_path: Path) -> None: assert sys.platform in {"darwin", "ios"}, "Apple writes .aiff, not .wav files." -@pytest.mark.skipif( - sys.platform == "win32", reason="TODO: Fix this test to pass on Windows" -) +@pytest.mark.skipif(sys.platform == "win32", reason="TODO: Fix this test to pass on Windows") def test_listening_for_events(engine) -> None: onStart = mock.Mock() onWord = mock.Mock() @@ -190,9 +178,7 @@ def onWord(name, location, length) -> None: assert onWord_mock.called -@pytest.mark.skipif( - sys.platform == "win32", reason="TODO: Fix this test to pass on Windows" -) +@pytest.mark.skipif(sys.platform == "win32", reason="TODO: Fix this test to pass on Windows") def test_changing_speech_rate(engine) -> None: rate = engine.getProperty("rate") rate_plus_fifty = rate + 50 @@ -202,9 +188,7 @@ def test_changing_speech_rate(engine) -> None: engine.setProperty("rate", rate) # Reset rate to original value -@pytest.mark.skipif( - sys.platform == "win32", reason="TODO: Fix this test to pass on Windows" -) +@pytest.mark.skipif(sys.platform == "win32", reason="TODO: Fix this test to pass on Windows") def test_changing_volume(engine) -> None: volume = engine.getProperty("volume") volume_minus_a_quarter = volume - 0.25 @@ -214,22 +198,16 @@ def test_changing_volume(engine) -> None: engine.setProperty("volume", volume) # Reset volume to original value -@pytest.mark.skipif( - sys.platform == "win32", reason="TODO: Fix this test to pass on Windows" -) +@pytest.mark.skipif(sys.platform == "win32", reason="TODO: Fix this test to pass on Windows") def test_changing_voices(engine) -> None: voices = engine.getProperty("voices") - for ( - voice - ) in voices: # TODO: This could be lots of voices! (e.g. 176 on macOS v15.x) + for voice in voices: # TODO: This could be lots of voices! (e.g. 176 on macOS v15.x) engine.setProperty("voice", voice.id) engine.say(f"{voice.id = }. {quick_brown_fox}") engine.runAndWait() -@pytest.mark.skipif( - sys.platform == "win32", reason="TODO: Fix this test to pass on Windows" -) +@pytest.mark.skipif(sys.platform == "win32", reason="TODO: Fix this test to pass on Windows") def test_running_driver_event_loop(engine) -> None: def onStart(name) -> None: print("starting", name)