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('.'))