From 816553f1bc2eba3e63d4af46feb2660e981789ab Mon Sep 17 00:00:00 2001 From: Axel Andersson <axel.andersson@volvocars.com> Date: Thu, 17 Oct 2024 10:52:44 +0200 Subject: [PATCH] Added powertrain-build entrypoint The entrypoint will make it easier to install powertrain-build to an isolated venv without having to call python -m powertrain_build. Change-Id: I3850c97d17707f9bc03640bd1d997508637d97ba --- docs/powertrain_build.md | 29 ++++ powertrain_build/__main__.py | 6 + powertrain_build/check_interface.py | 98 ++++++------ powertrain_build/cli.py | 141 ++++++++++++++++++ powertrain_build/config.py | 35 +++-- powertrain_build/create_conversion_table.py | 36 +++-- .../interface/export_global_vars.py | 41 +++-- .../interface/generate_adapters.py | 27 ++-- .../interface/generate_hi_interface.py | 51 ++++--- .../interface/generate_service.py | 35 +++-- .../interface/generate_wrappers.py | 39 +++-- .../interface/generation_utils.py | 4 +- .../interface/model_yaml_verification.py | 30 ++-- .../interface/update_call_sources.py | 29 ++-- .../interface/update_model_yaml.py | 37 +++-- powertrain_build/replace_compu_tab_ref.py | 37 +++-- .../signal_inconsistency_check.py | 21 ++- powertrain_build/wrapper.py | 26 ++-- requirements.txt | 2 + setup.cfg | 4 + tests/powertrain_build/func_cli.py | 24 +++ tests/powertrain_build/test_cli.py | 46 ++++++ tox.ini | 11 +- zuul.d/tox.yaml | 2 +- 24 files changed, 613 insertions(+), 198 deletions(-) create mode 100644 powertrain_build/__main__.py create mode 100644 powertrain_build/cli.py create mode 100644 tests/powertrain_build/func_cli.py create mode 100644 tests/powertrain_build/test_cli.py diff --git a/docs/powertrain_build.md b/docs/powertrain_build.md index 3b62df4..9d9b464 100644 --- a/docs/powertrain_build.md +++ b/docs/powertrain_build.md @@ -34,6 +34,35 @@ In git bash: py -3.6 -m powertrain_build.wrapper --codegen --models Models/ICEAES/VcAesTx/VcAesTx.mdl ``` +#### CLI Entrypoint + +If the Python `bin`/`Scripts` folder has been added to the `PATH`, you can also use +`powertrain-build`'s CLI entrypoint in a similar way: + +```bash +powertrain-build wrapper --codegen --models Models/ICEAES/VcAesTx/VcAesTx.mdl +``` + +In general, the python call + +```bash +python -m powertrain_build.submodule.subsubmodule <args> +``` + +corresponds to the CLI entrypoint call + +```bash +powertrain-build submodule subsubmodule <args> +``` + +Run + +```bash +powertrain-build --help +``` + +for more information on how to use the CLI entrypoint. + #### Set Matlab 2017 as Environmental Variable Add New User Variables diff --git a/powertrain_build/__main__.py b/powertrain_build/__main__.py new file mode 100644 index 0000000..7e04402 --- /dev/null +++ b/powertrain_build/__main__.py @@ -0,0 +1,6 @@ +import sys + +from powertrain_build.cli import main + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/powertrain_build/check_interface.py b/powertrain_build/check_interface.py index af5670d..fda9da8 100644 --- a/powertrain_build/check_interface.py +++ b/powertrain_build/check_interface.py @@ -12,6 +12,8 @@ import re import sys from itertools import product from pathlib import Path +from textwrap import dedent +from typing import List, Optional import git @@ -340,14 +342,7 @@ def check_signals(left_signals, right_signals, errors, left_path=None, right_pat return serious_mismatch -def parse_args(): - """Parse arguments - - Returns: - Namespace: the parsed arguments - """ - parser = argparse.ArgumentParser( - description=""" +PARSER_HELP = dedent(r""" Checks attributes and existence of signals Produced but not consumed signals are giving warnings @@ -368,50 +363,58 @@ def parse_args(): py -3.6 -m powertrain_build.check_interface projects <Projects> \ --projects ProjectOne ProjectTwo ProjectThree Checks the interfaces of ProjectOne, ProjectTwo and ProjectThree in the folder Projects - """, - formatter_class=argparse.RawTextHelpFormatter, +""").strip() + + +def configure_parser(parser: argparse.ArgumentParser): + """Configure arguments in parser.""" + subparsers = parser.add_subparsers( + help="help for subcommand", + dest="mode", + required=True, ) - subparsers = parser.add_subparsers(help="help for subcommand", dest="mode") # create the parser for the different commands model = subparsers.add_parser( "models", - description=""" - Check models independently of projects. + description=dedent(""" + Check models independently of projects. - All signals are assumed to be active. - Any signal that gives and error is used in a model but is not produced in any model or project - interface. - """, + All signals are assumed to be active. + Any signal that gives and error is used in a model but is not produced in any model or project + interface. + """).strip(), ) add_model_args(model) + model.set_defaults(func=model_check) project = subparsers.add_parser( "projects", - description=""" - Check projects as a whole. + description=dedent(""" + Check projects as a whole. - It checks all models intenally and the SPM vs the interface. - """, + It checks all models intenally and the SPM vs the interface. + """).strip(), ) add_project_args(project) + project.set_defaults(func=projects_check) models_in_projects = subparsers.add_parser( "models_in_projects", - description=""" - Check models specifically for projects. + description=dedent(""" + Check models specifically for projects. - Codeswitches are used to determine if the signals are produced and consumed in each model. - """, + Codeswitches are used to determine if the signals are produced and consumed in each model. + """).strip(), ) add_project_args(models_in_projects) add_model_args(models_in_projects) models_in_projects.add_argument("--properties", help="Check properties such as type", action="store_true") models_in_projects.add_argument("--existence", help="Check signal existence consistency", action="store_true") - return parser.parse_args() + models_in_projects.set_defaults(func=models_in_projects_check) -def add_project_args(parser): +def add_project_args(parser: argparse.ArgumentParser): """Add project arguments to subparser""" parser.add_argument("project_root", help="Path to start looking for projects", type=Path) parser.add_argument( @@ -419,7 +422,7 @@ def add_project_args(parser): ) -def add_model_args(parser): +def add_model_args(parser: argparse.ArgumentParser): """Add model arguments to subparser""" parser.add_argument("model_root", help="Path to start looking for models", type=Path) parser.add_argument("--models", help="Name of models to check", nargs="+") @@ -446,7 +449,7 @@ def model_path_to_name(model_paths): return model_names -def model_check(args): +def model_check(args: argparse.Namespace): """Entry point for models command.""" serious_mismatch = False all_models = get_all_models(args.model_root) @@ -460,10 +463,14 @@ def model_check(args): model_names = [model.name for model in all_models] serious_mismatch |= check_models_generic(all_models, model_names, []) + + if serious_mismatch: + LOGGER.error("Serious interface errors found.") + return serious_mismatch -def projects_check(args): +def projects_check(args: argparse.Namespace): """Entry point for projects command.""" serious_mismatch = False projects = get_projects(args.project_root, args.projects) @@ -472,10 +479,14 @@ def projects_check(args): serious_mismatch |= check_internal_signals(app, None) if ems is not None: serious_mismatch |= check_external_signals(ems, app, None) + + if serious_mismatch: + LOGGER.error("Serious interface errors found.") + return serious_mismatch -def models_in_projects_check(args): +def models_in_projects_check(args: argparse.Namespace): """Entry point for models_in_projects command.""" serious_mismatch = False projects = get_projects(args.project_root, args.projects) @@ -489,6 +500,10 @@ def models_in_projects_check(args): all_models = get_all_models(args.model_root) model_names = [model.name for model in all_models] if args.models is None else args.models serious_mismatch |= signal_existence(projects, model_names) + + if serious_mismatch: + LOGGER.error("Serious interface errors found.") + return serious_mismatch @@ -545,19 +560,16 @@ def signal_match(signals_to_check, signals_to_check_against, matches): matches[a_signal.name] = True -def main(): +def main(argv: Optional[List[str]] = None): """Main function for stand alone execution.""" - args = parse_args() - if args.mode == "models": - serious_errors = model_check(args) - if args.mode == "projects": - serious_errors = projects_check(args) - if args.mode == "models_in_projects": - serious_errors = models_in_projects_check(args) - if serious_errors: - LOGGER.error("Serious interface errors found.") - sys.exit(1) + parser = argparse.ArgumentParser( + description=PARSER_HELP, + formatter_class=argparse.RawTextHelpFormatter, + ) + configure_parser(parser) + args = parser.parse_args(argv) + args.func(args) if __name__ == "__main__": - main() + main(sys.argv[1:]) diff --git a/powertrain_build/cli.py b/powertrain_build/cli.py new file mode 100644 index 0000000..c82af9b --- /dev/null +++ b/powertrain_build/cli.py @@ -0,0 +1,141 @@ +# Copyright 2024 Volvo Car Corporation +# Licensed under Apache 2.0. + +from argparse import ArgumentParser, Namespace +from typing import List, Optional + +import powertrain_build.check_interface +import powertrain_build.create_conversion_table +import powertrain_build.interface.export_global_vars +import powertrain_build.interface.generate_adapters +import powertrain_build.interface.generate_hi_interface +import powertrain_build.interface.generate_service +import powertrain_build.interface.generate_wrappers +import powertrain_build.interface.model_yaml_verification +import powertrain_build.interface.update_model_yaml +import powertrain_build.interface.update_call_sources +import powertrain_build.replace_compu_tab_ref +import powertrain_build.signal_inconsistency_check +from powertrain_build import __version__ +from powertrain_build.config import ProcessHandler +from powertrain_build.lib import logger +from powertrain_build.wrapper import PyBuildWrapper + +LOGGER = logger.create_logger(__file__) + + +def parse_args(argv: Optional[List[str]] = None) -> Namespace: + """Parse command line arguments.""" + parser = ArgumentParser( + prog="powertrain-build", + description="Powertrain-build", + ) + parser.add_argument( + "-V", "--version", + action="version", + version=f"%(prog)s {__version__}", + ) + + command_subparsers = parser.add_subparsers(title="Commands", dest="command", required=True) + + wrapper_parser = command_subparsers.add_parser( + "wrapper", + help=PyBuildWrapper.PARSER_HELP, + ) + PyBuildWrapper.add_args(wrapper_parser) + + config_parser = command_subparsers.add_parser( + "config", + help=ProcessHandler.PARSER_HELP, + ) + ProcessHandler.configure_parser(config_parser) + + check_interface_parser = command_subparsers.add_parser( + "check-interface", + help=powertrain_build.check_interface.PARSER_HELP, + ) + powertrain_build.check_interface.configure_parser(check_interface_parser) + + create_conversion_table_parser = command_subparsers.add_parser( + "create-conversion-table", + help=powertrain_build.create_conversion_table.PARSER_HELP, + ) + powertrain_build.create_conversion_table.configure_parser(create_conversion_table_parser) + + replace_compu_tab_ref_parser = command_subparsers.add_parser( + "replace-compu-tab-ref", + help=powertrain_build.replace_compu_tab_ref.PARSER_HELP, + ) + powertrain_build.replace_compu_tab_ref.configure_parser(replace_compu_tab_ref_parser) + + signal_inconsistency_check_parser = command_subparsers.add_parser( + "signal-inconsistency-check", + help=powertrain_build.signal_inconsistency_check.PARSER_HELP, + ) + powertrain_build.signal_inconsistency_check.configure_parser(signal_inconsistency_check_parser) + + interface_parser = command_subparsers.add_parser( + "interface", + help="Interface commands", + ) + interface_subparsers = interface_parser.add_subparsers( + title="Interface commands", + dest="interface_command", + required=True, + ) + + export_global_vars_parser = interface_subparsers.add_parser( + "export-global-vars", + help=powertrain_build.interface.export_global_vars.PARSER_HELP, + ) + powertrain_build.interface.export_global_vars.configure_parser(export_global_vars_parser) + + generate_adapters_parser = interface_subparsers.add_parser( + "generate-adapters", + help=powertrain_build.interface.generate_adapters.PARSER_HELP, + ) + powertrain_build.interface.generate_adapters.configure_parser(generate_adapters_parser) + + generate_hi_interface_parser = interface_subparsers.add_parser( + "generate-hi-interface", + help=powertrain_build.interface.generate_hi_interface.PARSER_HELP, + ) + powertrain_build.interface.generate_hi_interface.configure_parser(generate_hi_interface_parser) + + generate_service_parser = interface_subparsers.add_parser( + "generate-service", + help=powertrain_build.interface.generate_service.PARSER_HELP, + ) + powertrain_build.interface.generate_service.configure_parser(generate_service_parser) + + generate_wrappers_parser = interface_subparsers.add_parser( + "generate-wrappers", + help=powertrain_build.interface.generate_wrappers.PARSER_HELP, + ) + powertrain_build.interface.generate_wrappers.configure_parser(generate_wrappers_parser) + + model_yaml_verification_parser = interface_subparsers.add_parser( + "model-yaml-verification", + help=powertrain_build.interface.model_yaml_verification.PARSER_HELP, + ) + powertrain_build.interface.model_yaml_verification.configure_parser(model_yaml_verification_parser) + + update_model_yaml_parser = interface_subparsers.add_parser( + "update-model-yaml", + help=powertrain_build.interface.update_model_yaml.PARSER_HELP, + ) + powertrain_build.interface.update_model_yaml.configure_parser(update_model_yaml_parser) + + update_call_sources_parser = interface_subparsers.add_parser( + "update-call-sources", + help=powertrain_build.interface.update_call_sources.PARSER_HELP, + ) + powertrain_build.interface.update_call_sources.configure_parser(update_call_sources_parser) + + return parser.parse_args(argv) + + +def main(argv: Optional[List[str]] = None) -> Namespace: + """Run main function.""" + args = parse_args(argv) + return args.func(args) diff --git a/powertrain_build/config.py b/powertrain_build/config.py index 9ac44cd..dfcc439 100644 --- a/powertrain_build/config.py +++ b/powertrain_build/config.py @@ -10,7 +10,9 @@ import json import operator import os import re +import sys from pprint import pformat +from typing import List, Optional from powertrain_build.lib import logger @@ -412,17 +414,25 @@ class HeaderConfigParser(ConfigParserCommon): class ProcessHandler: """Class to collect functions for the process.""" + PARSER_HELP = "Parse configs.json and c-files, to update code switch configs" + @staticmethod - def parse_args(): + def configure_parser(parser: argparse.ArgumentParser): """Parse arguments.""" - parser = argparse.ArgumentParser("Parse configs.json and c-files, to update code switch configes") - subparser = parser.add_subparsers(title='Operation mode', dest='mode', - help="Run chosen files on in a number of directories") + parser.description = "Parse configs.json and c-files, to update code switch configs" + + subparser = parser.add_subparsers( + title='Operation mode', + dest='mode', + help="Run chosen files on in a number of directories", + required=True, + ) dir_parser = subparser.add_parser( 'models', help="Run for one or multiple models. Script finds files generated from the model(s).") dir_parser.add_argument('models', nargs='+', help="Space separated list of model directories") + file_parser = subparser.add_parser('files', help="Choose specific files. Mainly for manually written configs.") file_parser.add_argument('c_file', @@ -433,8 +443,8 @@ class ProcessHandler: help="Full path to tl_aux file. (Optional) ") file_parser.add_argument('--local_file', help="Full path to OPort file. (Optional) ") - args = parser.parse_args() - return args + + parser.set_defaults(func=ProcessHandler.main) @staticmethod def get_files(model_path): @@ -505,9 +515,8 @@ class ProcessHandler: return parser.get_config() @classmethod - def main(cls): + def main(cls, args: argparse.Namespace): """Run the main function of the script.""" - args = cls.parse_args() if args.mode == 'files': LOGGER.info('Using manually supplied files %s', args) local_defs = cls.get_header_config(args.local_file, {}) @@ -522,5 +531,13 @@ class ProcessHandler: cls.update_config_file(c_file, config_file, aux_defs) +def main(argv: Optional[List[str]] = None): + """Run main function.""" + parser = argparse.ArgumentParser(ProcessHandler.PARSER_HELP) + ProcessHandler.configure_parser(parser) + args = parser.parse_args(argv) + return args.func(args) + + if __name__ == "__main__": - ProcessHandler.main() + main(sys.argv[1:]) diff --git a/powertrain_build/create_conversion_table.py b/powertrain_build/create_conversion_table.py index 5d87c7c..18dd624 100644 --- a/powertrain_build/create_conversion_table.py +++ b/powertrain_build/create_conversion_table.py @@ -5,7 +5,12 @@ import argparse import json +import sys from pathlib import Path +from typing import List, Optional + + +PARSER_HELP = "Create a2l file from conversion_table.json file." def get_vtab_text(vtab): @@ -40,22 +45,29 @@ def create_conversion_table(input_json: Path, output_a2l: Path): f_h.write(get_vtab_text(vtab)) -def parse_args(): - """Parse args.""" - parser = argparse.ArgumentParser('Create a2l file from conversion_table.json file') +def create_conversion_table_cli(args: argparse.Namespace): + """CLI wrapper function for passing in Namespace object. + + This allows maintaining a standardized CLI function signature while not breaking backwards + compatibility with create_converstion_table. + """ + create_conversion_table(args.input_file, args.output_file) + + +def configure_parser(parser: argparse.ArgumentParser): + """Set up parser for CLI.""" parser.add_argument('input_file', type=Path) parser.add_argument('output_file', type=Path) - args = parser.parse_args() - return args + parser.set_defaults(func=create_conversion_table_cli) -def main(): - """Main.""" - args = parse_args() - conversion_table_json = args.input_file - conversion_table_a2l = args.output_file - create_conversion_table(conversion_table_json, conversion_table_a2l) +def main(argv: Optional[List[str]] = None): + """Main function for CLI.""" + parser = argparse.ArgumentParser(description=PARSER_HELP) + configure_parser(parser) + args = parser.parse_args(argv) + args.func(args) if __name__ == '__main__': - main() + main(sys.argv[1:]) diff --git a/powertrain_build/interface/export_global_vars.py b/powertrain_build/interface/export_global_vars.py index 6688886..074a873 100644 --- a/powertrain_build/interface/export_global_vars.py +++ b/powertrain_build/interface/export_global_vars.py @@ -6,7 +6,7 @@ import argparse import os import sys -from typing import Dict, Tuple +from typing import Dict, List, Optional, Tuple from ruamel.yaml import YAML @@ -15,6 +15,9 @@ from powertrain_build.feature_configs import FeatureConfigs from powertrain_build.unit_configs import UnitConfigs +PARSER_HELP = "Export global variables." + + def get_global_variables(project_config_path: str) -> Dict: """Get global variables connected to PyBuild project. @@ -81,18 +84,38 @@ def _export_yaml(data: Dict, file_path: str) -> None: yaml.dump(data, yaml_file) -def _main(): - args = _parse_args() +def export_global_vars(args: argparse.Namespace): + """Exports global variables as yaml file.""" global_variables = get_global_variables(args.project_config) _export_yaml(global_variables, args.output_file) -def _parse_args(): - parser = argparse.ArgumentParser(description="Export global variables.") - parser.add_argument("--project-config", help="Project root configuration file.", required=True) - parser.add_argument("--output-file", help="Output file to export global variables.", required=True) - return parser.parse_args() +def _main(argv: Optional[List[str]] = None): + """Main function for CLI.""" + parser = argparse.ArgumentParser(description=PARSER_HELP) + configure_parser(parser) + args = parser.parse_args(argv) + args.func(args) + + +def configure_parser(parser: argparse.ArgumentParser): + """Configures the argument parser for the script. + + Args: + parser (argparse.ArgumentParser): Argument parser. + """ + parser.add_argument( + "--project-config", + help="Project root configuration file.", + required=True, + ) + parser.add_argument( + "--output-file", + help="Output file to export global variables.", + required=True, + ) + parser.set_defaults(func=export_global_vars) if __name__ == "__main__": - sys.exit(_main()) + sys.exit(_main(sys.argv[1:])) diff --git a/powertrain_build/interface/generate_adapters.py b/powertrain_build/interface/generate_adapters.py index ab49b11..ee451b1 100644 --- a/powertrain_build/interface/generate_adapters.py +++ b/powertrain_build/interface/generate_adapters.py @@ -3,8 +3,12 @@ # -*- coding: utf-8 -*- """Python module used for calculating interfaces for CSP""" +import argparse +import sys from pathlib import Path from os import path +from typing import List, Optional + from powertrain_build.interface.hal import HALA from powertrain_build.interface.device_proxy import DPAL from powertrain_build.interface.service import ServiceFramework @@ -15,14 +19,11 @@ from powertrain_build.lib.helper_functions import deep_json_update LOGGER = logger.create_logger("CSP adapters") +PARSER_HELP = "Generate adapters" -def parse_args(): - """Parse arguments - Returns: - Namespace: the parsed arguments - """ - parser = generation_utils.base_parser() +def configure_parser(parser: argparse.ArgumentParser): + generation_utils.add_base_args(parser) parser.add_argument( "--dp-interface", help="Add dp interface to adapter specification", @@ -48,14 +49,20 @@ def parse_args(): help="Update project config file with path to adapter specifications", action="store_true" ) - return parser.parse_args() + parser.set_defaults(func=generate_adapters) -def main(): +def main(argv: Optional[List[str]] = None): """ Main function for stand alone execution. Mostly useful for testing and generation of dummy hal specifications """ - args = parse_args() + parser = argparse.ArgumentParser(description=PARSER_HELP) + configure_parser(parser) + args = parser.parse_args(argv) + args.func(args) + + +def generate_adapters(args: argparse.Namespace): app = generation_utils.process_app(args.config) adapters(args, app) @@ -122,4 +129,4 @@ def adapters(args, app): if __name__ == "__main__": - main() + main(sys.argv[1:]) diff --git a/powertrain_build/interface/generate_hi_interface.py b/powertrain_build/interface/generate_hi_interface.py index 6e634d8..ecf578f 100644 --- a/powertrain_build/interface/generate_hi_interface.py +++ b/powertrain_build/interface/generate_hi_interface.py @@ -3,7 +3,11 @@ # -*- coding: utf-8 -*- """Python module used for calculating interfaces for CSP HI""" +import argparse +import sys from pathlib import Path +from typing import List, Optional + from powertrain_build.interface import generation_utils from powertrain_build.interface.device_proxy import DPAL from powertrain_build.lib.helper_functions import recursive_default_dict, to_normal_dict @@ -11,6 +15,8 @@ from powertrain_build.lib.helper_functions import recursive_default_dict, to_nor OP_READ = 'read' OP_WRITE = 'write' +PARSER_HELP = "Generate HI YAML interface file." + def generate_hi_interface(args, hi_interface): """Generate HI YAML interface file. @@ -44,31 +50,38 @@ def generate_hi_interface(args, hi_interface): generation_utils.write_to_file(to_normal_dict(result), args.output, is_yaml=True) -def parse_args(): - """Parse arguments. +def generate_hi_interface_cli(args: argparse.Namespace): + """CLI entrypoint for generating HI YAML interface file. - Returns: - Namespace: the parsed arguments. + Args: + args (Namespace): Arguments from command line. """ - parser = generation_utils.base_parser() - parser.add_argument( - "output", - help="Output file with interface specifications.", - type=Path - ) - return parser.parse_args() - - -def main(): - """ Main function for stand alone execution. - Mostly useful for testing and generation of dummy hal specifications. - """ - args = parse_args() app = generation_utils.process_app(args.config) hi_app = DPAL(app) interface = generation_utils.get_interface(app, hi_app) generate_hi_interface(args, interface) +def configure_parser(parser: argparse.ArgumentParser): + """Configure parser for generating HI YAML interface file.""" + generation_utils.add_base_args(parser) + parser.add_argument( + "output", + help="Output file with interface specifications.", + type=Path + ) + parser.set_defaults(func=generate_hi_interface_cli) + + +def main(argv: Optional[List[str]] = None): + """ Main function for stand alone execution. + Mostly useful for testing and generation of dummy hal specifications. + """ + parser = argparse.ArgumentParser(description=PARSER_HELP) + configure_parser(parser) + args = parser.parse_args(argv) + args.func(args) + + if __name__ == "__main__": - main() + main(sys.argv[1:]) diff --git a/powertrain_build/interface/generate_service.py b/powertrain_build/interface/generate_service.py index db012a4..eb15bde 100644 --- a/powertrain_build/interface/generate_service.py +++ b/powertrain_build/interface/generate_service.py @@ -3,21 +3,23 @@ # -*- coding: utf-8 -*- """Python module used for calculating interfaces for CSP""" +import argparse +import sys from pathlib import Path +from typing import List, Optional + from powertrain_build.interface.service import get_service from powertrain_build.lib import logger from powertrain_build.interface import generation_utils LOGGER = logger.create_logger("CSP service") +PARSER_HELP = "Generate CSP service models" -def parse_args(): - """Parse command line arguments - Returns: - Namespace: Arguments from command line - """ - parser = generation_utils.base_parser() +def configure_parser(parser: argparse.ArgumentParser): + """Configure parser for CSP service generation""" + generation_utils.add_base_args(parser) parser.add_argument( "--client-name", help="Name of the context object in CSP. Defaults to project name." @@ -27,19 +29,26 @@ def parse_args(): help="Output directory for service models", type=Path ) - return parser.parse_args() + parser.set_defaults(func=generate_service_cli) -def main(): - """ Main function for stand alone execution. - Mostly useful for testing and generation of dummy hal specifications - """ - args = parse_args() +def generate_service_cli(args: argparse.Namespace): + """CLI function for CSP service generation""" app = generation_utils.process_app(args.config) client_name = generation_utils.get_client_name(args, app) service(args, app, client_name) +def main(argv: Optional[List[str]] = None): + """ Main function for stand alone execution. + Mostly useful for testing and generation of dummy hal specifications + """ + parser = argparse.ArgumentParser(description=PARSER_HELP) + configure_parser(parser) + args = parser.parse_args(argv) + args.func(args) + + def service(args, app, client_name): """ Generate specifications for pt-scheduler wrappers. @@ -57,4 +66,4 @@ def service(args, app, client_name): if __name__ == "__main__": - main() + main(sys.argv[1:]) diff --git a/powertrain_build/interface/generate_wrappers.py b/powertrain_build/interface/generate_wrappers.py index 899a077..d93d458 100644 --- a/powertrain_build/interface/generate_wrappers.py +++ b/powertrain_build/interface/generate_wrappers.py @@ -3,7 +3,11 @@ # -*- coding: utf-8 -*- """Python module used for calculating interfaces for CSP""" +import argparse +import sys from pathlib import Path +from typing import List, Optional + from powertrain_build.interface.hal import HALA, get_hal_list from powertrain_build.interface.device_proxy import DPAL from powertrain_build.interface.service import ServiceFramework, get_service_list @@ -12,6 +16,8 @@ from powertrain_build.interface import generation_utils LOGGER = logger.create_logger("CSP wrappers") +PARSER_HELP = "Generate specifications for pt-scheduler wrappers" + def get_manifest(app, domain, client_name): """Get signal manifest for application @@ -40,13 +46,9 @@ def get_manifest(app, domain, client_name): return dpal.to_manifest(client_name) -def parse_args(): - """Parse command line arguments - - Returns: - Namespace: Arguments from command line - """ - parser = generation_utils.base_parser() +def configure_parser(parser: argparse.ArgumentParser): + """Configure parser for generating pt-scheduler wrappers.""" + generation_utils.add_base_args(parser) parser.add_argument( "--client-name", help="Name of the context object in CSP. Defaults to project name." @@ -71,19 +73,30 @@ def parse_args(): help="Output file with service interface specifications", type=Path ) - return parser.parse_args() + parser.set_defaults(func=generate_wrappers_cli) -def main(): - """ Main function for stand alone execution. - Mostly useful for testing and generation of dummy hal specifications +def generate_wrappers_cli(args: argparse.Namespace): + """Generate specifications for pt-scheduler wrappers. + + Args: + args (Namespace): Arguments from command line """ - args = parse_args() app = generation_utils.process_app(args.config) client_name = generation_utils.get_client_name(args, app) wrappers(args, app, client_name) +def main(argv: Optional[List[str]] = None): + """ Main function for stand alone execution. + Mostly useful for testing and generation of dummy hal specifications + """ + parser = argparse.ArgumentParser(description=PARSER_HELP) + configure_parser(parser) + args = parser.parse_args(argv) + args.func(args) + + def wrappers(args, app, client_name): """ Generate specifications for pt-scheduler wrappers. @@ -131,4 +144,4 @@ def wrappers(args, app, client_name): if __name__ == "__main__": - main() + main(sys.argv[1:]) diff --git a/powertrain_build/interface/generation_utils.py b/powertrain_build/interface/generation_utils.py index c23064d..c5937c9 100644 --- a/powertrain_build/interface/generation_utils.py +++ b/powertrain_build/interface/generation_utils.py @@ -13,15 +13,13 @@ from powertrain_build.lib import logger LOGGER = logger.create_logger("CSP interface generation utils") -def base_parser(): +def add_base_args(parser: argparse.ArgumentParser): """ Base parser that adds config argument. Returns: parser (ArgumentParser): Base parser """ - parser = argparse.ArgumentParser() parser.add_argument("config", help="The project configuration file", type=Path) - return parser def get_client_name(args, app): diff --git a/powertrain_build/interface/model_yaml_verification.py b/powertrain_build/interface/model_yaml_verification.py index 8fdcccd..320cf97 100644 --- a/powertrain_build/interface/model_yaml_verification.py +++ b/powertrain_build/interface/model_yaml_verification.py @@ -5,12 +5,16 @@ import argparse import logging +import sys +import typing from pathlib import Path from voluptuous import All, MultipleInvalid, Optional, Required, Schema from ruamel.yaml import YAML from powertrain_build.interface.application import Application from powertrain_build.interface.base import BaseApplication +PARSER_HELP = "Verify the model yaml files." + class ModelYmlVerification(BaseApplication): """Class for verifying the model yaml files.""" @@ -317,20 +321,14 @@ def get_app(project_config): return app -def parse_args(): - """Parse command line arguments. - - Returns: - (Namespace): parsed command line arguments. - """ - parser = argparse.ArgumentParser() +def configure_parser(parser: argparse.ArgumentParser): + """Configure the argument parser.""" parser.add_argument("config", help="The SPA2 project config file", type=Path) - return parser.parse_args() + parser.set_defaults(func=model_yaml_verification_cli) -def main(): - """Main function for model yaml verification.""" - args = parse_args() +def model_yaml_verification_cli(args: argparse.Namespace): + """CLI function for model yaml verification.""" app = get_app(args.config) model_yamls = app.get_translation_files() model_yaml_ver = ModelYmlVerification(app) @@ -338,5 +336,13 @@ def main(): model_yaml_ver.print_success_msg() +def main(argv: typing.Optional[typing.List[str]] = None): + """Main function for model yaml verification.""" + parser = argparse.ArgumentParser(description=PARSER_HELP) + configure_parser(parser) + args = parser.parse_args(argv) + args.func(args) + + if __name__ == "__main__": - main() + main(sys.argv[1:]) diff --git a/powertrain_build/interface/update_call_sources.py b/powertrain_build/interface/update_call_sources.py index 6c25722..dfa456a 100644 --- a/powertrain_build/interface/update_call_sources.py +++ b/powertrain_build/interface/update_call_sources.py @@ -6,17 +6,17 @@ import argparse import re +import sys +from typing import List, Optional from ruamel.yaml import YAML from pathlib import Path -def parse_args(): - """ Parse arguments +PARSER_HELP = "Update call sources for method calls in source files." - Returns: - Namespace: the parsed arguments - """ - parser = argparse.ArgumentParser() + +def configure_parser(parser: argparse.ArgumentParser): + """Configure the parser for the update call sources command.""" parser.add_argument("interface", help="Interface specification dict", type=Path) parser.add_argument("src_dir", help="Path to source file directory", type=Path) parser.add_argument( @@ -26,12 +26,11 @@ def parse_args(): default=None, help="Path to project config json file", ) - return parser.parse_args() + parser.set_defaults(func=update_call_sources_cli) -def main(): - """ Main function for stand alone execution.""" - args = parse_args() +def update_call_sources_cli(args: argparse.Namespace): + """CLI function for updating call sources.""" method_config = read_project_config(args.project_config) with open(args.interface, encoding="utf-8") as interface_file: yaml = YAML(typ='safe', pure=True) @@ -39,6 +38,14 @@ def main(): update_call_sources(args.src_dir, adapter_spec, method_config) +def main(argv: Optional[List[str]] = None): + """ Main function for stand alone execution.""" + parser = argparse.ArgumentParser(description=PARSER_HELP) + configure_parser(parser) + args = parser.parse_args(argv) + args.func(args) + + def read_project_config(project_config_path): """ Reads project config file and extract method specific settings if they are present. @@ -170,4 +177,4 @@ def generate_src_code(adapter, method, old_src, method_config): if __name__ == "__main__": - main() + main(sys.argv[1:]) diff --git a/powertrain_build/interface/update_model_yaml.py b/powertrain_build/interface/update_model_yaml.py index 4eb1211..016e7c0 100644 --- a/powertrain_build/interface/update_model_yaml.py +++ b/powertrain_build/interface/update_model_yaml.py @@ -5,11 +5,16 @@ import argparse +import sys from pathlib import Path +from typing import List, Optional + from ruamel.yaml import YAML from powertrain_build.interface.application import Application from powertrain_build.interface.base import BaseApplication +PARSER_HELP = "Update model yaml files." + class BadYamlFormat(Exception): """Exception to raise when signal is not in/out signal.""" @@ -155,25 +160,27 @@ def get_app(config): return app -def parse_args(): - """Parse command line arguments - - Returns: - (Namespace): Parsed command line arguments - """ - parser = argparse.ArgumentParser() - parser.add_argument("config", help="The SPA2 project config file", type=Path) - return parser.parse_args() - - -def main(): - """Main function for update model yaml.""" - args = parse_args() +def update_model_yaml_cli(args: argparse.Namespace): + """CLI for update model yaml.""" app = get_app(args.config) translation_files = app.get_translation_files() uymlf = UpdateYmlFormat(app) uymlf.parse_definition(translation_files) +def configure_parser(parser: argparse.ArgumentParser): + """Configure parser for update model yaml.""" + parser.add_argument("config", help="The SPA2 project config file", type=Path) + parser.set_defaults(func=update_model_yaml_cli) + + +def main(argv: Optional[List[str]] = None): + """Main function for update model yaml.""" + parser = argparse.ArgumentParser(description=PARSER_HELP) + configure_parser(parser) + args = parser.parse_args(argv) + return args.func(args) + + if __name__ == "__main__": - main() + main(sys.argv[1:]) diff --git a/powertrain_build/replace_compu_tab_ref.py b/powertrain_build/replace_compu_tab_ref.py index 6b41197..6e7a8b9 100644 --- a/powertrain_build/replace_compu_tab_ref.py +++ b/powertrain_build/replace_compu_tab_ref.py @@ -3,18 +3,29 @@ """Module for replacing $CVC_* style references in a2l file.""" -import re import argparse - +import re +import sys from pathlib import Path +from typing import List, Optional -def parse_args(): - """Parse args.""" - parser = argparse.ArgumentParser("Replace $CVC_* style references in a2l file") +PARSER_HELP = "Replace $CVC_* style references in a2l file" + + +def configure_parser(parser: argparse.ArgumentParser): + """Configure parser for CLI.""" parser.add_argument("a2l_target_file") - args = parser.parse_args() - return args + parser.set_defaults(func=replace_tab_verb_cli) + + +def replace_tab_verb_cli(args: argparse.Namespace): + """CLI wrapper function for passing in Namespace object. + + This allows maintaining a standardized CLI function signature while not breaking backwards + compatibility with replace_tab_verb. + """ + replace_tab_verb(args.a2l_target_file) def replace_tab_verb(file_path: Path): @@ -82,11 +93,13 @@ def replace_tab_verb(file_path: Path): f_h.write(a2l_patched) -def main(): - """Main.""" - args = parse_args() - replace_tab_verb(args.a2l_target_file) +def main(argv: Optional[List[str]] = None): + """Main function for CLI.""" + parser = argparse.ArgumentParser(description=PARSER_HELP) + configure_parser(parser) + args = parser.parse_args(argv) + args.func(args) if __name__ == "__main__": - main() + main(sys.argv[1:]) diff --git a/powertrain_build/signal_inconsistency_check.py b/powertrain_build/signal_inconsistency_check.py index 343b038..1b25c59 100644 --- a/powertrain_build/signal_inconsistency_check.py +++ b/powertrain_build/signal_inconsistency_check.py @@ -11,6 +11,7 @@ import sys from collections import defaultdict from os.path import join from pathlib import Path +from typing import List, Optional import git @@ -60,6 +61,8 @@ TEMPLATE = """<!DOCTYPE html> </body> </html>""" +PARSER_HELP = "Run signal inconsistency check." + def gen_sig_incons_index_file(project_list): """Generate Index_SigCheck_All.html.""" @@ -74,9 +77,8 @@ def gen_sig_incons_index_file(project_list): f_h.write(TEMPLATE.format(project_rows=rows)) -def parse_args(): +def configure_parser(parser: argparse.ArgumentParser): """Parse the arguments sent to the script.""" - parser = argparse.ArgumentParser("") parser.add_argument( "-m", "--models", @@ -89,7 +91,7 @@ def parse_args(): parser.add_argument( "-r", "--report", help="Create report for all projects", action="store_true" ) - return parser.parse_args() + parser.set_defaults(func=run_signal_inconsistency_check) def get_project_configs(): @@ -612,12 +614,19 @@ class SignalInconsistency: return exit_code -def main(): +def run_signal_inconsistency_check(args: argparse.Namespace) -> int: """Create Signal Inconsistency instance and run checks.""" - args = parse_args() sig_in = SignalInconsistency(args) return sig_in.run(args) +def main(argv: Optional[List[str]] = None) -> int: + """Create Signal Inconsistency instance and run checks.""" + parser = argparse.ArgumentParser(description=PARSER_HELP) + configure_parser(parser) + args = parser.parse_args(argv) + return args.func(args) + + if __name__ == "__main__": - sys.exit(main()) + sys.exit(main(sys.argv[1:])) diff --git a/powertrain_build/wrapper.py b/powertrain_build/wrapper.py index 5c2ced1..742bbcb 100644 --- a/powertrain_build/wrapper.py +++ b/powertrain_build/wrapper.py @@ -10,6 +10,7 @@ import os import sys from pathlib import Path from re import search +from typing import List, Optional try: from importlib.resources import files @@ -28,6 +29,7 @@ class PyBuildWrapper(pt_matlab.Matlab): """Performs upgrade of Matlab models to PyBuild system.""" HASH_FILE_NAME = "pybuild_file_hashes.json" + PARSER_HELP = "Run PyBuild, update and/or generate code for selected models and/or build." def __init__(self, args): """Constructor, initializes paths for PyBuild upgrader. @@ -469,8 +471,10 @@ class PyBuildWrapper(pt_matlab.Matlab): default="matlab-scripts", help="Path to folder containing Matlab scripts and simulink libraries to include.", ) - powertrain_build_parser = parser.add_subparsers(help="PyBuild specific build.") - build_specific_parser = powertrain_build_parser.add_parser( + parser.set_defaults(func=run_wrapper) + + subparsers = parser.add_subparsers(help="PyBuild specific build.") + build_specific_parser = subparsers.add_parser( "build-specific", help="Run PyBuild for project with specific settings." ) build.add_args(build_specific_parser) @@ -479,12 +483,8 @@ class PyBuildWrapper(pt_matlab.Matlab): pt_matlab.Matlab.add_args(parser) -def main(): - """Run main function.""" - parser = argparse.ArgumentParser("PyBuild Wrapper") - PyBuildWrapper.add_args(parser) - args = parser.parse_args() - +def run_wrapper(args: argparse.Namespace) -> int: + """Run PyBuildWrapper.""" if args.build is not None and getattr(args, "project_config", None) is not None: LOGGER.error("Cannot run both PyBuild quick build (--build <PROJECT>) " "and specific build (build-specific).") return 1 @@ -498,5 +498,13 @@ def main(): return wrapper.run() +def main(argv: Optional[List[str]] = None) -> int: + """Run PyBuildWrapper""" + parser = argparse.ArgumentParser("PyBuild Wrapper") + PyBuildWrapper.add_args(parser) + args = parser.parse_args(argv) + return run_wrapper(args) + + if __name__ == "__main__": - sys.exit(main()) + sys.exit(main(sys.argv[1:])) diff --git a/requirements.txt b/requirements.txt index b2b6843..a62eca3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,5 @@ scipy==1.5.4; python_version < "3.8" scipy==1.9.1; python_version == "3.8" or python_version == "3.9" or python_version == "3.10" scipy==1.14.1; python_version >= "3.11" importlib-resources==5.4.0; python_version < "3.9" +pywin32==305; python_version == "3.6" and sys_platform == "win32" +pywin32==308; python_version > "3.6" and sys_platform == "win32" diff --git a/setup.cfg b/setup.cfg index b73371b..d4a7a5c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,3 +34,7 @@ packages = [pbr] skip_git_sdist = 1 + +[options.entry_points] +console_scripts = + powertrain-build = powertrain_build.cli:main diff --git a/tests/powertrain_build/func_cli.py b/tests/powertrain_build/func_cli.py new file mode 100644 index 0000000..60c9406 --- /dev/null +++ b/tests/powertrain_build/func_cli.py @@ -0,0 +1,24 @@ +import sys +from subprocess import run +from unittest import TestCase + + +class TestCli(TestCase): + """Test the cli module.""" + + def test_entry_points(self): + """Tests that entrypoints are correctly defined and work both as __main__ and sub-commands.""" + modules = [ + "powertrain_build.wrapper", + "powertrain_build.interface.generate_adapters", + "powertrain_build.interface.generate_hi_interface", + "powertrain_build.interface.generate_service", + "powertrain_build.interface.generate_wrappers", + "powertrain_build.interface.model_yaml_verification", + "powertrain_build.interface.update_model_yaml", + "powertrain_build.interface.update_call_sources", + ] + for module in modules: + entrypoint = module.replace("_", "-").split(".") + run(entrypoint + ["--help"], check=True) + run([sys.executable, "-m", module, "--help"], check=True) diff --git a/tests/powertrain_build/test_cli.py b/tests/powertrain_build/test_cli.py new file mode 100644 index 0000000..e139808 --- /dev/null +++ b/tests/powertrain_build/test_cli.py @@ -0,0 +1,46 @@ +"""Tests for the cli module.""" + +from pathlib import Path +from unittest import TestCase +from unittest.mock import patch + +from powertrain_build.cli import main + + +class TestCli(TestCase): + """Test the cli module.""" + + def test_main(self): + """Test that the main function parses arguments and calls correct function.""" + # Simple command without arguments + with patch("powertrain_build.wrapper.run_wrapper") as run_wrapper_mock: + main(["wrapper"]) + run_wrapper_mock.assert_called_once() + args = run_wrapper_mock.call_args[0][0] + self.assertEqual(args.func, run_wrapper_mock) + self.assertEqual(args.command, "wrapper") + # Sub-command with arguments + with patch("powertrain_build.interface.generate_adapters.generate_adapters") as generate_adapters_mock: + main([ + "interface", "generate-adapters", + "--dp-interface", + "--hal-interface", + "--service-interface", + "--update-config", + "path/to/config", + "path/to/output", + ]) + generate_adapters_mock.assert_called_once() + args = generate_adapters_mock.call_args[0][0] + self.assertEqual(args.func, generate_adapters_mock) + self.assertEqual(args.command, "interface") + self.assertEqual(args.interface_command, "generate-adapters") + self.assertTrue(args.dp_interface) + self.assertTrue(args.hal_interface) + self.assertTrue(args.service_interface) + self.assertEqual(args.config, Path("path/to/config")) + self.assertEqual(args.output, Path("path/to/output")) + # Command without enough arguments + with self.assertRaises(SystemExit) as system_exit: + main(["interface", "generate-adapters"]) + self.assertEqual(system_exit.exception.code, 2) diff --git a/tox.ini b/tox.ini index 3006869..28ab6d2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,10 @@ [tox] -skipsdist = True requires = tox >= 2.0 +envlist = + flake8 + pytest + functest [flake8] exclude = @@ -42,3 +45,9 @@ exclude_lines = [coverage:html] directory = cover + +[testenv:functest] +skipsdist = False +package = editable +commands = + pytest tests/powertrain_build/func_cli.py diff --git a/zuul.d/tox.yaml b/zuul.d/tox.yaml index 477e6fe..f6bd335 100644 --- a/zuul.d/tox.yaml +++ b/zuul.d/tox.yaml @@ -2,5 +2,5 @@ name: powertrain-build-tox parent: tox-cover vars: - tox_envlist: flake8,pytest + tox_envlist: flake8,pytest,functest nodeset: ubuntu-jammy