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
This commit is contained in:
Axel Andersson 2024-10-17 10:52:44 +02:00
parent ec77977757
commit 816553f1bc
24 changed files with 613 additions and 198 deletions

@ -34,6 +34,35 @@ In git bash:
py -3.6 -m powertrain_build.wrapper --codegen --models Models/ICEAES/VcAesTx/VcAesTx.mdl 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 #### Set Matlab 2017 as Environmental Variable
Add New User Variables Add New User Variables

@ -0,0 +1,6 @@
import sys
from powertrain_build.cli import main
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))

@ -12,6 +12,8 @@ import re
import sys import sys
from itertools import product from itertools import product
from pathlib import Path from pathlib import Path
from textwrap import dedent
from typing import List, Optional
import git import git
@ -340,14 +342,7 @@ def check_signals(left_signals, right_signals, errors, left_path=None, right_pat
return serious_mismatch return serious_mismatch
def parse_args(): PARSER_HELP = dedent(r"""
"""Parse arguments
Returns:
Namespace: the parsed arguments
"""
parser = argparse.ArgumentParser(
description="""
Checks attributes and existence of signals Checks attributes and existence of signals
Produced but not consumed signals are giving warnings 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> \ py -3.6 -m powertrain_build.check_interface projects <Projects> \
--projects ProjectOne ProjectTwo ProjectThree --projects ProjectOne ProjectTwo ProjectThree
Checks the interfaces of ProjectOne, ProjectTwo and ProjectThree in the folder Projects Checks the interfaces of ProjectOne, ProjectTwo and ProjectThree in the folder Projects
""", """).strip()
formatter_class=argparse.RawTextHelpFormatter,
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 # create the parser for the different commands
model = subparsers.add_parser( model = subparsers.add_parser(
"models", "models",
description=""" description=dedent("""
Check models independently of projects. Check models independently of projects.
All signals are assumed to be active. 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 Any signal that gives and error is used in a model but is not produced in any model or project
interface. interface.
""", """).strip(),
) )
add_model_args(model) add_model_args(model)
model.set_defaults(func=model_check)
project = subparsers.add_parser( project = subparsers.add_parser(
"projects", "projects",
description=""" description=dedent("""
Check projects as a whole. 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) add_project_args(project)
project.set_defaults(func=projects_check)
models_in_projects = subparsers.add_parser( models_in_projects = subparsers.add_parser(
"models_in_projects", "models_in_projects",
description=""" description=dedent("""
Check models specifically for projects. 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_project_args(models_in_projects)
add_model_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("--properties", help="Check properties such as type", action="store_true")
models_in_projects.add_argument("--existence", help="Check signal existence consistency", 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""" """Add project arguments to subparser"""
parser.add_argument("project_root", help="Path to start looking for projects", type=Path) parser.add_argument("project_root", help="Path to start looking for projects", type=Path)
parser.add_argument( 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""" """Add model arguments to subparser"""
parser.add_argument("model_root", help="Path to start looking for models", type=Path) 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="+") 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 return model_names
def model_check(args): def model_check(args: argparse.Namespace):
"""Entry point for models command.""" """Entry point for models command."""
serious_mismatch = False serious_mismatch = False
all_models = get_all_models(args.model_root) 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] model_names = [model.name for model in all_models]
serious_mismatch |= check_models_generic(all_models, model_names, []) serious_mismatch |= check_models_generic(all_models, model_names, [])
if serious_mismatch:
LOGGER.error("Serious interface errors found.")
return serious_mismatch return serious_mismatch
def projects_check(args): def projects_check(args: argparse.Namespace):
"""Entry point for projects command.""" """Entry point for projects command."""
serious_mismatch = False serious_mismatch = False
projects = get_projects(args.project_root, args.projects) projects = get_projects(args.project_root, args.projects)
@ -472,10 +479,14 @@ def projects_check(args):
serious_mismatch |= check_internal_signals(app, None) serious_mismatch |= check_internal_signals(app, None)
if ems is not None: if ems is not None:
serious_mismatch |= check_external_signals(ems, app, None) serious_mismatch |= check_external_signals(ems, app, None)
if serious_mismatch:
LOGGER.error("Serious interface errors found.")
return serious_mismatch return serious_mismatch
def models_in_projects_check(args): def models_in_projects_check(args: argparse.Namespace):
"""Entry point for models_in_projects command.""" """Entry point for models_in_projects command."""
serious_mismatch = False serious_mismatch = False
projects = get_projects(args.project_root, args.projects) 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) 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 model_names = [model.name for model in all_models] if args.models is None else args.models
serious_mismatch |= signal_existence(projects, model_names) serious_mismatch |= signal_existence(projects, model_names)
if serious_mismatch:
LOGGER.error("Serious interface errors found.")
return serious_mismatch return serious_mismatch
@ -545,19 +560,16 @@ def signal_match(signals_to_check, signals_to_check_against, matches):
matches[a_signal.name] = True matches[a_signal.name] = True
def main(): def main(argv: Optional[List[str]] = None):
"""Main function for stand alone execution.""" """Main function for stand alone execution."""
args = parse_args() parser = argparse.ArgumentParser(
if args.mode == "models": description=PARSER_HELP,
serious_errors = model_check(args) formatter_class=argparse.RawTextHelpFormatter,
if args.mode == "projects": )
serious_errors = projects_check(args) configure_parser(parser)
if args.mode == "models_in_projects": args = parser.parse_args(argv)
serious_errors = models_in_projects_check(args) args.func(args)
if serious_errors:
LOGGER.error("Serious interface errors found.")
sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":
main() main(sys.argv[1:])

141
powertrain_build/cli.py Normal file

@ -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)

@ -10,7 +10,9 @@ import json
import operator import operator
import os import os
import re import re
import sys
from pprint import pformat from pprint import pformat
from typing import List, Optional
from powertrain_build.lib import logger from powertrain_build.lib import logger
@ -412,17 +414,25 @@ class HeaderConfigParser(ConfigParserCommon):
class ProcessHandler: class ProcessHandler:
"""Class to collect functions for the process.""" """Class to collect functions for the process."""
PARSER_HELP = "Parse configs.json and c-files, to update code switch configs"
@staticmethod @staticmethod
def parse_args(): def configure_parser(parser: argparse.ArgumentParser):
"""Parse arguments.""" """Parse arguments."""
parser = argparse.ArgumentParser("Parse configs.json and c-files, to update code switch configes") 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") 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( dir_parser = subparser.add_parser(
'models', 'models',
help="Run for one or multiple models. Script finds files generated from the model(s).") help="Run for one or multiple models. Script finds files generated from the model(s).")
dir_parser.add_argument('models', nargs='+', dir_parser.add_argument('models', nargs='+',
help="Space separated list of model directories") help="Space separated list of model directories")
file_parser = subparser.add_parser('files', file_parser = subparser.add_parser('files',
help="Choose specific files. Mainly for manually written configs.") help="Choose specific files. Mainly for manually written configs.")
file_parser.add_argument('c_file', file_parser.add_argument('c_file',
@ -433,8 +443,8 @@ class ProcessHandler:
help="Full path to tl_aux file. (Optional) ") help="Full path to tl_aux file. (Optional) ")
file_parser.add_argument('--local_file', file_parser.add_argument('--local_file',
help="Full path to OPort file. (Optional) ") help="Full path to OPort file. (Optional) ")
args = parser.parse_args()
return args parser.set_defaults(func=ProcessHandler.main)
@staticmethod @staticmethod
def get_files(model_path): def get_files(model_path):
@ -505,9 +515,8 @@ class ProcessHandler:
return parser.get_config() return parser.get_config()
@classmethod @classmethod
def main(cls): def main(cls, args: argparse.Namespace):
"""Run the main function of the script.""" """Run the main function of the script."""
args = cls.parse_args()
if args.mode == 'files': if args.mode == 'files':
LOGGER.info('Using manually supplied files %s', args) LOGGER.info('Using manually supplied files %s', args)
local_defs = cls.get_header_config(args.local_file, {}) 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) 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__": if __name__ == "__main__":
ProcessHandler.main() main(sys.argv[1:])

@ -5,7 +5,12 @@
import argparse import argparse
import json import json
import sys
from pathlib import Path from pathlib import Path
from typing import List, Optional
PARSER_HELP = "Create a2l file from conversion_table.json file."
def get_vtab_text(vtab): 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)) f_h.write(get_vtab_text(vtab))
def parse_args(): def create_conversion_table_cli(args: argparse.Namespace):
"""Parse args.""" """CLI wrapper function for passing in Namespace object.
parser = argparse.ArgumentParser('Create a2l file from conversion_table.json file')
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('input_file', type=Path)
parser.add_argument('output_file', type=Path) parser.add_argument('output_file', type=Path)
args = parser.parse_args() parser.set_defaults(func=create_conversion_table_cli)
return args
def main(): def main(argv: Optional[List[str]] = None):
"""Main.""" """Main function for CLI."""
args = parse_args() parser = argparse.ArgumentParser(description=PARSER_HELP)
conversion_table_json = args.input_file configure_parser(parser)
conversion_table_a2l = args.output_file args = parser.parse_args(argv)
create_conversion_table(conversion_table_json, conversion_table_a2l) args.func(args)
if __name__ == '__main__': if __name__ == '__main__':
main() main(sys.argv[1:])

@ -6,7 +6,7 @@
import argparse import argparse
import os import os
import sys import sys
from typing import Dict, Tuple from typing import Dict, List, Optional, Tuple
from ruamel.yaml import YAML from ruamel.yaml import YAML
@ -15,6 +15,9 @@ from powertrain_build.feature_configs import FeatureConfigs
from powertrain_build.unit_configs import UnitConfigs from powertrain_build.unit_configs import UnitConfigs
PARSER_HELP = "Export global variables."
def get_global_variables(project_config_path: str) -> Dict: def get_global_variables(project_config_path: str) -> Dict:
"""Get global variables connected to PyBuild project. """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) yaml.dump(data, yaml_file)
def _main(): def export_global_vars(args: argparse.Namespace):
args = _parse_args() """Exports global variables as yaml file."""
global_variables = get_global_variables(args.project_config) global_variables = get_global_variables(args.project_config)
_export_yaml(global_variables, args.output_file) _export_yaml(global_variables, args.output_file)
def _parse_args(): def _main(argv: Optional[List[str]] = None):
parser = argparse.ArgumentParser(description="Export global variables.") """Main function for CLI."""
parser.add_argument("--project-config", help="Project root configuration file.", required=True) parser = argparse.ArgumentParser(description=PARSER_HELP)
parser.add_argument("--output-file", help="Output file to export global variables.", required=True) configure_parser(parser)
return parser.parse_args() 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__": if __name__ == "__main__":
sys.exit(_main()) sys.exit(_main(sys.argv[1:]))

@ -3,8 +3,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Python module used for calculating interfaces for CSP""" """Python module used for calculating interfaces for CSP"""
import argparse
import sys
from pathlib import Path from pathlib import Path
from os import path from os import path
from typing import List, Optional
from powertrain_build.interface.hal import HALA from powertrain_build.interface.hal import HALA
from powertrain_build.interface.device_proxy import DPAL from powertrain_build.interface.device_proxy import DPAL
from powertrain_build.interface.service import ServiceFramework 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") LOGGER = logger.create_logger("CSP adapters")
PARSER_HELP = "Generate adapters"
def parse_args():
"""Parse arguments
Returns: def configure_parser(parser: argparse.ArgumentParser):
Namespace: the parsed arguments generation_utils.add_base_args(parser)
"""
parser = generation_utils.base_parser()
parser.add_argument( parser.add_argument(
"--dp-interface", "--dp-interface",
help="Add dp interface to adapter specification", help="Add dp interface to adapter specification",
@ -48,14 +49,20 @@ def parse_args():
help="Update project config file with path to adapter specifications", help="Update project config file with path to adapter specifications",
action="store_true" 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. """ Main function for stand alone execution.
Mostly useful for testing and generation of dummy hal specifications 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) app = generation_utils.process_app(args.config)
adapters(args, app) adapters(args, app)
@ -122,4 +129,4 @@ def adapters(args, app):
if __name__ == "__main__": if __name__ == "__main__":
main() main(sys.argv[1:])

@ -3,7 +3,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Python module used for calculating interfaces for CSP HI""" """Python module used for calculating interfaces for CSP HI"""
import argparse
import sys
from pathlib import Path from pathlib import Path
from typing import List, Optional
from powertrain_build.interface import generation_utils from powertrain_build.interface import generation_utils
from powertrain_build.interface.device_proxy import DPAL from powertrain_build.interface.device_proxy import DPAL
from powertrain_build.lib.helper_functions import recursive_default_dict, to_normal_dict 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_READ = 'read'
OP_WRITE = 'write' OP_WRITE = 'write'
PARSER_HELP = "Generate HI YAML interface file."
def generate_hi_interface(args, hi_interface): def generate_hi_interface(args, hi_interface):
"""Generate HI YAML interface file. """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) generation_utils.write_to_file(to_normal_dict(result), args.output, is_yaml=True)
def parse_args(): def generate_hi_interface_cli(args: argparse.Namespace):
"""Parse arguments. """CLI entrypoint for generating HI YAML interface file.
Returns: Args:
Namespace: the parsed arguments. 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) app = generation_utils.process_app(args.config)
hi_app = DPAL(app) hi_app = DPAL(app)
interface = generation_utils.get_interface(app, hi_app) interface = generation_utils.get_interface(app, hi_app)
generate_hi_interface(args, interface) 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__": if __name__ == "__main__":
main() main(sys.argv[1:])

@ -3,21 +3,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Python module used for calculating interfaces for CSP""" """Python module used for calculating interfaces for CSP"""
import argparse
import sys
from pathlib import Path from pathlib import Path
from typing import List, Optional
from powertrain_build.interface.service import get_service from powertrain_build.interface.service import get_service
from powertrain_build.lib import logger from powertrain_build.lib import logger
from powertrain_build.interface import generation_utils from powertrain_build.interface import generation_utils
LOGGER = logger.create_logger("CSP service") LOGGER = logger.create_logger("CSP service")
PARSER_HELP = "Generate CSP service models"
def parse_args():
"""Parse command line arguments
Returns: def configure_parser(parser: argparse.ArgumentParser):
Namespace: Arguments from command line """Configure parser for CSP service generation"""
""" generation_utils.add_base_args(parser)
parser = generation_utils.base_parser()
parser.add_argument( parser.add_argument(
"--client-name", "--client-name",
help="Name of the context object in CSP. Defaults to project 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", help="Output directory for service models",
type=Path type=Path
) )
return parser.parse_args() parser.set_defaults(func=generate_service_cli)
def main(): def generate_service_cli(args: argparse.Namespace):
""" Main function for stand alone execution. """CLI function for CSP service generation"""
Mostly useful for testing and generation of dummy hal specifications
"""
args = parse_args()
app = generation_utils.process_app(args.config) app = generation_utils.process_app(args.config)
client_name = generation_utils.get_client_name(args, app) client_name = generation_utils.get_client_name(args, app)
service(args, app, client_name) 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): def service(args, app, client_name):
""" Generate specifications for pt-scheduler wrappers. """ Generate specifications for pt-scheduler wrappers.
@ -57,4 +66,4 @@ def service(args, app, client_name):
if __name__ == "__main__": if __name__ == "__main__":
main() main(sys.argv[1:])

@ -3,7 +3,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Python module used for calculating interfaces for CSP""" """Python module used for calculating interfaces for CSP"""
import argparse
import sys
from pathlib import Path from pathlib import Path
from typing import List, Optional
from powertrain_build.interface.hal import HALA, get_hal_list from powertrain_build.interface.hal import HALA, get_hal_list
from powertrain_build.interface.device_proxy import DPAL from powertrain_build.interface.device_proxy import DPAL
from powertrain_build.interface.service import ServiceFramework, get_service_list 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") LOGGER = logger.create_logger("CSP wrappers")
PARSER_HELP = "Generate specifications for pt-scheduler wrappers"
def get_manifest(app, domain, client_name): def get_manifest(app, domain, client_name):
"""Get signal manifest for application """Get signal manifest for application
@ -40,13 +46,9 @@ def get_manifest(app, domain, client_name):
return dpal.to_manifest(client_name) return dpal.to_manifest(client_name)
def parse_args(): def configure_parser(parser: argparse.ArgumentParser):
"""Parse command line arguments """Configure parser for generating pt-scheduler wrappers."""
generation_utils.add_base_args(parser)
Returns:
Namespace: Arguments from command line
"""
parser = generation_utils.base_parser()
parser.add_argument( parser.add_argument(
"--client-name", "--client-name",
help="Name of the context object in CSP. Defaults to project 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", help="Output file with service interface specifications",
type=Path type=Path
) )
return parser.parse_args() parser.set_defaults(func=generate_wrappers_cli)
def main(): def generate_wrappers_cli(args: argparse.Namespace):
""" Main function for stand alone execution. """Generate specifications for pt-scheduler wrappers.
Mostly useful for testing and generation of dummy hal specifications
Args:
args (Namespace): Arguments from command line
""" """
args = parse_args()
app = generation_utils.process_app(args.config) app = generation_utils.process_app(args.config)
client_name = generation_utils.get_client_name(args, app) client_name = generation_utils.get_client_name(args, app)
wrappers(args, app, client_name) 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): def wrappers(args, app, client_name):
""" Generate specifications for pt-scheduler wrappers. """ Generate specifications for pt-scheduler wrappers.
@ -131,4 +144,4 @@ def wrappers(args, app, client_name):
if __name__ == "__main__": if __name__ == "__main__":
main() main(sys.argv[1:])

@ -13,15 +13,13 @@ from powertrain_build.lib import logger
LOGGER = logger.create_logger("CSP interface generation utils") LOGGER = logger.create_logger("CSP interface generation utils")
def base_parser(): def add_base_args(parser: argparse.ArgumentParser):
""" Base parser that adds config argument. """ Base parser that adds config argument.
Returns: Returns:
parser (ArgumentParser): Base parser parser (ArgumentParser): Base parser
""" """
parser = argparse.ArgumentParser()
parser.add_argument("config", help="The project configuration file", type=Path) parser.add_argument("config", help="The project configuration file", type=Path)
return parser
def get_client_name(args, app): def get_client_name(args, app):

@ -5,12 +5,16 @@
import argparse import argparse
import logging import logging
import sys
import typing
from pathlib import Path from pathlib import Path
from voluptuous import All, MultipleInvalid, Optional, Required, Schema from voluptuous import All, MultipleInvalid, Optional, Required, Schema
from ruamel.yaml import YAML from ruamel.yaml import YAML
from powertrain_build.interface.application import Application from powertrain_build.interface.application import Application
from powertrain_build.interface.base import BaseApplication from powertrain_build.interface.base import BaseApplication
PARSER_HELP = "Verify the model yaml files."
class ModelYmlVerification(BaseApplication): class ModelYmlVerification(BaseApplication):
"""Class for verifying the model yaml files.""" """Class for verifying the model yaml files."""
@ -317,20 +321,14 @@ def get_app(project_config):
return app return app
def parse_args(): def configure_parser(parser: argparse.ArgumentParser):
"""Parse command line arguments. """Configure the argument parser."""
Returns:
(Namespace): parsed command line arguments.
"""
parser = argparse.ArgumentParser()
parser.add_argument("config", help="The SPA2 project config file", type=Path) 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(): def model_yaml_verification_cli(args: argparse.Namespace):
"""Main function for model yaml verification.""" """CLI function for model yaml verification."""
args = parse_args()
app = get_app(args.config) app = get_app(args.config)
model_yamls = app.get_translation_files() model_yamls = app.get_translation_files()
model_yaml_ver = ModelYmlVerification(app) model_yaml_ver = ModelYmlVerification(app)
@ -338,5 +336,13 @@ def main():
model_yaml_ver.print_success_msg() 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__": if __name__ == "__main__":
main() main(sys.argv[1:])

@ -6,17 +6,17 @@
import argparse import argparse
import re import re
import sys
from typing import List, Optional
from ruamel.yaml import YAML from ruamel.yaml import YAML
from pathlib import Path from pathlib import Path
def parse_args(): PARSER_HELP = "Update call sources for method calls in source files."
""" Parse arguments
Returns:
Namespace: the parsed arguments def configure_parser(parser: argparse.ArgumentParser):
""" """Configure the parser for the update call sources command."""
parser = argparse.ArgumentParser()
parser.add_argument("interface", help="Interface specification dict", type=Path) 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("src_dir", help="Path to source file directory", type=Path)
parser.add_argument( parser.add_argument(
@ -26,12 +26,11 @@ def parse_args():
default=None, default=None,
help="Path to project config json file", help="Path to project config json file",
) )
return parser.parse_args() parser.set_defaults(func=update_call_sources_cli)
def main(): def update_call_sources_cli(args: argparse.Namespace):
""" Main function for stand alone execution.""" """CLI function for updating call sources."""
args = parse_args()
method_config = read_project_config(args.project_config) method_config = read_project_config(args.project_config)
with open(args.interface, encoding="utf-8") as interface_file: with open(args.interface, encoding="utf-8") as interface_file:
yaml = YAML(typ='safe', pure=True) yaml = YAML(typ='safe', pure=True)
@ -39,6 +38,14 @@ def main():
update_call_sources(args.src_dir, adapter_spec, method_config) 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): def read_project_config(project_config_path):
""" Reads project config file and extract method specific settings if they are present. """ 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__": if __name__ == "__main__":
main() main(sys.argv[1:])

@ -5,11 +5,16 @@
import argparse import argparse
import sys
from pathlib import Path from pathlib import Path
from typing import List, Optional
from ruamel.yaml import YAML from ruamel.yaml import YAML
from powertrain_build.interface.application import Application from powertrain_build.interface.application import Application
from powertrain_build.interface.base import BaseApplication from powertrain_build.interface.base import BaseApplication
PARSER_HELP = "Update model yaml files."
class BadYamlFormat(Exception): class BadYamlFormat(Exception):
"""Exception to raise when signal is not in/out signal.""" """Exception to raise when signal is not in/out signal."""
@ -155,25 +160,27 @@ def get_app(config):
return app return app
def parse_args(): def update_model_yaml_cli(args: argparse.Namespace):
"""Parse command line arguments """CLI for update model yaml."""
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()
app = get_app(args.config) app = get_app(args.config)
translation_files = app.get_translation_files() translation_files = app.get_translation_files()
uymlf = UpdateYmlFormat(app) uymlf = UpdateYmlFormat(app)
uymlf.parse_definition(translation_files) 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__": if __name__ == "__main__":
main() main(sys.argv[1:])

@ -3,18 +3,29 @@
"""Module for replacing $CVC_* style references in a2l file.""" """Module for replacing $CVC_* style references in a2l file."""
import re
import argparse import argparse
import re
import sys
from pathlib import Path from pathlib import Path
from typing import List, Optional
def parse_args(): PARSER_HELP = "Replace $CVC_* style references in a2l file"
"""Parse args."""
parser = argparse.ArgumentParser("Replace $CVC_* style references in a2l file")
def configure_parser(parser: argparse.ArgumentParser):
"""Configure parser for CLI."""
parser.add_argument("a2l_target_file") parser.add_argument("a2l_target_file")
args = parser.parse_args() parser.set_defaults(func=replace_tab_verb_cli)
return args
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): def replace_tab_verb(file_path: Path):
@ -82,11 +93,13 @@ def replace_tab_verb(file_path: Path):
f_h.write(a2l_patched) f_h.write(a2l_patched)
def main(): def main(argv: Optional[List[str]] = None):
"""Main.""" """Main function for CLI."""
args = parse_args() parser = argparse.ArgumentParser(description=PARSER_HELP)
replace_tab_verb(args.a2l_target_file) configure_parser(parser)
args = parser.parse_args(argv)
args.func(args)
if __name__ == "__main__": if __name__ == "__main__":
main() main(sys.argv[1:])

@ -11,6 +11,7 @@ import sys
from collections import defaultdict from collections import defaultdict
from os.path import join from os.path import join
from pathlib import Path from pathlib import Path
from typing import List, Optional
import git import git
@ -60,6 +61,8 @@ TEMPLATE = """<!DOCTYPE html>
</body> </body>
</html>""" </html>"""
PARSER_HELP = "Run signal inconsistency check."
def gen_sig_incons_index_file(project_list): def gen_sig_incons_index_file(project_list):
"""Generate Index_SigCheck_All.html.""" """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)) f_h.write(TEMPLATE.format(project_rows=rows))
def parse_args(): def configure_parser(parser: argparse.ArgumentParser):
"""Parse the arguments sent to the script.""" """Parse the arguments sent to the script."""
parser = argparse.ArgumentParser("")
parser.add_argument( parser.add_argument(
"-m", "-m",
"--models", "--models",
@ -89,7 +91,7 @@ def parse_args():
parser.add_argument( parser.add_argument(
"-r", "--report", help="Create report for all projects", action="store_true" "-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(): def get_project_configs():
@ -612,12 +614,19 @@ class SignalInconsistency:
return exit_code return exit_code
def main(): def run_signal_inconsistency_check(args: argparse.Namespace) -> int:
"""Create Signal Inconsistency instance and run checks.""" """Create Signal Inconsistency instance and run checks."""
args = parse_args()
sig_in = SignalInconsistency(args) sig_in = SignalInconsistency(args)
return sig_in.run(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__": if __name__ == "__main__":
sys.exit(main()) sys.exit(main(sys.argv[1:]))

@ -10,6 +10,7 @@ import os
import sys import sys
from pathlib import Path from pathlib import Path
from re import search from re import search
from typing import List, Optional
try: try:
from importlib.resources import files from importlib.resources import files
@ -28,6 +29,7 @@ class PyBuildWrapper(pt_matlab.Matlab):
"""Performs upgrade of Matlab models to PyBuild system.""" """Performs upgrade of Matlab models to PyBuild system."""
HASH_FILE_NAME = "pybuild_file_hashes.json" 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): def __init__(self, args):
"""Constructor, initializes paths for PyBuild upgrader. """Constructor, initializes paths for PyBuild upgrader.
@ -469,8 +471,10 @@ class PyBuildWrapper(pt_matlab.Matlab):
default="matlab-scripts", default="matlab-scripts",
help="Path to folder containing Matlab scripts and simulink libraries to include.", help="Path to folder containing Matlab scripts and simulink libraries to include.",
) )
powertrain_build_parser = parser.add_subparsers(help="PyBuild specific build.") parser.set_defaults(func=run_wrapper)
build_specific_parser = powertrain_build_parser.add_parser(
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-specific", help="Run PyBuild for project with specific settings."
) )
build.add_args(build_specific_parser) build.add_args(build_specific_parser)
@ -479,12 +483,8 @@ class PyBuildWrapper(pt_matlab.Matlab):
pt_matlab.Matlab.add_args(parser) pt_matlab.Matlab.add_args(parser)
def main(): def run_wrapper(args: argparse.Namespace) -> int:
"""Run main function.""" """Run PyBuildWrapper."""
parser = argparse.ArgumentParser("PyBuild Wrapper")
PyBuildWrapper.add_args(parser)
args = parser.parse_args()
if args.build is not None and getattr(args, "project_config", None) is not None: 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).") LOGGER.error("Cannot run both PyBuild quick build (--build <PROJECT>) " "and specific build (build-specific).")
return 1 return 1
@ -498,5 +498,13 @@ def main():
return wrapper.run() 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__": if __name__ == "__main__":
sys.exit(main()) sys.exit(main(sys.argv[1:]))

@ -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.9.1; python_version == "3.8" or python_version == "3.9" or python_version == "3.10"
scipy==1.14.1; python_version >= "3.11" scipy==1.14.1; python_version >= "3.11"
importlib-resources==5.4.0; python_version < "3.9" 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"

@ -34,3 +34,7 @@ packages =
[pbr] [pbr]
skip_git_sdist = 1 skip_git_sdist = 1
[options.entry_points]
console_scripts =
powertrain-build = powertrain_build.cli:main

@ -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)

@ -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)

11
tox.ini

@ -1,7 +1,10 @@
[tox] [tox]
skipsdist = True
requires = requires =
tox >= 2.0 tox >= 2.0
envlist =
flake8
pytest
functest
[flake8] [flake8]
exclude = exclude =
@ -42,3 +45,9 @@ exclude_lines =
[coverage:html] [coverage:html]
directory = cover directory = cover
[testenv:functest]
skipsdist = False
package = editable
commands =
pytest tests/powertrain_build/func_cli.py

@ -2,5 +2,5 @@
name: powertrain-build-tox name: powertrain-build-tox
parent: tox-cover parent: tox-cover
vars: vars:
tox_envlist: flake8,pytest tox_envlist: flake8,pytest,functest
nodeset: ubuntu-jammy nodeset: ubuntu-jammy