#!/usr/bin/python3 # Copyright (c) 2025 Salvador E. Tropea # Copyright (c) 2025 Instituto Nacional de TecnologĂ­a Industrial # License: GPLv3 # Project: ComfyUI-AudioSeparation # # Tool to fix some common style errors in code generated by Gemini # Created by Gemini itself ;-) import argparse import os import shutil # For file backup import logging # Setup logging logger = logging.getLogger("StyleFixer") logger.setLevel(logging.INFO) # Default level # Create console handler and set level to debug ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) # Let handler decide what to show based on logger's effective level # Create formatter formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # Add formatter to ch ch.setFormatter(formatter) # Add ch to logger logger.addHandler(ch) def expand_tabs(line, tab_size=4): """Expands tabs to spaces, respecting tab stops.""" out_line = [] current_col = 0 for char in line: if char == '\t': spaces_to_add = tab_size - (current_col % tab_size) out_line.append(' ' * spaces_to_add) current_col += spaces_to_add else: out_line.append(char) current_col += 1 return "".join(out_line) def fix_indentation(line, tab_size=4): """Rounds leading whitespace indentation to the nearest multiple of tab_size.""" stripped_line = line.lstrip(' ') if not stripped_line or stripped_line == line: # No leading spaces or empty line return line leading_spaces_count = len(line) - len(stripped_line) remainder = leading_spaces_count % tab_size if remainder == 0: new_indent_count = leading_spaces_count elif remainder <= tab_size / 2: # Prioritize rounding down or to current if exact multiple new_indent_count = leading_spaces_count - remainder else: # Round up new_indent_count = leading_spaces_count + (tab_size - remainder) new_indent_count = max(0, new_indent_count) # Ensure non-negative if new_indent_count != leading_spaces_count: logger.debug(f"Adjusting indent: from {leading_spaces_count} to {new_indent_count} for line: {line.rstrip()!r}") return ' ' * new_indent_count + stripped_line def process_line(line_num, line_content_with_eol, expand_tabs_enabled=True): original_line_for_debug = line_content_with_eol # Keep for debugging if needed line = line_content_with_eol # 1. Expand tabs (if enabled) if expand_tabs_enabled: line = expand_tabs(line) if line != original_line_for_debug and not line.isspace(): # Log only if actual content changed logger.debug(f"L{line_num}: Tabs expanded.") # 2. W291: Remove trailing whitespace (done before other checks that might rely on EOL) # Also handles W293 (blank line contains whitespace) implicitly if the line becomes empty. processed_line = line.rstrip() if len(processed_line) < len(line.rstrip('\r\n')): # Compare lengths without EOL logger.debug(f"L{line_num}: Trailing whitespace removed.") # If the line became empty after rstrip, it's a truly blank line if not processed_line and line.strip() == "": # Return an empty string for a blank line, newline char will be added later if original had it if line.endswith(('\n', '\r\n', '\r')): return '\n' return "" # 3. Fix indentation (E111) - only if line is not blank if processed_line: indented_line = fix_indentation(processed_line) if indented_line != processed_line: # Log if indentation changed # Already logged inside fix_indentation with more detail pass processed_line = indented_line # 4. E261: At least two spaces before inline comment in_string_char = None temp_part = "" comment_starts_at = -1 for idx, char_val in enumerate(processed_line): if in_string_char: temp_part += char_val if char_val == in_string_char: if len(temp_part) >= 2 and temp_part[-2] == '\\': pass else: in_string_char = None elif char_val in ("'", '"'): temp_part += char_val in_string_char = char_val elif char_val == '#': comment_starts_at = idx break # Found the first non-string comment char else: temp_part += char_val if comment_starts_at != -1: code_part = processed_line[:comment_starts_at] comment_part = processed_line[comment_starts_at:] # Includes '#' rstripped_code_part = code_part.rstrip() # Only add spaces if there's actual code before the comment if rstripped_code_part: if not code_part.endswith(' '): # Needs fixing (less than 2 spaces) new_line_with_comment_spacing = rstripped_code_part + ' ' + comment_part if new_line_with_comment_spacing != processed_line: logger.debug(f"L{line_num}: Adjusted inline comment spacing.") processed_line = new_line_with_comment_spacing # If rstripped_code_part is empty, it's a comment at the start of the line (after indent), leave it. # Add back the newline character that rstrip might have removed, if the original line had one # or if it's not an empty line that was purely whitespace if line_content_with_eol.endswith('\n'): return processed_line + '\n' elif line_content_with_eol.endswith('\r\n'): return processed_line + '\r\n' elif line_content_with_eol.endswith('\r'): return processed_line + '\r' else: # Original line did not end with EOL return processed_line def process_file(filepath, expand_tabs_enabled=True, dry_run=False, no_backup=False): logger.info(f"Processing {filepath}...") try: # Read with universal newlines mode, then splitlines to preserve EOLs correctly for rejoining with open(filepath, 'r', encoding='utf-8', newline='') as f: # original_content = f.read() # Reading all at once # original_lines = original_content.splitlines(keepends=True) # This is better original_lines = f.readlines() # Simpler, usually works fine except Exception as e: logger.error(f"Error reading file {filepath}: {e}") return False fixed_lines = [] changes_made = False for i, line_content_with_eol in enumerate(original_lines): fixed_line_content = process_line(i + 1, line_content_with_eol, expand_tabs_enabled) fixed_lines.append(fixed_line_content) if fixed_line_content != line_content_with_eol: changes_made = True logger.debug(f"L{i+1}: Original: {line_content_with_eol.rstrip()!r}") logger.debug(f"L{i+1}: Fixed : {fixed_line_content.rstrip()!r}") if changes_made: if dry_run: logger.info(f"Would fix style issues in {filepath} (dry run)") else: if not no_backup: backup_dir = os.path.dirname(filepath) backup_filename = "." + os.path.basename(filepath) + "~" backup_path = os.path.join(backup_dir, backup_filename) try: shutil.copy2(filepath, backup_path) # copy2 preserves metadata logger.info(f"Backup of original file created at {backup_path}") except Exception as e: logger.error(f"Failed to create backup for {filepath}: {e}. File not modified.") return False try: # Write back lines using the original EOLs if possible, or common EOL with open(filepath, 'w', encoding='utf-8', newline='') as f: f.writelines(fixed_lines) logger.info(f"Fixed style issues in {filepath}") except Exception as e: logger.error(f"Error writing to file {filepath}: {e}") return False else: logger.info(f"No style issues to fix in {filepath}") return True def main(): parser = argparse.ArgumentParser(description="Fix common Python code style issues (Flake8).") parser.add_argument("files", metavar="FILE", type=str, nargs='+', help="Python files to process") parser.add_argument("--no-expand-tabs", action="store_false", dest="expand_tabs", help="Disable expansion of tabs to spaces.") parser.add_argument("--dry-run", action="store_true", help="Show what would be changed without modifying files.") parser.add_argument("--no-backup", action="store_true", help="Do not create a backup of the original file.") parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose (debug) logging.") parser.set_defaults(expand_tabs=True) args = parser.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) for filepath in args.files: if not os.path.isfile(filepath): logger.warning(f"File not found: {filepath}. Skipping.") continue process_file(filepath, args.expand_tabs, args.dry_run, args.no_backup) if __name__ == "__main__": main()