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

231 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-23 03: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 pathlib import Path 

12from datetime import date 

13import click 

14from rich.table import Table 

15from rich.text import Text 

16from rich import box 

17from rich import markup 

18from rich.color import ANSI_COLOR_NAMES 

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

20from apio.common import proto_util 

21from apio.common.debug_util import is_under_vscode_debugger 

22from apio.utils import util, apio_platforms, env_options 

23from apio.commands import options 

24from apio.apio_context import ( 

25 ApioContext, 

26 PackagesPolicy, 

27 ProjectPolicy, 

28 RemoteConfigPolicy, 

29) 

30from apio.common.proto.apio_common_pb2 import ApioArch 

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

32from apio.common.apio_themes import THEMES_TABLE, THEME_LIGHT 

33from apio.managers.remote_config import ( 

34 get_datetime_stamp, 

35 days_between_datetime_stamps, 

36) 

37from apio.common.apio_console import ( 

38 PADDING, 

39 cout, 

40 cwrite, 

41 cstyle, 

42 ctable, 

43 get_theme, 

44 configure, 

45) 

46 

47# ------ apio info system 

48 

49 

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

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

52 status of the cached remote config.""" 

53 config = apio_ctx.remote_config.data 

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

55 timestamp_now = get_datetime_stamp() 

56 config_status = [] 

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

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

59 config_days = days_between_datetime_stamps( 

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

61 ) 

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

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

64 config_status.append( 

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

66 ) 

67 else: 

68 config_status.append("Cached") 

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

70 if "refresh-failure-on" in metadata: 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 # -- Handle the case of not having a cached config. 

73 else: 

74 config_status.append("Not cached") 

75 

76 # -- Concatenate and return. 

77 return ", ".join(config_status) 

78 

79 

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

81APIO_INFO_SYSTEM_HELP = """ 

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

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

84CLI installation issues. 

85 

86Examples:[code] 

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

88 

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

90 

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

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

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

94environment variable 'APIO_HOME'. 

95""" 

96 

97 

98@click.command( 

99 name="system", 

100 cls=ApioCommand, 

101 short_help="Show system information.", 

102 help=APIO_INFO_SYSTEM_HELP, 

103) 

104def _system_cli(): 

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

106 

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

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

109 apio_ctx = ApioContext( 

110 project_policy=ProjectPolicy.NO_PROJECT, 

111 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

112 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

113 ) 

114 

115 platform = apio_ctx.platform 

116 

117 # -- Define the table. 

118 table = Table( 

119 show_header=True, 

120 show_lines=True, 

121 padding=PADDING, 

122 box=box.SQUARE, 

123 border_style=BORDER, 

124 title="Apio System Information", 

125 title_justify="left", 

126 ) 

127 

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

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

130 

131 # -- Add rows 

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

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

134 table.add_row( 

135 "Yosys release tag", 

136 apio_ctx.package_manager.get_yosys_release_tag(), 

137 ) 

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

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

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

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

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

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

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

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

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

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

148 table.add_row( 

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

150 ) 

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

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

153 table.add_row( 

154 "Remote config URL", apio_ctx.remote_config.remote_config_url 

155 ) 

156 table.add_row( 

157 "Remote config status", construct_remote_config_status_str(apio_ctx) 

158 ) 

159 table.add_row( 

160 "Verible formatter", 

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

162 ) 

163 table.add_row( 

164 "Verible language server", 

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

166 ) 

167 

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

169 for var in env_options.get_defined(): 

170 val = env_options.get(var) 

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

172 

173 # -- Render the table. 

174 cout() 

175 ctable(table) 

176 cout( 

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

178 style=INFO, 

179 ) 

180 

181 

182# ------ apio info project 

183 

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

185APIO_INFO_PROJECT_HELP = """ 

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

187project and displays it in a table format. 

188 

189Examples:[code] 

190 apio info project # Project info. 

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

192 

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

194""" 

195 

196 

197@click.command( 

198 name="project", 

199 cls=ApioCommand, 

200 short_help="Show project information.", 

201 help=APIO_INFO_PROJECT_HELP, 

202) 

203@options.env_option_gen() 

204@options.project_dir_option 

205def _project_cli( 

206 *, 

207 # Options 

208 env: str, 

209 project_dir: Path | None, 

210): 

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

212 

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

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

215 apio_ctx = ApioContext( 

216 project_policy=ProjectPolicy.PROJECT_REQUIRED, 

217 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

218 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

219 project_dir_arg=project_dir, 

220 env_arg=env, 

221 report_env=False, 

222 ) 

223 

224 # -- Shortcuts 

225 project = apio_ctx.project 

226 res = apio_ctx.project_resources 

227 

228 # -- Define the table. 

229 table = Table( 

230 show_header=True, 

231 show_lines=True, 

232 padding=PADDING, 

233 box=box.SQUARE, 

234 border_style=BORDER, 

235 title="Apio project Information", 

236 title_justify="left", 

237 ) 

238 

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

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

241 

242 # -- Add rows 

243 board_id = project.get_str_option("board") 

244 assert isinstance(board_id, str) 

245 

246 fpga_id = res.fpga_id 

247 programmer_id = res.programmer_id 

248 

249 defs = apio_ctx.definitions 

250 assert defs is not None 

251 

252 board_definition_src = ( 

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

254 ) 

255 fpga_definition_src = ( 

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

257 ) 

258 programmer_definition_src = ( 

259 "User custom" 

260 if defs.is_custom_programmer(programmer_id) 

261 else "Apio standard" 

262 ) 

263 

264 fpga_definition = res.fpga_definition 

265 proto_util.check_is_required(fpga_definition, "arch", "part_num", "size") 

266 

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

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

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

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

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

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

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

274 table.add_row("FPGA part num", fpga_definition.part_num) 

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

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

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

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

279 

280 # -- Render the table. 

281 cout() 

282 ctable(table) 

283 cout( 

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

285 style=INFO, 

286 ) 

287 

288 

289# ------ apio info platforms 

290 

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

292APIO_INFO_PLATFORMS_HELP = """ 

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

294with the effective platform ID of your system highlighted. 

295 

296Examples:[code] 

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

298 

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

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

301""" 

302 

303 

304@click.command( 

305 name="platforms", 

306 cls=ApioCommand, 

307 short_help="Supported platforms.", 

308 help=APIO_INFO_PLATFORMS_HELP, 

309) 

310def _platforms_cli(): 

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

312 

313 # Create the apio context. 

314 apio_ctx = ApioContext( 

315 project_policy=ProjectPolicy.NO_PROJECT, 

316 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

317 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

318 ) 

319 

320 # -- Define the table. 

321 table = Table( 

322 show_header=True, 

323 show_lines=True, 

324 padding=PADDING, 

325 box=box.SQUARE, 

326 border_style=BORDER, 

327 title="Apio Supported Platforms", 

328 title_justify="left", 

329 ) 

330 

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

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

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

334 

335 # -- Add rows. 

336 for ( 

337 platform_id, 

338 apio_platform, 

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

340 

341 # -- Mark the current platform. 

342 if platform_id == apio_ctx.platform_id: 

343 style = EMPH3 

344 marker = "* " 

345 else: 

346 style = None 

347 marker = " " 

348 

349 table.add_row( 

350 f"{marker}{platform_id}", 

351 apio_platform.type, 

352 apio_platform.variant, 

353 style=style, 

354 ) 

355 

356 # -- Render the table. 

357 cout() 

358 ctable(table) 

359 

360 

361# ------ apio info colors 

362 

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

364APIO_INFO_COLORS_HELP = """ 

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

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

367 

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

369 

370Examples:[code] 

371 apio info colors # Rich library output (default) 

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

373""" 

374 

375 

376@click.command( 

377 name="colors", 

378 cls=ApioCommand, 

379 short_help="Colors table.", 

380 help=APIO_INFO_COLORS_HELP, 

381) 

382def _colors_cli(): 

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

384 

385 # -- This initializes the output console. 

386 ApioContext( 

387 project_policy=ProjectPolicy.NO_PROJECT, 

388 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

389 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

390 ) 

391 

392 # -- Print title. 

393 cout("", "ANSI Colors", "") 

394 

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

396 lookup = {} 

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

398 assert 0 <= num <= 255 

399 lookup[num] = name 

400 

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

402 # -- suppressed 

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

404 saved_theme_name = None 

405 else: 

406 saved_theme_name = get_theme().name 

407 configure(theme_name=THEME_LIGHT.name) 

408 

409 # -- Print the table. 

410 num_rows = 64 

411 num_cols = 4 

412 for row in range(num_rows): 

413 values = [] 

414 for col in range(num_cols): 

415 num = row + (col * num_rows) 

416 name = lookup.get(num, None) 

417 if name is None: 

418 # -- No color name. 

419 values.append(" " * 24) 

420 else: 

421 # -- Color name is available. 

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

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

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

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

426 

427 # -- Construct the line. 

428 line = " ".join(values) 

429 

430 # -- Output the line. 

431 cout(line) 

432 

433 cout() 

434 

435 # -- Restore the original theme. 

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

437 configure(theme_name=saved_theme_name) 

438 

439 

440# ------ apio info themes 

441 

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

443APIO_INFO_THEMES_HELP = """ 

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

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

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

447 

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

449 

450[code] 

451Examples: 

452 apio info themes # Show themes colors 

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

454""" 

455 

456 

457@click.command( 

458 name="themes", 

459 cls=ApioCommand, 

460 short_help="Show apio themes.", 

461 help=APIO_INFO_THEMES_HELP, 

462) 

463def _themes_cli() -> None: 

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

465 

466 # -- This initializes the output console. 

467 ApioContext( 

468 project_policy=ProjectPolicy.NO_PROJECT, 

469 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

470 packages_policy=PackagesPolicy.ENSURE_PACKAGES, 

471 ) 

472 

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

474 style_names_set = set() 

475 for theme_info in THEMES_TABLE.values(): 

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

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

478 

479 # -- Define the table. 

480 table = Table( 

481 show_header=True, 

482 show_lines=True, 

483 padding=PADDING, 

484 box=box.SQUARE, 

485 border_style=BORDER, 

486 title="Apio Themes Style Colors", 

487 title_justify="left", 

488 ) 

489 

490 # -- Get selected theme 

491 selected_theme = get_theme() 

492 selected_theme_name = selected_theme.name 

493 

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

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

496 assert theme_name == theme.name 

497 column_name = theme_name.upper() 

498 if theme_name == selected_theme_name: 

499 column_name = f"*{column_name}*" 

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

501 

502 # -- Append the table rows 

503 for style_name in style_names_list: 

504 row_values: list[Text] = [] 

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

506 # Get style 

507 colors_enabled = theme_info.colors_enabled 

508 if colors_enabled: 

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

510 styled_text = Text("---") 

511 else: 

512 styled_text = Text( 

513 style_name, style=theme_info.styles[style_name] 

514 ) 

515 else: 

516 styled_text = Text(style_name) 

517 

518 # -- Apply the style 

519 row_values.append(styled_text) 

520 

521 table.add_row(*row_values) 

522 

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

524 # -- suppressed 

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

526 saved_theme_name = None 

527 else: 

528 saved_theme_name = get_theme().name 

529 configure(theme_name=THEME_LIGHT.name) 

530 

531 # -- Render the table. 

532 cout() 

533 ctable(table) 

534 

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

536 configure(theme_name=saved_theme_name) 

537 

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

539 cout() 

540 

541 

542# ------ apio info commands 

543 

544 

545def _list_boards_table_format(commands): 

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

547 list of [command_name, command_description] 

548 """ 

549 # -- Generate the table. 

550 table = Table( 

551 show_header=True, 

552 show_lines=True, 

553 padding=PADDING, 

554 box=box.SQUARE, 

555 border_style=BORDER, 

556 title="Apio commands", 

557 title_justify="left", 

558 ) 

559 

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

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

562 

563 for cmd in commands: 

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

565 

566 # -- Render the table. 

567 cout() 

568 ctable(table) 

569 

570 

571def _list_boards_docs_format(commands): 

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

573 list of [command_name, command_description] 

574 """ 

575 

576 header1 = "COMMAND" 

577 header2 = "DESCRIPTION" 

578 

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

580 tagged_commands = [ 

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

582 ] 

583 

584 # -- Determine column sizes 

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

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

587 

588 # -- Print page header 

589 today = date.today() 

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

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

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

593 cwrite( 

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

595 ) 

596 

597 # -- Table header 

598 cwrite( 

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

600 header1.ljust(w1), 

601 header2.ljust(w2), 

602 ) 

603 ) 

604 

605 cwrite( 

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

607 "-" * w1, 

608 "-" * w2, 

609 ) 

610 ) 

611 

612 # -- Add the rows 

613 for tagged_cmd in tagged_commands: 

614 cwrite( 

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

616 tagged_cmd[0].ljust(w1), 

617 tagged_cmd[1].ljust(w2), 

618 ) 

619 ) 

620 

621 # -- All done. 

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

623 

624 

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

626APIO_INFO_COMMANDS_HELP = """ 

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

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

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

630Apio documentation. 

631 

632Examples:[code] 

633 apio info commands 

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

635""" 

636 

637 

638@click.command( 

639 name="commands", 

640 cls=ApioCommand, 

641 short_help="Show apio commands.", 

642 help=APIO_INFO_COMMANDS_HELP, 

643) 

644@options.docs_format_option 

645def _commands_cli( 

646 *, 

647 # Options 

648 docs, 

649): 

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

651 

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

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

654 # 

655 # pylint: disable=import-outside-toplevel 

656 # pylint: disable=cyclic-import 

657 from apio.commands import apio as apio_main 

658 

659 # -- This initializes the output console. 

660 ApioContext( 

661 project_policy=ProjectPolicy.NO_PROJECT, 

662 remote_config_policy=RemoteConfigPolicy.CACHED_OK, 

663 packages_policy=PackagesPolicy.IGNORE_PACKAGES, 

664 ) 

665 

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

667 commands = [] 

668 for subgroup in apio_main.SUBGROUPS: 

669 for cmd in subgroup.commands: 

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

671 

672 # -- Sort the commands list alphabetically. 

673 commands.sort() 

674 

675 # -- Generate the output 

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

677 _list_boards_docs_format(commands) 

678 else: 

679 _list_boards_table_format(commands) 

680 

681 

682# ------ apio info 

683 

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

685APIO_INFO_HELP = """ 

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

687additional information about Apio and your system. 

688""" 

689 

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

691SUBGROUPS = [ 

692 ApioSubgroup( 

693 "Subcommands", 

694 [ 

695 _system_cli, 

696 _project_cli, 

697 _platforms_cli, 

698 _colors_cli, 

699 _themes_cli, 

700 _commands_cli, 

701 ], 

702 ), 

703] 

704 

705 

706@click.command( 

707 name="info", 

708 cls=ApioGroup, 

709 subgroups=SUBGROUPS, 

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

711 help=APIO_INFO_HELP, 

712) 

713def cli(): 

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

715 

716 # pass