Coverage for src/mafw/devtools/cli/toolchain/bootstrap_cmd.py: 100%

35 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-26 09:13 +0000

1# Copyright 2026 European Union 

2# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu) 

3# SPDX-License-Identifier: EUPL-1.2 

4""" 

5Bootstrap subcommand for the toolchain command group. 

6 

7This module implements the ``devtools toolchain bootstrap`` command, which 

8iterates over all applicable :class:`~mafw.devtools.toolchain.base.ToolChainTool` 

9instances and attempts to install any that are not yet present. Each tool's 

10:meth:`~mafw.devtools.toolchain.base.ToolChainTool.bootstrap` method is called; 

11the result is categorised as successfully installed, already present, or failed. 

12 

13A Rich spinner is displayed while each tool is being processed, providing 

14visual feedback during potentially slow installation operations. 

15 

16Project tools raise :class:`~mafw.devtools.DevtoolsError` from ``bootstrap()`` 

17to indicate they do not support standalone bootstrapping (they are managed 

18through pyproject.toml and installed automatically via Hatch environments). 

19This is treated as a skip and does not affect the exit code. 

20""" 

21 

22from __future__ import annotations 

23 

24import sys 

25 

26import click 

27from rich.console import Console 

28from rich.panel import Panel 

29from rich.status import Status 

30 

31from mafw.devtools import DevtoolsError 

32from mafw.devtools.cli.toolchain._group import common_options, toolchain 

33from mafw.devtools.toolchain.filtering import resolve_tools 

34 

35 

36@toolchain.command(name='bootstrap') 

37@common_options 

38@click.pass_context 

39def bootstrap(ctx: click.Context, include: tuple[str, ...], exclude: tuple[str, ...]) -> None: 

40 """Install missing tools to set up the development environment. 

41 

42 Iterates over the filtered tool set in registration order, calling each 

43 tool's ``bootstrap()`` method. A Rich spinner provides visual feedback 

44 while each tool is being processed. Results are reported per-tool: 

45 

46 - Successfully installed tools get a green confirmation message. 

47 - Tools already present (indicated by a :class:`~mafw.devtools.DevtoolsError` 

48 mentioning project-tool status) get a dimmed skip message. 

49 - Real installation failures are displayed in a Rich Panel and count 

50 towards a non-zero exit code. 

51 """ 

52 registry = ctx.obj['registry'] 

53 include_host: bool = ctx.obj['include_host'] 

54 console = Console() 

55 

56 tools = resolve_tools(registry, include=include, exclude=exclude, include_host=include_host) 

57 

58 has_failure = False 

59 

60 for tool in tools: 

61 # Display a spinner while processing each tool (requirement 8.7). 

62 with Status(f'Bootstrapping [bold]{tool.name}[/bold]…', console=console): 

63 try: 

64 installed = tool.bootstrap() 

65 except DevtoolsError as exc: 

66 # Project tools raise DevtoolsError to indicate they don't support 

67 # standalone bootstrapping — treat this as a skip, not a failure. 

68 if tool.category == 'project': 

69 console.print(f'[dim]—[/dim] [bold]{tool.name}[/bold]: skipped (project tool)') 

70 else: 

71 # Host tools raising DevtoolsError indicates a real failure 

72 # (e.g. pipx not available, install command failed). 

73 has_failure = True 

74 console.print( 

75 Panel( 

76 f'[bold]{tool.name}[/bold]: {exc}', 

77 title='Bootstrap Error', 

78 border_style='red', 

79 ) 

80 ) 

81 except Exception as exc: # noqa: BLE001 

82 # Unexpected errors are always treated as failures. 

83 has_failure = True 

84 console.print( 

85 Panel( 

86 f'[bold]{tool.name}[/bold]: {exc}', 

87 title='Bootstrap Error', 

88 border_style='red', 

89 ) 

90 ) 

91 else: 

92 if installed: 

93 console.print(f'[green]✓[/green] [bold]{tool.name}[/bold] installed successfully') 

94 else: 

95 # bootstrap() returned False — tool was already present. 

96 console.print(f'[dim]—[/dim] [bold]{tool.name}[/bold] already installed') 

97 

98 # Exit with non-zero code if any tool failed to install. 

99 if has_failure: 

100 sys.exit(1)