Source code for src.args

#!/usr/bin/env python3
# -*- coding: utf-8 -*-


"""
Provides classes and functions for parsing and validating command-line
arguments.

:module: args
:author: Le Bars, Yoann
"""

# Defines the public names of this module.
__all__ = ['ExitCode', 'LogLevel',
           'CmdLineValidationError', 'CmdLineArgs']


import argparse
from typing import Final
from enum import IntEnum, Enum
from PySide6.QtWidgets import QApplication
from .hformatter import PersonalisedArgumentParser, InvalidCommandLine
from . import constants


[docs] class ExitCode(IntEnum): """ Exit codes for the application. """ SUCCESS = 0 CMD_LINE_ERROR = -1 MISSING_ABOUT_FILE = -2 NEGATIVE_LOG_FREQ = -10 NOT_ENOUGH_ITERATIONS = -3 NOT_ENOUGH_BODIES = -4 NEGATIVE_DENSITY = -5 NEGATIVE_TIME_STEP = -6 NEGATIVE_GRAVITY = -7 NEGATIVE_EPSILON = -8 NEGATIVE_SEED = -9 UNKNOWN_EXCEPTION = -11
[docs] class LogLevel(Enum): """ Log level for the application. """ DEBUG = "debug" INFO = "info" WARNING = "warning" CRITICAL = "critical" FATAL = "fatal" def __str__(self) -> str: """ Returns the string representation of the enum member's value. :returns: The string value of the log level. :rtype: str """ return self.value
[docs] class CmdLineValidationError(Exception): """ Exception for command line validation errors. """ def __init__(self, message: str, code: ExitCode): """ Class constructor. :param str message: Error message. :param ExitCode code: Exit code. """ super().__init__(message) self.code = code
[docs] class CmdLineArgs(argparse.Namespace): """ Holds and parses command-line arguments. :ivar int n_iter: Number of iterations. :ivar int n_bodies: Number of bodies. :ivar float dens: Object density. :ivar float dt: Model time step (in s). :ivar float universal_g: Universal gravity constant (in m³/kg/s²). :ivar float epsilon: Computing precision. :ivar int seed: Seed for random number generation. :ivar bool debug_console: Whether to enable logs through the standard output. :ivar str | None debug_file: File path for logging. :ivar str | None debug_syslog: Identifier for syslog logging. :ivar LogLevel log_level: Log level filter. :ivar int log_freq: Frequency of energy log output. :ivar float restitution: Coefficient of restitution for collisions. :ivar float friction: Coefficient of kinetic friction. :ivar float static_friction: Coefficient of static friction. :ivar float linear_damping: Damping factor for linear velocity. :ivar float angular_damping: Damping factor for angular velocity. """ n_iter: int = constants.DEFAULT_N_ITER n_bodies: int = constants.DEFAULT_N_BODIES dens: float = constants.DEFAULT_DENS dt: float = constants.DEFAULT_DT universal_g: float = constants.G epsilon: float = constants.EPSILON seed: int = constants.DEFAULT_SEED debug_console: bool = False debug_file: str | None = None debug_syslog: str | None = None log_level: LogLevel = LogLevel.CRITICAL log_freq: int = constants.DEFAULT_LOG_FREQ show_torque_arrow: bool = False show_alpha_arrow: bool = False
[docs] @classmethod def parse(cls, app: QApplication, argv: list[str] | None = None) -> 'CmdLineArgs': """ Parses and validates command-line arguments. This method uses the `QApplication` instance to provide translated help strings for the command-line interface, ensuring a consistent user experience. :param QApplication app: The application instance, used for translations. :param list[str] | None argv: Command-line arguments to parse. Defaults to `sys.argv[1:]`. :returns: An initialised instance of `CmdLineArgs`. """ parser = PersonalisedArgumentParser.create( # These strings are for command-line help, which is typically not translated. app.translate( 'main', 'A benchmark for Python using Qt framework.'), app.translate('main', 'Positional arguments'), app.translate('main', 'Optional arguments'), constants.__version__, app.translate('main', 'Display program version and exit.'), app.translate('main', 'Show this help message and exit.'), app.translate('main', 'Usage: '), app.translate('main', 'Default') ) argument_definitions: Final[list[dict]] = [ { "flags": ['-i', '--niter'], "dest": constants.N_ITER_ARG, "type": int, "default": cls.n_iter, "help": app.translate('main', 'Number of simulation iterations to run.') }, { "flags": ['-n', '--nbodies'], "dest": constants.N_BODIES_ARG, "type": int, "default": cls.n_bodies, "help": app.translate('main', 'Number of bodies to simulate.') }, { "flags": ['-D', '--diagnostics'], "dest": constants.DEBUG_CONSOLE_ARG, "action": 'store_true', "help": app.translate('main', 'Enable diagnostic output (alias for --debugConsole).'), }, { "flags": ['-d', '--dens'], "dest": constants.DENS_ARG, "type": float, "default": cls.dens, "help": app.translate('main', 'Object density, used as a mass-to-radius proportionality ' 'coefficient.') }, { "flags": ['-t', '--dt'], "dest": constants.DT_ARG, "type": float, "default": cls.dt, "help": app.translate('main', 'Initial time step (in s) for the simulation. The time step ' 'is adaptive.') }, { "flags": ['-g', '--universalg'], "dest": constants.UNIVERSAL_G_ARG, "type": float, "default": constants.G, "help": app.translate('main', 'Universal gravity constant (in m³/kg/s²).') }, { "flags": ['-e', '--epsilon'], "dest": constants.EPSILON_ARG, "type": float, "default": constants.EPSILON, "help": app.translate('main', 'Softening factor for force calculations to prevent ' 'singularities.') }, { "flags": ['-s', '--seed'], "dest": constants.SEED_ARG, "type": int, "default": constants.DEFAULT_SEED, "help": app.translate('main', 'Seed for random number generation. If 0, a random seed is ' 'used.') }, { "flags": ['-c', '--debugConsole'], "dest": constants.DEBUG_CONSOLE_ARG, "action": 'store_true', "help": app.translate('main', 'Enable logs through the standard output.') }, { "flags": ['-f', '--debugFile'], "dest": constants.DEBUG_FILE_ARG, "type": str, "help": app.translate('main', 'Enable logs into a file.') }, { "flags": ['-S', '--debugSyslog'], "dest": constants.DEBUG_SYSLOG_ARG, "type": str, "help": app.translate('main', 'Enable logs through Syslog.') }, { "flags": ['--log-freq'], "dest": constants.LOG_FREQ_ARG, "type": int, "default": cls.log_freq, "help": app.translate('main', 'Frequency of energy log output (every N iterations).'), }, { "flags": ['-l', '--level'], "dest": constants.LOG_LEVEL_ARG, "type": LogLevel, "default": LogLevel.CRITICAL, "choices": list(LogLevel), "help": app.translate('main', 'Set the logging verbosity level.'), }, { "flags": ['-r', '--restitution'], "dest": constants.COEFF_RESTITUTION_ARG, "type": float, "default": constants.COEFF_RESTITUTION, "help": app.translate('main', 'Coefficient of restitution (bounciness).') }, { "flags": ['-F', '--friction'], "dest": constants.COEFF_FRICTION_ARG, "type": float, "default": constants.COEFF_FRICTION, "help": app.translate('main', 'Coefficient of kinetic (sliding) friction.'), }, { "flags": ['-T', '--static-friction'], "dest": constants.COEFF_STATIC_FRICTION_ARG, "type": float, "default": constants.COEFF_STATIC_FRICTION, "help": app.translate('main', 'Coefficient of static friction.'), }, { "flags": ['-L', '--linear-damping'], "dest": constants.LINEAR_DAMPING_ARG, "type": float, "default": constants.LINEAR_DAMPING, "help": app.translate('main', 'Damping factor for linear velocity.'), }, { "flags": ['-a', '--angular-damping'], "dest": constants.ANGULAR_DAMPING_ARG, "type": float, "default": constants.ANGULAR_DAMPING, "help": app.translate('main', 'Damping factor for angular velocity.'), }, ] for arg_def in argument_definitions: parser.add_argument(*arg_def.pop("flags"), **arg_def) parsed_args = cls() try: # Parse arguments directly into the CmdLineArgs instance # The 'version' and 'help' actions exit, so we won't reach here if they are used. parser.parse_args(args=argv, namespace=parsed_args) except (InvalidCommandLine, argparse.ArgumentError) as e: raise CmdLineValidationError( f"Incorrect command line.: {e}", ExitCode.CMD_LINE_ERROR) from e return parsed_args
[docs] def validate(self) -> None: """ Validates the arguments. """ # These validation messages are for developer-facing exceptions and are not # intended for translation. checks = [ (self.n_iter <= 0, 'Number of iterations must be positive.', ExitCode.NOT_ENOUGH_ITERATIONS), (self.n_bodies <= 0, 'Number of bodies must be positive.', ExitCode.NOT_ENOUGH_BODIES), (self.dens <= 0.0, 'Density must be positive.', ExitCode.NEGATIVE_DENSITY), (self.dt <= 0.0, 'Time step must be positive.', ExitCode.NEGATIVE_TIME_STEP), (self.universal_g <= 0.0, 'Universal gravity constant must be positive.', ExitCode.NEGATIVE_GRAVITY), (self.epsilon <= 0.0, 'Epsilon (precision) must be positive.', ExitCode.NEGATIVE_EPSILON), (self.log_freq <= 0, 'Log frequency must be positive.', ExitCode.NEGATIVE_LOG_FREQ), (self.seed < 0, 'Seed must be non-negative.', ExitCode.NEGATIVE_SEED), ] for condition, message, code in checks: if condition: raise CmdLineValidationError(message, code)