Source code for mafw.scripts.click_groups

#  Copyright 2025–2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""
Reusable Click group classes for MAFw command-line interfaces.

This module centralizes the command abbreviation behavior used by the
``mafw`` executable and the development tools so nested command groups can
inherit the same resolution policy without repeating the ``cls=...``
configuration.

.. versionadded:: 2.2

Authors
-------
Bulgheroni Antonio <antonio.bulgheroni@ec.europa.eu>
"""

from __future__ import annotations

import sys
from typing import TYPE_CHECKING, Any

import click
from click.exceptions import ClickException

if TYPE_CHECKING:
    pass


[docs] class AbbreviateGroup(click.Group): """Click group that resolves unique command prefixes.""" group_class: type[click.Group] = click.Group
[docs] def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None: """Return a command by exact name or a unique abbreviation.""" # let's give it a try to the cmd_name. if the user provided the full command, # then we might be lucky and get it working right away, otherwise we will have to try # to get the command using the abbreviations. rv = super().get_command(ctx, cmd_name) if rv is not None: return rv matches = [name for name in self.list_commands(ctx) if name.startswith(cmd_name)] if not matches: return None if len(matches) == 1: return click.Group.get_command(self, ctx, matches[0]) ctx.fail(f'Too many matches: {", ".join(sorted(matches))}')
[docs] def resolve_command( self, ctx: click.Context, args: list[str] ) -> tuple[str | None, click.Command | None, list[str]]: """Return the canonical command name for abbreviated commands.""" _, cmd, args = super().resolve_command(ctx, args) if TYPE_CHECKING: assert isinstance(cmd, click.Command) return cmd.name if cmd is not None else None, cmd, args
AbbreviateGroup.group_class = AbbreviateGroup
[docs] class MAFwGroup(AbbreviateGroup): """Click group with abbreviation and MAFw-specific exit handling.""" group_class: type[click.Group] = AbbreviateGroup
[docs] def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: """Parse arguments and record the selected root command before invoke.""" rv = super().parse_args(ctx, args) if ( ctx.parent is None ): # this is assuring that we store only the first command corresponding to the root context ctx.meta['_mafw_invoked_subcommand'] = self._detect_command_name(args) return rv
[docs] def _detect_command_name(self, args: list[str]) -> str | None: """Return the first subcommand token while skipping root option values.""" # This is probably an overkill. After the default parsing the first element in args should be the first # (abbreviated or fully typed) command. All global options will be removed from args. # so potentially we could return self.get_command(ctx, args[0]), # I will leave it like this because it might be useful in the future. option_params = {opt for param in self.params for opt in getattr(param, 'opts', [])} expects_value = { opt for param in self.params if not getattr(param, 'is_flag', False) for opt in getattr(param, 'opts', []) } skip_next = False for token in args: if skip_next: skip_next = False continue if token in option_params: if token in expects_value: skip_next = True continue if token.startswith('-'): continue selected = self.get_command(click.Context(self), token) return selected.name if selected is not None else token return None
[docs] def invoke(self, ctx: click.Context) -> Any: """Invoke the command and normalize Click exceptions to MAFw exits.""" from mafw.scripts.mafw_exe import print_banner print_banner(ctx) try: return super().invoke(ctx) except ClickException as exc: exc.show() sys.exit(exc.exit_code) except (SystemExit, click.exceptions.Exit): raise except Exception: from mafw.scripts.mafw_exe import ReturnValue sys.exit(ReturnValue.Error)
[docs] def main(self, *args: Any, **kwargs: Any) -> None: # type: ignore[override] """Run the CLI and convert returned statuses to process exit codes.""" from mafw.scripts.mafw_exe import ReturnValue try: rv = super().main(*args, standalone_mode=False, **kwargs) # type: ignore[call-overload] if isinstance(rv, (ReturnValue, int)): sys.exit(rv) sys.exit(ReturnValue.OK) except ClickException as exc: exc.show() sys.exit(exc.exit_code) except SystemExit: raise except Exception: sys.exit(ReturnValue.Error)