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

230 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-07 05:08 +0000

1# This file is part of the Apio project 

2# (C) 2016-present FPGAwars 

3# Project authors: 

4# * Jesús Arroyo (Jesus89) (2016-2019) 

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

6# * The Apio Team (2024-present) 

7# License: GPLv2 

8 

9"""Implementation of 'apio info' command""" 

10 

11import sys 

12from pathlib import Path 

13from datetime import date 

14import click 

15from rich.table import Table 

16from rich.text import Text 

17from rich import box 

18from rich import markup 

19from rich.color import ANSI_COLOR_NAMES 

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

21from apio.common import proto_util, apio_platforms 

22from apio.common.debug_util import is_under_vscode_debugger 

23from apio.utils import util, env_options 

24from apio.commands import options 

25from apio.apio_context import ( 

26 ApioContext, 

27 PackagesPolicy, 

28 ProjectPolicy, 

29 RemoteConfigPolicy, 

30) 

31from apio.common.proto.stubs.apio_common_pb2 import ApioArch 

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

33from apio.common.apio_themes import THEMES_TABLE, THEME_LIGHT 

34from apio.managers.remote_config import ( 

35 get_datetime_stamp, 

36 days_between_datetime_stamps, 

37) 

38from apio.common.apio_console import ( 

39 PADDING, 

40 cout, 

41 cwrite, 

42 cstyle, 

43 ctable, 

44 get_theme, 

45 configure, 

46) 

47 

48# ------ apio info system 

49 

50 

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

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

53 status of the cached remote config.""" 

54 metadata = apio_ctx.remote_config.metadata 

55 timestamp_now = get_datetime_stamp() 

56 config_status = [] 

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

58 config_days = days_between_datetime_stamps( 

59 metadata.loaded_at, timestamp_now, default=None 

60 ) 

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

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

63 config_status.append( 

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

65 ) 

66 else: 

67 config_status.append("cache timestamp error") 

68 

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

70 if metadata.HasField("refresh_failure_on"): 70 ↛ 71line 70 didn't jump to line 71 because the condition on line 70 was never true

71 config_status.append("refresh failed.") 

72 

73 # -- Concatenate and return. 

74 return ", ".join(config_status) 

75 

76 

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

78APIO_INFO_SYSTEM_HELP = """ 

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

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

81CLI installation issues. 

82 

83Examples:[code] 

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

85 

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

87 

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

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

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

91environment variable 'APIO_HOME'. 

92""" 

93 

94 

95@click.command( 

96 name="system", 

97 cls=ApioCommand, 

98 short_help="Show system information.", 

99 help=APIO_INFO_SYSTEM_HELP, 

100) 

101def _system_cli(): 

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

103 

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

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

106 apio_ctx = ApioContext( 

107 project_policy=ProjectPolicy.NO_PROJECT, 

108 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

109 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

110 ) 

111 

112 platform = apio_ctx.platform 

113 

114 # -- Define the table. 

115 table = Table( 

116 show_header=True, 

117 show_lines=True, 

118 padding=PADDING, 

119 box=box.SQUARE, 

120 border_style=BORDER, 

121 title="Apio System Information", 

122 title_justify="left", 

123 ) 

124 

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

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

127 

128 # -- Add rows 

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

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

131 table.add_row( 

132 "Yosys release tag", 

133 apio_ctx.package_manager.get_yosys_release_tag(), 

134 ) 

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

136 table.add_row("Executable", sys.executable) 

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

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

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

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

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

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

143 table.add_row("VSCode debugger", str(is_under_vscode_debugger())) 

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

145 table.add_row( 

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

147 ) 

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

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

150 assert apio_ctx.definitions is not None 

151 table.add_row( 

152 "Apio definitions dir", 

153 str(apio_ctx.definitions.package_definitions_dir), 

154 ) 

155 table.add_row( 

156 "Remote config file URL", apio_ctx.remote_config.remote_config_file_url 

157 ) 

158 table.add_row( 

159 "Remote config status", construct_remote_config_status_str(apio_ctx) 

160 ) 

161 table.add_row( 

162 "Verible formatter", 

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

164 ) 

165 table.add_row( 

166 "Verible language server", 

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

168 ) 

169 

170 # -- Add a row for each define apio env var (e.g.APIO_DEBUG) 

171 for var in env_options.get_defined(): 

172 val = env_options.get(var) 

173 table.add_row(var, markup.escape(val)) 

174 

175 # -- Render the table. 

176 cout() 

177 ctable(table) 

178 cout( 

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

180 style=INFO, 

181 ) 

182 

183 

184# ------ apio info project 

185 

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

187APIO_INFO_PROJECT_HELP = """ 

188The command 'apio info project' collects general information an apio \ 

189project and displays it in a table format. 

190 

191Examples:[code] 

192 apio info project # Project info. 

193 apio info project --env my-env # Select a specific project env.[/code] 

194 

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

196""" 

197 

198 

199@click.command( 

200 name="project", 

201 cls=ApioCommand, 

202 short_help="Show project information.", 

203 help=APIO_INFO_PROJECT_HELP, 

204) 

205@options.env_option_gen() 

206@options.project_dir_option 

207def _project_cli( 

208 *, 

209 # Options 

210 env: str, 

211 project_dir: Path | None, 

212): 

213 """Implements the 'apio info project' command.""" 

214 

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

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

217 apio_ctx = ApioContext( 

218 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

219 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

220 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

221 project_dir_arg=project_dir, 

222 env_arg=env, 

223 report_env=False, 

224 ) 

225 

226 # -- Shortcuts 

227 project = apio_ctx.project 

228 res = apio_ctx.project_resources 

229 

230 # -- Define the table. 

231 table = Table( 

232 show_header=True, 

233 show_lines=True, 

234 padding=PADDING, 

235 box=box.SQUARE, 

236 border_style=BORDER, 

237 title="Apio project Information", 

238 title_justify="left", 

239 ) 

240 

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

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

243 

244 # -- Add rows 

245 board_id = project.get_str_option("board") 

246 assert isinstance(board_id, str) 

247 

248 fpga_id = res.fpga_id 

249 programmer_id = res.programmer_id 

250 

251 defs = apio_ctx.definitions 

252 assert defs is not None 

253 

254 board_definition_src = ( 

255 "User custom" if defs.is_custom_board(board_id) else "Apio standard" 

256 ) 

257 fpga_definition_src = ( 

258 "User custom" if defs.is_custom_fpga(fpga_id) else "Apio standard" 

259 ) 

260 programmer_definition_src = ( 

261 "User custom" 

262 if defs.is_custom_programmer(programmer_id) 

263 else "Apio standard" 

264 ) 

265 

266 fpga_definition = res.fpga_definition 

267 proto_util.check_is_required( 

268 fpga_definition, "arch", "vendor_part", "size" 

269 ) 

270 

271 table.add_row("Total project envs", str(len(project.env_names))) 

272 table.add_row("Active project env", project.env_name) 

273 table.add_row("Top module name", project.get_str_option("top-module", "")) 

274 table.add_row("Board id", board_id) 

275 table.add_row("Board definition", board_definition_src) 

276 table.add_row("FPGA id", fpga_id) 

277 table.add_row("FPGA definition", fpga_definition_src) 

278 table.add_row("FPGA part", fpga_definition.vendor_part) 

279 table.add_row("FPGA Architecture", ApioArch.Name(fpga_definition.arch)) 

280 table.add_row("FPGA size", fpga_definition.size) 

281 table.add_row("Programmer id", programmer_id) 

282 table.add_row("Programmer definition", programmer_definition_src) 

283 

284 # -- Render the table. 

285 cout() 

286 ctable(table) 

287 cout( 

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

289 style=INFO, 

290 ) 

291 

292 

293# ------ apio info platforms 

294 

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

296APIO_INFO_PLATFORMS_HELP = """ 

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

298with the effective platform ID of your system highlighted. 

299 

300Examples:[code] 

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

302 

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

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

305""" 

306 

307 

308@click.command( 

309 name="platforms", 

310 cls=ApioCommand, 

311 short_help="Supported platforms.", 

312 help=APIO_INFO_PLATFORMS_HELP, 

313) 

314def _platforms_cli(): 

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

316 

317 # Create the apio context. 

318 apio_ctx = ApioContext( 

319 project_policy=ProjectPolicy.NO_PROJECT, 

320 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

321 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

322 ) 

323 

324 # -- Define the table. 

325 table = Table( 

326 show_header=True, 

327 show_lines=True, 

328 padding=PADDING, 

329 box=box.SQUARE, 

330 border_style=BORDER, 

331 title="Apio Supported Platforms", 

332 title_justify="left", 

333 ) 

334 

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

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

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

338 

339 # -- Add rows. 

340 for ( 

341 platform_id, 

342 apio_platform, 

343 ) in apio_platforms.get_apio_platforms().items(): 

344 

345 # -- Mark the current platform. 

346 if platform_id == apio_ctx.platform.id: 

347 style = EMPH3 

348 marker = "* " 

349 else: 

350 style = None 

351 marker = " " 

352 

353 table.add_row( 

354 f"{marker}{platform_id}", 

355 apio_platform.type, 

356 apio_platform.variant, 

357 style=style, 

358 ) 

359 

360 # -- Render the table. 

361 cout() 

362 ctable(table) 

363 

364 

365# ------ apio info colors 

366 

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

368APIO_INFO_COLORS_HELP = """ 

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

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

371 

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

373 

374Examples:[code] 

375 apio info colors # Rich library output (default) 

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

377""" 

378 

379 

380@click.command( 

381 name="colors", 

382 cls=ApioCommand, 

383 short_help="Colors table.", 

384 help=APIO_INFO_COLORS_HELP, 

385) 

386def _colors_cli(): 

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

388 

389 # -- This initializes the output console. 

390 ApioContext( 

391 project_policy=ProjectPolicy.NO_PROJECT, 

392 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

393 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

394 ) 

395 

396 # -- Print title. 

397 cout("", "ANSI Colors", "") 

398 

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

400 lookup = {} 

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

402 assert 0 <= num <= 255 

403 lookup[num] = name 

404 

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

406 # -- suppressed 

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

408 saved_theme_name = None 

409 else: 

410 saved_theme_name = get_theme().name 

411 configure(theme_name=THEME_LIGHT.name) 

412 

413 # -- Print the table. 

414 num_rows = 64 

415 num_cols = 4 

416 for row in range(num_rows): 

417 values = [] 

418 for col in range(num_cols): 

419 num = row + (col * num_rows) 

420 name = lookup.get(num, None) 

421 if name is None: 

422 # -- No color name. 

423 values.append(" " * 24) 

424 else: 

425 # -- Color name is available. 

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

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

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

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

430 

431 # -- Construct the line. 

432 line = " ".join(values) 

433 

434 # -- Output the line. 

435 cout(line) 

436 

437 cout() 

438 

439 # -- Restore the original theme. 

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

441 configure(theme_name=saved_theme_name) 

442 

443 

444# ------ apio info themes 

445 

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

447APIO_INFO_THEMES_HELP = """ 

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

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

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

451 

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

453 

454[code] 

455Examples: 

456 apio info themes # Show themes colors 

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

458""" 

459 

460 

461@click.command( 

462 name="themes", 

463 cls=ApioCommand, 

464 short_help="Show apio themes.", 

465 help=APIO_INFO_THEMES_HELP, 

466) 

467def _themes_cli() -> None: 

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

469 

470 # -- This initializes the output console. 

471 ApioContext( 

472 project_policy=ProjectPolicy.NO_PROJECT, 

473 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

474 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

475 ) 

476 

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

478 style_names_set = set() 

479 for theme_info in THEMES_TABLE.values(): 

480 style_names_set.update(list(theme_info.styles.keys())) 

481 style_names_list = sorted(list(style_names_set), key=str.lower) 

482 

483 # -- Define the table. 

484 table = Table( 

485 show_header=True, 

486 show_lines=True, 

487 padding=PADDING, 

488 box=box.SQUARE, 

489 border_style=BORDER, 

490 title="Apio Themes Style Colors", 

491 title_justify="left", 

492 ) 

493 

494 # -- Get selected theme 

495 selected_theme = get_theme() 

496 selected_theme_name = selected_theme.name 

497 

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

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

500 assert theme_name == theme.name 

501 column_name = theme_name.upper() 

502 if theme_name == selected_theme_name: 

503 column_name = f"*{column_name}*" 

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

505 

506 # -- Append the table rows 

507 for style_name in style_names_list: 

508 row_values: list[Text] = [] 

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

510 # Get style 

511 colors_enabled = theme_info.colors_enabled 

512 if colors_enabled: 

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

514 styled_text = Text("---") 

515 else: 

516 styled_text = Text( 

517 style_name, style=theme_info.styles[style_name] 

518 ) 

519 else: 

520 styled_text = Text(style_name) 

521 

522 # -- Apply the style 

523 row_values.append(styled_text) 

524 

525 table.add_row(*row_values) 

526 

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

528 # -- suppressed 

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

530 saved_theme_name = None 

531 else: 

532 saved_theme_name = get_theme().name 

533 configure(theme_name=THEME_LIGHT.name) 

534 

535 # -- Render the table. 

536 cout() 

537 ctable(table) 

538 

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

540 configure(theme_name=saved_theme_name) 

541 

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

543 cout() 

544 

545 

546# ------ apio info commands 

547 

548 

549def _list_boards_table_format(commands): 

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

551 list of [command_name, command_description] 

552 """ 

553 # -- Generate the table. 

554 table = Table( 

555 show_header=True, 

556 show_lines=True, 

557 padding=PADDING, 

558 box=box.SQUARE, 

559 border_style=BORDER, 

560 title="Apio commands", 

561 title_justify="left", 

562 ) 

563 

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

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

566 

567 for cmd in commands: 

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

569 

570 # -- Render the table. 

571 cout() 

572 ctable(table) 

573 

574 

575def _list_boards_docs_format(commands): 

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

577 list of [command_name, command_description] 

578 """ 

579 

580 header1 = "COMMAND" 

581 header2 = "DESCRIPTION" 

582 

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

584 tagged_commands = [ 

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

586 ] 

587 

588 # -- Determine column sizes 

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

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

591 

592 # -- Print page header 

593 today = date.today() 

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

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

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

597 cwrite( 

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

599 ) 

600 

601 # -- Table header 

602 cwrite( 

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

604 header1.ljust(w1), 

605 header2.ljust(w2), 

606 ) 

607 ) 

608 

609 cwrite( 

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

611 "-" * w1, 

612 "-" * w2, 

613 ) 

614 ) 

615 

616 # -- Add the rows 

617 for tagged_cmd in tagged_commands: 

618 cwrite( 

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

620 tagged_cmd[0].ljust(w1), 

621 tagged_cmd[1].ljust(w2), 

622 ) 

623 ) 

624 

625 # -- All done. 

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

627 

628 

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

630APIO_INFO_COMMANDS_HELP = """ 

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

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

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

634Apio documentation. 

635 

636Examples:[code] 

637 apio info commands 

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

639""" 

640 

641 

642@click.command( 

643 name="commands", 

644 cls=ApioCommand, 

645 short_help="Show apio commands.", 

646 help=APIO_INFO_COMMANDS_HELP, 

647) 

648@options.docs_format_option 

649def _commands_cli( 

650 *, 

651 # Options 

652 docs, 

653): 

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

655 

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

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

658 # 

659 # pylint: disable=import-outside-toplevel 

660 # pylint: disable=cyclic-import 

661 from apio.commands import apio as apio_main 

662 

663 # -- This initializes the output console. 

664 ApioContext( 

665 project_policy=ProjectPolicy.NO_PROJECT, 

666 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

667 packages_policy=PackagesPolicy.IGNORE_PACKAGES, 

668 ) 

669 

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

671 commands = [] 

672 for subgroup in apio_main.SUBGROUPS: 

673 for cmd in subgroup.commands: 

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

675 

676 # -- Sort the commands list alphabetically. 

677 commands.sort() 

678 

679 # -- Generate the output 

680 if docs: 680 ↛ 681line 680 didn't jump to line 681 because the condition on line 680 was never true

681 _list_boards_docs_format(commands) 

682 else: 

683 _list_boards_table_format(commands) 

684 

685 

686# ------ apio info 

687 

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

689APIO_INFO_HELP = """ 

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

691additional information about Apio and your system. 

692""" 

693 

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

695SUBGROUPS = [ 

696 ApioSubgroup( 

697 "Subcommands", 

698 [ 

699 _system_cli, 

700 _project_cli, 

701 _platforms_cli, 

702 _colors_cli, 

703 _themes_cli, 

704 _commands_cli, 

705 ], 

706 ), 

707] 

708 

709 

710@click.command( 

711 name="info", 

712 cls=ApioGroup, 

713 subgroups=SUBGROUPS, 

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

715 help=APIO_INFO_HELP, 

716) 

717def cli(): 

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

719 

720 # pass