Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
@@ -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.
35 changes: 18 additions & 17 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,23 +159,6 @@
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:
Expand Down Expand Up @@ -677,21 +660,39 @@

# 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
for folder_name, folder_id in folders.items()
}

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")

Check warning

Code scanning / Prospector (reported by Codacy)

Using an f-string that does not have any interpolated variables (f-string-without-interpolation) Warning

Using an f-string that does not have any interpolated variables (f-string-without-interpolation)

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Using an f-string that does not have any interpolated variables Note

Using an f-string that does not have any interpolated variables
sys.stderr.flush()
Comment on lines 667 to +694
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure the progress bar line is always cleared, even if an error occurs during the fetching process (like a KeyboardInterrupt), it's more robust to use a try...finally block. This guarantees that the cleanup code for the progress bar is executed regardless of whether the operation completes successfully or is interrupted.

        try:
            with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
                future_to_folder = {
                    executor.submit(_fetch_folder_rules, folder_id): folder_id
                    for folder_name, folder_id in folders.items()
                }

                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="πŸ”"
                    )
        finally:
            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:
Expand Down
28 changes: 28 additions & 0 deletions test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
monkeypatch.setattr(m, "list_existing_folders", mock_list_folders)

# Mock _api_get to return different rules for root vs folders
def side_effect(client, url):

Check warning

Code scanning / Pylint (reported by Codacy)

Missing function docstring Warning test

Missing function docstring

Check notice

Code scanning / Pylint (reported by Codacy)

Unused argument 'client' Note test

Unused argument 'client'

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused argument 'client' Note test

Unused argument 'client'
mock_resp = MagicMock()
if url.endswith("/rules"): # Root rules
mock_resp.json.return_value = {"body": {"rules": [{"PK": "rule_root"}]}}
Expand Down Expand Up @@ -174,7 +174,7 @@

# Case 5: push_rules writes colored progress and completion messages to stderr when USE_COLORS is True
def test_push_rules_writes_colored_stderr(monkeypatch):
m = reload_main_with_env(monkeypatch, no_color=None, isatty=True)

Check warning

Code scanning / Pylint (reported by Codacy)

Variable name "m" doesn't conform to snake_case naming style Warning test

Variable name "m" doesn't conform to snake_case naming style

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Variable name "m" doesn't conform to snake_case naming style Warning test

Variable name "m" doesn't conform to snake_case naming style
monkeypatch.setattr(m, "_api_post_form", MagicMock())

mock_stderr = MagicMock()
Expand Down Expand Up @@ -510,3 +510,31 @@
# 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):

Check warning

Code scanning / Pylint (reported by Codacy)

Missing function docstring Warning test

Missing function docstring

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Missing function or method docstring Warning test

Missing function or method docstring
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):

Check notice

Code scanning / Pylint (reported by Codacy)

Unused argument 'client' Note test

Unused argument 'client'

Check warning

Code scanning / Prospector (reported by Codacy)

Unused argument 'client' (unused-argument) Warning test

Unused argument 'client' (unused-argument)

Check warning

Code scanning / Prospector (reported by Codacy)

Unused argument 'url' (unused-argument) Warning test

Unused argument 'url' (unused-argument)

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused argument 'url' Note test

Unused argument '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

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
assert "πŸ”" in combined

Check notice

Code scanning / Bandit

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

Check notice

Code scanning / Bandit (reported by Codacy)

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Loading