Skip to content

Python Project Fixer

Python Project Fixer (fix_python_project.py)

Автоматически исправляет структуру Python-проекта: 1. Удаляет BOM-маркеры из текстовых файлов (.py, .json, .txt) 2. Создаёт недостающие init.py файлы (игнорируя служебные папки) 3. Сохраняет подробный лог всех выполненных операций

Использование: python fix_python_project.py [путь_к_проекту]

ProjectFixer

Основной класс для исправления структуры Python-проекта.

Атрибуты

root_dir (str): Корневая директория проекта. log (List[str]): Список записей лога выполненных операций.

Source code in tools\fix_python_project.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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))

__init__(root_dir='.')

Инициализирует экземпляр ProjectFixer.

Parameters:

Name Type Description Default
root_dir str

Корневая директория проекта. По умолчанию текущая директория ('.').

'.'
Source code in tools\fix_python_project.py
34
35
36
37
38
39
40
41
def __init__(self, root_dir: str = '.'):
    """Инициализирует экземпляр ProjectFixer.

    Args:
        root_dir (str): Корневая директория проекта. По умолчанию текущая директория ('.').
    """
    self.root_dir = os.path.abspath(root_dir)
    self.log: List[str] = []

create_init_py(dir_path)

Создаёт файл init.py в указанной директории.

Parameters:

Name Type Description Default
dir_path str

Путь к директории для создания init.py.

required

Returns:

Name Type Description
bool bool

True, если файл был успешно создан.

Raises:

Type Description
Exception

Если произошла ошибка при создании файла.

Source code in tools\fix_python_project.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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

needs_init_py(dir_path)

Определяет, требуется ли создание init.py в директории.

Parameters:

Name Type Description Default
dir_path str

Путь к проверяемой директории.

required

Returns:

Name Type Description
bool bool

True, если init.py отсутствует, но требуется.

Source code in tools\fix_python_project.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
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

process_directory()

Рекурсивно обрабатывает проект, применяя все исправления.

Source code in tools\fix_python_project.py
127
128
129
130
131
132
133
134
135
136
137
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)

remove_bom(filepath)

Удаляет BOM-маркер из файла, если он присутствует.

Обрабатывает все файлы, включая находящиеся в tests/.

Parameters:

Name Type Description Default
filepath str

Путь к файлу для обработки.

required

Returns:

Name Type Description
bool bool

True, если BOM был удалён, False в противном случае.

Raises:

Type Description
Exception

Если произошла ошибка при чтении/записи файла.

Source code in tools\fix_python_project.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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

save_log(log_file='project_fix.log')

Сохраняет лог выполненных операций в файл.

Parameters:

Name Type Description Default
log_file str

Имя файла для сохранения лога. По умолчанию 'project_fix.log'.

'project_fix.log'
Source code in tools\fix_python_project.py
139
140
141
142
143
144
145
146
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))

should_skip_init(dir_path)

Проверяет, нужно ли пропустить создание init.py в директории.

Игнорирует служебные папки (tests/, .git/ и др.).

Parameters:

Name Type Description Default
dir_path str

Путь к проверяемой директории.

required

Returns:

Name Type Description
bool bool

True, если директорию следует пропустить.

Source code in tools\fix_python_project.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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('.'))