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