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

183 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-24 01:53 +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 

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 

41# ------ apio info system 

42 

43 

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

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

46 status of the cached remote config.""" 

47 config = apio_ctx.profile.remote_config 

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

49 timestamp_now = get_datetime_stamp() 

50 config_status = [] 

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

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

53 config_days = days_between_datetime_stamps( 

54 metadata.get("loaded-at", ""), timestamp_now, None 

55 ) 

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

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

58 config_status.append( 

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

60 ) 

61 else: 

62 config_status.append("Cached") 

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

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

65 config_status.append("refresh failed.") 

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

67 else: 

68 config_status.append("Not cached") 

69 

70 # -- Concatenate and return. 

71 config_status = ", ".join(config_status) 

72 return config_status 

73 

74 

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

76APIO_INFO_SYSTEM_HELP = """ 

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

78system and Apio installation, which is useful for diagnosing Apio \ 

79installation issues. 

80 

81Examples:[code] 

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

83 

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

85 

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

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

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

89environment variable 'APIO_HOME'. 

90""" 

91 

92 

93@click.command( 

94 name="system", 

95 cls=ApioCommand, 

96 short_help="Show system information.", 

97 help=APIO_INFO_SYSTEM_HELP, 

98) 

99def _system_cli(): 

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

101 

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

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

104 apio_ctx = ApioContext( 

105 project_policy=ProjectPolicy.NO_PROJECT, 

106 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

107 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

108 ) 

109 

110 # -- Define the table. 

111 table = Table( 

112 show_header=True, 

113 show_lines=True, 

114 padding=PADDING, 

115 box=box.SQUARE, 

116 border_style=BORDER, 

117 title="Apio System Information", 

118 title_justify="left", 

119 ) 

120 

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

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

123 

124 # -- Add rows 

125 table.add_row("Apio version", util.get_apio_version_str()) 

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

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

128 table.add_row("Platform id", apio_ctx.platform_id) 

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

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

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

132 table.add_row( 

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

134 ) 

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

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

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

138 table.add_row( 

139 "Remote config status", construct_remote_config_status_str(apio_ctx) 

140 ) 

141 table.add_row( 

142 "Veriable formatter", 

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

144 ) 

145 table.add_row( 

146 "Veriable language server", 

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

148 ) 

149 

150 # -- Render the table. 

151 cout() 

152 ctable(table) 

153 cout( 

154 "To force a remote config refresh, run 'apio packages update'.", 

155 style=INFO, 

156 ) 

157 

158 

159# ------ apio info platforms 

160 

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

162APIO_INFO_PLATFORMS_HELP = """ 

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

164with the effective platform ID of your system highlighted. 

165 

166Examples:[code] 

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

168 

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

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

171""" 

172 

173 

174@click.command( 

175 name="platforms", 

176 cls=ApioCommand, 

177 short_help="Supported platforms.", 

178 help=APIO_INFO_PLATFORMS_HELP, 

179) 

180def _platforms_cli(): 

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

182 

183 # Create the apio context. 

184 apio_ctx = ApioContext( 

185 project_policy=ProjectPolicy.NO_PROJECT, 

186 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

187 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

188 ) 

189 

190 # -- Define the table. 

191 table = Table( 

192 show_header=True, 

193 show_lines=True, 

194 padding=PADDING, 

195 box=box.SQUARE, 

196 border_style=BORDER, 

197 title="Apio Supported Platforms", 

198 title_justify="left", 

199 ) 

200 

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

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

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

204 

205 # -- Add rows. 

206 for platform_id, platform_info in apio_ctx.platforms.items(): 

207 platform_type = platform_info.get("type") 

208 platform_variant = platform_info.get("variant") 

209 

210 # -- Mark the current platform. 

211 if platform_id == apio_ctx.platform_id: 

212 style = EMPH3 

213 marker = "* " 

214 else: 

215 style = None 

216 marker = " " 

217 

218 table.add_row( 

219 f"{marker}{platform_id}", 

220 platform_type, 

221 platform_variant, 

222 style=style, 

223 ) 

224 

225 # -- Render the table. 

226 cout() 

227 ctable(table) 

228 

229 

230# ------ apio info colors 

231 

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

233APIO_INFO_COLORS_HELP = """ 

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

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

236 

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

238 

239Examples:[code] 

240 apio info colors # Rich library output (default) 

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

242""" 

243 

244 

245@click.command( 

246 name="colors", 

247 cls=ApioCommand, 

248 short_help="Colors table.", 

249 help=APIO_INFO_COLORS_HELP, 

250) 

251def _colors_cli(): 

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

253 

254 # -- This initializes the output console. 

255 ApioContext( 

256 project_policy=ProjectPolicy.NO_PROJECT, 

257 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

258 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

259 ) 

260 

261 # -- Print title. 

262 cout("", "ANSI Colors", "") 

263 

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

265 lookup = {} 

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

267 assert 0 <= num <= 255 

268 lookup[num] = name 

269 

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

271 # -- suppressed 

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

273 saved_theme_name = None 

274 else: 

275 saved_theme_name = get_theme().name 

276 configure(theme_name=THEME_LIGHT.name) 

277 

278 # -- Print the table. 

279 num_rows = 64 

280 num_cols = 4 

281 for row in range(num_rows): 

282 values = [] 

283 for col in range(num_cols): 

284 num = row + (col * num_rows) 

285 name = lookup.get(num, None) 

286 if name is None: 

287 # -- No color name. 

288 values.append(" " * 24) 

289 else: 

290 # -- Color name is available. 

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

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

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

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

295 

296 # -- Construct the line. 

297 line = " ".join(values) 

298 

299 # -- Output the line. 

300 cout(line) 

301 

302 cout() 

303 

304 # -- Restore the original theme. 

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

306 configure(theme_name=saved_theme_name) 

307 

308 

309# ------ apio info themes 

310 

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

312APIO_INFO_THEMES_HELP = """ 

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

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

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

316 

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

318 

319[code] 

320Examples: 

321 apio info themes # Show themes colors 

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

323""" 

324 

325 

326@click.command( 

327 name="themes", 

328 cls=ApioCommand, 

329 short_help="Show apio themes.", 

330 help=APIO_INFO_THEMES_HELP, 

331) 

332def _themes_cli(): 

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

334 

335 # -- This initializes the output console. 

336 ApioContext( 

337 project_policy=ProjectPolicy.NO_PROJECT, 

338 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

339 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

340 ) 

341 

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

343 style_names = set() 

344 for theme_info in THEMES_TABLE.values(): 

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

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

347 

348 # -- Define the table. 

349 table = Table( 

350 show_header=True, 

351 show_lines=True, 

352 padding=PADDING, 

353 box=box.SQUARE, 

354 border_style=BORDER, 

355 title="Apio Themes Style Colors", 

356 title_justify="left", 

357 ) 

358 

359 # -- Get selected theme 

360 selected_theme = get_theme() 

361 selected_theme_name = selected_theme.name 

362 

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

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

365 assert theme_name == theme.name 

366 column_name = theme_name.upper() 

367 if theme_name == selected_theme_name: 

368 column_name = f"*{column_name}*" 

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

370 

371 # -- Append the table rows 

372 for style_name in style_names: 

373 row_values: List[Text] = [] 

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

375 # Get style 

376 colors_enabled = theme_info.colors_enabled 

377 if colors_enabled: 

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

379 styled_text = Text("---") 

380 else: 

381 styled_text = Text( 

382 style_name, style=theme_info.styles[style_name] 

383 ) 

384 else: 

385 styled_text = Text(style_name) 

386 

387 # -- Apply the style 

388 row_values.append(styled_text) 

389 

390 table.add_row(*row_values) 

391 

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

393 # -- suppressed 

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

395 saved_theme_name = None 

396 else: 

397 saved_theme_name = get_theme().name 

398 configure(theme_name=THEME_LIGHT.name) 

399 

400 # -- Render the table. 

401 cout() 

402 ctable(table) 

403 

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

405 configure(theme_name=saved_theme_name) 

406 

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

408 cout() 

409 

410 

411# ------ apio info commands 

412 

413 

414def _list_boards_table_format(commands): 

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

416 list of [command_name, command_description] 

417 """ 

418 # -- Generate the table. 

419 table = Table( 

420 show_header=True, 

421 show_lines=True, 

422 padding=PADDING, 

423 box=box.SQUARE, 

424 border_style=BORDER, 

425 title="Apio commands", 

426 title_justify="left", 

427 ) 

428 

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

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

431 

432 for cmd in commands: 

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

434 

435 # -- Render the table. 

436 cout() 

437 ctable(table) 

438 

439 

440def _list_boards_docs_format(commands): 

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

442 list of [command_name, command_description] 

443 """ 

444 

445 header1 = "APIO COMMAND" 

446 header2 = "DESCRIPTION" 

447 

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

449 tagged_commands = [ 

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

451 ] 

452 

453 # -- Determine column sizes 

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

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

456 

457 # -- Print page header 

458 today = date.today() 

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

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

461 cwrite("\n# Apio commands\n") 

462 cwrite( 

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

464 ) 

465 

466 # -- Table header 

467 cwrite( 

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

469 header1.ljust(w1), 

470 header2.ljust(w2), 

471 ) 

472 ) 

473 

474 cwrite( 

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

476 "-" * w1, 

477 "-" * w2, 

478 ) 

479 ) 

480 

481 # -- Add the rows 

482 for tagged_cmd in tagged_commands: 

483 cwrite( 

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

485 tagged_cmd[0].ljust(w1), 

486 tagged_cmd[1].ljust(w2), 

487 ) 

488 ) 

489 

490 # -- All done. 

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

492 

493 

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

495APIO_INFO_COMMANDS_HELP = """ 

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

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

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

499Apio documentation. 

500 

501Examples:[code] 

502 apio info commands 

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

504""" 

505 

506 

507@click.command( 

508 name="commands", 

509 cls=ApioCommand, 

510 short_help="Show apio commands.", 

511 help=APIO_INFO_COMMANDS_HELP, 

512) 

513@options.docs_format_option 

514def _commands_cli( 

515 # Options 

516 docs, 

517): 

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

519 

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

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

522 # 

523 # pylint: disable=import-outside-toplevel 

524 # pylint: disable=cyclic-import 

525 from apio.commands import apio as apio_main 

526 

527 # -- This initializes the output console. 

528 ApioContext( 

529 project_policy=ProjectPolicy.NO_PROJECT, 

530 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

531 packages_policy=PackagesPolicy.IGNORE_PACKAGES, 

532 ) 

533 

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

535 commands = [] 

536 for subgroup in apio_main.SUBGROUPS: 

537 for cmd in subgroup.commands: 

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

539 

540 # -- Sort the commands list alphabetically. 

541 commands.sort() 

542 

543 # -- Generate the output 

544 if docs: 

545 _list_boards_docs_format(commands) 

546 else: 

547 _list_boards_table_format(commands) 

548 

549 

550# ------ apio info 

551 

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

553APIO_INFO_HELP = """ 

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

555additional information about Apio and your system. 

556""" 

557 

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

559SUBGROUPS = [ 

560 ApioSubgroup( 

561 "Subcommands", 

562 [ 

563 _system_cli, 

564 _platforms_cli, 

565 _colors_cli, 

566 _themes_cli, 

567 _commands_cli, 

568 ], 

569 ), 

570] 

571 

572 

573@click.command( 

574 name="info", 

575 cls=ApioGroup, 

576 subgroups=SUBGROUPS, 

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

578 help=APIO_INFO_HELP, 

579) 

580def cli(): 

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

582 

583 # pass