162 lines
6.5 KiB
Python
162 lines
6.5 KiB
Python
#!/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") |