From 14472574d7d60ee4bd50c2d24b51708e0538d110 Mon Sep 17 00:00:00 2001 From: Zapta Date: Tue, 24 Sep 2024 08:19:51 -0700 Subject: [PATCH 1/3] Tweaked the help text of the apio raw command. No change in behavior. --- apio/commands/raw.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apio/commands/raw.py b/apio/commands/raw.py index 726294b6..9b7472e1 100644 --- a/apio/commands/raw.py +++ b/apio/commands/raw.py @@ -22,10 +22,11 @@ \b Examples: - apio raw "yosys --version" # yosys version - apio raw "nextpnr-ice40 --version" # nextpnr version + apio raw "yosys --version" # Yosys version + apio raw "nextpnr-ice40 --version" # Nextpnr version apio raw "yosys -p 'read_verilog leds.v; show' -q" # Graph a module - apio raw "verilator --lint-only leds.v" # lint a module + apio raw "verilator --lint-only leds.v" # Lint a module + apio raw "icepll -i 12 -o 30" # ICE PLL parameters [Note] If you find a raw command that would benefit other apio users consider suggesting it as an apio feature request. From 0b7997fbbe353d55b9c95895beeab2a8e811596e Mon Sep 17 00:00:00 2001 From: Zapta Date: Tue, 24 Sep 2024 21:23:34 -0700 Subject: [PATCH 2/3] Minor tweak to tox.ini. Removed apio directory from pytest list since its not supposed to include tests. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index fc1e1cfe..e4cce999 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,6 @@ deps = commands = black apio test test-boards #-- Python formating flake8 apio test test-boards #-- python lint pylint apio test test-boards #-- python lint - pytest apio test #-- Tests + pytest test #-- Tests From 4d64a5f8fab79d288d929640079a4271a8a063ed Mon Sep 17 00:00:00 2001 From: Zapta Date: Wed, 25 Sep 2024 10:45:44 -0700 Subject: [PATCH 3/3] Added verifications of at-most-one and at-least-one parameter groups. Also, util.py became too long for lint so divided to util.py and cmd_util.py. --- apio/cmd_util.py | 217 +++++++++++++++++++++++++++++++++++++ apio/commands/boards.py | 21 ++-- apio/commands/build.py | 4 +- apio/commands/clean.py | 4 +- apio/commands/create.py | 3 +- apio/commands/drivers.py | 38 ++++--- apio/commands/examples.py | 25 +++-- apio/commands/graph.py | 4 +- apio/commands/install.py | 20 +++- apio/commands/lint.py | 10 +- apio/commands/modify.py | 14 +-- apio/commands/options.py | 35 +++--- apio/commands/raw.py | 3 +- apio/commands/sim.py | 4 +- apio/commands/system.py | 28 ++--- apio/commands/test.py | 4 +- apio/commands/time.py | 4 +- apio/commands/uninstall.py | 22 ++-- apio/commands/upgrade.py | 4 +- apio/commands/upload.py | 8 +- apio/commands/verify.py | 4 +- apio/util.py | 63 +---------- pyproject.toml | 1 + 23 files changed, 353 insertions(+), 187 deletions(-) create mode 100644 apio/cmd_util.py diff --git a/apio/cmd_util.py b/apio/cmd_util.py new file mode 100644 index 00000000..28170338 --- /dev/null +++ b/apio/cmd_util.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +# -- This file is part of the Apio project +# -- (C) 2016-2018 FPGAwars +# -- Author Jesús Arroyo +# -- Licence GPLv2 +# -- Derived from: +# ---- Platformio project +# ---- (C) 2014-2016 Ivan Kravets +# ---- Licence Apache v2 +"""Utility functionality for apio click commands. """ + + +from typing import Mapping, List, Tuple, Any, Dict, Union +import click + + +# This text marker is inserted into the help text to indicates +# deprecated options. +DEPRECATED_MARKER = "[DEPRECATED]" + + +def fatal_usage_error(ctx: click.Context, msg: str) -> None: + """Prints a an error message and command help hint, and exists the program + with an error status. + ctx: The context that was passed to the command. + msg: A single line short error message. + """ + # Mimiking the usage error message from click/exceptions.py. + # E.g. "Try 'apio install -h' for help." + click.secho(ctx.get_usage()) + click.secho( + f"Try '{ctx.command_path} {ctx.help_option_names[0]}' for help." + ) + click.secho() + click.secho(f"Error: {msg}") + ctx.exit(1) + + +def _get_params_objs( + ctx: click.Context, +) -> Dict[str, Union[click.Option, click.Argument]]: + """Return a mapping from param id to param obj.""" + result = {} + for param_obj in ctx.command.get_params(ctx): + assert isinstance(param_obj, (click.Option, click.Argument)), type( + param_obj + ) + result[param_obj.name] = param_obj + return result + + +def _params_ids_to_aliases( + ctx: click.Context, params_ids: List[str] +) -> List[str]: + """Maps param ids to their respective user facing canonical aliases. + The order of the params is in the inptut list is preserved. + + For the definition of param ids see check_exclusive_params(). + + The canonical alias of an option is it's longest alias, + for example "--dir" for the option ["-d", "--dir"]. The canonical + alias of an argument is the argument name as shown in the command's help, + e.g. "PACKAGES" for the argument packages. + """ + # Param id -> param obj. + params_dict = _get_params_objs(ctx) + + # Map the param ids to their canonical aliases. + result = [] + for param_id in params_ids: + param_obj: Union[click.Option, click.Argument] = params_dict[param_id] + assert isinstance(param_obj, (click.Option, click.Argument)), type( + param_obj + ) + if isinstance(param_obj, click.Option): + # For options we pick their longest alias + param_alias = max(param_obj.aliases, key=len) + else: + # For arguments we pick its user facing name, e.g. "PACKAGES" + # for argument packages. + param_alias = param_obj.human_readable_name + assert param_obj is not None, param_id + result.append(param_alias) + return result + + +def _is_param_specified(ctx, param_id) -> bool: + """Determine if the param with given id was specified in the + command line.""" + # Mapping: param id -> param obj. + params_dict = _get_params_objs(ctx) + # Get the official status. + param_src = ctx.get_parameter_source(param_id) + is_specified = param_src == click.core.ParameterSource.COMMANDLINE + # A special case for repeating arguments. Click considers the + # empty tuple value to come with the command line but we consider + # it to come from the default. + is_arg = isinstance(params_dict[param_id], click.Argument) + if is_specified and is_arg: + arg_value = ctx.params[param_id] + if arg_value == tuple(): + is_specified = False + # All done + return is_specified + + +def _specified_params(ctx: click.Context, param_ids: List[str]) -> List[str]: + """Returns the subset of param ids that were used in the command line. + The original order of the list is preserved. + For definition of params and param ids see check_exclusive_params(). + """ + result = [] + for param_id in param_ids: + if _is_param_specified(ctx, param_id): + result.append(param_id) + return result + + +def check_exclusive_params(ctx: click.Context, param_ids: List[str]) -> None: + """Checks that at most one of given params were specified in + the command line. If more than one param was specified, exits the + program with a message and error status. + + Params are click options and arguments that are passed to a command. + Param ids are the names of variables that are used to pass options and + argument values to the command. A safe way to construct param_ids + is nameof(param_var1, param_var2, ...) + """ + # The the subset of ids of params that where used in the command. + specified_param_ids = _specified_params(ctx, param_ids) + # If more 2 or more print an error and exit. + if len(specified_param_ids) >= 2: + canonical_aliases = _params_ids_to_aliases(ctx, specified_param_ids) + aliases_str = ", ".join(canonical_aliases) + fatal_usage_error(ctx, f"{aliases_str} are mutually exclusive.") + + +def check_required_params(ctx: click.Context, param_ids: List[str]) -> None: + """Checks that at least one of given params is specified in + the command line. If none of the params is specified, exits the + program with a message and error status. + + Params are click options and arguments that are passed to a command. + Param ids are the names of variables that are used to pass options and + argument values to the command. A safe way to construct param_ids + is nameof(param_var1, param_var2, ...) + """ + # The the subset of ids of params that where used in the command. + specified_param_ids = _specified_params(ctx, param_ids) + # If more 2 or more print an error and exit. + if len(specified_param_ids) < 1: + canonical_aliases = _params_ids_to_aliases(ctx, param_ids) + aliases_str = ", ".join(canonical_aliases) + fatal_usage_error( + ctx, f"At list one of {aliases_str} must be specified." + ) + + +class ApioOption(click.Option): + """Custom class for apio click options. Currently it adds handling + of deprecated options. + """ + + def __init__(self, *args, **kwargs): + # Cache a list of option's aliases. E.g. ["-t", "--top-model"]. + self.aliases = [k for k in args[0] if k.startswith("-")] + # Consume the "deprecated" arg is specified. This args is + # added by this class and is not passed to super. + self.deprecated = kwargs.pop("deprecated", False) + # Tweak the help text to have a [DEPRECATED] prefix. + if self.deprecated: + kwargs["help"] = ( + DEPRECATED_MARKER + " " + kwargs.get("help", "").strip() + ) + super().__init__(*args, **kwargs) + + # @override + def handle_parse_result( + self, ctx: click.Context, opts: Mapping[str, Any], args: List[str] + ) -> Tuple[Any, List[str]]: + """Overides the parent method to print a deprecated option message.""" + if self.deprecated and self.name in opts: + click.secho(f"Info: {self.aliases} is deprecated.", fg="yellow") + return super().handle_parse_result(ctx, opts, args) + + +DEPRECATION_NOTE = f""" +[Note] Flags marked with {DEPRECATED_MARKER} are not recomanded for use. +For project configuration, use an apio.ini project file and if neaded, +project specific 'boards.json' and 'fpga.json' definition files. +""" + + +class ApioCommand(click.Command): + """Override click.Command with Apio specific behavior. + Currently it adds a clarification note to the help text of + commands that contains deprecated ApioOptions. + """ + + def _num_deprecated_options(self, ctx: click.Context) -> None: + """Returns the number of deprecated options of this command.""" + deprecated_options = 0 + for param in self.get_params(ctx): + if isinstance(param, ApioOption) and param.deprecated: + deprecated_options += 1 + return deprecated_options + + # @override + def format_help_text( + self, ctx: click.Context, formatter: click.HelpFormatter + ) -> None: + super().format_help_text(ctx, formatter) + deprecated = self._num_deprecated_options(ctx) + if deprecated > 0: + formatter.write_paragraph() + with formatter.indentation(): + formatter.write_text(DEPRECATION_NOTE) diff --git a/apio/commands/boards.py b/apio/commands/boards.py index f489a652..dd6c8488 100644 --- a/apio/commands/boards.py +++ b/apio/commands/boards.py @@ -8,10 +8,11 @@ """Implementation of 'apio boards' command""" from pathlib import Path +from varname import nameof import click from click.core import Context from apio.resources import Resources -from apio import util +from apio import cmd_util from apio.commands import options # --------------------------- @@ -23,7 +24,7 @@ "--fpga", is_flag=True, help="List supported FPGA chips.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -51,7 +52,7 @@ "boards", short_help="List supported boards and FPGAs.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @options.project_dir_option @@ -68,11 +69,8 @@ def cli( and FPGAs. """ - # pylint: disable=fixme - # TODO: Exit with error status if both --list and --fpga are specified. - - # pylint: disable=fixme - # TODO: rename options --list, --fpga to --boards, --fpgas. + # Make sure these params are exclusive. + cmd_util.check_exclusive_params(ctx, nameof(list_, fpgas)) # -- Access to the apio resources resources = Resources(project_dir=project_dir) @@ -80,11 +78,12 @@ def cli( # -- Option 1: List boards if list_: resources.list_boards() + ctx.exit(0) # -- Option 2: List fpgas - elif fpgas: + if fpgas: resources.list_fpgas() + ctx.exit(0) # -- No options: show help - else: - click.secho(ctx.get_help()) + click.secho(ctx.get_help()) diff --git a/apio/commands/build.py b/apio/commands/build.py index 19337d31..1cbb496d 100644 --- a/apio/commands/build.py +++ b/apio/commands/build.py @@ -11,7 +11,7 @@ import click from click.core import Context from apio.managers.scons import SCons -from apio import util +from apio import cmd_util from apio.commands import options @@ -38,7 +38,7 @@ "build", short_help="Synthesize the bitstream.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @options.project_dir_option diff --git a/apio/commands/clean.py b/apio/commands/clean.py index 93fa07ba..b6145283 100644 --- a/apio/commands/clean.py +++ b/apio/commands/clean.py @@ -11,7 +11,7 @@ import click from click.core import Context from apio.managers.scons import SCons -from apio import util +from apio import cmd_util from apio.commands import options @@ -37,7 +37,7 @@ "clean", short_help="Clean the apio generated files.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @options.project_dir_option diff --git a/apio/commands/create.py b/apio/commands/create.py index 438fa816..6c327493 100644 --- a/apio/commands/create.py +++ b/apio/commands/create.py @@ -12,6 +12,7 @@ from click.core import Context from apio.managers.project import Project, DEFAULT_TOP_MODULE, PROJECT_FILENAME from apio import util +from apio import cmd_util from apio.commands import options @@ -50,7 +51,7 @@ "create", short_help="Create an apio.ini project file.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @options.board_option_gen(help="Set the board.", required=True) diff --git a/apio/commands/drivers.py b/apio/commands/drivers.py index 4dbea116..6139e410 100644 --- a/apio/commands/drivers.py +++ b/apio/commands/drivers.py @@ -7,10 +7,11 @@ # -- Licence GPLv2 """Implementation of 'apio drivers' command""" +from varname import nameof import click from click.core import Context from apio.managers.drivers import Drivers -from apio import util +from apio import cmd_util # --------------------------- # -- COMMAND SPECIFIC OPTIONS @@ -20,7 +21,7 @@ "--ftdi-enable", is_flag=True, help="Enable FTDI drivers.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) ftdi_disable_option = click.option( @@ -28,7 +29,7 @@ "--ftdi-disable", is_flag=True, help="Disable FTDI drivers.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) serial_enable_option = click.option( @@ -36,7 +37,7 @@ "--serial-enable", is_flag=True, help="Enable Serial drivers.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) serial_disable_option = click.option( @@ -44,7 +45,7 @@ "--serial-disable", is_flag=True, help="Disable Serial drivers.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -72,7 +73,7 @@ "drivers", short_help="Manage the operating system drivers.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @frdi_enable_option @@ -89,32 +90,33 @@ def cli( ): """Implements the drivers command.""" + # Make sure these params are exclusive. + cmd_util.check_exclusive_params( + ctx, nameof(ftdi_enable, ftdi_disable, serial_enable, serial_disable) + ) + # -- Access to the Drivers drivers = Drivers() - # pylint: disable=fixme - # TODO: Exit with an error if more than one flag is is specified. - # -- FTDI enable option if ftdi_enable: exit_code = drivers.ftdi_enable() + ctx.exit(exit_code) # -- FTDI disable option - elif ftdi_disable: + if ftdi_disable: exit_code = drivers.ftdi_disable() + ctx.exit(exit_code) # -- Serial enable option - elif serial_enable: + if serial_enable: exit_code = drivers.serial_enable() + ctx.exit(exit_code) # -- Serial disable option - elif serial_disable: + if serial_disable: exit_code = drivers.serial_disable() + ctx.exit(exit_code) # -- No options. Show the help - else: - exit_code = 0 - click.secho(ctx.get_help()) - - # -- Return exit code - ctx.exit(exit_code) + click.secho(ctx.get_help()) diff --git a/apio/commands/examples.py b/apio/commands/examples.py index 8ae45542..ed838ad6 100644 --- a/apio/commands/examples.py +++ b/apio/commands/examples.py @@ -8,10 +8,11 @@ """Implementation of 'apio examples' command""" from pathlib import Path +from varname import nameof import click from click.core import Context from apio.managers.examples import Examples -from apio import util +from apio import cmd_util from apio.commands import options # --------------------------- @@ -24,7 +25,7 @@ type=str, metavar="name", help="Copy the selected example directory.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) files_option = click.option( @@ -34,7 +35,7 @@ type=str, metavar="name", help="Copy the selected example files.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -61,7 +62,7 @@ "examples", short_help="List and fetch apio examples.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @options.list_option_gen(help="List all available examples.") @@ -81,24 +82,26 @@ def cli( """Manage verilog examples.\n Install with `apio install examples`""" + # Make sure these params are exclusive. + cmd_util.check_exclusive_params(ctx, nameof(list_, dir_, files)) + # -- Access to the Drivers examples = Examples() # -- Option: List all the available examples if list_: exit_code = examples.list_examples() + ctx.exit(exit_code) # -- Option: Copy the directory - elif dir_: + if dir_: exit_code = examples.copy_example_dir(dir_, project_dir, sayno) + ctx.exit(exit_code) # -- Option: Copy only the example files (not the initial folders) - elif files: + if files: exit_code = examples.copy_example_files(files, project_dir, sayno) + ctx.exit(exit_code) # -- no options: Show help! - else: - click.secho(ctx.get_help()) - exit_code = 0 - - ctx.exit(exit_code) + click.secho(ctx.get_help()) diff --git a/apio/commands/graph.py b/apio/commands/graph.py index 878f7d85..0b4d0ced 100644 --- a/apio/commands/graph.py +++ b/apio/commands/graph.py @@ -11,7 +11,7 @@ import click from click.core import Context from apio.managers.scons import SCons -from apio import util +from apio import cmd_util from apio.commands import options @@ -42,7 +42,7 @@ "graph", short_help="Generate a visual graph of the code.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @options.project_dir_option diff --git a/apio/commands/install.py b/apio/commands/install.py index 18d85f0b..045e89ba 100644 --- a/apio/commands/install.py +++ b/apio/commands/install.py @@ -9,11 +9,12 @@ from pathlib import Path from typing import Tuple +from varname import nameof import click from click.core import Context from apio.managers.installer import Installer, list_packages from apio.resources import Resources -from apio import util +from apio import cmd_util from apio.commands import options @@ -54,13 +55,15 @@ def install_packages( """ +# R0801: Similar lines in 2 files +# pylint: disable=R0801 # R0913: Too many arguments (7/5) # pylint: disable=R0913 @click.command( "install", short_help="Install apio packages.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @click.argument("packages", nargs=-1, required=False) @@ -84,22 +87,27 @@ def cli( manage the installation of apio packages. """ + # Make sure these params are exclusive. + cmd_util.check_exclusive_params(ctx, nameof(packages, all_, list_)) + # -- Load the resources. resources = Resources(platform=platform, project_dir=project_dir) # -- Install the given apio packages if packages: install_packages(packages, platform, resources, force) + ctx.exit(0) # -- Install all the available packages (if any) - elif all_: + if all_: # -- Install all the available packages for this platform! install_packages(resources.packages, platform, resources, force) + ctx.exit(0) # -- List all the packages (installed or not) - elif list_: + if list_: list_packages(platform) + ctx.exit(0) # -- Invalid option. Just show the help - else: - click.secho(ctx.get_help()) + click.secho(ctx.get_help()) diff --git a/apio/commands/lint.py b/apio/commands/lint.py index a3f522b9..d360182a 100644 --- a/apio/commands/lint.py +++ b/apio/commands/lint.py @@ -11,7 +11,7 @@ import click from click.core import Context from apio.managers.scons import SCons -from apio import util +from apio import cmd_util from apio.commands import options @@ -23,7 +23,7 @@ "--nostyle", is_flag=True, help="Disable all style warnings.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -33,7 +33,7 @@ type=str, metavar="nowarn", help="Disable specific warning(s).", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) warn_option = click.option( @@ -42,7 +42,7 @@ type=str, metavar="warn", help="Enable specific warning(s).", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -69,7 +69,7 @@ "lint", short_help="Lint the verilog code.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @options.all_option_gen( diff --git a/apio/commands/modify.py b/apio/commands/modify.py index 845ee02f..66c08b15 100644 --- a/apio/commands/modify.py +++ b/apio/commands/modify.py @@ -8,10 +8,12 @@ """Implementation of 'apio modify' command""" from pathlib import Path +from varname import nameof import click from click.core import Context from apio.managers.project import Project from apio import util +from apio import cmd_util from apio.commands import options @@ -42,7 +44,7 @@ "modify", short_help="Modify the apio.ini project file.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @options.board_option_gen(help="Set the board.") @@ -57,14 +59,8 @@ def cli( ): """Modify the project file.""" - if not (board or top_module): - click.secho( - "Error: at least one of --board or --top-module must be " - "specified.\n" - "Type 'apio modify -h' for help.", - fg="red", - ) - ctx.exit(0) + # At least one of these options are required. + cmd_util.check_required_params(ctx, nameof(board, top_module)) project_dir = util.get_project_dir(project_dir) diff --git a/apio/commands/options.py b/apio/commands/options.py index eebd6baa..d88d26e0 100644 --- a/apio/commands/options.py +++ b/apio/commands/options.py @@ -10,6 +10,7 @@ from pathlib import Path import click from apio import util +from apio import cmd_util # The design is based on the idea here https://stackoverflow.com/a/77732441. @@ -35,7 +36,7 @@ def all_option_gen(*, help: str): "--all", is_flag=True, help=help, - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -49,7 +50,7 @@ def force_option_gen(*, help: str): "--force", is_flag=True, help=help, - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -63,7 +64,7 @@ def list_option_gen(*, help: str): "--list", is_flag=True, help=help, - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -82,7 +83,7 @@ def board_option_gen( metavar="str", deprecated=deprecated, help=help, - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -102,7 +103,7 @@ def top_module_option_gen( metavar="name", deprecated=deprecated, help=help, - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -118,7 +119,7 @@ def top_module_option_gen( metavar="str", deprecated=True, help="Set the FPGA.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) ftdi_id = click.option( @@ -136,7 +137,7 @@ def top_module_option_gen( metavar="str", deprecated=True, help="Set the FPGA package.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -146,7 +147,7 @@ def top_module_option_gen( "--platform", type=click.Choice(util.PLATFORMS), help=("(Advanced, for developers) Set the platform."), - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -157,7 +158,7 @@ def top_module_option_gen( type=Path, metavar="path", help="Set the root directory for the project.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -167,7 +168,7 @@ def top_module_option_gen( "--sayno", is_flag=True, help="Automatically answer NO to all the questions.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) sayyes = click.option( @@ -176,7 +177,7 @@ def top_module_option_gen( "--sayyes", is_flag=True, help="Automatically answer YES to all the questions.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) serial_port_option = click.option( @@ -185,7 +186,7 @@ def top_module_option_gen( type=str, metavar="serial-port", help="Set the serial port.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -196,7 +197,7 @@ def top_module_option_gen( metavar="str", deprecated=True, help="Set the FPGA type (1k/8k).", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -207,7 +208,7 @@ def top_module_option_gen( metavar="str", deprecated=True, help="Set the FPGA type (hx/lp).", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -217,7 +218,7 @@ def top_module_option_gen( "--verbose", is_flag=True, help="Show the entire output of the command.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -226,7 +227,7 @@ def top_module_option_gen( "--verbose-pnr", is_flag=True, help="Show the pnr output.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -235,5 +236,5 @@ def top_module_option_gen( "--verbose-yosys", is_flag=True, help="Show the yosys output.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) diff --git a/apio/commands/raw.py b/apio/commands/raw.py index 9b7472e1..9fc5acdd 100644 --- a/apio/commands/raw.py +++ b/apio/commands/raw.py @@ -10,6 +10,7 @@ import click from click.core import Context from apio import util +from apio import cmd_util # --------------------------- @@ -37,7 +38,7 @@ "raw", short_help="Execute commands directly from the Apio packages.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @click.argument("cmd") diff --git a/apio/commands/sim.py b/apio/commands/sim.py index 0862cbd2..67d15232 100644 --- a/apio/commands/sim.py +++ b/apio/commands/sim.py @@ -10,7 +10,7 @@ from pathlib import Path import click from apio.managers.scons import SCons -from apio import util +from apio import cmd_util from apio.commands import options # --------------------------- @@ -42,7 +42,7 @@ "sim", short_help="Simulate a testbench with graphic results.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @click.argument("testbench", nargs=1, required=True) diff --git a/apio/commands/system.py b/apio/commands/system.py index f4f2c339..89ae0643 100644 --- a/apio/commands/system.py +++ b/apio/commands/system.py @@ -8,9 +8,11 @@ """Implementation of 'apio system' command""" from pathlib import Path +from varname import nameof import click from click.core import Context from apio import util +from apio import cmd_util from apio.util import get_systype from apio.managers.system import System from apio.resources import Resources @@ -24,7 +26,7 @@ "--lsftdi", is_flag=True, help="List all connected FTDI devices.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) lsusb_option = click.option( @@ -32,7 +34,7 @@ "--lsusb", is_flag=True, help="List all connected USB devices.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) lsserial_option = click.option( @@ -40,7 +42,7 @@ "--lsserial", is_flag=True, help="List all connected Serial devices.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) info_option = click.option( @@ -49,7 +51,7 @@ "--info", is_flag=True, help="Show platform id and other info.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -78,7 +80,7 @@ "system", short_help="Provides system info.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @options.project_dir_option @@ -98,24 +100,15 @@ def cli( """Implements the system command. This command executes assorted system tools""" + # Make sure these params are exclusive. + cmd_util.check_exclusive_params(ctx, nameof(lsftdi, lsusb, lsserial, info)) + # Load the various resource files. resources = Resources(project_dir=project_dir) # -- Create the system object system = System(resources) - # -- Verify exlusive flags. - flags_count = int(lsftdi) + int(lsusb) + int(lsserial) + int(info) - if flags_count > 1: - click.secho( - ( - "Error: --lsftdi, --lsusb, --lsserial, and --info" - " are mutually exclusive." - ), - fg="red", - ) - ctx.exit(1) - # -- List all connected ftdi devices if lsftdi: exit_code = system.lsftdi() @@ -144,4 +137,3 @@ def cli( # -- Invalid option. Just show the help click.secho(ctx.get_help()) - ctx.exit(0) diff --git a/apio/commands/test.py b/apio/commands/test.py index 2717b43b..f4b35c17 100644 --- a/apio/commands/test.py +++ b/apio/commands/test.py @@ -11,7 +11,7 @@ import click from click.core import Context from apio.managers.scons import SCons -from apio import util +from apio import cmd_util from apio.commands import options @@ -43,7 +43,7 @@ "test", short_help="Test all or a single verilog testbench module.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @click.argument("testbench_file", nargs=1, required=False) diff --git a/apio/commands/time.py b/apio/commands/time.py index 4338534e..6a61b697 100644 --- a/apio/commands/time.py +++ b/apio/commands/time.py @@ -11,7 +11,7 @@ import click from click.core import Context from apio.managers.scons import SCons -from apio import util +from apio import cmd_util from apio.commands import options @@ -41,7 +41,7 @@ "time", short_help="Report design timing.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @options.project_dir_option diff --git a/apio/commands/uninstall.py b/apio/commands/uninstall.py index e58d4f01..bef68edf 100644 --- a/apio/commands/uninstall.py +++ b/apio/commands/uninstall.py @@ -9,11 +9,12 @@ from pathlib import Path from typing import Tuple +from varname import nameof import click from click.core import Context from apio.managers.installer import Installer, list_packages from apio.profile import Profile -from apio import util +from apio import cmd_util from apio.resources import Resources from apio.commands import options @@ -55,13 +56,15 @@ def _uninstall(packages: list, platform: str, resources: Resources): """ +# R0801: Similar lines in 2 files +# pylint: disable=R0801 # R0913: Too many arguments (6/5) # pylint: disable=R0913 @click.command( "uninstall", short_help="Uninstall apio packages.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @click.argument("packages", nargs=-1, required=False) @@ -81,26 +84,29 @@ def cli( ): """Implements the uninstall command.""" + # Make sure these params are exclusive. + cmd_util.check_exclusive_params(ctx, nameof(packages, list_, all_)) + # -- Load the resources. resources = Resources(platform=platform, project_dir=project_dir) # -- Uninstall the given apio packages if packages: _uninstall(packages, platform, resources) + ctx.exit(0) # -- Uninstall all the packages - elif all_: - + if all_: # -- Get all the installed apio packages packages = Profile().packages - # -- Uninstall them! _uninstall(packages, platform, resources) + ctx.exit(0) # -- List all the packages (installed or not) - elif list_: + if list_: list_packages(platform) + ctx.exit(0) # -- Invalid option. Just show the help - else: - click.secho(ctx.get_help()) + click.secho(ctx.get_help()) diff --git a/apio/commands/upgrade.py b/apio/commands/upgrade.py index 87b37656..3a3852fb 100644 --- a/apio/commands/upgrade.py +++ b/apio/commands/upgrade.py @@ -12,7 +12,7 @@ from click.core import Context from packaging import version from apio.util import get_pypi_latest_version -from apio import util +from apio import cmd_util # --------------------------- @@ -32,7 +32,7 @@ "upgrade", short_help="Check the latest Apio version.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context def cli(ctx: Context): diff --git a/apio/commands/upload.py b/apio/commands/upload.py index 63fe4315..b1d28f20 100644 --- a/apio/commands/upload.py +++ b/apio/commands/upload.py @@ -12,7 +12,7 @@ from click.core import Context from apio.managers.scons import SCons from apio.managers.drivers import Drivers -from apio import util +from apio import cmd_util from apio.commands import options @@ -25,7 +25,7 @@ "--sram", is_flag=True, help="Perform SRAM programming.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) flash_option = click.option( @@ -34,7 +34,7 @@ "--flash", is_flag=True, help="Perform FLASH programming.", - cls=util.ApioOption, + cls=cmd_util.ApioOption, ) @@ -62,7 +62,7 @@ "upload", short_help="Upload the bitstream to the FPGA.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @options.project_dir_option diff --git a/apio/commands/verify.py b/apio/commands/verify.py index dfe9eb22..3af1e9d5 100644 --- a/apio/commands/verify.py +++ b/apio/commands/verify.py @@ -11,7 +11,7 @@ import click from click.core import Context from apio.managers.scons import SCons -from apio import util +from apio import cmd_util from apio.commands import options @@ -38,7 +38,7 @@ "verify", short_help="Verify project's verilog code.", help=HELP, - cls=util.ApioCommand, + cls=cmd_util.ApioCommand, ) @click.pass_context @options.project_dir_option diff --git a/apio/util.py b/apio/util.py index 8031b6f1..51a4e094 100644 --- a/apio/util.py +++ b/apio/util.py @@ -7,7 +7,7 @@ # ---- Platformio project # ---- (C) 2014-2016 Ivan Kravets # ---- Licence Apache v2 -"""Utility functions""" +"""Misc utility functions and classes.""" import sys import os @@ -16,7 +16,6 @@ import subprocess from threading import Thread from pathlib import Path -from typing import Mapping, List, Tuple, Any import click import semantic_version from serial.tools.list_ports import comports @@ -41,8 +40,6 @@ OSS_CAD_SUITE_FOLDER = f"tools-{OSS_CAD_SUITE}" GTKWAVE_FOLDER = f"tool-{GTKWAVE}" -DEPRECATED_MARKER = "[DEPRECATED]" - # -- AVAILABLE PLATFORMS PLATFORMS = [ "linux", @@ -903,61 +900,3 @@ def safe_click(text, *args, **kwargs): # most common character error cleaned_text = "".join([ch if ord(ch) < 128 else "=" for ch in text]) click.echo(cleaned_text, err=error_flag, *args, **kwargs) - - -class ApioOption(click.Option): - """Custom class for apio click options. Currently it adds handling - of deprecated options. - """ - - def __init__(self, *args, **kwargs): - # Cache a list of option's aliases. E.g. ["-t", "--top-model"]. - self.aliases = [k for k in args[0] if k.startswith("-")] - # Consume the "deprecated" arg is specified. This args is - # added by this class and is not passed to super. - self.deprecated = kwargs.pop("deprecated", False) - # Tweak the help text to have a [DEPRECATED] prefix. - if self.deprecated: - kwargs["help"] = ( - DEPRECATED_MARKER + " " + kwargs.get("help", "").strip() - ) - super().__init__(*args, **kwargs) - - # @override - def handle_parse_result( - self, ctx: click.Context, opts: Mapping[str, Any], args: List[str] - ) -> Tuple[Any, List[str]]: - """Overides the parent method to print a deprecated option message.""" - if self.deprecated and self.name in opts: - click.secho(f"Info: {self.aliases} is deprecated.", fg="yellow") - return super().handle_parse_result(ctx, opts, args) - - -DEPRECATION_NOTE = f""" -[Note] Flags marked with {DEPRECATED_MARKER} are not recomanded for use. -For project configuration, use an apio.ini project file and if neaded, -project specific 'boards.json' and 'fpga.json' definition files. -""" - - -class ApioCommand(click.Command): - """Override click.Command with Apio specific behavior.""" - - def _num_deprecated_options(self, ctx: click.Context) -> None: - """Return sthe number of deprecated options of this command.""" - deprecated_options = 0 - for param in self.get_params(ctx): - if isinstance(param, ApioOption) and param.deprecated: - deprecated_options += 1 - return deprecated_options - - # @override - def format_help_text( - self, ctx: click.Context, formatter: click.HelpFormatter - ) -> None: - super().format_help_text(ctx, formatter) - deprecated = self._num_deprecated_options(ctx) - if deprecated > 0: - formatter.write_paragraph() - with formatter.indentation(): - formatter.write_text(DEPRECATION_NOTE) diff --git a/pyproject.toml b/pyproject.toml index 77cc2af2..3c8d17a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ requires = [ 'pyserial==3.5', 'wheel>=0.35.0,<1', 'configobj==5.0.8', + 'varname==0.13.3', 'scons==4.2.0' ]