Source code for gratools.useful_function

from __future__ import annotations  # Ensures type hints are parsed correctly, good practice

import logging
from datetime import datetime

# Third-party imports
import click  # Though Group and Command are from click_extra, BadParameter is from click
from click_extra import Command, Group  # Using Command and Group from click_extra
from cloup import Context, HelpFormatter, HelpTheme, Style  # For advanced help formatting

# Local application/library specific imports
from .logger_config import shared_console  # For printing the header

# ===========================
# HELP CONTEXT & FORMATTING SETTINGS
# ===========================
# These settings define the appearance and behavior of the help screens
# generated by Cloup for commands and groups.
CONTEXT_SETTINGS = Context.settings(
    help_option_names=["-h", "--help"],  # Standard help option flags
    ignore_unknown_options=False,  # Raise an error for unknown options
    align_option_groups=True,  # Align option groups in help output
    align_sections=True,  # Align command sections in help output
    show_constraints=True,  # Show constraints (e.g., choices, ranges) in help
    formatter_settings=HelpFormatter.settings(
        max_width=120,  # Maximum width of the help output
        col1_max_width=30,  # Max width for the first column (options/commands)
        col2_min_width=60,  # Min width for the second column (help text)
        indent_increment=2,  # Spaces for indentation levels
        col_spacing=1,  # Spaces between columns
        row_sep=None,  # Separator between rows (None for default)
        theme=HelpTheme.dark().with_(  # Using a dark theme with custom style overrides
            invoked_command=Style(fg="bright_yellow"),  # Style for the invoked command itself
            command_help=Style(fg="bright_cyan", bold=False),  # Style for the command's short help
            heading=Style(fg="bright_green", bold=True),  # Style for section headings (e.g., "Options:", "Commands:")
            constraint=Style(fg="magenta"),  # Style for option constraints (e.g., "[required]")
            section_help=Style(fg="cyan"),  # Style for the help text of sections (e.g., group's help) - was red
            col1=Style(fg="bright_blue"),  # Style for the first column (options/command names) - was bright_cyan
            col2=Style(fg="white"),  # Style for the second column (help descriptions)
            epilog=Style(fg="bright_white", italic=True),  # Style for the epilog text at the end of help
        ),
    ),
)

# Vous pouvez mettre ceci comme une méthode statique ou une fonction utilitaire
# pour ne pas le recréer à chaque appel.
# C'est plus propre de le définir une seule fois.
RC_TRANS = str.maketrans("ACGTNacgtn", "TGCANtgcan")

[docs] def reverse_complement_string(s: str) -> str: """Calcule rapidement le complément inverse d'une chaîne d'ADN.""" return s.translate(RC_TRANS)[::-1]
[docs] class CustomCommand(Command): # Inherits from click_extra.Command """ Custom Command class that applies global context settings (for help formatting) and prepends a specific header to the help message of individual commands. """
[docs] def __init__(self, *args, **kwargs): """ Initializes the CustomCommand. Ensures that the predefined CONTEXT_SETTINGS are applied by default to this command. """ kwargs.setdefault("context_settings", CONTEXT_SETTINGS) self.logger = logging.getLogger("GraTools") super().__init__(*args, **kwargs)
[docs] def invoke(self, ctx): """Mesure et affiche le temps d'exécution autour de l’appel réel.""" start = datetime.now() try: return super().invoke(ctx) except Exception as e: self.logger.error(f"Error during command execution: {e}") finally: elapsed = datetime.now() - start self.logger.info( f"Command '[green]{self.name}[/green]' completed in: [magenta]{elapsed}[/magenta]" )
[docs] def get_help(self, ctx: click.Context) -> str: """ Overrides the default help generation to prepend a custom header. The header is printed directly to the console using `shared_console` before the standard help text is generated and returned. Args: ctx (click.Context): The current Click context. Returns: str: The formatted help text, including the prepended header. """ # Dynamically import header_tool to avoid circular dependencies # if __init__.py imports from this module. try: from .__init__ import header_tool # Assuming header_tool is defined in your __init__.py shared_console.print(header_tool) # Print the header to the console except ImportError: shared_console.print("[bold red]Warning: GraTools header could not be loaded.[/bold red]") original_help_text = super().get_help(ctx) return f"{original_help_text}\n" # Add a newline for spacing after Cloup's help
[docs] class CustomGroup(Group): # Inherits from click_extra.Group """ Custom Group class that applies global context settings, prepends a header to its own help message, and ensures that all subcommands added via its `command` decorator use `CustomCommand` by default. """
[docs] def __init__(self, *args, **kwargs): """ Initializes the CustomGroup. Ensures that the predefined CONTEXT_SETTINGS are applied by default to this group and its subcommands (if they don't override). """ kwargs.setdefault("context_settings", CONTEXT_SETTINGS) super().__init__(*args, **kwargs)
[docs] def command(self, *args, **kwargs): """ Overrides the default `command` decorator registration. This ensures that any command registered using this group's `command` method will automatically use `CustomCommand` as its class, thereby inheriting the custom help formatting and header. Args: *args: Positional arguments for the command decorator. **kwargs: Keyword arguments for the command decorator. Returns: Callable: The decorator that registers the command. """ kwargs.setdefault("cls", CustomCommand) # Default to CustomCommand for subcommands return super().command(*args, **kwargs)
[docs] def get_help(self, ctx: click.Context) -> str: """ Overrides the default help generation for the group to prepend a custom header. Args: ctx (click.Context): The current Click context. Returns: str: The formatted help text for the group, including the prepended header. """ try: from .__init__ import header_tool shared_console.print(header_tool) # Print the header except ImportError: shared_console.print("[bold red]Warning: GraTools header could not be loaded.[/bold red]") original_help_text = super().get_help(ctx) return f"{original_help_text}\n" # Add a newline for spacing
[docs] def validate_percentage_or_int(ctx: click.Context, param: click.Parameter, value: str | int | float) -> int | float: """ Click callback for validating that an option's value is either an integer or a float representing a percentage (between 0.0 and 1.0 inclusive). This function is intended to be used as a `callback` for a Click option. Args: ctx (click.Context): The current Click context. param (click.Parameter): The Click parameter (option) being validated. value (str | int | float): The input value provided by the user for the option. Click might pass it as str, or already converted if type is set. Returns: int | float: The validated value, converted to `int` if it's a whole number, or `float` if it's a percentage (0.0-1.0). Raises: click.BadParameter: If the value is not a valid integer or a float between 0.0 and 1.0. """ if value is None: # Handle cases where the option might not be provided (e.g. if not required) return value # Or raise BadParameter if it's always required to be non-None try: # Attempt to convert to float first, as int can be represented as float. float_value = float(value) # This will raise ValueError if `value` is not number-like string # Check if the float is effectively an integer (e.g., 5.0, -2.0) if float_value.is_integer(): # It's a whole number, return as int. # Further checks for integer range could be added here if needed. return int(float_value) # Check if it's a valid percentage (0.0 to 1.0) elif 0.0 <= float_value <= 1.0: return float_value # Return as float for percentages else: # It's a float, but not in the 0-1 range for percentages, # and not a whole number. This case implies an invalid input if # only integers or 0-1 floats are allowed. # If non-0-1 floats that are not whole numbers are also invalid, this error is correct. # If they *are* valid under some other condition, this logic needs adjustment. # Based on the original docstring, it seems only integers or 0-1 floats are desired. raise click.BadParameter( f"Value '{value}' is not a whole number, nor a valid percentage (0.0 to 1.0)." ) except ValueError: # Conversion to float failed, so it's not a number. raise click.BadParameter( f"Value '{value}' is not a valid number (integer or percentage format)." ) except TypeError: # If value is already a number but of unexpected type for some operations raise click.BadParameter( f"Unexpected type for value '{value}'. Expected a string or number." )