#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Benchmark main module.
:module: main
:author: Le Bars, Yoann
This file is part of the pure Python benchmark.
This Python benchmark is free software: you can redistribute it and/or modify it under the terms of
the GNU General Public License as published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This Python benchmark is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
The file LICENSE is a copy of the GNU General Public License. You can also see it on
https://www.gnu.org/licenses/.
"""
# Defines the public names of this module.
__all__ = ['main']
import sys
from typing import Final
from multiprocess import freeze_support
from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QMessageBox
from PySide6.QtCore import (
QLibraryInfo, QTranslator, QLocale, Slot, QFile, QIODevice)
from PySide6.QtGui import QIcon
from src.args import CmdLineArgs, CmdLineValidationError, ExitCode
from src import constants
from src import display
from src.logger import Logger
from src import ui_mainwindow
from src import rc_linguist, rc_about, rc_icons # pylint: disable=unused-import
class MissingAboutFile(Exception):
"""
Exeption when the about file is missing.
"""
class MainWindow(QMainWindow):
"""
Class to manage application main window.
:ivar ui_mainwindow.Ui_MainWindow __ui: Window descriptor.
:ivar ui_mainwindow.Ui_MainWindow __ui: Window descriptor.
:ivar display.Display __view: View showing objects.
:ivar QLocale __locale: System locale description.
"""
__ui: ui_mainwindow.Ui_MainWindow
__view: display.Display
__locale: QLocale
def __init__(self, locale: QLocale, args: 'CmdLineArgs') -> None:
"""
Class initialiser.
:param QLocale locale: System locale descriptor.
:param CmdLineArgs args: Parsed command-line arguments.
"""
super().__init__()
self.__locale = locale
# Set up the UI from the generated Ui_MainWindow class.
self.__ui = ui_mainwindow.Ui_MainWindow()
self.__ui.setupUi(self)
# Create the 3D display and embed it in the main window.
self.__view = display.Display(args)
container: QWidget = QWidget.createWindowContainer(self.__view)
self.setCentralWidget(container)
# Connect all UI signals to their corresponding slots.
self._connect_signals()
# Start the simulation after the window is set up.
self.__view.run_simulation()
def _connect_signals(self) -> None:
"""Connects all UI signals to their respective slots."""
self.__ui.actionAbout_Qt.triggered.connect(QApplication.aboutQt)
self.__ui.actionAbout.triggered.connect(self.about)
self.__ui.actionQuit.triggered.connect(QApplication.quit)
self.__view.signal_quit.connect(self._receive_quit)
self.__ui.menubar.setNativeMenuBar(True)
def cleanup(self) -> None:
"""
Explicitly cleans up resources, like the multiprocessing pool.
"""
self.__view.cleanup()
@Slot(bool)
def _receive_quit(self) -> None:
"""
Receiving message indicating the application should stop.
"""
QApplication.quit()
def about(self) -> None:
"""
Displays information about the application.
"""
# Try to open the locale-specific about file first, then fall back to the default.
locale_file_path = f':about/locales/about_{self.__locale.name()}.htm'
default_file_path = ':about/locales/about.htm'
about_file = QFile(locale_file_path)
if not about_file.open(QIODevice.ReadOnly | QIODevice.Text):
about_file.setFileName(default_file_path)
# If neither file can be opened, show a critical error message to the user.
if not about_file.isOpen() and not about_file.open(QIODevice.ReadOnly | QIODevice.Text):
QMessageBox.critical(
self,
self.tr('Error'),
self.tr('The about file is missing from the application resources.')
)
return
# Read the file content, format it with the version, and display the dialog.
about_message = str(about_file.readAll(), 'utf-8').format(
constants.__version__)
about_file.close()
QMessageBox.about(self, self.tr('About Pure Python'), about_message)
[docs]
def main(**kwargs: int | float | bool) -> ExitCode:
"""
Main entry point for the application.
This function sets up the Qt application, parses command-line arguments (or accepts them
programmatically via `kwargs` for testing), creates the main window, and runs the simulation.
:param kwargs: Keyword arguments for testing, corresponding to command-line options
(e.g., `n_iter=100`, `n_bodies=10`). If provided, command-line parsing is
skipped.
:returns: The application's exit code.
:rtype: ExitCode
"""
# Actual application.
app: QApplication = QApplication(sys.argv)
try:
cmd_args: CmdLineArgs
if kwargs:
# For testing purposes
cmd_args = CmdLineArgs(**kwargs) # type: ignore
else:
# Parse arguments from the actual command line.
cmd_args = CmdLineArgs.parse(app, sys.argv[1:])
# Validate the parsed arguments.
cmd_args.validate(app)
except CmdLineValidationError as e:
print(f"[error] {e}", file=sys.stderr)
return e.code
# Translation path.
path: str = QLibraryInfo.path(QLibraryInfo.TranslationsPath)
# Current localization.
locale: Final[QLocale] = QLocale.system()
# Application translator.
translator = QTranslator(app)
if translator.load(locale, 'qtbase', '_', path):
app.installTranslator(translator)
translator = QTranslator(app)
path = ':/translations/locales'
if translator.load(QLocale.system(), 'pure-python', '_', path):
app.installTranslator(translator)
# Set up the logging system based on command-line arguments.
# This instance must be kept alive for the duration of the application.
logger = Logger(cmd_args)
# Application name.
app_name = app.translate('main', 'Pure Python')
app.setApplicationName(app_name)
app.setApplicationDisplayName(app_name)
app.setApplicationVersion(constants.__version__)
app.setOrganizationName('eiG')
app.setOrganizationDomain('eig.fr')
app.setWindowIcon(QIcon(':/icons/resources/avatar-farfadet.jpg'))
window = MainWindow(locale=locale, args=cmd_args)
window.show()
try:
exit_code = app.exec()
except Exception as e: # pylint: disable=broad-except
print(f"[error] An unknown error occurred: {e}", file=sys.stderr)
return ExitCode.UNKNOWN_EXCEPTION
finally:
# Ensure resources are cleaned up regardless of how the app exits.
window.cleanup()
logger.cleanup()
return exit_code
if __name__ == "__main__":
# The return value of main() is used as the exit code.
# argparse's --help and --version actions call sys.exit() themselves.
# The path hack for direct script execution has been removed in favor of
# standard module execution (`python -m src.main`) to adhere to PEP 8
# and resolve Pylint's `wrong-import-position` warnings.
# If you need to run this file directly, you must ensure the project root
# is in your PYTHONPATH.
if __package__ is None:
print("Error: This script is designed to be run as a module.", file=sys.stderr)
print("Use `python3 -m src.main` from the project root.", file=sys.stderr)
sys.exit(1)
# This is required for PyInstaller to correctly handle multiprocessing
# when the application is frozen into a single executable.
freeze_support()
sys.exit(main())