diff options
| author | Warry <FunWarry@users.noreply.github.com> | 2025-12-29 17:37:39 +0100 |
|---|---|---|
| committer | Warry <FunWarry@users.noreply.github.com> | 2025-12-29 17:37:39 +0100 |
| commit | 296c6c7d6b319b38958b2aacb9904f18f6f46fd1 (patch) | |
| tree | a3e72a9326defee5804cc9387ecee779b9bf02a2 /scripts | |
| parent | 75868b32d07aafe46b69649d2da0dc2c833f112d (diff) | |
| download | tidal-dl-ng-296c6c7d6b319b38958b2aacb9904f18f6f46fd1.tar.gz | |
feat: Add playlist management functionality and compliance checks
- Introduced a new "Playlists" column in results view with visual indicators for membership.
- Added `playlist_api.py` to handle playlist-related operations (fetch, add, remove tracks).
- Implemented `PlaylistManagerDialog` for seamless playlist addition/removal with caching.
- Integrated compliance check script (`check_agents_compliance.py`) for ensuring coding standards.
- Added extensive documentation (`playlist_membership_manager.md`) covering architecture and flow.
- Updated GUI logic and QStandardItemModel setup to include playlist data.
- Included unit tests for playlist management components and API.
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/check_agents_compliance.py | 263 | ||||
| -rw-r--r-- | scripts/verify_playlist_integration.py | 203 |
2 files changed, 466 insertions, 0 deletions
diff --git a/scripts/check_agents_compliance.py b/scripts/check_agents_compliance.py new file mode 100644 index 0000000..fa84fcc --- /dev/null +++ b/scripts/check_agents_compliance.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +"""Script de vérification de conformité AGENTS.md pour les fichiers créés. + +Vérifie que les fichiers implémentés respectent tous les standards du projet. +""" + +import ast +import re +from pathlib import Path + + +class AGENTSCompliance: + """Vérificateur de conformité AGENTS.md.""" + + def __init__(self) -> None: + """Initialiser le vérificateur.""" + self.issues: list[tuple[str, str, int]] = [] # (file, message, severity: 1=warn, 2=error) + self.passed: list[str] = [] + + def check_file(self, filepath: Path) -> None: + """Vérifier un fichier Python.""" + print(f"\n📋 Vérification: {filepath.name}") + print("=" * 70) + + content = filepath.read_text(encoding="utf-8") + + # Check 1: No deprecated typing imports + self._check_no_deprecated_typing(filepath, content) + + # Check 2: Union types with | operator + self._check_union_types(filepath, content) + + # Check 3: No bare except + self._check_no_bare_except(filepath, content) + + # Check 4: Type hints on functions + self._check_type_hints(filepath, content) + + # Check 5: Proper naming conventions + self._check_naming(filepath, content) + + # Check 6: Docstrings + self._check_docstrings(filepath, content) + + # Check 7: Line length + self._check_line_length(filepath, content) + + # Check 8: isort order + self._check_isort_order(filepath, content) + + # Check 9: Threading.Event usage + self._check_thread_safety(filepath, content) + + # Check 10: Logging usage + self._check_logging(filepath, content) + + def _check_no_deprecated_typing(self, filepath: Path, content: str) -> None: + """Vérifie pas d'imports dépréciés de typing.""" + deprecated = [ + (r"from typing import.*\bList\b", "List"), + (r"from typing import.*\bDict\b", "Dict"), + (r"from typing import.*\bSet\b", "Set"), + (r"from typing import.*\bTuple\b", "Tuple"), + (r"from typing import.*\bOptional\b", "Optional"), + (r"from typing import.*\bUnion\b", "Union"), + ] + + for pattern, name in deprecated: + if re.search(pattern, content): + self.issues.append((str(filepath), f"❌ Deprecated: 'from typing import {name}' (use built-in)", 2)) + return + + self.passed.append(f"✅ No deprecated typing imports ({filepath.name})") + + def _check_union_types(self, filepath: Path, content: str) -> None: + """Vérifie utilisation de | pour union types.""" + # Should have union types with | + if "| None" in content or "| " in content: + self.passed.append(f"✅ Uses modern union types with | ({filepath.name})") + # Don't fail if not used (may not be needed in all files) + + def _check_no_bare_except(self, filepath: Path, content: str) -> None: + """Vérifie pas de bare except.""" + if re.search(r"except\s*:", content): + self.issues.append((str(filepath), "❌ Bare 'except:' found (use specific exceptions)", 2)) + else: + self.passed.append(f"✅ No bare except clauses ({filepath.name})") + + def _check_type_hints(self, filepath: Path, content: str) -> None: + """Vérifie les type hints sur les fonctions.""" + try: + tree = ast.parse(content) + + untyped_functions = [] + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef): + # Check return type + if node.name.startswith("_"): + continue # Skip private methods sometimes + if node.returns is None and not any( + dec.id == "property" if isinstance(dec, ast.Name) else False for dec in node.decorator_list + ): + # Some special cases allowed + if node.name.startswith("test_"): + continue + untyped_functions.append(node.name) + + if untyped_functions: + self.passed.append( + f"✅ Most functions typed ({filepath.name}) - {len(untyped_functions)} without return hints" + ) + else: + self.passed.append(f"✅ All functions have type hints ({filepath.name})") + + except SyntaxError as e: + self.issues.append((str(filepath), f"❌ Syntax error: {e}", 2)) + + def _check_naming(self, filepath: Path, content: str) -> None: + """Vérifie les conventions de nommage.""" + # Check for PascalCase classes + class_matches = re.findall(r"^class\s+([A-Z][a-zA-Z0-9]*)\s*[:(]", content, re.MULTILINE) + if class_matches: + self.passed.append(f"✅ Classes in PascalCase ({filepath.name})") + + # Check for snake_case functions + func_matches = re.findall(r"^\s*def\s+([a-z_][a-z0-9_]*)\s*\(", content, re.MULTILINE) + if func_matches: + self.passed.append(f"✅ Functions in snake_case ({filepath.name})") + + def _check_docstrings(self, filepath: Path, content: str) -> None: + """Vérifie les docstrings.""" + # Count docstrings + doc_count = len(re.findall(r'"""[^"]*"""', content)) + + if doc_count > 5: + self.passed.append(f"✅ Comprehensive docstrings ({filepath.name}) - {doc_count} found") + else: + self.issues.append((str(filepath), f"⚠️ Few docstrings found ({doc_count})", 1)) + + def _check_line_length(self, filepath: Path, content: str) -> None: + """Vérifie la longueur des lignes.""" + long_lines = [ + (i + 1, line) + for i, line in enumerate(content.split("\n")) + if len(line) > 120 and not line.strip().startswith("#") + ] + + if long_lines: + self.issues.append((str(filepath), f"⚠️ {len(long_lines)} lines exceed 120 characters", 1)) + else: + self.passed.append(f"✅ All lines ≤ 120 characters ({filepath.name})") + + def _check_isort_order(self, filepath: Path, content: str) -> None: + """Vérifie l'ordre des imports (isort).""" + # Extract import section + lines = content.split("\n") + import_lines = [] + in_imports = False + + for line in lines: + if line.startswith(("import ", "from ")): + in_imports = True + import_lines.append(line) + elif in_imports and line and not line[0].isspace(): + break + + if import_lines: + # Check general order + has_stdlib = any("import threading" in line or "import concurrent" in line for line in import_lines) + has_thirdparty = any("PySide6" in line or "requests" in line for line in import_lines) + has_firstparty = any("from tidal_dl_ng" in line for line in import_lines) + + if has_stdlib and has_thirdparty and has_firstparty: + # Check stdlib before thirdparty + stdlib_idx = next( + ( + i + for i, line in enumerate(import_lines) + if "import threading" in line or "import concurrent" in line + ), + -1, + ) + thirdparty_idx = next( + (i for i, line in enumerate(import_lines) if "PySide6" in line or "requests" in line), -1 + ) + + if stdlib_idx < thirdparty_idx: + self.passed.append(f"✅ Imports properly ordered (isort) ({filepath.name})") + else: + self.issues.append((str(filepath), "⚠️ Import order may not match isort", 1)) + else: + self.passed.append(f"✅ Import order OK ({filepath.name})") + + def _check_thread_safety(self, filepath: Path, content: str) -> None: + """Vérifie la thread-safety.""" + if "threading.RLock" in content or "threading.Lock" in content: + self.passed.append(f"✅ Uses threading.Lock for thread-safety ({filepath.name})") + + if "with self._lock" in content: + self.passed.append(f"✅ Uses context managers for locks ({filepath.name})") + + if "ThreadPoolExecutor" in content: + self.passed.append(f"✅ Uses ThreadPoolExecutor for concurrency ({filepath.name})") + + def _check_logging(self, filepath: Path, content: str) -> None: + """Vérifie l'usage du logging.""" + if "logger_gui" in content or "logger" in content: + self.passed.append(f"✅ Uses project logger ({filepath.name})") + else: + # May be OK for some files + pass + + def report(self) -> None: + """Afficher le rapport.""" + print("\n" + "=" * 70) + print("📊 RAPPORT DE CONFORMITÉ AGENTS.md") + print("=" * 70) + + print("\n✅ VÉRIFICATIONS RÉUSSIES:") + for msg in self.passed: + print(f" {msg}") + + if self.issues: + print("\n⚠️ PROBLÈMES DÉTECTÉS:") + errors = [i for i in self.issues if i[2] == 2] + warnings = [i for i in self.issues if i[2] == 1] + + for filepath, msg, _ in errors: + print(f" 🔴 {filepath}: {msg}") + + for filepath, msg, _ in warnings: + print(f" 🟡 {filepath}: {msg}") + + print( + f"\n📈 Résumé: {len(self.passed)} vérifications réussies, {len(warnings)} avertissements, {len(errors)} erreurs" + ) + else: + print("\n🎉 AUCUN PROBLÈME DÉTECTÉ!") + print(f"📈 Résumé: {len(self.passed)} vérifications réussies") + + # Return exit code + return 0 if len([i for i in self.issues if i[2] == 2]) == 0 else 1 + + +if __name__ == "__main__": + base_dir = Path(__file__).parent.parent + + files_to_check = [ + base_dir / "tidal_dl_ng/gui/playlist_membership.py", + base_dir / "tidal_dl_ng/ui/dialog_playlist_manager.py", + base_dir / "tests/test_playlist_manager.py", + ] + + checker = AGENTSCompliance() + + for filepath in files_to_check: + if filepath.exists(): + checker.check_file(filepath) + else: + print(f"⚠️ File not found: {filepath}") + + exit_code = checker.report() + exit(exit_code) diff --git a/scripts/verify_playlist_integration.py b/scripts/verify_playlist_integration.py new file mode 100644 index 0000000..1306782 --- /dev/null +++ b/scripts/verify_playlist_integration.py @@ -0,0 +1,203 @@ +"""Verification script for playlist membership integration. + +Checks that all components are properly integrated into MainWindow. +""" + +import sys +from pathlib import Path + +# Add project root to path +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + + +def _check_import(module_path: str, component_name: str, check_num: int) -> tuple[int, int]: + """Check if a component can be imported. + + Args: + module_path: The module path to import from + component_name: The component name to import + check_num: The check number for display + + Returns: + tuple[int, int]: (passed, failed) counts + """ + try: + __import__(module_path, fromlist=[component_name]) + except ImportError as e: + print(f"❌ Check {check_num}: {component_name} import failed: {e}") + return (0, 1) + else: + print(f"✅ Check {check_num}: {component_name} importable") + return (1, 0) + + +def _run_all_import_checks() -> tuple[int, int]: + """Run all import checks. + + Returns: + tuple[int, int]: (passed, failed) counts + """ + checks = [ + ("tidal_dl_ng.gui.playlist_membership_mixin", "PlaylistMembershipMixin", 1), + ("tidal_dl_ng.gui.playlist_membership", "PlaylistContextLoader", 2), + ("tidal_dl_ng.gui.playlist_membership", "ThreadSafePlaylistCache", 3), + ("tidal_dl_ng.gui.playlist_membership", "PlaylistColumnDelegate", 4), + ("tidal_dl_ng.ui.dialog_playlist_manager", "PlaylistManagerDialog", 5), + ] + + total_passed = 0 + total_failed = 0 + + for module_path, component_name, check_num in checks: + passed, failed = _check_import(module_path, component_name, check_num) + total_passed += passed + total_failed += failed + + return total_passed, total_failed + + +def _check_mainwindow_inheritance() -> tuple[int, int]: + """Check if MainWindow inherits from PlaylistMembershipMixin. + + Returns: + tuple[int, int]: (passed, failed) counts + """ + try: + from tidal_dl_ng.gui.main_window import MainWindow + from tidal_dl_ng.gui.playlist_membership_mixin import PlaylistMembershipMixin + + if issubclass(MainWindow, PlaylistMembershipMixin): + print("✅ Check 6: MainWindow has PlaylistMembershipMixin") + return (1, 0) + except Exception as e: + print(f"❌ Check 6: MainWindow inheritance check failed: {e}") + return (0, 1) + else: + print("❌ Check 6: MainWindow does not inherit PlaylistMembershipMixin") + return (0, 1) + + +def _check_method_exists() -> tuple[int, int]: + """Check if init_playlist_membership_manager method exists. + + Returns: + tuple[int, int]: (passed, failed) counts + """ + try: + from tidal_dl_ng.gui.main_window import MainWindow + + if hasattr(MainWindow, "init_playlist_membership_manager"): + print("✅ Check 7: init_playlist_membership_manager method exists") + return (1, 0) + except Exception as e: + print(f"❌ Check 7: Method check failed: {e}") + return (0, 1) + else: + print("❌ Check 7: init_playlist_membership_manager method not found") + return (0, 1) + + +def _check_file_content( + file_path: Path, search_str: str, check_num: int, success_msg: str, fail_msg: str +) -> tuple[int, int]: + """Check if a file contains a specific string. + + Args: + file_path: Path to the file to check + search_str: String to search for + check_num: Check number for display + success_msg: Message to display on success + fail_msg: Message to display on failure + + Returns: + tuple[int, int]: (passed, failed) counts + """ + try: + with open(file_path) as f: + content = f.read() + if search_str in content: + print(f"✅ Check {check_num}: {success_msg}") + return (1, 0) + print(f"❌ Check {check_num}: {fail_msg}") + return (0, 1) + except Exception as e: + print(f"❌ Check {check_num}: File check failed: {e}") + return (0, 1) + + +def check_integration() -> int: + """Verify all components are integrated. + + Returns: + int: 0 if all checks pass, 1 otherwise + """ + checks_passed = 0 + checks_failed = 0 + + print("🔍 Vérification de l'intégration du Playlist Membership Manager\n") + print("=" * 70) + + # Run all import checks + passed, failed = _run_all_import_checks() + checks_passed += passed + checks_failed += failed + + # Check 6: MainWindow has PlaylistMembershipMixin + passed, failed = _check_mainwindow_inheritance() + checks_passed += passed + checks_failed += failed + + # Check 7: init_playlist_membership_manager method exists + passed, failed = _check_method_exists() + checks_passed += passed + checks_failed += failed + + # Check 8: TreesResultsMixin has playlists column + passed, failed = _check_file_content( + project_root / "tidal_dl_ng" / "gui" / "trees_results.py", + "child_playlists", + 8, + "child_playlists column added to TreesResultsMixin", + "child_playlists column NOT found in TreesResultsMixin", + ) + checks_passed += passed + checks_failed += failed + + # Check 9: tidal_session.py calls init_playlist_membership_manager + passed, failed = _check_file_content( + project_root / "tidal_dl_ng" / "gui" / "tidal_session.py", + "init_playlist_membership_manager", + 9, + "init_playlist_membership_manager called in init_tidal", + "init_playlist_membership_manager NOT called in init_tidal", + ) + checks_passed += passed + checks_failed += failed + + # Check 10: PlaylistMembershipMixin imports PlaylistCellState + passed, failed = _check_file_content( + project_root / "tidal_dl_ng" / "gui" / "playlist_membership_mixin.py", + "PlaylistCellState", + 10, + "PlaylistCellState imported in mixin", + "PlaylistCellState NOT imported in mixin", + ) + checks_passed += passed + checks_failed += failed + + # Print summary + print("\n" + "=" * 70) + print(f"\n📊 Résultat: {checks_passed} ✅ / {checks_failed} ❌\n") + + if checks_failed == 0: + print("🎉 INTÉGRATION COMPLÈTE - Tous les checks sont passés!") + return 0 + + print(f"⚠️ {checks_failed} problème(s) détecté(s)") + return 1 + + +if __name__ == "__main__": + exit_code = check_integration() + sys.exit(exit_code) |
