🐛 | Fix SonarQube issues across codebase
All checks were successful
SonarQube Code Quality Scan / SonarQube Scan (push) Successful in 1m41s

- Add docstrings to all empty Qt stub methods (S1186)
- Fix BLOCKER S1845: rename playbackState to get_playback_state
- Rename enum fields to snake_case with CamelCase aliases (S116)
- Rename stub methods to snake_case (set_source, set_position, etc.) (S100)
- Rename unused timeout param to msecs (S1172)
- Remove unused state variables in tests (S1481)
- Replace 'is' with '==' for enum comparisons (S5795)
- Remove unnecessary list() calls (S7504)
- Rename QTh to qt_cls (S117)
- Remove commented-out code blocks (S125)
- Add docstring to FallbackServiceInterface.emit_properties_changed (S1186)
This commit is contained in:
2026-06-09 18:34:54 +03:00
parent e2d64cd464
commit 7a70232239
6 changed files with 82 additions and 60 deletions

View File

@@ -52,7 +52,7 @@ except ImportError:
def __init__(self, name: str) -> None: def __init__(self, name: str) -> None:
self._interface_name = name self._interface_name = name
def emit_properties_changed(self, changed, invalidated) -> None: def emit_properties_changed(self, changed, invalidated) -> None:
pass """Stub — no-op; real implementation emits D-Bus PropertiesChanged."""
ServiceInterface = _FallbackServiceInterface # type: ignore[assignment,misc] ServiceInterface = _FallbackServiceInterface # type: ignore[assignment,misc]
dbus_method = lambda **kw: lambda f: f dbus_method = lambda **kw: lambda f: f

103
player.py
View File

@@ -61,9 +61,13 @@ except (ImportError, OSError):
class _StubQThread: class _StubQThread:
"""Stub replacement for PySide6.QtCore.QThread.""" """Stub replacement for PySide6.QtCore.QThread."""
def start(self) -> None: pass def start(self) -> None:
def quit(self) -> None: pass """Stub — no-op; real QThread would start the event loop."""
def wait(self, timeout: int = 3000) -> bool: return True def quit(self) -> None:
"""Stub — no-op; real QThread would exit the event loop."""
def wait(self, msecs: int = 3000) -> bool:
"""Stub — returns True immediately without waiting."""
return True
QThread = _StubQThread QThread = _StubQThread
Signal = lambda *a: lambda f: f Signal = lambda *a: lambda f: f
@@ -71,47 +75,84 @@ except (ImportError, OSError):
QUrl = None QUrl = None
class _StubPlaybackState: class _StubPlaybackState:
StoppedState = 0 """Stub mirroring QMediaPlayer.PlaybackState enum values."""
PlayingState = 1 stopped_state = 0
PausedState = 2 playing_state = 1
paused_state = 2
# Backward-compatible aliases for code that uses Qt CamelCase names.
StoppedState = stopped_state
PlayingState = playing_state
PausedState = paused_state
class _StubMediaStatus: class _StubMediaStatus:
NoMedia = 0 """Stub mirroring QMediaPlayer.MediaStatus enum values."""
LoadingMedia = 1 no_media = 0
LoadedMedia = 2 loading_media = 1
StalledMedia = 3 loaded_media = 2
BufferingMedia = 4 stalled_media = 3
BufferedMedia = 5 buffering_media = 4
EndOfMedia = 6 buffered_media = 5
InvalidMedia = 7 end_of_media = 6
invalid_media = 7
# Backward-compatible aliases for code that uses Qt CamelCase names.
NoMedia = no_media
LoadingMedia = loading_media
LoadedMedia = loaded_media
StalledMedia = stalled_media
BufferingMedia = buffering_media
BufferedMedia = buffered_media
EndOfMedia = end_of_media
InvalidMedia = invalid_media
class _StubError: class _StubError:
NoError = 0 """Stub mirroring QMediaPlayer.Error enum values."""
ResourceError = 1 no_error = 0
FormatError = 2 resource_error = 1
NetworkError = 3 format_error = 2
AccessDeniedError = 4 network_error = 3
ServiceMissingError = 5 access_denied_error = 4
service_missing_error = 5
# Backward-compatible aliases for code that uses Qt CamelCase names.
NoError = no_error
ResourceError = resource_error
FormatError = format_error
NetworkError = network_error
AccessDeniedError = access_denied_error
ServiceMissingError = service_missing_error
class _StubQMediaPlayer: class _StubQMediaPlayer:
"""Stub replacement for PySide6.QtMultimedia.QMediaPlayer."""
PlaybackState = _StubPlaybackState PlaybackState = _StubPlaybackState
MediaStatus = _StubMediaStatus MediaStatus = _StubMediaStatus
Error = _StubError Error = _StubError
def __init__(self): def __init__(self):
self._playback_state = _StubPlaybackState.StoppedState self._playback_state = _StubPlaybackState.stopped_state
def setSource(self, url) -> None: pass def set_source(self, url) -> None:
def play(self) -> None: pass """Stub — no-op; real QMediaPlayer would load media from *url*."""
def pause(self) -> None: pass def play(self) -> None:
def stop(self) -> None: pass """Stub — no-op; real QMediaPlayer would start playback."""
def setPosition(self, ms: int) -> None: pass def pause(self) -> None:
def position(self) -> int: return 0 """Stub — no-op; real QMediaPlayer would pause playback."""
def duration(self) -> int: return 0 def stop(self) -> None:
def playbackState(self) -> int: """Stub — no-op; real QMediaPlayer would stop playback."""
def set_position(self, ms: int) -> None:
"""Stub — no-op; real QMediaPlayer would seek to *ms*."""
def position(self) -> int:
"""Stub — returns 0; real QMediaPlayer returns current position in ms."""
return 0
def duration(self) -> int:
"""Stub — returns 0; real QMediaPlayer returns media duration in ms."""
return 0
def get_playback_state(self) -> int:
"""Stub — returns the current playback state value."""
return self._playback_state return self._playback_state
def audioOutput(self): return None def get_audio_output(self):
def setAudioOutput(self, output) -> None: pass """Stub — returns None; real QMediaPlayer returns its QAudioOutput."""
return None
def set_audio_output(self, output) -> None:
"""Stub — no-op; real QMediaPlayer would set its audio output."""
QMediaPlayer = _StubQMediaPlayer QMediaPlayer = _StubQMediaPlayer
QAudioOutput = None QAudioOutput = None

View File

@@ -72,11 +72,6 @@ def visualizer_mock():
return mock.MagicMock(name="visualizer") return mock.MagicMock(name="visualizer")
# ═══════════════════════════════════════════════════════════════════════════════
# AudioVisualizer
# ═══════════════════════════════════════════════════════════════════════════════
class TestAudioVisualizer: class TestAudioVisualizer:
def test_reset_activity(self, app): def test_reset_activity(self, app):
@@ -90,11 +85,6 @@ class TestAudioVisualizer:
_cleanup(viz, app) _cleanup(viz, app)
# ═══════════════════════════════════════════════════════════════════════════════
# PlaybackBar
# ═══════════════════════════════════════════════════════════════════════════════
class TestPlaybackBar: class TestPlaybackBar:
def test_clear_seek_flag(self, app, player_mock, visualizer_mock): def test_clear_seek_flag(self, app, player_mock, visualizer_mock):
@@ -154,11 +144,6 @@ class TestPlaybackBar:
_cleanup(bar, app) _cleanup(bar, app)
# ═══════════════════════════════════════════════════════════════════════════════
# SearchPage
# ═══════════════════════════════════════════════════════════════════════════════
class TestSearchPage: class TestSearchPage:
def test_on_show_more_clicked_from_button(self, app): def test_on_show_more_clicked_from_button(self, app):

View File

@@ -27,7 +27,7 @@ import pytest
def _clean_pyside_modules(): def _clean_pyside_modules():
"""Remove PySide6 and target modules from sys.modules.""" """Remove PySide6 and target modules from sys.modules."""
keys_to_del = [] keys_to_del = []
for key in list(sys.modules.keys()): for key in sys.modules:
if key.startswith("PySide6") or key in ("player", "gui", "discord_rpc"): if key.startswith("PySide6") or key in ("player", "gui", "discord_rpc"):
keys_to_del.append(key) keys_to_del.append(key)
for key in keys_to_del: for key in keys_to_del:
@@ -73,7 +73,7 @@ class TestPlayerQtFallback:
sys.modules.pop("player", None) sys.modules.pop("player", None)
# PySide6 modules may have been removed, restore them by clearing # PySide6 modules may have been removed, restore them by clearing
# the cached import so that a fresh import re-imports the real lib. # the cached import so that a fresh import re-imports the real lib.
for key in list(sys.modules.keys()): for key in sys.modules:
if key.startswith("PySide6") and key not in _ORIGINAL_PYSIDE_MODS: if key.startswith("PySide6") and key not in _ORIGINAL_PYSIDE_MODS:
del sys.modules[key] del sys.modules[key]
@@ -145,8 +145,8 @@ class TestPlayerQtFallback:
assert callable(player.QThread.quit) assert callable(player.QThread.quit)
assert callable(player.QThread.wait) assert callable(player.QThread.wait)
# Quick functional check # Quick functional check
QTh = player.QThread qt_cls = player.QThread
inst = QTh() inst = qt_cls()
inst.start() # should not raise inst.start() # should not raise
inst.quit() # should not raise inst.quit() # should not raise
assert inst.wait() is True assert inst.wait() is True
@@ -212,7 +212,7 @@ class TestGuiQtFallback:
"""Remove stale fallback modules so subsequent tests get the real ones.""" """Remove stale fallback modules so subsequent tests get the real ones."""
for mod_name in ("gui", "player", "discord_rpc"): for mod_name in ("gui", "player", "discord_rpc"):
sys.modules.pop(mod_name, None) sys.modules.pop(mod_name, None)
for key in list(sys.modules.keys()): for key in sys.modules:
if key.startswith("PySide6") and key not in _ORIGINAL_PYSIDE_MODS: if key.startswith("PySide6") and key not in _ORIGINAL_PYSIDE_MODS:
del sys.modules[key] del sys.modules[key]

View File

@@ -21,10 +21,6 @@ from unittest import mock
import pytest import pytest
# ═══════════════════════════════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════════════════════════════
def _reload_main(): def _reload_main():
"""Re-import the ``main`` module with a fresh module object. """Re-import the ``main`` module with a fresh module object.
@@ -113,13 +109,13 @@ class TestModuleConstants:
"""``_LOG_LEVEL`` is ``logging.INFO`` when not verbose.""" """``_LOG_LEVEL`` is ``logging.INFO`` when not verbose."""
with mock.patch.dict("os.environ", {}, clear=True): with mock.patch.dict("os.environ", {}, clear=True):
main_mod = _reload_main() main_mod = _reload_main()
assert main_mod._LOG_LEVEL is logging.INFO assert main_mod._LOG_LEVEL == logging.INFO
def test_log_level_debug_when_verbose(self): def test_log_level_debug_when_verbose(self):
"""``_LOG_LEVEL`` is ``logging.DEBUG`` when verbose.""" """``_LOG_LEVEL`` is ``logging.DEBUG`` when verbose."""
with mock.patch.dict("os.environ", {"TUNETTI_VERBOSE": "1"}, clear=True): with mock.patch.dict("os.environ", {"TUNETTI_VERBOSE": "1"}, clear=True):
main_mod = _reload_main() main_mod = _reload_main()
assert main_mod._LOG_LEVEL is logging.DEBUG assert main_mod._LOG_LEVEL == logging.DEBUG
# ── _LOG_FORMAT ────────────────────────────────────────────────────── # ── _LOG_FORMAT ──────────────────────────────────────────────────────

View File

@@ -217,7 +217,7 @@ class TestMprisPlayerInterface:
assert iface.CanControl is True assert iface.CanControl is True
def test_methods_emit_via_bridge(self): def test_methods_emit_via_bridge(self):
state, bridge, iface = self._make() _, bridge, iface = self._make()
for method_name, signal_name in [ for method_name, signal_name in [
("Play", "play_requested"), ("Play", "play_requested"),
("Pause", "pause_requested"), ("Pause", "pause_requested"),
@@ -231,7 +231,7 @@ class TestMprisPlayerInterface:
mock_sig.emit.assert_called_once() mock_sig.emit.assert_called_once()
def test_seek_emits_with_offset(self): def test_seek_emits_with_offset(self):
state, bridge, iface = self._make() _, bridge, iface = self._make()
with mock.patch.object(bridge, "seek_requested") as mock_sig: with mock.patch.object(bridge, "seek_requested") as mock_sig:
iface.Seek(5000) iface.Seek(5000)
mock_sig.emit.assert_called_once_with(5000) mock_sig.emit.assert_called_once_with(5000)