#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Module for command-line argument parsing and validation.
:module: args
:author: Le Bars, Yoann
"""
import argparse
from enum import IntEnum, Enum
from PySide6.QtWidgets import QApplication
from src.hformatter import create_parser, InvalidCommandLine
from src import constants
[docs]
class ExitCode(IntEnum):
"""
Exit codes for the application.
"""
SUCCESS = 0
CMD_LINE_ERROR = -1
MISSING_ABOUT_FILE = -2
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"
[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: Objects 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.
"""
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
[docs]
@classmethod
def parse(cls, app: QApplication, args: list[str] | None = None) -> 'CmdLineArgs':
"""
Parses and validates command-line arguments.
:param QApplication app: The application instance for translations.
:param list[str] | None args: Command-line arguments to parse. Defaults to `sys.argv[1:]`.
:returns: An instance of CmdLineArgs.
:rtype: CmdLineArgs
"""
parser = create_parser(
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')
)
parser.add_argument(
'-i', '--niter', dest='n_iter', type=int, default=constants.DEFAULT_N_ITER,
help=app.translate(
'main', 'Number of simulation iterations to run.')
)
parser.add_argument(
'-n', '--nbodies', dest='n_bodies', type=int, default=constants.DEFAULT_N_BODIES,
help=app.translate('main', 'Number of bodies to simulate.')
)
parser.add_argument(
'-d', '--dens', type=float, default=constants.DEFAULT_DENS,
help=app.translate('main', 'Object density, used as a mass-to-radius '
'proportionality coefficient.')
)
parser.add_argument(
'-t', '--dt', dest='dt', type=float, default=constants.DEFAULT_DT,
help=app.translate('main', 'Initial time step (in s) for the simulation. '
'The time step is adaptive.')
)
parser.add_argument(
'-g', '--universalg', dest='universal_g', type=float, default=constants.G,
help=app.translate(
'main', 'Universal gravity constant (in m³/kg/s²).')
)
parser.add_argument(
'-e', '--epsilon', dest='epsilon', type=float, default=constants.EPSILON,
help=app.translate('main', 'Softening factor for force calculations to prevent '
'singularities.')
)
parser.add_argument(
'-S', '--seed', dest='seed', type=int, default=constants.DEFAULT_SEED,
help=app.translate('main', 'Seed for random number generation. If 0, a '
'random seed is used.')
)
parser.add_argument(
'-c', '--debugConsole', dest='debug_console', action='store_true',
help=app.translate(
'main', 'Enable logs through the standard output.')
)
parser.add_argument(
'-f', '--debugFile', dest='debug_file', type=str,
help=app.translate('main', 'Enable logs into a file.')
)
parser.add_argument(
'-s', '--debugSyslog', dest='debug_syslog', type=str,
help=app.translate('main', 'Enable logs through Syslog.')
)
parser.add_argument(
'-l', '--level', dest='log_level', type=LogLevel, default=LogLevel.CRITICAL,
choices=list(LogLevel),
help=app.translate(
'main',
'Log level filter (debug, info, warning, critical, fatal). '
)
)
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=args, namespace=parsed_args)
except (InvalidCommandLine, argparse.ArgumentError) as e:
raise CmdLineValidationError(
f"{app.translate('main', 'Incorrect command line.')}: {e}",
ExitCode.CMD_LINE_ERROR) from e
return parsed_args
[docs]
def validate(self, app: QApplication) -> None:
"""
Validates the arguments.
:param QApplication app: The application instance for translations.
"""
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.seed < 0, 'Seed must be non-negative.', ExitCode.NEGATIVE_SEED),
]
for condition, message, code in checks:
if condition:
raise CmdLineValidationError(
app.translate('main', message), code)