Source code for src.main

#!/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())