GUI Генератор паролей на Python
- пятница, 23 сентября 2022 г. в 00:34:59
Штош. В этой статье я научу вас делать кроссплатформенное приложение генератор паролей с графическим интерфейсом. Мы будем использовать язык Python и библиотеку PySide6 - привязку к инструментарию фреймворка Qt.
Приложение умеет генерировать пароли, скрывать и копировать их в буфер обмена. Длина задается с помощью слайдера и счетчика. Пул допустимых символов меняется кнопками. Сила пароля рассчитывается по информационной энтропии.
В статье я постарался затронуть все моменты создания и сборки приложения. Ознакомиться с проектом можно на GitHub.
Скачайте и установите Python последней версии (желательно). Приложение должно работать с Python версии 3.7+
Создадим директорию проекта password-generator
и виртуальное окружение, в моем случае venvPasswordGenerator
Установим библиотеку PySide6.
pip install pyside6
Переходим к созданию интерфейса. Для этого нам понадобится программа Qt Designer. Её можно найти в папке установленной ранее библиотеки.
venv*/Lib/site-packages/PySide6/designer
Создаем MainWindow
. Убираем menubar
и statusbar
Сразу сохраняем файл интерфейса. Я называю его main.ui
, потому что в будущем я могу захотеть сделать отдельную форму для настроек или еще чего-то.
Сначала закинем 4 Horizontal Layout
для компоновки элементов. Скопировать элемент можно перетаскиванием с зажатой клавишей Ctrl
.
Выберем вертикальное расположение для центрального виджета. centralwidget -> Lay Out Vertically
Закинем одну кнопку выбора символов в нижнюю горизонтальную компоновку.
Чтобы кнопка имела 2 состояния, нужно поставить свойство checkable
Скопируем кнопку 3 раза. Поставим текст. Я думаю, так должно быть понятно, за какой набор символов отвечает каждая отдельная кнопка.
Дадим элементам осмысленные имена.
Проставим свойство checked
на всех символах, кроме специальных.
Здесь будет указываться длина пароля с помощью слайдера и счетчика. Компоновку я назову layout_length
, слайдер - slider_length
, счетчик - spinbox_length
Закидываем лейбл.
Ставим горизонтальное выравнивание по центру.
Копируем элемент и вводим примерный текст.
Перед нами встает интересная задача. Как поместить кнопку видимости пароля в элемент Line Edit? К сожалению, встроенных в Qt Designer методов для такого действия я не нашел. Лучшим решением я посчитал поместить поле и кнопку рядом во фрейме. Если вы знаете способ лучше, поделитесь в комментариях.
Widget Box -> Containers -> Frame
Поставим горизонтальное выравнивание для фрейма.
Добавим 2 кнопки в горизонтальную компоновку, а не во фрейм. Перетаскивать элемент нужно в самый правый край компоновки.
Еще я захотел поставить сверху изображение замка. На удивление, в Qt Designer это тоже реализовано через одно место, в котором никто не хочет побывать. Есть способ с помощью лейбла и его свойства pixmap
, но в таком случае нельзя изменять размер изображения. Поэтому я решил сделать через иконку заблокированной кнопки.
Возьмем иконки для приложения. Я буду использовать бесплатные Material Icons.
Скачаем белую векторную иконку замка. Я выберу первый вид - Outlined. Вы можете взять другой, как вам приятнее.
Для повторной генерации возьмем Refresh. Копирование в буфер обмена - Content Copy. Видимость пароля - Visibility и Visibility Off. Для иконки приложения возьмем черную иконку ключа в формате png
и сконвертируем её в формат для иконок ico
Создадим файл ресурсов, который будет хранить иконки. Позже мы сконвертируем его в Python код. Resource Browser -> Edit Resources -> New Resource File -> resources.qrc
Добавляем префикс icons
Добавляем все файлы иконок.
В заблокированную кнопку ставим иконку замка с помощью свойства icon -> Disabled On
. Выбираем иконку с помощью Choose Resource
Для кнопки видимости выбираем свойство checkable
, так как она тоже имеет 2 состояния. Сразу проставим checked
по умолчанию.
В Normal Off
берем иконку невидимости, в Normal On
- видимости.
Остальные иконки проставляем в Normal On
Для стилизации приложения я буду использовать урезанный язык CSS. Писать советую в каком-нибудь редакторе. Я пишу в Visual Studio Code, конечно же.
Создадим файл QMainWindow.css
. Для главного виджета поставим цвет фона #121212
, белый цвет текста, шрифт Verdana с размером 16 поинтов и внешний отступ 10 пикселей.
QWidget {
background-color: #121212;
color: white;
font-family: Verdana;
font-size: 16pt;
margin: 10px;
}
Для кнопок ставим сплошную серую границу 2 пикселя с радиусом границы 5 пикселей.
QPushButton {
border: 2px solid gray;
border-radius: 5px;
}
Вставим код в элемент MainWindow
с помощью опции Change styleSheet
Отдельно для кнопок символов я поставлю внутренний отступ 10 пикселей, текст располагается слишком близко к границам.
QPushButton#btn_lower,
#btn_upper,
#btn_digits,
#btn_special {
padding: 10px;
}
При наведении на кнопку цвет границ будет меняться на зеленый #090
.
QPushButton:hover {
border-color: #090;
}
При нажатии граница будет увеличиваться до 4 пикселей.
QPushButton:pressed {
border: 4px solid #090;
border-radius: 5px;
}
Для отмеченного состояния кнопки поставим на фон темно-зеленый цвет #006300
.
QPushButton:checked {
background-color: #006300;
border-color: #090;
}
QWidget {
background-color: #121212;
color: white;
font-family: Verdana;
font-size: 16pt;
margin: 10px;
}
QPushButton {
border: 2px solid gray;
border-radius: 5px;
}
QPushButton#btn_lower,
#btn_upper,
#btn_digits,
#btn_special {
padding: 10px;
}
QPushButton:hover {
border-color: #090;
}
QPushButton:pressed {
border: 4px solid #090;
border-radius: 5px;
}
QPushButton:checked {
background-color: #006300;
border-color: #090;
}
Чтобы посмотреть превью интерфейса, нужно нажать сочетание клавиш Ctrl + R
Уберем границы с помощью стиля border: none;
Поставим размер 100 на 100 пикселей.
Поставим для фрейма пароля такую же границу, как и для кнопок. Уберем отступ до правого элемента.
QFrame {
border: 2px solid gray;
border-radius: 5px;
margin-right: 0;
}
QFrame:hover {
border-color: #090;
}
Поставим вертикальную политику Maximum
Для поля пароля уберем границы и внешние отступы. Поставим размер шрифта 20 поинтов.
QLineEdit {
border: none;
margin: 0;
font-size: 20pt;
}
Для кнопки видимости пароля так же уберем границы и внешние отступы. Еще поставим прозрачный фон и размер 30 на 30 пикселей.
QPushButton {
border: none;
margin: 0;
background-color: transparent;
}
Для кнопки генерации пароля поставим размер иконки 52 на 52 пикселя. Так границы кнопки будут идти ровно по границам фрейма. Уберем правый и левый внешний отступ.
QPushButton {
margin-right: 0;
margin-left: 0;
}
Почему-то иконка копирования очень плотно прилегает к границам. Я нашел размер 42 на 42 пикселя и внутренний отступ 5 пикселей. Уберем отступ до левого элемента.
QPushButton {
padding: 5px;
margin-left: 0;
}
Для псевдоэлемента groove
уберем цвет и поставим высоту 5 пикселей. Грув - это линия слайдера, или "желобок", "канавка", "борозда", если верить гугл переводчику.
QSlider::groove:horizontal {
background-color: transparent;
height: 5px;
}
Слева от ручки слайдера будет зеленый цвет. Для этого используется селектор псевдоэлемента sub-page
QSlider::sub-page:horizontal {
background-color: #090;
}
Справа от ручки будет серый цвет. Псевдоэлемент add-page
QSlider::add-page:horizontal {
background-color: gray;
}
Ручку слайдера я сделаю белой с шириной 22 пикселя, радиусом 10 пикселей и внешними отступами снизу и сверху по -8 пикселей.
QSlider::handle:horizontal {
background-color: white;
width: 22px;
border-radius: 10px;
margin-top: -8px;
margin-bottom: -8px;
}
Поставим максимум 100 пикселей и значение по умолчанию 12.
Счетчик похож на кнопки.
QSpinBox {
border: 2px solid gray;
border-radius: 5px;
background: transparent;
padding: 2px;
}
QSpinBox:hover {
border-color: #009900;
}
Поставим значения, как у слайдера. Сделаем горизонтальное выравнивание по центру и уберем эти отвратительные стрелочки.
Если растягивать окно интерфейса по вертикали, большую часть будут занимать лейблы информации. Мы этого не хотим, и поэтому ставим у двух лейблов вертикальную политику Maximum
Поставим размер приложения по умолчанию. Мне нравится 542 на 418 пикселей. Можете сделать круглые числа, как вам угодно.
Напишем название приложения в свойстве windowTitle
. Поставим иконку в windowIcon
Поставим курсор Pointing Hand
для кнопок и слайдера.
Уберем демонстрационный текст из лейблов. Он будет генерироваться программой.
Штош. Интерфейс готов, переходим к следующему этапу.
Для того, чтобы сконвертировать файл ресурсов, нужно написать в терминал pyside6-rcc
, название файла ресурсов, флаг -o
и название файла на выходе.
pyside6-rcc resources.qrc -o resources.py
Файл интерфейса конвертируется таким же образом, только с помощью приложения pyside6-uic
pyside6-uic main.ui -o ui_main.py
Этот файл хочет, чтобы я добавлял в конце _rc
к файлу ресурсов, а я не хочу. Поменяем import resources_rc
на import resources
Рекомендую писать код в среде разработки PyCharm или Visual Studio Code. Vim-еры, не бейте.
Для начала создадим модуль приложения - app.py
. Я вставлю готовый сниппет для запуска приложения с файлом дизайна.
import sys
from PySide6.QtWidgets import QApplication, QMainWindow
from ui_main import Ui_MainWindow
class App(QMainWindow):
def __init__(self):
super(App, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = App()
window.show()
sys.exit(app.exec())
Поменяю название класса App
на PasswordGenerator
Давайте сделаем отдельный модуль с привязкой сущностей к кнопкам - buttons.py
. В стандартной библиотеке string есть строковые переменные для пула символов. Возьмем ascii_lowercase, ascii_uppercase, digits и punctuation.
from string import ascii_lowercase, ascii_uppercase, digits, punctuation
Вот так они выглядят.
ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz'
ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
digits = '0123456789'
punctuation = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
Давайте сделаем перечисление, которое привязывает кнопки выбора символов к этим строковым переменным.
from enum import Enum
class Characters(Enum):
btn_upper = ascii_uppercase
btn_lower = ascii_lowercase
btn_digits = digits
btn_special = punctuation
Сразу создадим словарь с количеством символов на каждый пул. Нам это пригодится для расчета энтропии.
CHARACTER_NUMBER = {
'btn_lower': len(Characters.btn_lower.value),
'btn_upper': len(Characters.btn_upper.value),
'btn_digits': len(Characters.btn_digits.value),
'btn_special': len(Characters.btn_special.value)
}
Давайте сделаем модуль генерации паролей без привязки к интерфейсу - password.py
. Напишем функцию создания нового пароля. Аргументами будут длина пароля и все используемые символы в виде строки.
Для генерации безопасного пароля мы будем использовать библиотеку secrets. Сделаем генератор из случайно выбранных символов и соединим их в строку с помощью метода join
import secrets
def create_new(length: int, characters: str) -> str:
return "".join(secrets.choice(characters) for _ in range(length))
Теперь сделаем функцию для получения энтропии. В нее нужно будет передавать длину пароля и количество символов. Формула энтропии пароля выглядит следующим образом:
где N — это количество возможных символов, а L — количество символов в пароле. H измеряется в битах.
Давайте импортируем логарифм по основанию 2 из библиотеки math.
from math import log2
Возвращаем значение, округленное до 2 знаков после запятой.
def get_entropy(length: int, character_number: int) -> float:
entropy = length * log2(character_number)
return round(entropy, 2)
Создадим целочисленное перечисление сложности пароля к его энтропии по нижней границе. Я не нашел единого стандарта, в разных генераторах сила пароля считается по-разному, поэтому сделаю так: ничтожный пароль от 0 до 30 бит, слабый после 30, хороший после 50, сильный после 70 и замечательный после 120.
from enum import IntEnum
class StrengthToEntropy(IntEnum):
Pathetic = 0
Weak = 30
Good = 50
Strong = 70
Excellent = 120
Давайте сделаем метод класса приложения, который будет связывать значения слайдера и счетчика. Для этого соединим сигнал изменения значения valueChanged
с установкой значения другого элемента.
def connect_slider_to_spinbox(self) -> None:
self.ui.slider_length.valueChanged.connect(self.ui.spinbox_length.setValue)
self.ui.spinbox_length.valueChanged.connect(self.ui.slider_length.setValue)
Не забудьте прописать метод в конструкторе класса.
class PasswordGenerator(QMainWindow):
def __init__(self):
...
self.connect_slider_to_spinbox()
Создадим новый метод для получения символов отмеченных кнопок. Объявим пустую строку chars
. Для каждой кнопки в перечислении добавляем её символы в строку, если она отмечена.
def get_characters(self) -> str:
chars = ""
for btn in buttons.Characters:
if getattr(self.ui, btn.name).isChecked():
chars += btn.value
return chars
Ну и наконец-то, метод установки пароля. Ставим в элемент line_password
новый пароль с помощью метода setText
. Длину берем из слайдера или счетчика, не имеет значения.
def set_password(self) -> None:
self.ui.line_password.setText(
password.create_new(
length=self.ui.slider_length.value(),
characters=self.get_characters())
)
Добавим метод в конструктор. Приложение должно сгенерировать пароль при запуске.
Соединим изменение слайдера или счетчика с установкой пароля.
def connect_slider_to_spinbox(self) -> None:
...
self.ui.spinbox_length.valueChanged.connect(self.set_password)
Позалипаем на быстрое изменение паролей :3
Когда не нажата ни одна кнопка символов, происходит IndexError
. Обработаем этот случай, просто очищая поле пароля.
def set_password(self) -> None:
try:
self.ui.line_password.setText(
password.create_new(
length=self.ui.slider_length.value(),
characters=self.get_characters())
)
except IndexError:
self.ui.line_password.clear()
Принцип тот же, что и у получения строки символов, только в этот раз мы работаем со словарем, а не с перечислением.
def get_character_number(self) -> int:
num = 0
for btn in buttons.CHARACTER_NUMBER.items():
if getattr(self.ui, btn[0]).isChecked():
num += btn[1]
return num
Делаем метод для лейбла энтропии. Берем длину из текста пароля, а не из слайдера или счетчика, так нужно. Ставим в лейбл выражение с помощью f-строки.
def set_entropy(self) -> None:
length = len(self.ui.line_password.text())
char_num = self.get_character_number()
self.ui.label_entropy.setText(
f"Entropy: {password.get_entropy(length, char_num)} bit"
)
Добавим установку энтропии в конце метода установки пароля.
def set_password(self) -> None:
...
self.set_entropy()
Без отмеченных кнопок символов получается ValueError
. Обработаем эту ошибку в функции get_entropy
модуля password
. В таком случае она будет возвращать 0.0.
def get_entropy(length: int, character_number: int) -> float:
try:
entropy = length * log2(character_number)
except ValueError:
return 0.0
return round(entropy, 2)
Теперь сделаем метод для лейбла сложности пароля. Для каждой сложности из перечисления сравниваем энтропию со значением.
def set_strength(self) -> None:
length = len(self.ui.line_password.text())
char_num = self.get_character_number()
for strength in password.StrengthToEntropy:
if password.get_entropy(length, char_num) >= strength.value:
self.ui.label_strength.setText(f"Strength: {strength.name}")
Добавим в конец метода установки пароля.
Запишу в модуль buttons
кортеж с именами кнопок, при нажатии на которые будет генерироваться новый пароль.
GENERATE_PASSWORD = (
'btn_refresh', 'btn_lower', 'btn_upper', 'btn_digits', 'btn_special'
)
Теперь соединим кнопки с методом в конструкторе класса.
for btn in buttons.GENERATE_PASSWORD:
getattr(self.ui, btn).clicked.connect(self.set_password)
Напишем метод для изменения видимости пароля. Если кнопка отмечена, ставим нормальный эхо мод, иначе ставим эхо мод пароля.
from PySide6.QtWidgets import QApplication, QMainWindow, QLineEdit
...
def change_password_visibility(self) -> None:
if self.ui.btn_visibility.isChecked():
self.ui.line_password.setEchoMode(QLineEdit.Normal)
else:
self.ui.line_password.setEchoMode(QLineEdit.Password)
Соединим нажатие кнопки с методом в конструкторе класса.
class PasswordGenerator(QMainWindow):
def __init__(self):
...
self.ui.btn_visibility.clicked.connect(self.change_password_visibility)
Чтобы скопировать текст в буфер обмена, нужно вызвать метод clipboard
класса QApplication
и поставить в него текст.
def copy_to_clipboard(self) -> None:
QApplication.clipboard().setText(self.ui.line_password.text())
Так же соединим нажатие кнопки с методом в конструкторе класса. Я думаю, вы справитесь.
Остался последний штрих. При ручном изменении пароля энтропия не меняется. Решить проблему можно с помощью сигнала редактирования текста textEdited
def do_when_password_edit(self) -> None:
self.ui.line_password.textEdited.connect(self.set_entropy)
self.ui.line_password.textEdited.connect(self.set_strength)
Фишка в том, что это будет не совсем правильная энтропия, потому что количество допустимых символов берется по прежнему из отмеченных кнопок. Предлагаю вам самим решить эту проблему.
from string import ascii_lowercase, ascii_uppercase, digits, punctuation
from enum import Enum
class Characters(Enum):
btn_lower = ascii_lowercase
btn_upper = ascii_uppercase
btn_digits = digits
btn_special = punctuation
CHARACTER_NUMBER = {
'btn_lower': len(Characters.btn_lower.value),
'btn_upper': len(Characters.btn_upper.value),
'btn_digits': len(Characters.btn_digits.value),
'btn_special': len(Characters.btn_special.value)
}
GENERATE_PASSWORD = (
'btn_refresh', 'btn_lower', 'btn_upper', 'btn_digits', 'btn_special'
)
from enum import IntEnum
from math import log2
import secrets
class StrengthToEntropy(IntEnum):
Pathetic = 0
Weak = 30
Good = 50
Strong = 70
Excellent = 120
def create_new(length: int, characters: str) -> str:
return ''.join(secrets.choice(characters) for _ in range(length))
def get_entropy(length: int, character_number: int) -> float:
try:
entropy = length * log2(character_number)
except ValueError:
return 0.0
return round(entropy, 2)
Штош. Давайте поменяем структуру проекта. Поместим все файлы в папку password-generator. Здесь создадим папку ui, в которую поместим все файлы, связанные с интерфейсом. Поменяем импортирование модулей, чтобы все работало. Из ui_main.py
я убрал импортирование ресурсов и добавил его в главный модуль приложения.
from ui.ui_main import Ui_MainWindow
import ui.resources
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QLineEdit
import buttons
import password
from ui.ui_main import Ui_MainWindow
import ui.resources
class PasswordGenerator(QMainWindow):
def __init__(self):
super(PasswordGenerator, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.connect_slider_to_spinbox()
self.set_password()
self.do_when_password_edit()
for btn in buttons.GENERATE_PASSWORD:
getattr(self.ui, btn).clicked.connect(self.set_password)
self.ui.btn_visibility.clicked.connect(self.change_password_visibility)
self.ui.btn_copy.clicked.connect(self.copy_to_clipboard)
def connect_slider_to_spinbox(self) -> None:
self.ui.slider_length.valueChanged.connect(self.ui.spinbox_length.setValue)
self.ui.spinbox_length.valueChanged.connect(self.ui.slider_length.setValue)
self.ui.spinbox_length.valueChanged.connect(self.set_password)
def do_when_password_edit(self) -> None:
self.ui.line_password.textEdited.connect(self.set_entropy)
self.ui.line_password.textEdited.connect(self.set_strength)
def get_characters(self) -> str:
chars = ''
for btn in buttons.Characters:
if getattr(self.ui, btn.name).isChecked():
chars += btn.value
return chars
def set_password(self) -> None:
try:
self.ui.line_password.setText(
password.create_new(
length=self.ui.slider_length.value(),
characters=self.get_characters())
)
except IndexError:
self.ui.line_password.clear()
self.set_entropy()
self.set_strength()
def get_character_number(self) -> int:
num = 0
for btn in buttons.CHARACTER_NUMBER.items():
if getattr(self.ui, btn[0]).isChecked():
num += btn[1]
return num
def set_entropy(self) -> None:
length = len(self.ui.line_password.text())
char_num = self.get_character_number()
self.ui.label_entropy.setText(
f'Entropy: {password.get_entropy(length, char_num)} bit'
)
def set_strength(self) -> None:
length = len(self.ui.line_password.text())
char_num = self.get_character_number()
for strength in password.StrengthToEntropy:
if password.get_entropy(length, char_num) >= strength.value:
self.ui.label_strength.setText(f'Strength: {strength.name}')
def change_password_visibility(self) -> None:
if self.ui.btn_visibility.isChecked():
self.ui.line_password.setEchoMode(QLineEdit.Normal)
else:
self.ui.line_password.setEchoMode(QLineEdit.Password)
def copy_to_clipboard(self) -> None:
QApplication.clipboard().setText(self.ui.line_password.text())
if __name__ == "__main__":
app = QApplication(sys.argv)
window = PasswordGenerator()
window.show()
sys.exit(app.exec())
Создадим файл зависимостей с помощью команды pip freeze > requirements.txt
PySide6==6.3.1
PySide6-Addons==6.3.1
PySide6-Essentials==6.3.1
shiboken6==6.3.1
Инициализируем систему контроля версий, конечно же Git. Установите систему, если вы еще этого не сделали. Пропишите в терминал git init
из корневого каталога или используйте удобный графический интерфейс в PyCharm.
Создадим файл .gitignore
. Будем игнорировать любую папку виртуального окружения, папку PyCharm в моем случае и питонячий кэш. Для VS Code можно прописать .vscode
venv*
.idea
.vscode
__pycache__
Теперь можно сделать первый коммит. Опять же, в PyCharm это делается очень удобно, но если вы любите работать в терминале, то пропишите команды:
git add .
git commit -m "Initial commit"
Переходим к созданию исполняемых файлов. У Qt есть понятная документация, в которой собраны разные способы дистрибуции приложения. Самый простой способ - это PyInstaller. Нет, вру, самый простой способ - это PyInstaller с интерфейсом, auto-py-to-exe. Все эти библиотеки хороши своей понятностью и легкостью, но обычно на выходе получаются небезопасные и тяжелые бинарники.
Я решил показать вам инструмент получше - компилятор Nuitka. Он доступен для всех версий Qt, работает на всех платформах и распространяется по свободной лицензии MIT.
Установим библиотеку с помощью команды pip install nuitka
. Для сборки приложения в один файл понадобится библиотека zstandard
, добавим ее в команду через пробел.
Пишем команду компиляции. Собираем приложение в один файл с помощью флага --onefile
. Флаг --follow-imports
нужен для соблюдения всех импортов. Добавляем плагин PySide6 с помощью флага --enable-plugin=pyside6
. Чтобы убрать консольное окно в системе Windows добавим --windows-disable-console
. Добавим иконку с помощью флага --windows-icon-from-ico
Чтобы убрать все генерируемые нуиткой папки сборки добавим флаг --remove-output
Можно указать название файла через флаг -o
, но это вообще необязательно, всегда можно просто переименовать файл. В конце напишем путь к файлу скрипта, который мы собираемся собирать.
nuitka --onefile --follow-imports --enable-plugin=pyside6 --windows-disable-console --windows-icon-from-ico=ui\icons\app-icon.ico --remove-output -o password-generator.exe app.py
На выходе получилось приложение с размером 16.7 мегабайт.
Я использовал дистрибутив Ubuntu версии 22.04. С нуиткой вышло громадное приложение в 145 мегабайт. Если вы знаете, как сделать меньше - пишите в комментарии.
python -m nuitka --onefile --follow-imports --enable-plugin=pyside6 --remove-output app.py
Я попробовал собрать приложение с PyInstaller, получилось уже получше, 57,6 мегабайт. С иконкой в докбаре и проводнике вообще беда, она просто не хотела ставиться.
pyinstaller --name="Password Generator" --windowed app.py
Я использовал версию 10.15 Catalina. Для сборки приложения под macOS нужно прописать специальный параметр --macos-create-app-bundle
python3 -m nuitka --onefile --follow-imports --enable-plugin=pyside6 --macos-create-app-bundle --remove-output -o password-generator.app app.py
Вышло почти так же компактно, как и на винде - 18 мегабайт. С иконкой тоже возникли проблемы, поэтому собирал без нее.
Надеюсь, вам понравился такой комплексный туториал. Генерируйте сильные пароли, дегенерируйте слабые, и будет вам счастье. До встречи.