Coverage for apio/commands/apio_info.py: 92%

187 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-09 01:55 +0000

1# -*- coding: utf-8 -*- 

2# -- This file is part of the Apio project 

3# -- (C) 2016-2024 FPGAwars 

4# -- Authors 

5# -- * Jesús Arroyo (2016-2019) 

6# -- * Juan Gonzalez (obijuan) (2019-2024) 

7# -- License GPLv2 

8"""Implementation of 'apio info' command""" 

9 

10import sys 

11from typing import List 

12from datetime import date 

13import click 

14from rich.table import Table 

15from rich.text import Text 

16from rich import box 

17from rich.color import ANSI_COLOR_NAMES 

18from apio.common.apio_styles import BORDER, EMPH1, EMPH2, EMPH3, INFO 

19from apio.utils import util, apio_platforms 

20from apio.commands import options 

21from apio.apio_context import ( 

22 ApioContext, 

23 PackagesPolicy, 

24 ProjectPolicy, 

25 RemoteConfigPolicy, 

26) 

27from apio.utils.cmd_util import ApioGroup, ApioSubgroup, ApioCommand 

28from apio.common.apio_themes import THEMES_TABLE, THEME_LIGHT 

29from apio.profile import get_datetime_stamp, days_between_datetime_stamps 

30from apio.common.apio_console import ( 

31 PADDING, 

32 cout, 

33 cwrite, 

34 cstyle, 

35 ctable, 

36 get_theme, 

37 configure, 

38) 

39 

40# ------ apio info system 

41 

42 

43def construct_remote_config_status_str(apio_ctx: ApioContext) -> str: 

44 """Query the apio profile and construct a short string indicating the 

45 status of the cached remote config.""" 

46 config = apio_ctx.profile.remote_config 

47 metadata = config.get("metadata", {}) 

48 timestamp_now = get_datetime_stamp() 

49 config_status = [] 

50 # -- Handle the case of a having a cached config. 

51 if config: 51 ↛ 67line 51 didn't jump to line 67 because the condition on line 51 was always true

52 config_days = days_between_datetime_stamps( 

53 metadata.get("loaded-at", ""), timestamp_now, 0 

54 ) 

55 # -- Determine cache age in days, if possible. 

56 if config_days is not None: 56 ↛ 61line 56 didn't jump to line 61 because the condition on line 56 was always true

57 config_status.append( 

58 f"Cached {util.plurality(config_days, 'day')} ago" 

59 ) 

60 else: 

61 config_status.append("Cached") 

62 # -- Indicate if there is a sign of a failed refresh attempt. 

63 if "refresh-failure-on" in metadata: 63 ↛ 64line 63 didn't jump to line 64 because the condition on line 63 was never true

64 config_status.append("refresh failed.") 

65 # -- Handle the case of not having a cached config. 

66 else: 

67 config_status.append("Not cached") 

68 

69 # -- Concatenate and return. 

70 config_status = ", ".join(config_status) 

71 return config_status 

72 

73 

74# -- Text in the rich-text format of the python rich library. 

75APIO_INFO_SYSTEM_HELP = """ 

76The command 'apio info system' provides general information about your \ 

77system and Apio CLI installation, which is useful for diagnosing Apio \ 

78CLI installation issues. 

79 

80Examples:[code] 

81 apio info system # System info.[/code] 

82 

83[NOTE] For programmatic access to this information use 'apio api get-system'. 

84 

85[ADVANCED] The default location of the Apio CLI home directory, \ 

86where apio saves preferences and packages, is in the '.apio' directory \ 

87under the user home directory but can be changed using the system \ 

88environment variable 'APIO_HOME'. 

89""" 

90 

91 

92@click.command( 

93 name="system", 

94 cls=ApioCommand, 

95 short_help="Show system information.", 

96 help=APIO_INFO_SYSTEM_HELP, 

97) 

98def _system_cli(): 

99 """Implements the 'apio info system' command.""" 

100 

101 # -- Create the apio context. We use 'cached_ok' to cause the config 

102 # -- to be loaded so we can report it. 

103 apio_ctx = ApioContext( 

104 project_policy=ProjectPolicy.NO_PROJECT, 

105 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

106 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

107 ) 

108 

109 platform = apio_ctx.platform 

110 

111 # -- Define the table. 

112 table = Table( 

113 show_header=True, 

114 show_lines=True, 

115 padding=PADDING, 

116 box=box.SQUARE, 

117 border_style=BORDER, 

118 title="Apio System Information", 

119 title_justify="left", 

120 ) 

121 

122 table.add_column("ITEM", no_wrap=True, min_width=20) 

123 table.add_column("VALUE", no_wrap=True, style=EMPH1) 

124 

125 # -- Add rows 

126 table.add_row("Apio CLI version", util.get_apio_version_str()) 

127 table.add_row("Release info", util.get_apio_release_info() or "(none)") 

128 table.add_row("Python version", util.get_python_version()) 

129 table.add_row("Python executable", sys.executable) 

130 table.add_row("System info", apio_platforms.get_system_info()) 

131 table.add_row("Platform id", platform.id) 

132 table.add_row("Is Darwin", str(platform.is_darwin)) 

133 table.add_row("Is Linux", str(platform.is_linux)) 

134 table.add_row("Is Windows", str(platform.is_windows)) 

135 table.add_row("Scons shell id", apio_ctx.scons_shell_id) 

136 table.add_row("VSCode debugger", str(util.is_under_vscode_debugger())) 

137 table.add_row("Pyinstaller", str(util.is_pyinstaller_app())) 

138 table.add_row( 

139 "Apio Python package", str(util.get_path_in_apio_package("")) 

140 ) 

141 table.add_row("Apio home dir", str(apio_ctx.apio_home_dir)) 

142 table.add_row("Apio packages dir", str(apio_ctx.apio_packages_dir)) 

143 table.add_row("Remote config URL", apio_ctx.profile.remote_config_url) 

144 table.add_row( 

145 "Remote config status", construct_remote_config_status_str(apio_ctx) 

146 ) 

147 table.add_row( 

148 "Veriable formatter", 

149 str(apio_ctx.apio_packages_dir / "verible/bin/verible-verilog-format"), 

150 ) 

151 table.add_row( 

152 "Veriable language server", 

153 str(apio_ctx.apio_packages_dir / "verible/bin/verible-verilog-ls"), 

154 ) 

155 

156 # -- Render the table. 

157 cout() 

158 ctable(table) 

159 cout( 

160 "For programmatic system info use 'apio api get-system'.", 

161 style=INFO, 

162 ) 

163 

164 

165# ------ apio info platforms 

166 

167# -- Text in the rich-text format of the python rich library. 

168APIO_INFO_PLATFORMS_HELP = """ 

169The command 'apio info platforms' lists the platform IDs supported by Apio, \ 

170with the effective platform ID of your system highlighted. 

171 

172Examples:[code] 

173 apio info platforms # List supported platform ids.[/code] 

174 

175The automatic platform ID detection of Apio can be overridden by \ 

176defining a different platform ID using the APIO_PLATFORM environment variable. 

177""" 

178 

179 

180@click.command( 

181 name="platforms", 

182 cls=ApioCommand, 

183 short_help="Supported platforms.", 

184 help=APIO_INFO_PLATFORMS_HELP, 

185) 

186def _platforms_cli(): 

187 """Implements the 'apio info platforms' command.""" 

188 

189 # Create the apio context. 

190 apio_ctx = ApioContext( 

191 project_policy=ProjectPolicy.NO_PROJECT, 

192 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

193 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

194 ) 

195 

196 # -- Define the table. 

197 table = Table( 

198 show_header=True, 

199 show_lines=True, 

200 padding=PADDING, 

201 box=box.SQUARE, 

202 border_style=BORDER, 

203 title="Apio Supported Platforms", 

204 title_justify="left", 

205 ) 

206 

207 table.add_column(" PLATFORM ID", no_wrap=True) 

208 table.add_column("TYPE", no_wrap=True) 

209 table.add_column("VARIANT", no_wrap=True) 

210 

211 # -- Add rows. 

212 for ( 

213 platform_id, 

214 apio_platform, 

215 ) in apio_platforms.get_all_apio_platforms().items(): 

216 

217 # -- Mark the current platform. 

218 if platform_id == apio_ctx.platform_id: 

219 style = EMPH3 

220 marker = "* " 

221 else: 

222 style = None 

223 marker = " " 

224 

225 table.add_row( 

226 f"{marker}{platform_id}", 

227 apio_platform.type, 

228 apio_platform.variant, 

229 style=style, 

230 ) 

231 

232 # -- Render the table. 

233 cout() 

234 ctable(table) 

235 

236 

237# ------ apio info colors 

238 

239# -- Text in the rich-text format of the python rich library. 

240APIO_INFO_COLORS_HELP = """ 

241The command 'apio info colors' shows how ansi colors are rendered on \ 

242the platform, and is typically used to diagnose color related issues. 

243 

244The command shows the themes colors even if the current theme is 'no-colors'. 

245 

246Examples:[code] 

247 apio info colors # Rich library output (default) 

248 apio inf col -p # Using shortcuts.[/code] 

249""" 

250 

251 

252@click.command( 

253 name="colors", 

254 cls=ApioCommand, 

255 short_help="Colors table.", 

256 help=APIO_INFO_COLORS_HELP, 

257) 

258def _colors_cli(): 

259 """Implements the 'apio info colors' command.""" 

260 

261 # -- This initializes the output console. 

262 ApioContext( 

263 project_policy=ProjectPolicy.NO_PROJECT, 

264 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

265 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

266 ) 

267 

268 # -- Print title. 

269 cout("", "ANSI Colors", "") 

270 

271 # -- Create a reversed num->name map 

272 lookup = {} 

273 for name, num in ANSI_COLOR_NAMES.items(): 

274 assert 0 <= num <= 255 

275 lookup[num] = name 

276 

277 # -- Make sure the current theme supports colors, otherwise they will 

278 # -- suppressed 

279 if get_theme().colors_enabled: 279 ↛ 282line 279 didn't jump to line 282 because the condition on line 279 was always true

280 saved_theme_name = None 

281 else: 

282 saved_theme_name = get_theme().name 

283 configure(theme_name=THEME_LIGHT.name) 

284 

285 # -- Print the table. 

286 num_rows = 64 

287 num_cols = 4 

288 for row in range(num_rows): 

289 values = [] 

290 for col in range(num_cols): 

291 num = row + (col * num_rows) 

292 name = lookup.get(num, None) 

293 if name is None: 

294 # -- No color name. 

295 values.append(" " * 24) 

296 else: 

297 # -- Color name is available. 

298 # -- Note that the color names and styling is always done by 

299 # -- the rich library regardless of the choice of output. 

300 s = f"{num:3} {name:20}" 

301 values.append(cstyle(s, style=name)) 

302 

303 # -- Construct the line. 

304 line = " ".join(values) 

305 

306 # -- Output the line. 

307 cout(line) 

308 

309 cout() 

310 

311 # -- Restore the original theme. 

312 if saved_theme_name: 312 ↛ 313line 312 didn't jump to line 313 because the condition on line 312 was never true

313 configure(theme_name=saved_theme_name) 

314 

315 

316# ------ apio info themes 

317 

318# -- Text in the rich-text format of the python rich library. 

319APIO_INFO_THEMES_HELP = """ 

320The command 'apio info themes' shows the colors of the Apio themes. It can \ 

321be used to select the theme that works the best for you. Type \ 

322'apio preferences -h' for information on our to select a theme. 

323 

324The command shows colors even if the current theme is 'no-colors'. 

325 

326[code] 

327Examples: 

328 apio info themes # Show themes colors 

329 apio inf col -p # Using shortcuts.[/code] 

330""" 

331 

332 

333@click.command( 

334 name="themes", 

335 cls=ApioCommand, 

336 short_help="Show apio themes.", 

337 help=APIO_INFO_THEMES_HELP, 

338) 

339def _themes_cli(): 

340 """Implements the 'apio info themes' command.""" 

341 

342 # -- This initializes the output console. 

343 ApioContext( 

344 project_policy=ProjectPolicy.NO_PROJECT, 

345 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

346 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

347 ) 

348 

349 # -- Collect the list of apio list names. 

350 style_names = set() 

351 for theme_info in THEMES_TABLE.values(): 

352 style_names.update(list(theme_info.styles.keys())) 

353 style_names = sorted(list(style_names), key=str.lower) 

354 

355 # -- Define the table. 

356 table = Table( 

357 show_header=True, 

358 show_lines=True, 

359 padding=PADDING, 

360 box=box.SQUARE, 

361 border_style=BORDER, 

362 title="Apio Themes Style Colors", 

363 title_justify="left", 

364 ) 

365 

366 # -- Get selected theme 

367 selected_theme = get_theme() 

368 selected_theme_name = selected_theme.name 

369 

370 # -- Add the table columns, one per theme. 

371 for theme_name, theme in THEMES_TABLE.items(): 

372 assert theme_name == theme.name 

373 column_name = theme_name.upper() 

374 if theme_name == selected_theme_name: 

375 column_name = f"*{column_name}*" 

376 table.add_column(column_name, no_wrap=True, justify="center") 

377 

378 # -- Append the table rows 

379 for style_name in style_names: 

380 row_values: List[Text] = [] 

381 for theme_name, theme_info in THEMES_TABLE.items(): 

382 # Get style 

383 colors_enabled = theme_info.colors_enabled 

384 if colors_enabled: 

385 if style_name not in theme_info.styles: 385 ↛ 386line 385 didn't jump to line 386 because the condition on line 385 was never true

386 styled_text = Text("---") 

387 else: 

388 styled_text = Text( 

389 style_name, style=theme_info.styles[style_name] 

390 ) 

391 else: 

392 styled_text = Text(style_name) 

393 

394 # -- Apply the style 

395 row_values.append(styled_text) 

396 

397 table.add_row(*row_values) 

398 

399 # -- Make sure the current theme supports colors, otherwise they will 

400 # -- suppressed 

401 if get_theme().colors_enabled: 401 ↛ 404line 401 didn't jump to line 404 because the condition on line 401 was always true

402 saved_theme_name = None 

403 else: 

404 saved_theme_name = get_theme().name 

405 configure(theme_name=THEME_LIGHT.name) 

406 

407 # -- Render the table. 

408 cout() 

409 ctable(table) 

410 

411 if saved_theme_name: 411 ↛ 412line 411 didn't jump to line 412 because the condition on line 411 was never true

412 configure(theme_name=saved_theme_name) 

413 

414 cout("To change your theme use 'apio preferences -t ...'", style=INFO) 

415 cout() 

416 

417 

418# ------ apio info commands 

419 

420 

421def _list_boards_table_format(commands): 

422 """Format and output the commands table. 'commands' is a sorted 

423 list of [command_name, command_description] 

424 """ 

425 # -- Generate the table. 

426 table = Table( 

427 show_header=True, 

428 show_lines=True, 

429 padding=PADDING, 

430 box=box.SQUARE, 

431 border_style=BORDER, 

432 title="Apio commands", 

433 title_justify="left", 

434 ) 

435 

436 table.add_column("APIO COMMAND", no_wrap=True, min_width=20, style=EMPH2) 

437 table.add_column("DESCRIPTION", no_wrap=True) 

438 

439 for cmd in commands: 

440 table.add_row(cmd[0], cmd[1]) 

441 

442 # -- Render the table. 

443 cout() 

444 ctable(table) 

445 

446 

447def _list_boards_docs_format(commands): 

448 """Format and output the commands markdown doc. 'commands' is a sorted 

449 list of [command_name, command_description] 

450 """ 

451 

452 header1 = "COMMAND" 

453 header2 = "DESCRIPTION" 

454 

455 # -- Replace the command names with markdown link to the command doc page. 

456 tagged_commands = [ 

457 [f"[{c[0]}](cmd-apio-{c[0]}.md)", c[1]] for c in commands 

458 ] 

459 

460 # -- Determine column sizes 

461 w1 = max(len(header1), *(len(cmd[0]) for cmd in tagged_commands)) 

462 w2 = max(len(header2), *(len(cmd[1]) for cmd in tagged_commands)) 

463 

464 # -- Print page header 

465 today = date.today() 

466 today_str = f"{today.strftime('%B')} {today.day}, {today.year}" 

467 cwrite("\n<!-- BEGIN generation by 'apio commands --docs' -->\n") 

468 cwrite("\n# Apio CLI commands\n") 

469 cwrite( 

470 f"\nThis markdown page was generated automatically on {today_str}.\n\n" 

471 ) 

472 

473 # -- Table header 

474 cwrite( 

475 "| {0} | {1} |\n".format( 

476 header1.ljust(w1), 

477 header2.ljust(w2), 

478 ) 

479 ) 

480 

481 cwrite( 

482 "| {0} | {1} |\n".format( 

483 "-" * w1, 

484 "-" * w2, 

485 ) 

486 ) 

487 

488 # -- Add the rows 

489 for tagged_cmd in tagged_commands: 

490 cwrite( 

491 "| {0} | {1} |\n".format( 

492 tagged_cmd[0].ljust(w1), 

493 tagged_cmd[1].ljust(w2), 

494 ) 

495 ) 

496 

497 # -- All done. 

498 cwrite("\n<!-- END generation by 'apio commands --docs' -->\n\n") 

499 

500 

501# -- Text in the rich-text format of the python rich library. 

502APIO_INFO_COMMANDS_HELP = """ 

503The command 'apio info commands' lists the the available apio commands \ 

504in a table format. If the option '--docs' is specified, the command outputs \ 

505the list as a markdown document that is used to automatically update the \ 

506Apio documentation. 

507 

508Examples:[code] 

509 apio info commands 

510 apio info commands --docs > docs/commands-list.md[/code] 

511""" 

512 

513 

514@click.command( 

515 name="commands", 

516 cls=ApioCommand, 

517 short_help="Show apio commands.", 

518 help=APIO_INFO_COMMANDS_HELP, 

519) 

520@options.docs_format_option 

521def _commands_cli( 

522 *, 

523 # Options 

524 docs, 

525): 

526 """Implements the 'apio info commands' command.""" 

527 

528 # -- We perform this lazy cyclic import here to allow the two modules 

529 # -- to initialize properly without a cyclic import. 

530 # 

531 # pylint: disable=import-outside-toplevel 

532 # pylint: disable=cyclic-import 

533 from apio.commands import apio as apio_main 

534 

535 # -- This initializes the output console. 

536 ApioContext( 

537 project_policy=ProjectPolicy.NO_PROJECT, 

538 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

539 packages_policy=PackagesPolicy.IGNORE_PACKAGES, 

540 ) 

541 

542 # -- Collect commands as a list of <name, description> 

543 commands = [] 

544 for subgroup in apio_main.SUBGROUPS: 

545 for cmd in subgroup.commands: 

546 commands.append([cmd.name, cmd.get_short_help_str()]) 

547 

548 # -- Sort the commands list alphabetically. 

549 commands.sort() 

550 

551 # -- Generate the output 

552 if docs: 

553 _list_boards_docs_format(commands) 

554 else: 

555 _list_boards_table_format(commands) 

556 

557 

558# ------ apio info 

559 

560# -- Text in the rich-text format of the python rich library. 

561APIO_INFO_HELP = """ 

562The command group 'apio info' contains subcommands that provide \ 

563additional information about Apio and your system. 

564""" 

565 

566# -- We have only a single group with the title 'Subcommands'. 

567SUBGROUPS = [ 

568 ApioSubgroup( 

569 "Subcommands", 

570 [ 

571 _system_cli, 

572 _platforms_cli, 

573 _colors_cli, 

574 _themes_cli, 

575 _commands_cli, 

576 ], 

577 ), 

578] 

579 

580 

581@click.command( 

582 name="info", 

583 cls=ApioGroup, 

584 subgroups=SUBGROUPS, 

585 short_help="Apio's info and info.", 

586 help=APIO_INFO_HELP, 

587) 

588def cli(): 

589 """Implements the 'apio info' command group.""" 

590 

591 # pass