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
« 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"""
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)
41# ------ apio info system
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")
70 # -- Concatenate and return.
71 config_status = ", ".join(config_status)
72 return config_status
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.
81Examples:[code]
82 apio info system # System info.[/code]
84[NOTE] For programmatic access to this information use 'apio api get-system'.
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"""
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."""
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 )
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 )
121 table.add_column("ITEM", no_wrap=True, min_width=20)
122 table.add_column("VALUE", no_wrap=True, style=EMPH1)
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 )
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 )
159# ------ apio info platforms
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.
166Examples:[code]
167 apio info platforms # List supported platform ids.[/code]
169The automatic platform ID detection of Apio can be overridden by \
170defining a different platform ID using the APIO_PLATFORM environment variable.
171"""
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."""
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 )
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 )
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)
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")
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 = " "
218 table.add_row(
219 f"{marker}{platform_id}",
220 platform_type,
221 platform_variant,
222 style=style,
223 )
225 # -- Render the table.
226 cout()
227 ctable(table)
230# ------ apio info colors
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.
237The command shows the themes colors even if the current theme is 'no-colors'.
239Examples:[code]
240 apio info colors # Rich library output (default)
241 apio inf col -p # Using shortcuts.[/code]
242"""
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."""
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 )
261 # -- Print title.
262 cout("", "ANSI Colors", "")
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
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)
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))
296 # -- Construct the line.
297 line = " ".join(values)
299 # -- Output the line.
300 cout(line)
302 cout()
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)
309# ------ apio info themes
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.
317The command shows colors even if the current theme is 'no-colors'.
319[code]
320Examples:
321 apio info themes # Show themes colors
322 apio inf col -p # Using shortcuts.[/code]
323"""
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."""
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 )
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)
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 )
359 # -- Get selected theme
360 selected_theme = get_theme()
361 selected_theme_name = selected_theme.name
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")
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)
387 # -- Apply the style
388 row_values.append(styled_text)
390 table.add_row(*row_values)
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)
400 # -- Render the table.
401 cout()
402 ctable(table)
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)
407 cout("To change your theme use 'apio preferences -t ...'", style=INFO)
408 cout()
411# ------ apio info commands
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 )
429 table.add_column("APIO COMMAND", no_wrap=True, min_width=20, style=EMPH2)
430 table.add_column("DESCRIPTION", no_wrap=True)
432 for cmd in commands:
433 table.add_row(cmd[0], cmd[1])
435 # -- Render the table.
436 cout()
437 ctable(table)
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 """
445 header1 = "APIO COMMAND"
446 header2 = "DESCRIPTION"
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 ]
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))
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 )
466 # -- Table header
467 cwrite(
468 "| {0} | {1} |\n".format(
469 header1.ljust(w1),
470 header2.ljust(w2),
471 )
472 )
474 cwrite(
475 "| {0} | {1} |\n".format(
476 "-" * w1,
477 "-" * w2,
478 )
479 )
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 )
490 # -- All done.
491 cwrite("\n<!-- END generation by 'apio commands --docs' -->\n\n")
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.
501Examples:[code]
502 apio info commands
503 apio info commands --docs > docs/commands-list.md[/code]
504"""
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."""
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
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 )
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()])
540 # -- Sort the commands list alphabetically.
541 commands.sort()
543 # -- Generate the output
544 if docs:
545 _list_boards_docs_format(commands)
546 else:
547 _list_boards_table_format(commands)
550# ------ apio info
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"""
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]
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."""
583 # pass