Skip to content

# Python Project Fixer

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

Автоматически выполняет: 1. Удаление BOM-маркеров из файлов 2. Создание недостающих init.py 3. Логирование всех операций

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

## `ProjectFixer`

Исправляет структуру Python-проекта.

Атрибуты

root_dir: Корневая директория проекта. log: Лог выполненных операций.

Source code in `tools\fix_python_project.py`

     25
     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
    147
    148
    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))

### `__init__(root_dir='.')`

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

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`root_dir` | `str` | Корневая директория проекта. По умолчанию '.'. | `'.'`

Source code in `tools\fix_python_project.py`

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

        Args:
            root_dir: Корневая директория проекта. По умолчанию '.'.
        """

        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` | Путь к директории. | required

Returns:

Name | Type | Description
--- | --- | ---
`bool` | `bool` | True если файл создан.

Raises:

Type | Description
--- | ---
`Exception` | При ошибке создания.

Source code in `tools\fix_python_project.py`

    104
    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: Путь к директории.

        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`

     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    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

### `process_directory()`

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

Source code in `tools\fix_python_project.py`

    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    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-маркер из файла.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`filepath` | `str` | Путь к файлу. | required

Returns:

Name | Type | Description
--- | --- | ---
`bool` | `bool` | True если BOM был удалён.

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
    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

### `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`

    140
    141
    142
    143
    144
    145
    146
    147
    148
    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))

### `should_skip_init(dir_path)`

Проверяет нужно ли пропустить директорию.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`dir_path` | `str` | Путь к директории. | required

Returns:

Name | Type | Description
--- | --- | ---
`bool` | `bool` | True если директорию нужно пропустить.

Source code in `tools\fix_python_project.py`

    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    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('.'))