#!/usr/bin/env python3
"""Модуль исправления структуры Python-проекта.
Автоматически выполняет:
1. Удаление BOM-маркеров из файлов
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: Корневая директория проекта.
log: Лог выполненных операций.
"""
[документация]
def __init__(self, root_dir: str = '.'):
"""Инициализирует экземпляр ProjectFixer.
Args:
root_dir: Корневая директория проекта. По умолчанию '.'.
"""
self.root_dir = os.path.abspath(root_dir)
self.log: List[str] = []
[документация]
def remove_bom(self, filepath: str) -> bool:
"""Удаляет BOM-маркер из файла.
Args:
filepath: Путь к файлу.
Returns:
bool: True если BOM был удалён.
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:
"""Проверяет нужно ли пропустить директорию.
Args:
dir_path: Путь к директории.
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: Путь к директории.
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: Путь к директории.
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: Имя файла лога. По умолчанию '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("Подробности сохранены в project_fix.log")