#!/usr/bin/env python3 """ Python Project Fixer (fix_python_project.py) -------------------------------------------- Автоматически исправляет структуру Python-проекта: 1. Удаляет BOM-маркеры из текстовых файлов (.py, .json, .txt) 2. Создаёт недостающие __init__.py файлы (игнорируя служебные папки) 3. Сохраняет подробный лог всех выполненных операций Использование: python fix_python_project.py [путь_к_проекту] """ import os import sys from typing import List, Set # Настройки обработки проекта INIT_IGNORED_DIRS: Set[str] = {'tests', '.git', '__pycache__', 'venv', 'env', '.idea', '.vscode'} TARGET_EXTENSIONS: tuple = ('.py', '.json', '.txt') INIT_TEMPLATE: str = """# Auto-generated by fix_python_project.py \"\"\"Package initialization.\"\"\" """ class ProjectFixer: """Основной класс для исправления структуры Python-проекта. Атрибуты: root_dir (str): Корневая директория проекта. log (List[str]): Список записей лога выполненных операций. """ def __init__(self, root_dir: str = '.'): """Инициализирует экземпляр ProjectFixer. Args: root_dir (str): Корневая директория проекта. По умолчанию текущая директория ('.'). """ self.root_dir = os.path.abspath(root_dir) self.log: List[str] = [] def remove_bom(self, filepath: str) -> bool: """Удаляет BOM-маркер из файла, если он присутствует. Обрабатывает все файлы, включая находящиеся в tests/. Args: filepath (str): Путь к файлу для обработки. Returns: bool: True, если BOM был удалён, False в противном случае. Raises: Exception: Если произошла ошибка при чтении/записи файла. """ try: with open(filepath, 'rb') as f: content = f.read() if content.startswith(b'\xEF\xBB\xBF'): with open(filepath, 'wb') as f: f.write(content[3:]) self.log.append(f"REMOVED BOM: {filepath}") return True except Exception as e: self.log.append(f"ERROR processing {filepath}: {str(e)}") return False def should_skip_init(self, dir_path: str) -> bool: """Проверяет, нужно ли пропустить создание __init__.py в директории. Игнорирует служебные папки (tests/, .git/ и др.). Args: dir_path (str): Путь к проверяемой директории. Returns: bool: True, если директорию следует пропустить. """ dir_name = os.path.basename(dir_path) return (dir_name in INIT_IGNORED_DIRS or dir_name.startswith('.')) def needs_init_py(self, dir_path: str) -> bool: """Определяет, требуется ли создание __init__.py в директории. Args: dir_path (str): Путь к проверяемой директории. Returns: bool: True, если __init__.py отсутствует, но требуется. """ if self.should_skip_init(dir_path): return False try: items = os.listdir(dir_path) has_py_files = any(f.endswith('.py') and f != '__init__.py' for f in items) has_init = '__init__.py' in items return has_py_files and not has_init except Exception: return False def create_init_py(self, dir_path: str) -> bool: """Создаёт файл __init__.py в указанной директории. Args: dir_path (str): Путь к директории для создания __init__.py. Returns: bool: True, если файл был успешно создан. Raises: Exception: Если произошла ошибка при создании файла. """ init_path = os.path.join(dir_path, '__init__.py') try: with open(init_path, 'w', encoding='utf-8') as f: f.write(INIT_TEMPLATE) self.log.append(f"CREATED INIT: {init_path}") return True except Exception as e: self.log.append(f"ERROR creating {init_path}: {str(e)}") return False def process_directory(self): """Рекурсивно обрабатывает проект, применяя все исправления.""" for root, dirs, files in os.walk(self.root_dir): # Обработка файлов с целевыми расширениями for file in files: if file.endswith(TARGET_EXTENSIONS): self.remove_bom(os.path.join(root, file)) # Создание __init__.py где это необходимо if self.needs_init_py(root): self.create_init_py(root) def save_log(self, log_file: str = 'project_fix.log'): """Сохраняет лог выполненных операций в файл. Args: log_file (str): Имя файла для сохранения лога. По умолчанию 'project_fix.log'. """ with open(log_file, 'w', encoding='utf-8') as f: f.write("\n".join(self.log)) if __name__ == '__main__': # Обработка аргументов командной строки target_dir = sys.argv[1] if len(sys.argv) > 1 else '.' # Инициализация и запуск обработки fixer = ProjectFixer(target_dir) print(f"Исправление структуры проекта в: {fixer.root_dir}") fixer.process_directory() fixer.save_log() # Вывод результатов print(f"Готово! Внесено {len(fixer.log)} изменений.") print(f"Подробности сохранены в project_fix.log")