diff --git a/.jules/palette.md b/.jules/palette.md new file mode 100644 index 00000000..7b29b416 --- /dev/null +++ b/.jules/palette.md @@ -0,0 +1,9 @@ +# Palette's Journal + +## 2024-10-24 - Progress Bar in Parallel Tasks +**Learning:** When using `concurrent.futures`, standard logging can interfere with progress bars. The `render_progress_bar` implementation relies on `\r` to overwrite the line, but if another thread logs to stderr/stdout, it can break the visual. +**Action:** Always wrap logging calls inside the parallel loop with a line-clearing sequence (`\r\033[K`) if a progress bar is active. + +## 2024-10-24 - Duplicate Function Definitions +**Learning:** Duplicate function definitions in Python (later overwrites earlier) can be confusing for static analysis or human reviewers, even if the runtime behavior is well-defined. +**Action:** Always scan for and remove duplicate definitions when refactoring to avoid confusion. diff --git a/main.py b/main.py index 86792da4..1f5ae643 100644 --- a/main.py +++ b/main.py @@ -159,23 +159,6 @@ def sanitize_for_log(text: Any) -> str: return safe -def render_progress_bar( - current: int, total: int, label: str, prefix: str = "🚀" -) -> None: - if not USE_COLORS or total == 0: - return - - width = 20 - progress = min(1.0, current / total) - filled = int(width * progress) - bar = "█" * filled + "░" * (width - filled) - percent = int(progress * 100) - - # Use \033[K to clear line residue - sys.stderr.write( - f"\r\033[K{Colors.CYAN}{prefix} {label}: [{bar}] {percent}% ({current}/{total}){Colors.ENDC}" - ) - sys.stderr.flush() def countdown_timer(seconds: int, message: str = "Waiting") -> None: @@ -677,6 +660,10 @@ def _fetch_folder_rules(folder_id: str) -> List[str]: # Parallelize fetching rules from folders. # Using 5 workers to be safe with rate limits, though GETs are usually cheaper. + total_folders = len(folders) + completed_folders = 0 + render_progress_bar(0, total_folders, "Fetching existing rules", prefix="🔍") + with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: future_to_folder = { executor.submit(_fetch_folder_rules, folder_id): folder_id @@ -684,14 +671,28 @@ def _fetch_folder_rules(folder_id: str) -> List[str]: } for future in concurrent.futures.as_completed(future_to_folder): + completed_folders += 1 try: result = future.result() if result: all_rules.update(result) except Exception as e: + if USE_COLORS: + # Clear line to print warning cleanly + sys.stderr.write("\r\033[K") + sys.stderr.flush() + folder_id = future_to_folder[future] log.warning(f"Failed to fetch rules for folder ID {folder_id}: {e}") + render_progress_bar( + completed_folders, total_folders, "Fetching existing rules", prefix="🔍" + ) + + if USE_COLORS: + sys.stderr.write(f"\r\033[K") + sys.stderr.flush() + log.info(f"Total existing rules across all folders: {len(all_rules)}") return all_rules except Exception as e: diff --git a/test_main.py b/test_main.py index e15c805a..76bbef2d 100644 --- a/test_main.py +++ b/test_main.py @@ -510,3 +510,31 @@ def test_render_progress_bar(monkeypatch): # Color codes (accessing instance Colors or m.Colors) assert m.Colors.CYAN in combined assert m.Colors.ENDC in combined + +# Case 14: get_all_existing_rules shows progress bar when USE_COLORS is True +def test_get_all_existing_rules_shows_progress_bar(monkeypatch): + m = reload_main_with_env(monkeypatch, no_color=None, isatty=True) + mock_client = MagicMock() + mock_stderr = MagicMock() + monkeypatch.setattr(sys, "stderr", mock_stderr) + + # Mock list_existing_folders to return multiple folders + mock_list_folders = MagicMock(return_value={"FolderA": "id_A", "FolderB": "id_B"}) + monkeypatch.setattr(m, "list_existing_folders", mock_list_folders) + + # Mock _api_get + def side_effect(client, url): + mock_resp = MagicMock() + mock_resp.json.return_value = {"body": {"rules": []}} + return mock_resp + monkeypatch.setattr(m, "_api_get", side_effect) + + m.get_all_existing_rules(mock_client, "profile_id") + + # Check that progress bar was rendered + # We expect calls to stderr.write with "Fetching existing rules" + writes = [args[0] for args, _ in mock_stderr.write.call_args_list] + combined = "".join(writes) + + assert "Fetching existing rules" in combined + assert "🔍" in combined